Skip to content

Combat

Source: PROJECT_CONTEXT.md §5, §8.3; config.js player, combat, melee, weapons, move, enemyScale; game.js computeStats / setBonuses / doMelee / updatePlayerAttacks / perceive / setLean. Status: ✅ Implemented.

What it is

The fight model. The player has no attack button: a stance arbiter auto-picks melee or ranged based on the nearest threat's range. Mobs are armored from the front and must be circled to a rear weak-point; a backstab bypasses that armor entirely, so stealth beats brute force. Fights are tuned to be prolonged-but-survivable so progression is about kill speed, not surviving.

How it works

  • Stats & Power. Base {atk, hp, crit, speed} from config.js player + gear. (PROJECT_CONTEXT §8.3 frames the design baseline as {atk 30, hp 280, crit 50, speed 100}; live config currently runs baseHp 190 / baseSpeed 60 after a lethality pass.) shieldMax = hp × player.shieldFrac — a regenerating overshield buffer (§8.3 frames it as hp×0.5; live shieldFrac 0.18) that refills after shieldRegenDelay (6s) of no damage at shieldRegen/s up to shieldRegenCap. power = atk×powAtk + hp×powHp + crit×powCrit + speed×powSpeed (weights 6 / 0.9 / 4 / 2 → the §5 formula atk×6 + hp×0.9 + crit×4 + speed×2), computed in computeStats.
  • Stamina. A move.stamMax (100) pool drives sprint (sprintMult ×1.9, drains sprintDrain 32/s) and the dodge-roll (dodgeCost 30, dodgeTime 0.25s, dodgeSpeed 500px/s peak), which grants i-frames (stealth.invulnWindow 0.34s — un-spottable and invulnerable). Regens at stamRegen/s after stamRegenDelay.
  • Attack-stance arbiter (one stance at a time). updatePlayerAttacks picks a single stance by the nearest threat's distance: melee when it's within strike reach + combat.meleeBand (ranged is suppressed so you never auto-shoot point-blank, auto-swinging on player.meleeCd); ranged otherwise. Hysteresis (combat.bandHysteresis) + a stanceCommit timer keep the handoff from flickering. The master toggle is combat.autoMelee (true); set false to restore the old all-range ranged + tap/backstab-only melee.
  • Front armor → rear weak-point. Mobs are armored from the front: shots inside the combat.armorFront cone ping off (armorFrontMult ×0.25), flanks take armorFlankMult (×0.65), and the glowing rear weak-point (armorRear) takes armorRearMult (×1.6). Mobs juke side-to-side, so you must circle.
  • Backstab. A strike landing in the rear/side arc (combat.backstabArc, ×PI from facing) is a crit (critBase ×1.5) that bypasses armor and ignores the PvP Power check — stealth beats brute force (doMelee). The hands-off auto-backstab uses a stricter rear-only arc (autoBackstabArc, 0.72) than a committed tapped strike, so brushing a side won't auto-kill. A backstab kill triggers backstabHitstop (0.13s freeze-frame).
  • Silent-opener grace. After a silent backstab, the player's auto-fire is muted for combat.meleeToFireLock (0.5s) so a non-lethal opener isn't instantly blown by a loud auto-shot — a beat to re-stab or slip to cover. A loud frontal melee doesn't trigger it.
  • Ranged enemies: rare-but-hard + dodgeable telegraph. Ranged mobs fire on a slow fireRate with a chargeDur wind-up that shows a red charge telegraph; the dodge-roll evades it. Melee mobs (tier ≥ melee.lungeMinTier) gap-close with a telegraphed lunge (melee.windup then a locked dash) that you side-step off the line.
  • Enemy cadence. enemyScale.atkCdMul (1.5) multiplies every enemy attack cooldown — mob melee + lunge, mob ranged, rival fire — so enemies attack less often without losing per-hit damage (the prolonged-but-survivable dial). The stance arbiter is player-only; rivals keep their own kiting/burst AI but obey this cadence.
  • Sets (3 matching pieces to activate, SET_THRESHOLD). setBonuses: Hunter +30% dmg vs mobs (mobDmg ×1.30); Stalker +45% backstab (backstab ×1.45) & −30% detectability (stealth ×0.7); Bulwark +22% max HP (hpMult ×1.22).
  • Attack body-language (render-only). setLean / attackLeanOffset lean the body token toward a melee target (ui.attackLean) and kick back from a ranged shot (ui.shootRecoil) for ui.leanTime, while feet/shadow stay planted — a stand-in for sprite anim, applied to player and enemies so a fight reads as back-and-forth.

Tunables

player | Key | Default | Meaning | |---|---|---| | player.baseAtk / baseHp / baseCrit / baseSpeed | 30 / 190 / 50 / 60 | base stats before gear | | player.shieldFrac | 0.18 | overshield = maxHP × this | | player.meleeCd | 0.9 | melee swing cooldown (s); backstab opener exempt | | player.powAtk / powHp / powCrit / powSpeed | 6 / 0.9 / 4 / 2 | Power-score weights |

combat | Key | Default | Meaning | |---|---|---| | combat.autoMelee | true | master toggle for auto-melee + stance handoff | | combat.meleeBand / bandHysteresis / stanceCommit | 5 / 5 / 0.15 | stance switch lead / anti-flicker / min hold (s) | | combat.backstabArc / autoBackstabArc | 0.55 / 0.72 | landed-crit arc / stricter passive-trigger arc (×PI) | | combat.critBase | 1.5 | backstab crit × | | combat.meleeToFireLock | 0.5 | silent-opener auto-fire mute (s) | | combat.backstabHitstop | 0.13 | freeze-frame on a backstab kill (s) | | combat.armorFront / armorRear | 0.95 / 2.1 | front armor cone / rear weak-point angle (rad) | | combat.armorFrontMult / armorFlankMult / armorRearMult | 0.25 / 0.65 / 1.6 | hit damage × by angle | | combat.mobHpMult / mobDmgMult | 2 / 0.5 | spongier mobs / softer hits (prolonged-but-survivable) | | combat.mobMeleeCd | 0.7 | enemy melee cooldown (s) | | combat.powCheckExp / powCheckMin / powCheckMax | 0.35 / 0.55 / 1.7 | PvP power-ratio damage exponent + clamps |

move (stamina) | Key | Default | Meaning | |---|---|---| | move.stamMax / stamRegen / stamRegenDelay | 100 / 24 / 0.6 | stamina pool / regen rate / pause (s) | | move.sprintMult / sprintDrain | 1.9 / 32 | sprint × / drain (stam/s) | | move.dodgeCost / dodgeTime / dodgeSpeed / dodgeCd | 30 / 0.25 / 500 / 0.55 | roll stamina / duration / peak speed / cooldown |

melee / weapons / enemyScale | Key | Default | Meaning | |---|---|---| | melee.lungeMinTier | 2 | only tier ≥ this gap-close with a lunge | | melee.windup / lungeSpeed / dmgMult | 0.65 / 480 / 2.0 | lunge telegraph (s) / dash speed / damage × | | weapons.<archetype>.chargeDur (enemies, per-mob) | e.g. 0.4–0.7 | ranged wind-up telegraph (s) | | enemyScale.atkCdMul | 1.5 | × on every enemy attack cooldown |

Design intent

The stance arbiter + no-attack-button keep input minimal (Hunter Assassin's tap-to-move feel) so the skill is positioning, not aiming (pillar #2). Front-armor + rear weak-point makes a head-on fight a circling puzzle and risky; the backstab that bypasses both armor and the Power check is the mechanical embodiment of pillar #1 (hide-and-kill — stealth beats brute force). Prolonged-but-survivable knobs (mobHpMult up, mobDmgMult down, slow enemy cadence) make progression about kill speed while keeping the lengthened, deliberate melee cadence readable. Sets let players craft a style (aggressive Hunter, stealthy Stalker, tanky Bulwark).

Open questions / deltas

  • "Crit Damage" vs crit (§9.2): the concept lists Crit Damage as a core stat; code tracks a single crit value — naming/semantics to reconcile for the Unity port.
  • Prolonged-but-survivable defaults: PROJECT_CONTEXT §8.7 notes mobHpMult/mobDmgMult are pending a playtest pass; live config runs 2 / 0.5 (the Diablo direction) while §8.7 documents a 1/1 arcade fallback.
  • Stat-baseline drift: §8.3 cites {hp 280, shieldMax = hp×0.5}; live config runs baseHp 190 / shieldFrac 0.18 after a lethality pass — treat config.js as canonical.
  • RTS ranged mode parked: weapons.rtsMode (false) gates a no-magazine/no-reload auto-attack; the default is the arcade mag+reload model.
  • Body-language is render-only: setLean is a stand-in for sprite animation — the Unity port replaces it with real attack anims.