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 belowminDraw(0.12) fizzle and no arrow is spawned. - Arrow speed lerps
minSpeed16 tomaxSpeed46 m/s by draw fraction; damage lerpsminDamage4 tomaxDamage20. - Accuracy is a spread cone:
maxSpread(0.09 rad) at zero draw, tightening to near zero at full draw. Overholding pastoverholdAfter(3 s beyond full) addsoverholdSpreadand drains extra stamina; being airborne addsairSpread. - Drawing drains
staminaDrain(6/s), slows movement tomoveFactor(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 alightning_beameffect. 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 forBLOCK.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. Ablock_breakeffect 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.