Skip to content

Combat

Status: Current Last reviewed: 2026-07-07 Source: shared/src/combatdefs.ts, shared/src/constants.ts, server/src/game/zone.ts, server/src/game/ai.ts

Combat is manual-aim and server-authoritative. The client predicts movement and plays animation, camera shake, and local sound, but the server owns every hit, cost, and cooldown.

Weapon State Machine

Each player has a wstate on the server, mirrored to the client in every snapshot:

stateDiagram-v2
  [*] --> idle
  idle --> equipping: equip_weapon
  equipping --> idle
  idle --> windup: attack (melee)
  windup --> release: windup timer elapses
  release --> recovery
  recovery --> idle
  idle --> drawing: draw_start (bow)
  drawing --> recovery: draw_release
  drawing --> idle: draw_cancel / stamina out
  idle --> blocking: hold block
  blocking --> idle: release block
  idle --> staggered: guard broken / heavy stagger
  staggered --> idle: stagger timer elapses

While a player is busy (windup, release, recovery, equipping, drawing, staggered) they cannot start a new action, so spam-clicking cannot skip the windup or recovery frames. release is instantaneous on the server: it is the frame where the melee trace runs, after which the weapon enters recovery.

Melee Traces

An attack puts the player into windup for the weapon's windup time. When that timer elapses, the server runs a single release-frame cone trace from the eye along the aim direction, tests goblins within range and half-angle arc, applies damage, knockback, and skill gain, then enters recovery.

Weapon Damage Range Windup Recovery Stamina Knockback Lunge Impact
Sword 12 2.9 0.16 0.34 8 1.5 2.4 0.35
Mace 19 3.0 0.38 0.62 15 5.5 1.2 0.90
Staff (bonk) 6 2.4 0.20 0.50 6 1.0 0 0.25

The sword is fast with short recovery, good for quick exchanges. The mace is slow and heavy, with a large windup and enough knockback to stagger. lunge adds a forward shove during release if the player is already moving forward, so a committed sword swing closes a little distance. impact drives client feel: heavier weapons produce a longer hitstop micro-pause and a stronger camera impulse on a confirmed hit.

Limitation

The melee test is a single cone at the release frame, not a multi-frame swept volume across the swing arc. It is honest and cheap; a swept capsule is a possible upgrade if fast strafing targets slip through.

Bow Draw System

The bow is hold-to-draw, release-to-fire. draw_start records the draw start time on the server (drawStartedAt); the client also tracks a local start for the draw meter and view-model tension. draw_release computes the held time and derives the shot. Constants live in the BOW block of shared/src/constants.ts:

  • Draw fraction reaches full at fullDrawTime (0.9 s). Releases below minDraw (0.12) fizzle and no arrow is spawned.
  • Arrow speed lerps minSpeed 16 to maxSpeed 46 m/s by draw fraction; damage lerps minDamage 4 to maxDamage 20.
  • Accuracy is a spread cone: maxSpread (0.09 rad) at zero draw, tightening to near zero at full draw. Overholding past overholdAfter (3 s beyond full) adds overholdSpread and drains extra stamina; being airborne adds airSpread.
  • Drawing drains staminaDrain (6/s), slows movement to moveFactor (0.55), and if stamina runs out the draw collapses.

The server validates the draw start and release times and the stamina, so a client cannot claim a stronger shot than it earned. Successful arrow hits train Archery.

Spell System

Spells require the staff. Each puts the caster into a short recovery and spends mana and stamina.

  • Fireball is a medium-speed projectile (fireball) with a splash radius, an orange dynamic light, a smoke trail, an explosion, and a ground scorch decal. Lead your target.
  • Lightning Bolt is hitscan: the server raycasts along the aim ray up to hitscanRange, applies high single-target damage, and sends a lightning_beam effect. The client renders a jittered branching bolt with a brief flash.
  • Rift Burst is a slower arcing projectile (rift_burst) with a large splash, an expanding purple shockwave ring, and debris. It uses the raycast aim point so you can drop it where enemies will be.

See SPELLS in shared/src/combatdefs.ts for exact mana, cooldown, damage, and splash values.

Blocking and Parry

Right mouse raises a guard (only with a melee weapon, and only above BLOCK.minStamina). Blocking is checked per incoming melee hit in damagePlayer:

  • The attacker must be within the frontal arc (BLOCK.arc, 1.15 rad half-angle). Hits from the side or back are not blocked.
  • A block raised within BLOCK.parryWindow (0.22 s) of the hit is a parry: no damage, and the attacker is staggered for BLOCK.parryStagger (1.4 s). This rewards reading the telegraph.
  • A normal block reduces damage to BLOCK.damageFactor (35 %) and spends stamina proportional to the damage blocked (staminaPerDamage).
  • If blocking empties stamina, the guard breaks: the player is staggered for BLOCK.breakStagger (1.1 s) and takes the reduced hit. A block_break effect fires.

Goblin attacks are telegraphed with a visible windup, so a parry is a timing read rather than a reflex check.

Stamina, Mana, Health

Defined in the STATS block. Stamina powers sprinting, jumping, swinging, drawing, and blocking; it regenerates after a short delay since last use. Mana powers spells and regenerates slowly. Health regenerates only inside the safe zone, and slowly. The village safe zone blocks all incoming damage and goblins will not cross its boundary.

Server Validation Summary

The server is the sole authority on: whether an attack is allowed to start, windup and recovery timing, hit detection and damage, stamina and mana cost, projectile spawn and hit, block and parry outcome, cooldowns, mob and player health, and skill gain. The client's role is prediction, animation, and feedback after the server confirms.