- Split the shared `fire` event into `cannon_fire`, `cannon_hit`, and `fire` so each weapon gets its own distinct voice
- Add three new sound assets (combat-mass-cannon, combat-mass-cannon-hit, combat-laser) to data/sfx.json and load them in GameScene
- Remove the 0.45 volume multiplier on enemy fire; the laser now plays at full volume since it has its own voice
- Update CombatSfx event map, both combat contexts (CombatScene, CombatSystem), dev tests, and README to reflect the ten-event SFX set
- Kills grant the hostile type's own `xp` (the roster sets XP-per-threat;
the curve lives in data/progression.json, 100 x n^2, capped at level 80)
- Each crossed level fires one `levelup` event: +3 hull / +2 shields with
a full heal, cyan burst + a distinct voice, and a kill-feed line — the
placeholder toughness the skill layer will subscribe to later
- Combat bar gains an XP row below the orbs (level, live gold fill,
banked / next cost; hidden when the snapshot carries no xp, so the
range bar keeps its old face)
- Kills show a gold "+N" reward floater; the level-up's SFX is the new
8th combat event (solar_flare_01)
- XpManager is pure and tested headless (node dev/xp.test.mjs); the
ledger saves as { level, xp } and old saves load fresh at level 0
- dev hooks (xp/grantXp), sfxcheck (range seven, main eight), shot
captures, and README all follow along
- Add inView() pure geometry helper to test if a craft's visual circle is at least partly within the camera's visible rect
- Gate enemy fire, auto-fire target selection, currentTarget lock, and bullet collision checks behind craftVisible() so off-screen hostiles hold lances, can't be engaged, and are immune to tracers until they fly into view
- Extend pickTarget() with an optional qualifies predicate to narrow the candidate pool (used for the on-screen rule)
- Document the rule in data/combat.json and CombatSystem class comments
- Prevent overlapping hit flashes from stacking, which captured the in-flash
state as the restore value and left crafts permanently white
- Track flashed crafts in a Set so the expiry path can always settle them
- Add purgeCraftTints(), called every ~250 ms from update(), that re-asserts
normal colors on any craft (tracked or roster) not holding a live flash
- Wire purgeRoster in both CombatScene and CombatSystem so the purge covers
the full player + enemy pack, not just Fx's own flashes
- Prune destroyed crafts from the tracking set to avoid leaked references
- Add tests covering overlap extension, kill-seam settle, periodic purge,
roster coverage, and dead-craft pruning
- New main-story quest granted after 'Special Delivery' is claimed AND its payoff comms are ACKed all the way through (unlocks.acked gate)
- Three-step checklist: speak with Trestle (trestleQ4 warning beats), defend from a rolling wave of six raiders at the gate-to-capital midpoint, return to the Neutral Capital World
- CombatSystem.startWaves gains an origin option so ambush enemies materialize in a ring around a specified point rather than off-screen around the ship
- CommsHub fires a new onAck hook before onEnd so the scene can record acknowledgments and run its grant pass in the same tick
- QuestState.isUnlockMet / grantUnlocked accept a gates.acked set; GameScene supplies it from the comms acked-ledger
- SaveData captures/restores comms.acked, combat.done, and story.capitalReturned; resetRunState clears all three on New Game
- Combat is now built for every scene (not just ?combat demo); the hostile pack spawns only in the sandbox boot
- Test suites updated: quest unlock gating, ambush wave shape, roster count 25→28, save round-trip of new ledgers
- Waves.js: pure wave runner (rolling/fixed) + CombatSystem.startWaves
for quest-step combat; named scripts (probe/skirmish/raid) ship in
combat.json, driven from the console until the quest hook lands
- EnemyTypes.js: named hostiles (raider/bomber) deep-merged onto the
base craft, with per-type textures preloaded via enemySheetList()
- Hostiles now materialize off-screen (camera edge + game.spawnLead)
and fly in on their chase AI instead of popping into view mid-fight
- Wave ships are run-owned: no self-respawn, kills feed rolling
replacements, and the combat bar stays up through the lead and gaps
A shared readability layer (js/combat/CombatHud.js) for both combat
contexts — the training range and the main game's ?combat:
- Target/lock indicator: rotating brackets on the current target (the
auto-fire target — one rule, one target, js/combat/Combat.js#pickTarget).
- Threat cues: pulsing markers above each live hostile.
- Off-screen arrows: edge arrows toward hostiles outside the camera
(zoom-aware via a context-supplied worldToScreen).
- Kill feed: newest-first fading entries, top-right.
- Gauge flash: the combat bar's shields/hull gauge flashes on the hit
it took (CombatBar.gaugeRect feeds the shared CombatHud.flash).
Pure rules stay in Combat.js (pickTarget, feedPush, offscreenArrow),
covered by dev/combat.test.mjs. The main game re-anchors the pack
around the ship once its start position settles (CombatSystem
.resyncAroundShip + a short settle window in GameScene), so the demo
pack sits in auto-fire range instead of kilometers off.
All combat rules tests pass (0 failures); CDP-verified in both
/combat and index.html?combat.
- js/combat/CombatSfx.js (new): ONE event→voice map + playback path for
both contexts, using ONLY existing data/sfx.json files:
fire=scan · impact=ui_click · shield=solar_buzz · hull=mining_split
kill=thunder_01..04 (one at random) · down=deconstruct ·
respawn=discovery
plus combatSfxEnemyHit() (shield/hull decision in one place: a hit
that breaks through to the hull plays both — the shield-pop read),
loadCombatSfx() for the range's preload (the main game already loads
all of sfx.json), and a capped dev log on window.__COMBAT_SFX__ so
the wiring is verifiable headless (no audio manager) in both.
- js/combat/CombatScene.js: fire/enemy-fire/wall-clip/ram/lance/kill/
down/respawn sites call the shared path; the player-hit voices
(impact + shield/hull from applyDamage) funnel through damagePlayer.
- js/combat/CombatSystem.js: the same seven call sites, identical
wiring (no per-context copies).
- dev/combat.test.mjs: sfx block — seven events mapped, voices exist in
data/sfx.json, pairwise distinct, vol scaling, thunder pick, the
shield/hull decision, the dev log, the master switch (0 failures).
- dev/combat.sfxcheck.mjs (new): CDP check that drives real events in
/combat AND index.html?combat and asserts all seven events pass
through the shared path — PASS in both.
- js/combat/Combat.js: new pure handleFeel() step (smoothed turn
velocity, ramped/decaying throttle, instant brake) with EXACT
analytic integration of the eased turn — frame-rate independent by
construction (dev/combat.test.mjs: feel block, 0 failures).
- data/combat.json: combat.player tuning surface — thrust 2000, drag
1.8 (now actually overrides ship.drag in the range), rotSpeed 6.4,
rotApproach 20, thrustRamp 18, thrustDecay 30, brake 0.9.
- js/combat/CombatScene.js: updatePlayer runs the shared feel step.
ALSO FIXES WASD: this Phaser build's addKeys() object form only
resolves single KeyCodes names, so {up:'W,UP',...} put every name on
ONE shared Key with keyCode undefined and isDown polling could never
see a key-press (WASD was dead). Now keys drive off the working
keydown-/keyup- event API (the main game's pattern) with real Key
objects registered for capture; window-blur clears held keys.
- dev/combat-flycheck.mjs: CDP flight-feel check — thrust 60% in ~300
ms, 30° turn in ~150 ms, sustained spin ≥ rotSpeed, stop from full
in ~300 ms (headless ~24 fps budgets; PASS).
- Add CombatSystem: owns the hostile pack, tracers, player armor/energy,
auto-fire (nearest-in-range), swept-hit collisions, rams, respawn — all
driven by data/combat.json `game.*` / `energy` sections
- Add CombatBar (Diablo 4 style): vertical shields + hull gauges on the
left, energy gauge on the right, eight skill seats (keys 1–8) in the
middle; shown in place of the action deck while combat is live
- `?combat` now boots the normal game with the pack spawning when the
player starts (mouse steers, cannon auto-fires); the standalone WASD
range remains at `/combat` / `combat.html`
- GameScene wires the system into its update loop, solid constraints, and
click-through gate; exposes `window.__ORBIT_COMBAT__` for DevTools
- Extend combat tests to cover CombatSystem lifecycle (spawn, auto-fire,
hits, rams, armor, respawn, cast stub, destroy)
- New `combat.html` + `/combat` dev-server route + `index.html?combat`
boot a fixed arena where the player ship fights a pack of enemy craft;
all tuning lives in `data/combat.json` (thrust, hulls, shields, lance,
rams, respawns).
- Add `js/combat/` modules: CombatScene, EnemyShip (standoff-ring AI +
lance), Bullet, Fx, StarDrift, and pure rules in Combat.js (armor
damage, orbit step) exercised by the new `dev/combat.test.mjs`.
- Extract shared `awaitFonts` into `js/utils/Fonts.js` so both the game
and sandbox boots use one copy.
- Preload the enemy spritesheet in GameScene so the texture is ready when
combat ships in; add `ships-enemy.png` (+ `.psd`) art.
- Expose `window.__COMBAT__` (reset, spawnEnemy, player, kills) for
devtools tuning; R resets the range, P freezes it.