{ "_comment": "COMBAT — all combat tuning, one file. Two contexts share the numbers: the standalone range (combat.html / the dev server's /combat — the WASD arena, js/combat/CombatScene.js) and the MAIN GAME (index.html?combat — js/combat/CombatSystem.js drops the pack into the normal game: click-to-fly, the deck swaps for the combat bar, the ship auto-fires at the nearest hostile). Pure rules (armor damage, orbit steering) are js/combat/Combat.js (tested: dev/combat.test.mjs).", "enabled": true, "game": { "_comment": "COMBAT IN THE MAIN GAME (the ?combat boot). spawnCount = hostiles that appear around the ship when the player starts. SPAWNING: hostiles materialize OFF-SCREEN — the furthest of the (spawnRadius band, the camera's visible edge + spawnLead) — and FLY IN on their own chase AI (standoff ring, then lances): nothing pops into view mid-fight. spawnRadius px = the minimum materialization band around the ship; spawnLead px = how far PAST the camera's visible edge they materialize (the fly-in distance — bigger = a longer stream across the screen). autoFire = the ship's guns — while a hostile is within range the ship fires at the NEAREST one (the mouse steers, the cannon tracks; the cannon's own numbers are player.weapon below). ON-SCREEN RULE (js/combat/CombatSystem.js): a hostile must be VISUALLY ON SCREEN (at least partly in the camera's view) to fire or be fired upon — off-screen craft hold their lances, the cannon won't engage them, and their tracers pass straight through; they join the fight the moment they fly into view.", "spawnCount": 3, "spawnRadius": 340, "spawnLead": 140, "autoFire": { "enabled": true, "range": 800 } }, "energy": { "_comment": "The player's ENERGY — the skill resource (the combat bar's right ORB — the main game's and the range's). The eight seats (keys 1-8) spend it: a cast takes its cost off the orb and that skill's own recharge runs from there. regen = energy/s recovered while the ship is up — 6/s buys a 60-energy Ramming Charge back in 10s, so a seat is a TEMPO tool rather than one shot per fight; 0 puts the orb back on static.", "max": 100, "regen": 6 }, "player": { "_comment": "The combat feel of YOUR ship in the range (the art, hull stats, and ship name still come from data/ship.json). WASD/arrows fly, SPACE/J fires (hold = auto-fire). The flight feel (js/combat/Combat.js → handleFeel, tested): thrust px/s^2 at full throttle, maxSpeed px/s cap, drag 1/s (exponential coast decay — the range OVERRIDES ship.drag with this, so data/combat.json is the one tuning surface), brake = the S/Down reverse-throttle as a fraction of FULL thrust (instant — a responsive stop). rotSpeed rad/s = the SUSTAINED keyboard turn rate; rotApproach 1/s = how fast the turn rate builds (a tap is a nudge, a hold is a full spin); thrustRamp 1/s = how fast the throttle reaches full on W; thrustDecay 1/s = how fast the throttle cuts when W lets go (no lingering kick). weapon.* = the player's cannon: cooldown s between shots, bulletSpeed px/s, damage per hit, spread rad of aim jitter, bulletLife s before a shot fizzles, color/size the tracer's glow; boltLength/boltWidth px + muzzle px are the laser's VISUALS (the bolt's length × thickness and the muzzle flash's size — js/combat/Bullet.js + Fx.muzzle).", "thrust": 2000, "maxSpeed": 560, "drag": 1.8, "rotSpeed": 6.4, "rotApproach": 20, "thrustRamp": 18, "thrustDecay": 30, "brake": 0.9, "weapon": { "key": "space", "cooldown": 0.16, "bulletSpeed": 680, "damage": 10, "spread": 0.03, "bulletLife": 1.15, "color": "#00e5ff", "size": 16, "boltLength": 46, "boltWidth": 10, "muzzle": 46 } }, "enemy": { "_comment": "The hostile craft. texture = spritesheet of frameWidth×frameHeight frames (frame picks which one — frame 0 = the red triple-boost fighter, art facing north, filling its frame; the sheet is 10×10 of 128px frames). size = hull length in world px (render = size × the art's footprint), hitRadius = the collision circle in world px (bullets + rams). count = hostiles in the arena at once. speed px/s + turn (1/s velocity-approach rate) = the chase feel. orbitRadius px = the standoff ring they keep around the player; orbitSpeed rad/s = how fast they circle it (direction alternates per craft). hull/shields = their armor (shields absorb first — js/combat/Combat.js). xp = the EXP a kill of one is worth (the XP bank's currency — js/combat/XpManager.js; the curve is data/progression.json, and the base value is 10, so five kills of the base craft = level 1). weapon.* = their lance: range px, cooldown s, bulletSpeed px/s, damage, spread rad, bulletLife s; boltLength/boltWidth px + muzzle px are the lance's VISUALS (the bolt's length × thickness and the muzzle flash's size — js/combat/Bullet.js + Fx.muzzle). ramDamage = hull damage a COLLISION deals you; ramDamageSelf = what the collision deals THEM (per hit, ramGrace s apart). respawn = the pack reforms (respawnDelay s) after a kill, so the range stays stocked while you tune.", "texture": "assets/images/ships-enemy.png", "frameWidth": 128, "frameHeight": 128, "frame": 0, "artFacing": "north", "size": 54, "hitRadius": 20, "count": 3, "speed": 165, "turn": 3.2, "orbitRadius": 300, "orbitSpeed": 0.6, "hull": 60, "shields": 0, "xp": 10, "weapon": { "range": 560, "cooldown": 1.4, "bulletSpeed": 330, "damage": 8, "spread": 0.08, "bulletLife": 2.4, "color": "#ff2d6f", "size": 18, "boltLength": 54, "boltWidth": 12, "muzzle": 50 }, "ramDamage": 10, "ramDamageSelf": 22, "ramGrace": 0.8, "respawn": true, "respawnDelay": 2.5 }, "enemyTypes": { "_comment": "The hostile roster a WAVE (combat.waves.scripts) can mix — js/combat/EnemyTypes.js deep-merges a type's overrides onto the base `enemy` above, so a type declares only what differs. `raider` is the base craft under a named id (frame 0). Frames 1–7 are the SEVEN new hostiles, each a distinct craft with a distinct WEAPON PATTERN (weapon.pattern) and a MODERATE difficulty step-up: hull/shields (armor to soak) climb monotonically bomber → warden, so each is a little harder to defeat than the last. PATTERNS (js/combat/Weapons.js): 'lance' (the default — one aimed bolt, the base behavior), 'spray' (a FAN of `bolts` spread over `fan` rad), 'burst' (a TIGHT `bolts`-shot stinger — staggered speeds so it fans over distance), 'rapid' (single bolts on a short cooldown — a machine gun), 'ring' (`bolts` fired RADIAL around the craft — break the orbit), 'missile' (`missiles` HEAT-SEEKING shots that home with a CAPPED turn-rate (missileTurn rad/s) so the player can out-turn them), 'combo' (a heavy lance + a missile, the elite). Add a craft here (a new `frame` or its own `texture` + stat overrides) and name it in a wave's roster; EnemyTypes.enemySheetList() is the preload seam for its textures. Each type's `xp` = what a kill of it is worth (the rule of thumb is (hull+shields)/5, rounded, so a tougher craft is worth more of the XP bank).", "raider": { "$comment": "The PIRATE (data/quests.json → pirate_problems — the ambush wave combat.waves.scripts.pirate_ambush is all raiders). comms = the pirate's combat VOICE (the rolls live in js/combat/CombatSystem.js; the comms themselves are data/comms.json's pirate-attack / pirate-defeat — the box's clip + one random speech clip + the message). attack.onScreenChance = the chance a pirate shows an ATTACK comm the first time it flies ON SCREEN (0.5 = 50%); attack.spawnChance = the chance a NEW pirate sent into a rolling wave (the opening burst, each kill's replacement) ATTACK-comms on arrival (0.2 = 20%) — a hit marks the ship, so its later on-screen roll doesn't stack (a pirate never double-comms); defeat.chance = the chance a downed pirate shows a DEFEAT comm (0.2 = 20%).", "frame": 0, "comms": { "attack": { "onScreenChance": 0.5, "spawnChance": 0.2, "trigger": "pirateAttack" }, "defeat": { "chance": 0.2, "trigger": "pirateDefeat" } } }, "bomber": { "_comment": "frame 1 — the LONG LANCE: slow, sits back, one heavy bolt on a long fuse. The easy end of the new ramp.", "frame": 1, "speed": 105, "turn": 2.2, "hull": 80, "shields": 10, "xp": 18, "weapon": { "pattern": "lance", "range": 760, "cooldown": 2.0, "bulletSpeed": 250, "damage": 10, "spread": 0.04, "bulletLife": 3.0 } }, "spitter": { "_comment": "frame 2 — the SPRAY: a fan of cheap bolts covering an arc; low damage per bolt, wide cone.", "frame": 2, "speed": 130, "turn": 3.0, "hull": 90, "shields": 20, "xp": 22, "weapon": { "pattern": "spray", "range": 480, "cooldown": 1.9, "bulletSpeed": 300, "damage": 11, "spread": 0.03, "bulletLife": 2.2, "bolts": 5, "fan": 1.1, "color": "#ff7a3d" } }, "stinger": { "_comment": "frame 3 — the BURST: a fast, tight 3-shot stinger at close range (staggered speeds fan it out over distance).", "frame": 3, "speed": 185, "turn": 4.0, "hull": 100, "shields": 25, "xp": 25, "weapon": { "pattern": "burst", "range": 400, "cooldown": 1.5, "bulletSpeed": 420, "damage": 12, "spread": 0.02, "bulletLife": 1.8, "bolts": 3, "burstFan": 0.1, "color": "#ffe14d" } }, "hunter": { "_comment": "frame 4 — the HUNTER: launches a HEAT-SEEKING missile that homes with a capped turn-rate (missileTurn) — dodgeable by out-turning it.", "frame": 4, "speed": 150, "turn": 3.2, "hull": 110, "shields": 30, "xp": 28, "weapon": { "pattern": "missile", "range": 620, "cooldown": 2.4, "missiles": 1, "missileSpeed": 190, "missileTurn": 2.2, "missileDamage": 13, "missileLife": 4.0, "missileColor": "#ff5d2d", "missileSize": 18, "color": "#ff5d2d" } }, "gunner": { "_comment": "frame 5 — the GUNNER: rapid single bolts on a short cooldown (a machine gun); solid per-bolt punch at a high rate.", "frame": 5, "speed": 190, "turn": 4.2, "hull": 120, "shields": 40, "xp": 32, "weapon": { "pattern": "rapid", "range": 440, "cooldown": 0.5, "bulletSpeed": 380, "damage": 14, "spread": 0.06, "bulletLife": 1.6, "color": "#c8ff3d" } }, "barrage": { "_comment": "frame 6 — the BARRAGE: a RADIAL ring of bolts (break the orbit); slow and armored, pokes a hole in the ring.", "frame": 6, "speed": 100, "turn": 2.0, "hull": 135, "shields": 50, "xp": 37, "weapon": { "pattern": "ring", "range": 360, "cooldown": 2.2, "bulletSpeed": 260, "damage": 15, "spread": 0.0, "bulletLife": 2.6, "bolts": 8, "color": "#b44dff" } }, "warden": { "_comment": "frame 7 — the WARDEN (elite): a heavy lance PLUS a homing missile on every volley; the hardest of the seven.", "frame": 7, "speed": 90, "turn": 1.8, "hull": 150, "shields": 60, "xp": 42, "weapon": { "pattern": "combo", "range": 700, "cooldown": 2.6, "bulletSpeed": 240, "damage": 18, "spread": 0.02, "bulletLife": 3.2, "size": 20, "boltLength": 64, "boltWidth": 14, "missiles": 1, "missileSpeed": 170, "missileTurn": 2.4, "missileDamage": 20, "missileLife": 4.2, "missileColor": "#ff2d6f", "missileSize": 20, "color": "#ff2d6f" } }, "$factions_note": "FRAMES 30–37 = THE APEX, frames 40–47 = THE MINDWEAVE — the two FACTION rosters (the ?combat=apex / ?combat=mindweave duel arena — combat.factions below names them). BALANCE RULE (the user's numbers, vs the PIRATE raider — 60 armor, 8 bolt damage): BOTH FACTIONS = 2× DAMAGE (16 = 8 × 2, every bolt/seeker/shell) and 3.5× ARMOR (175 hull + 35 shields = 210 = 60 × 3.5). Cooldowns 0.6–3.0 s (pattern character, not power), ramDamage 13 (10 × 1.3). xp = (hull+shields)/5 = 42 (the roster's rule of thumb). DIFFERENTIATION comes from the WEAPON PATTERN (weapon.pattern — the shared dispatch, js/combat/Weapons.js) + the ABILITY LAYER (abilities[].id — js/combat/EnemyAbilities.js: cloaked_ambush / flank_strafe for the Apex's predatory kit, blink_reposition / illusion_decoy / telekinetic_wave for the Mindweave's psychic kit). Each fighter is its own craft; the faction's SHARED skills are the kit's common entries (the Apex all carry the cloak and most the flank; the Mindweave all carry the blink or the phantoms and several the tides) at per-ship numbers.", "apex_raptor": { "$comment": "APEX 1 (frame 30) — RAPTOR, the forward gun: a 5-bolt SWEEP out the nose (heavy forward firepower) on a medium beat; the flank's cut gets it behind the player every 10 s or so so the sweep lands on the back.", "name": "Raptor", "frame": 30, "speed": 175, "turn": 3.6, "hull": 175, "shields": 35, "xp": 42, "ramDamage": 13, "ramDamageSelf": 29, "weapon": { "pattern": "sweep", "range": 520, "cooldown": 1.2, "bulletSpeed": 360, "damage": 16, "spread": 0.03, "bulletLife": 2.0, "bolts": 5, "fan": 1.0, "color": "#ff4d2d" }, "abilities": [ { "id": "flank_strafe", "period": 10, "radius": 210, "color": "#ff4d2d" } ] }, "apex_mauler": { "$comment": "APEX 2 (frame 31) — MAULER, the heavy hitter: a BIG slow 3-bolt BURST (16 damage each, wide visual) — slow, armored, deliberate; comes out of CLOAK to swing it (the shared ambush).", "name": "Mauler", "frame": 31, "speed": 120, "turn": 2.4, "hull": 175, "shields": 35, "xp": 42, "ramDamage": 13, "ramDamageSelf": 29, "weapon": { "pattern": "burst", "range": 560, "cooldown": 1.6, "bulletSpeed": 240, "damage": 16, "spread": 0.02, "bulletLife": 2.6, "size": 22, "boltLength": 70, "boltWidth": 16, "bolts": 3, "burstFan": 0.12, "color": "#ff2d4d" }, "abilities": [ { "id": "cloaked_ambush", "cloakMs": 3200, "color": "#ff2d4d" } ] }, "apex_stalker": { "$comment": "APEX 3 (frame 32) — STALKER, the double-cloak (the signature): cloaks on contact, strikes, then DROPS BACK INTO CLOAK on its own clock (recloakCooldown/recloakMs) — strike, vanish, strike again. Fast, clean lance.", "name": "Stalker", "frame": 32, "speed": 185, "turn": 4.0, "hull": 175, "shields": 35, "xp": 42, "ramDamage": 13, "ramDamageSelf": 29, "weapon": { "pattern": "lance", "range": 520, "cooldown": 1.1, "bulletSpeed": 380, "damage": 16, "spread": 0.04, "bulletLife": 2.0, "color": "#ff8a2d" }, "abilities": [ { "id": "cloaked_ambush", "cloakMs": 3600, "recloakCooldown": 16, "recloakMs": 2800, "color": "#ff8a2d" } ] }, "apex_screamer": { "$comment": "APEX 4 (frame 33) — SCREAMER, the wide fan: a 9-bolt SPRAY across 1.6 rad (dodge the core, not the wall); the flank's cut re-positions it before each scream.", "name": "Screamer", "frame": 33, "speed": 150, "turn": 3.0, "hull": 175, "shields": 35, "xp": 42, "ramDamage": 13, "ramDamageSelf": 29, "weapon": { "pattern": "spray", "range": 480, "cooldown": 1.4, "bulletSpeed": 340, "damage": 16, "spread": 0.02, "bulletLife": 1.8, "bolts": 9, "fan": 1.6, "color": "#ffd84d" }, "abilities": [ { "id": "flank_strafe", "period": 9, "radius": 200, "color": "#ffd84d" } ] }, "apex_reaver": { "$comment": "APEX 5 (frame 34) — REAVER, the gun: RAPID bolts at 0.6 s (the pack's highest DPS — out-turn it) with the double-cloak signature (a gun you can't see).", "name": "Reaver", "frame": 34, "speed": 190, "turn": 4.4, "hull": 175, "shields": 35, "xp": 42, "ramDamage": 13, "ramDamageSelf": 29, "weapon": { "pattern": "rapid", "range": 460, "cooldown": 0.6, "bulletSpeed": 400, "damage": 16, "spread": 0.06, "bulletLife": 1.5, "color": "#ff6a2d" }, "abilities": [ { "id": "cloaked_ambush", "cloakMs": 2800, "recloakCooldown": 18, "recloakMs": 2200, "color": "#ff6a2d" } ] }, "apex_fang": { "$comment": "APEX 6 (frame 35) — FANG, the stinger: a TIGHT 4-shot BURST (staggered speeds open it over distance — the close-range snap) at 16 damage each; cuts behind before it snaps.", "name": "Fang", "frame": 35, "speed": 180, "turn": 4.2, "hull": 175, "shields": 35, "xp": 42, "ramDamage": 13, "ramDamageSelf": 29, "weapon": { "pattern": "burst", "range": 420, "cooldown": 1.5, "bulletSpeed": 420, "damage": 16, "spread": 0.02, "bulletLife": 1.6, "bolts": 4, "burstFan": 0.14, "color": "#ff3d2d" }, "abilities": [ { "id": "flank_strafe", "period": 12, "radius": 230, "color": "#ff3d2d" } ] }, "apex_tyrant": { "$comment": "APEX 7 (frame 36) — TYRANT, the swarm: TWO heat-seeking missiles per volley (out-turn both) on a long beat; cloaks in, then the seekers come — the flank's cut keeps it off your nose.", "name": "Tyrant", "frame": 36, "speed": 150, "turn": 3.2, "hull": 175, "shields": 35, "xp": 42, "ramDamage": 13, "ramDamageSelf": 29, "weapon": { "pattern": "missile", "range": 620, "cooldown": 2.2, "missiles": 2, "missileSpeed": 190, "missileTurn": 2.2, "missileDamage": 16, "missileLife": 4.0, "missileColor": "#ff5d2d", "missileSize": 18, "color": "#ff5d2d" }, "abilities": [ { "id": "cloaked_ambush", "cloakMs": 3000, "color": "#ff5d2d" }, { "id": "flank_strafe", "period": 13, "radius": 220, "color": "#ff5d2d" } ] }, "apex_alpha": { "$comment": "APEX 8 (frame 37) — ALPHA, the pack's top predator (the elite): a COMBO of heavy lance (16) + three seekers every 2.4 s, the FULL kit — the longest cloak, the double-cloak's nap, and the flank's cut. 2× the pirate's damage, 3.5× its armor — the whole predatory doctrine in everything else.", "name": "Alpha", "frame": 37, "speed": 130, "turn": 2.8, "hull": 175, "shields": 35, "xp": 42, "ramDamage": 13, "ramDamageSelf": 29, "weapon": { "pattern": "combo", "range": 640, "cooldown": 2.4, "bulletSpeed": 260, "damage": 16, "spread": 0.02, "bulletLife": 2.8, "size": 20, "boltLength": 64, "boltWidth": 14, "missiles": 2, "missileSpeed": 180, "missileTurn": 2.3, "missileDamage": 16, "missileLife": 4.2, "missileColor": "#ffb02d", "missileSize": 20, "color": "#ffb02d" }, "abilities": [ { "id": "cloaked_ambush", "cloakMs": 3800, "recloakCooldown": 20, "recloakMs": 3000, "color": "#ffb02d" }, { "id": "flank_strafe", "period": 10, "radius": 210, "color": "#ffb02d" } ] }, "weave_mindblade": { "$comment": "MINDWEAVE 1 (frame 40) — MINDBLADE, the spiral: a 10-bolt SPIRAL (each bolt a step further around, staggered speeds — the 1942-boss bloom) at 16 damage; BLINKS to a fresh stand (biasing behind) and leaves a phantom at the old one.", "name": "Mindblade", "frame": 40, "speed": 140, "turn": 3.0, "hull": 175, "shields": 35, "xp": 42, "ramDamage": 13, "ramDamageSelf": 29, "weapon": { "pattern": "spiral", "range": 560, "cooldown": 2.2, "bulletSpeed": 210, "damage": 16, "spread": 0.01, "bulletLife": 3.0, "bolts": 10, "spiralStep": 0.55, "stagger": 0.12, "color": "#b44dff" }, "abilities": [ { "id": "blink_reposition", "cooldown": 12, "color": "#b44dff" } ] }, "weave_telekine": { "$comment": "MINDWEAVE 2 (frame 41) — TELEKINE, the tide: a rapid mind-cannon (16 per 0.6 s) backed by a PSYCHIC TIDE — every 13 s a wave blooms and SHOVES the player out of its line (the dodge window: be far or be moved).", "name": "Telekine", "frame": 41, "speed": 175, "turn": 3.8, "hull": 175, "shields": 35, "xp": 42, "ramDamage": 13, "ramDamageSelf": 29, "weapon": { "pattern": "rapid", "range": 500, "cooldown": 0.6, "bulletSpeed": 400, "damage": 16, "spread": 0.05, "bulletLife": 1.6, "color": "#4dd7ff" }, "abilities": [ { "id": "telekinetic_wave", "cooldown": 13, "dir": "push", "radius": 290, "power": 280, "color": "#4dd7ff" }, { "id": "blink_reposition", "cooldown": 17, "color": "#4dd7ff" } ] }, "weave_phantasm": { "$comment": "MINDWEAVE 3 (frame 42) — PHANTASM, the court: a blossoming VOLLEY (8 staggered bolts — the wall) with the FULL illusion kit — projects a COURT of three phantoms (the auto-fire's pool chases them) and blinks among them.", "name": "Phantasm", "frame": 42, "speed": 150, "turn": 3.2, "hull": 175, "shields": 35, "xp": 42, "ramDamage": 13, "ramDamageSelf": 29, "weapon": { "pattern": "volley", "range": 540, "cooldown": 1.6, "bulletSpeed": 260, "damage": 16, "spread": 0.01, "bulletLife": 2.6, "bolts": 8, "fan": 1.5, "stagger": 0.4, "color": "#ff4dc8" }, "abilities": [ { "id": "illusion_decoy", "cooldown": 15, "count": 3, "radius": 180, "lifeMs": 2800, "color": "#ff4dc8" }, { "id": "blink_reposition", "cooldown": 16, "decoyMs": 2000, "color": "#ff4dc8" } ] }, "weave_weaver": { "$comment": "MINDWEAVE 4 (frame 43) — WEAVER, the threads: a 9-bolt SWEEP out its own heading (its threads fly where it FACES — aim your counter off its nose) with the most frequent BLINK of the faction (11 s — it is rarely where you last saw it).", "name": "Weaver", "frame": 43, "speed": 160, "turn": 3.6, "hull": 175, "shields": 35, "xp": 42, "ramDamage": 13, "ramDamageSelf": 29, "weapon": { "pattern": "sweep", "range": 520, "cooldown": 1.3, "bulletSpeed": 300, "damage": 16, "spread": 0.02, "bulletLife": 2.2, "bolts": 9, "fan": 1.4, "color": "#4dffc8" }, "abilities": [ { "id": "blink_reposition", "cooldown": 11, "decoyMs": 2200, "color": "#4dffc8" } ] }, "weave_lure": { "$comment": "MINDWEAVE 5 (frame 44) — LURE, the net: a RADIAL ring of ten (break the orbit) with the phantoms and a PULL tide — it drags the player IN toward the ring (the push tide's inverse: its net closes on you).", "name": "Lure", "frame": 44, "speed": 130, "turn": 2.6, "hull": 175, "shields": 35, "xp": 42, "ramDamage": 13, "ramDamageSelf": 29, "weapon": { "pattern": "ring", "range": 460, "cooldown": 2.3, "bulletSpeed": 240, "damage": 16, "spread": 0.0, "bulletLife": 2.6, "bolts": 10, "color": "#4da6ff" }, "abilities": [ { "id": "illusion_decoy", "cooldown": 14, "count": 2, "radius": 200, "lifeMs": 3000, "color": "#4da6ff" }, { "id": "telekinetic_wave", "cooldown": 16, "dir": "pull", "radius": 300, "power": 240, "color": "#4da6ff" } ] }, "weave_dreamer": { "$comment": "MINDWEAVE 6 (frame 45) — DREAMSHELL, the shells: one LARGE-RADIUS slow shell (44 px glow, 16 damage, a short fuse — the big slow rock) per volley, backed by a hard PUSH tide (300 px/s — the strongest shove of the faction).", "name": "Dreamshell", "frame": 45, "speed": 110, "turn": 2.2, "hull": 175, "shields": 35, "xp": 42, "ramDamage": 13, "ramDamageSelf": 29, "weapon": { "pattern": "shell", "range": 600, "cooldown": 2.6, "damage": 16, "shells": 1, "shellDamage": 16, "shellSize": 44, "shellSpeed": 140, "shellLife": 2.4, "color": "#8a4dff" }, "abilities": [ { "id": "telekinetic_wave", "cooldown": 14, "dir": "push", "radius": 300, "power": 300, "color": "#8a4dff" }, { "id": "illusion_decoy", "cooldown": 17, "count": 2, "radius": 200, "lifeMs": 2600, "color": "#8a4dff" } ] }, "weave_serpent": { "$comment": "MINDWEAVE 7 (frame 46) — SERPENTMIND, the coil: a LONG 14-bolt spiral (a tighter step, slower bolts — a COIL that hangs around longer than Mindblade's) with the blink and a PULL tide (it repositions, then reeled you in).", "name": "Serpentmind", "frame": 46, "speed": 145, "turn": 3.0, "hull": 175, "shields": 35, "xp": 42, "ramDamage": 13, "ramDamageSelf": 29, "weapon": { "pattern": "spiral", "range": 580, "cooldown": 2.4, "bulletSpeed": 190, "damage": 16, "spread": 0.01, "bulletLife": 3.2, "bolts": 14, "spiralStep": 0.42, "stagger": 0.1, "color": "#c84dff" }, "abilities": [ { "id": "blink_reposition", "cooldown": 13, "color": "#c84dff" }, { "id": "telekinetic_wave", "cooldown": 18, "dir": "pull", "radius": 310, "power": 260, "color": "#c84dff" } ] }, "weave_overmind": { "$comment": "MINDWEAVE 8 (frame 47) — OVERMIND, the faction's apex (the elite): a TRIPLE SHELL volley (three large orbs per turn — the slowest, heaviest barrage of the roster) with the FULL psychic kit — the most frequent blink, a standing phantom court, and the strongest PULL tide (300). Control every axis of the fight at 2× the pirate's offense and 3.5× its armor.", "name": "Overmind", "frame": 47, "speed": 100, "turn": 2.0, "hull": 175, "shields": 35, "xp": 42, "ramDamage": 13, "ramDamageSelf": 29, "weapon": { "pattern": "shell", "range": 620, "cooldown": 3.0, "damage": 16, "shells": 3, "shellDamage": 16, "shellSize": 44, "shellSpeed": 150, "shellLife": 2.6, "color": "#e04dff" }, "abilities": [ { "id": "blink_reposition", "cooldown": 10, "decoyMs": 2200, "color": "#e04dff" }, { "id": "illusion_decoy", "cooldown": 13, "count": 2, "radius": 190, "lifeMs": 2800, "color": "#e04dff" }, { "id": "telekinetic_wave", "cooldown": 15, "dir": "pull", "radius": 320, "power": 300, "color": "#e04dff" } ] }, "_zenith_note": "FRAMES 50–57 = THE ZENITH — the third FACTION roster (the ?combat=zenith duel arena). TWO ARCHETYPES, split 4/4: FRAMES 50–53 = THE COMBO SEEKERS — a fast low-damage WEAPON (the gun) paired with the flank_barrage ABILITY (js/combat/EnemyAbilities.js): a salvo of HEAT-SEEKERS out of BOTH WINGS at 90° off the heading, on a SLOW capped turn-rate (missileTurn 0.7–1.3, far below the nose seekers' 2.2) so they curve wide and a committed turn beats them. FRAMES 54–57 = THE RANGED ELEMENTALS — the shock_dart ABILITY: fast darts that detonate near the target and bloom an ELECTRIFIED ZONE (startRadius→maxRadius over growMs, finite lifeMs, fadeMs), ticking damage (tickDamage/tickMs) on anything inside — a growing, dodgeable field, not a one-shot. BALANCE (the same rule as the $factions_note above, vs the PIRATE raider — 60 armor, 8 bolt damage): 2× DAMAGE (16 every bolt/seeker/shell; the shock zones tick 3–5 per beat — a sustained, avoidable pressure, never a spike) and 3.5× ARMOR (175 hull + 35 shields = 210). xp = (hull+shields)/5 = 42. Differentiation comes from PACE AND SHAPE: the combo ships vary the gun (rapid / burst / spray / sweep) and the volley's size and seek-rate; the elemental ships vary the dart count, the zone's size, its bloom time and its tick rate.", "zenith_wingblade": { "$comment": "ZENITH 1 (frame 50) — WINGBLADE, the gun-wing: a RAPID gun (0.55 s) for constant low-damage pressure, and on its own clock a SIX-SEEKER wing volley (three out of each wing at 90°, a slight fan) that curves wide toward the player — the archetype's base kit at balanced tempo.", "name": "Wingblade", "frame": 50, "speed": 165, "turn": 3.4, "hull": 175, "shields": 35, "xp": 42, "ramDamage": 13, "ramDamageSelf": 29, "weapon": { "pattern": "rapid", "range": 500, "cooldown": 0.55, "bulletSpeed": 400, "damage": 16, "spread": 0.05, "bulletLife": 1.6, "color": "#ffd24d" }, "abilities": [ { "id": "flank_barrage", "cooldown": 13, "count": 6, "missileSpeed": 230, "missileTurn": 0.9, "missileDamage": 16, "missileLife": 5.5, "missileSpread": 0.28, "color": "#ffd24d" } ] }, "zenith_divefang": { "$comment": "ZENITH 2 (frame 51) — DIVEFANG, the slow arc: a TIGHT burst stinger (three staggered bolts) for the snap at point-blank, and the roster's WIDEST seek — eight seekers on the slowest turn-rate (0.7) that peel around the arena's edge, giving the longest wind-up and the hardest to shed late.", "name": "Divefang", "frame": 51, "speed": 150, "turn": 3.0, "hull": 175, "shields": 35, "xp": 42, "ramDamage": 13, "ramDamageSelf": 29, "weapon": { "pattern": "burst", "range": 480, "cooldown": 1.15, "bulletSpeed": 380, "damage": 16, "spread": 0.01, "bulletLife": 1.8, "bolts": 3, "burstFan": 0.1, "color": "#ffb84d" }, "abilities": [ { "id": "flank_barrage", "cooldown": 15, "count": 8, "missileSpeed": 200, "missileTurn": 0.7, "missileDamage": 16, "missileLife": 6.5, "missileSpread": 0.3, "color": "#ffb84d" } ] }, "zenith_kestrel": { "$comment": "ZENITH 3 (frame 52) — KESTREL, the heavy wing: a SPRAY fan of five across the aim, and FOUR heavy seekers — the fewest but the biggest (26 px, fastest at 260 px/s) on a quicker seek (1.1): fewer, meaner, tighter.", "name": "Kestrel", "frame": 52, "speed": 175, "turn": 3.6, "hull": 175, "shields": 35, "xp": 42, "ramDamage": 13, "ramDamageSelf": 29, "weapon": { "pattern": "spray", "range": 520, "cooldown": 1.3, "bulletSpeed": 340, "damage": 16, "spread": 0.02, "bulletLife": 2.0, "bolts": 5, "fan": 0.7, "color": "#ffe08a" }, "abilities": [ { "id": "flank_barrage", "cooldown": 12, "count": 4, "missileSpeed": 260, "missileTurn": 1.1, "missileDamage": 16, "missileLife": 4.5, "missileSize": 26, "missileSpread": 0.22, "color": "#ffe08a" } ] }, "zenith_reefrunner": { "$comment": "ZENITH 4 (frame 53) — REEFRUNNER, the pack's speed: a FORWARD sweep (seven-bolt cone out the nose — the craft commits to its heading while it fires), and a DOUBLE WING VOLLEY (ten seekers, quick seek at 1.3) on the shortest volley clock of the combo wing: the most shots in the air at once.", "name": "Reefrunner", "frame": 53, "speed": 185, "turn": 3.8, "hull": 175, "shields": 35, "xp": 42, "ramDamage": 13, "ramDamageSelf": 29, "weapon": { "pattern": "sweep", "range": 500, "cooldown": 1.2, "bulletSpeed": 360, "damage": 16, "spread": 0.02, "bulletLife": 1.9, "bolts": 7, "fan": 1.0, "color": "#ffd84d" }, "abilities": [ { "id": "flank_barrage", "cooldown": 11, "count": 10, "missileSpeed": 260, "missileTurn": 1.3, "missileDamage": 16, "missileLife": 4.5, "missileSpread": 0.34, "color": "#ffd84d" } ] }, "zenith_voltspire": { "$comment": "ZENITH 5 (frame 54) — VOLTSPIRE, the elemental base kit: a clean LANCE between volleys, and one VOLT DART per cycle that detonates near the player and blooms a field (40→150 px over 1.8 s, live 3.2 s) ticking 4 per 0.4 s inside — the archetype's signature at balanced tempo.", "name": "Voltspire", "frame": 54, "speed": 150, "turn": 3.0, "hull": 175, "shields": 35, "xp": 42, "ramDamage": 13, "ramDamageSelf": 29, "weapon": { "pattern": "lance", "range": 560, "cooldown": 1.8, "bulletSpeed": 380, "damage": 16, "spread": 0.03, "bulletLife": 1.9, "color": "#7dfcff" }, "abilities": [ { "id": "shock_dart", "cooldown": 11, "count": 1, "dartSpeed": 520, "proximity": 90, "dartLifeMs": 1400, "startRadius": 40, "maxRadius": 150, "growMs": 1800, "zoneLifeMs": 3200, "zoneFadeMs": 700, "tickMs": 400, "tickDamage": 4, "color": "#7dfcff" } ] }, "zenith_stormweft": { "$comment": "ZENITH 6 (frame 55) — STORMWEFT, the double line: RAPID gun cover, and TWO darts in a loose fan — the roster's bigger, slower-blooming field (45→180 px over 2 s) on a longer cycle: two overlapping discs that fence the player's escape.", "name": "Stormweft", "frame": 55, "speed": 140, "turn": 2.8, "hull": 175, "shields": 35, "xp": 42, "ramDamage": 13, "ramDamageSelf": 29, "weapon": { "pattern": "rapid", "range": 500, "cooldown": 0.7, "bulletSpeed": 380, "damage": 16, "spread": 0.05, "bulletLife": 1.6, "color": "#4dd7ff" }, "abilities": [ { "id": "shock_dart", "cooldown": 13, "count": 2, "dartSpread": 0.5, "dartSpeed": 500, "proximity": 95, "dartLifeMs": 1500, "startRadius": 45, "maxRadius": 180, "growMs": 2000, "zoneLifeMs": 3600, "zoneFadeMs": 800, "tickMs": 400, "tickDamage": 4, "color": "#4dd7ff" } ] }, "zenith_arcpulse": { "$comment": "ZENITH 7 (frame 56) — ARCPULSE, the strobe: a RADIAL eight-bolt net, and the fastest elemental cycle (9 s) with THREE quick small darts — little fields (30→110 px, blooming in 1.4 s) but the most detonations in the air, a flickering minefield at point-blank range.", "name": "Arcpulse", "frame": 56, "speed": 165, "turn": 3.4, "hull": 175, "shields": 35, "xp": 42, "ramDamage": 13, "ramDamageSelf": 29, "weapon": { "pattern": "ring", "range": 460, "cooldown": 2.2, "bulletSpeed": 260, "damage": 16, "spread": 0.0, "bulletLife": 2.2, "bolts": 8, "color": "#5ee6ff" }, "abilities": [ { "id": "shock_dart", "cooldown": 9, "count": 3, "dartSpread": 0.8, "dartSpeed": 560, "proximity": 80, "dartLifeMs": 1200, "startRadius": 30, "maxRadius": 110, "growMs": 1400, "zoneLifeMs": 2600, "zoneFadeMs": 600, "tickMs": 350, "tickDamage": 3, "color": "#5ee6ff" } ] }, "zenith_tempest": { "$comment": "ZENITH 8 (frame 57) — TEMPEST, the faction's apex (the elite): a heavy SHELL between the slowest cycles, and the roster's biggest field — one giant dart (50→230 px over 2.4 s, live 4.2 s, the hardest tick at 5/beat): a slow, enormous electrified storm that outlasts every escape and punishes every hold.", "name": "Tempest", "frame": 57, "speed": 110, "turn": 2.2, "hull": 175, "shields": 35, "xp": 42, "ramDamage": 13, "ramDamageSelf": 29, "weapon": { "pattern": "shell", "range": 600, "cooldown": 2.8, "damage": 16, "shells": 1, "shellDamage": 16, "shellSize": 44, "shellSpeed": 150, "shellLife": 2.5, "color": "#35c8ff" }, "abilities": [ { "id": "shock_dart", "cooldown": 15, "count": 1, "dartSpeed": 480, "proximity": 110, "dartLifeMs": 1700, "startRadius": 50, "maxRadius": 230, "growMs": 2400, "zoneLifeMs": 4200, "zoneFadeMs": 900, "tickMs": 450, "tickDamage": 5, "color": "#35c8ff" } ] } }, "bulletHoming": { "_comment": "THE MINDWEAVE'S WILL (js/combat/CombatSystem.js → stepHomingBullets): bolts and shells fired by ships of these factions bend toward the NEAREST LIVE HOSTILE within range for the first durationMs of flight — the weaver's threads find their mark without negating a committed dodge (a capped turn-rate: subtle, yet effective). Hostiles = the rival faction's wing (never the shooter's own) and, for bolts hostile to the player, the player itself. War-zone factions stay neutral-to-the-player, so in an observed duel the will only curves fire at the rival wing.", "factions": ["mindweave"], "turnRate": 0.3, "durationMs": 1000, "range": 720 }, "factions": { "_comment": "The FACTION DUEL ARENA (the ?combat=apex / ?combat=mindweave boots — js/scenes/GameScene.js → _combatBootMode, js/combat/DuelPicker.js the in-game picker, CombatSystem.startDuel/spawnDuel/spawnDuelPack the fight's seam). Each faction names its fighters (enemyTypes ids, display order = the picker's rows 1–8); 'style' is the picker's one-line read. Key '0' (or the row) sends the WHOLE pack. These rosters are NOT wired into any quest/galaxy content — the arena is their only home for now.", "apex": { "name": "Apex", "tagline": "apex predators — cloaked ambushes, flanking strikes, devastating forward firepower", "types": [ { "id": "apex_raptor", "name": "Raptor", "style": "forward fan sweep + the flank's cut" }, { "id": "apex_mauler", "name": "Mauler", "style": "one heavy bolt out of cloak" }, { "id": "apex_stalker", "name": "Stalker", "style": "strike, vanish, strike again (double cloak)" }, { "id": "apex_screamer", "name": "Screamer", "style": "a nine-bolt wall across your nose" }, { "id": "apex_reaver", "name": "Reaver", "style": "the unseen gun — rapid + double cloak" }, { "id": "apex_fang", "name": "Fang", "style": "a tight stinger from behind" }, { "id": "apex_tyrant", "name": "Tyrant", "style": "twin seekers out of the dark" }, { "id": "apex_alpha", "name": "Alpha", "style": "the pack's apex — the full predatory kit" } ] }, "mindweave": { "name": "Mindweave", "tagline": "psychic weavers — blinks, phantoms, telekinetic tides, spiralling barrages", "types": [ { "id": "weave_mindblade", "name": "Mindblade", "style": "a ten-bolt spiral that blooms in flight" }, { "id": "weave_telekine", "name": "Telekine", "style": "rapid fire + a tide that shoves you" }, { "id": "weave_phantasm", "name": "Phantasm", "style": "a court of phantoms + the blink" }, { "id": "weave_weaver", "name": "Weaver", "style": "threads out its heading, blinks fastest" }, { "id": "weave_lure", "name": "Lure", "style": "a radial net + a tide that drags you in" }, { "id": "weave_dreamer", "name": "Dreamshell", "style": "large slow shells + the hardest shove" }, { "id": "weave_serpent", "name": "Serpentmind", "style": "a long hanging coil + the reeling pull" }, { "id": "weave_overmind", "name": "Overmind", "style": "triple shells + the full psychic kit" } ] }, "zenith": { "name": "Zenith", "tagline": "summit strike craft — wing volleys of slow seekers, arcs that bloom into electrified fields", "types": [ { "id": "zenith_wingblade", "name": "Wingblade", "style": "rapid gun + a six-seeker wing volley" }, { "id": "zenith_divefang", "name": "Divefang", "style": "a tight stinger + the slowest, widest seek" }, { "id": "zenith_kestrel", "name": "Kestrel", "style": "a five-bolt spray + four heavy seekers" }, { "id": "zenith_reefrunner", "name": "Reefrunner", "style": "a nose sweep + a double wing volley" }, { "id": "zenith_voltspire", "name": "Voltspire", "style": "a lance + one blooming electrified field" }, { "id": "zenith_stormweft", "name": "Stormweft", "style": "rapid fire + two overlapping fields" }, { "id": "zenith_arcpulse", "name": "Arcpulse", "style": "a radial net + a flickering minefield" }, { "id": "zenith_tempest", "name": "Tempest", "style": "a heavy shell + the roster's giant storm" } ] } }, "neutral": { "_comment": "The base NEUTRAL CRAFT — the friendly player-side ships (data/combat.json → neutralTypes deep-merges its overrides onto this base, exactly like enemyTypes onto enemy). They ride the SAME enemy spritesheet (assets/images/ships-enemy.png) on frames 20–27, one distinct craft per frame. side 'player' is the seam: they NEVER target or damage the player, the player's auto-fire never targets them, and CombatSystem flies them in its OWN allies roster (not enemies — js/combat/CombatSystem.js). Their AI (js/combat/NeutralShip.js): escort the player for a per-ship window (escortMin–escortMax seconds — the spawner rolls one per ship), then break off and fly off-screen; while any pirate is up they instead fight the NEAREST one with their own weapon pattern (the same Weapons.js dispatch the hostiles run). The stats ramp: hull/shields climb monotonically frame 20 → 27 (xp = (hull+shields)/5 — the same rule of thumb as the pirate roster). respawn false: encounter ships are spent, never reformed; ramDamage 0: a friendly ship's ram can't hurt the player.", "texture": "assets/images/ships-enemy.png", "frameWidth": 128, "frameHeight": 128, "frame": 20, "artFacing": "north", "side": "player", "size": 54, "hitRadius": 20, "speed": 175, "turn": 3.2, "orbitRadius": 260, "orbitSpeed": 0.6, "escortRadius": 110, "escortMin": 10, "escortMax": 15, "hull": 60, "shields": 0, "xp": 12, "weapon": { "pattern": "lance", "range": 500, "cooldown": 1.6, "bulletSpeed": 340, "damage": 9, "spread": 0.05, "bulletLife": 2.2, "color": "#37e0a1", "size": 16, "boltLength": 48, "boltWidth": 10, "muzzle": 44 }, "ramDamage": 0, "ramDamageSelf": 0, "ramGrace": 0.8, "respawn": false }, "neutralTypes": { "_comment": "The NEUTRAL roster (frames 20–27 of assets/images/ships-enemy.png) — the random-encounter director (GameScene → CombatSystem.spawnAlly) deploys them, js/combat/NeutralShip.js flies them. Each entry declares ONLY what differs from the neutral base above (the merge is EnemyTypes.neutralTypeConfig — the same deep-merge contract as the enemyTypes): a DISTINCT weapon pattern (the Weapons.js set — lance/spray/burst/rapid/ring/missile/combo — plus 'volley', the eighth pattern) and the STATS RAMP (hull/shields climb monotonically frame 20 → 27; xp = (hull+shields)/5). side stays 'player' (inherited — never hostile).", "courier": { "$comment": "frame 20 — the LANCE: the scout. Fast and light, one aimed bolt; the easy end of the ramp.", "frame": 20, "speed": 195, "turn": 3.4, "hull": 60, "shields": 0, "xp": 12, "weapon": { "pattern": "lance", "range": 500, "cooldown": 1.6, "bulletSpeed": 340, "damage": 9, "spread": 0.05, "bulletLife": 2.2, "color": "#37e0a1" } }, "runner": { "$comment": "frame 21 — the RAPID: the fast gun. Short cooldown, honest per-bolt punch.", "frame": 21, "speed": 200, "turn": 4.0, "hull": 70, "shields": 10, "xp": 16, "weapon": { "pattern": "rapid", "range": 440, "cooldown": 0.5, "bulletSpeed": 380, "damage": 8, "spread": 0.06, "bulletLife": 1.6, "color": "#7df3c1" } }, "sloop": { "$comment": "frame 22 — the SPRAY: a fan of cheap bolts covering an arc.", "frame": 22, "speed": 150, "turn": 2.6, "hull": 80, "shields": 15, "xp": 19, "weapon": { "pattern": "spray", "range": 460, "cooldown": 2.0, "bulletSpeed": 300, "damage": 8, "spread": 0.03, "bulletLife": 2.2, "bolts": 5, "fan": 1.1, "color": "#37e0a1" } }, "sally": { "$comment": "frame 23 — the BURST: a fast, tight 3-shot stinger at close range.", "frame": 23, "speed": 180, "turn": 3.6, "hull": 90, "shields": 20, "xp": 22, "weapon": { "pattern": "burst", "range": 380, "cooldown": 1.5, "bulletSpeed": 420, "damage": 9, "spread": 0.02, "bulletLife": 1.8, "bolts": 3, "burstFan": 0.1, "color": "#aef27d" } }, "sentinel": { "$comment": "frame 24 — the RING: a radial volley; slow, armored, pokes a hole in the line.", "frame": 24, "speed": 130, "turn": 2.4, "hull": 100, "shields": 30, "xp": 26, "weapon": { "pattern": "ring", "range": 360, "cooldown": 2.2, "bulletSpeed": 260, "damage": 10, "spread": 0.0, "bulletLife": 2.6, "bolts": 8, "color": "#4dd7ff" } }, "herald": { "$comment": "frame 25 — the MISSILE: one heat-seeking shot per volley (dodgeable at the capped turn).", "frame": 25, "speed": 160, "turn": 3.0, "hull": 110, "shields": 35, "xp": 29, "weapon": { "pattern": "missile", "range": 620, "cooldown": 2.4, "missiles": 1, "missileSpeed": 190, "missileTurn": 2.2, "missileDamage": 13, "missileLife": 4.0, "missileColor": "#37e0a1", "missileSize": 16, "color": "#37e0a1" } }, "vanguard": { "$comment": "frame 26 — the COMBO: a heavy bolt PLUS a heat-seeker on every volley.", "frame": 26, "speed": 110, "turn": 2.0, "hull": 130, "shields": 45, "xp": 35, "weapon": { "pattern": "combo", "range": 700, "cooldown": 2.6, "bulletSpeed": 240, "damage": 16, "spread": 0.02, "bulletLife": 3.2, "size": 18, "boltLength": 56, "boltWidth": 12, "missiles": 1, "missileSpeed": 170, "missileTurn": 2.4, "missileDamage": 18, "missileLife": 4.2, "missileColor": "#7df3c1", "missileSize": 18, "color": "#7df3c1" } }, "bulwark": { "$comment": "frame 27 — the VOLLEY: the ramp's top — a WIDE fan whose speeds are staggered, so the arc blossoms into a wall over distance (Weapons.js 'volley': the burst's stagger across the spray's fan).", "frame": 27, "speed": 95, "turn": 1.8, "hull": 150, "shields": 60, "xp": 42, "weapon": { "pattern": "volley", "range": 520, "cooldown": 2.4, "bulletSpeed": 280, "damage": 11, "spread": 0.02, "bulletLife": 2.8, "bolts": 7, "fan": 1.5, "stagger": 0.35, "color": "#c8f54d" } } }, "transport": { "_comment": "The ESCORT CONTRACT's transport (sidequests.escort — data/sidequests.json): the neutral FREIGHTER the player rendevouses with at a local waypoint and leads through the jump gates to a planet / space station 1–2 hops away. A PERSISTENT player-side craft (side 'player' — it never targets or damages the player; CombatSystem's active/neutralOnly checks skip it via its `persistent` flag, so a standing convoy never holds the jump gate / the landing door). js/combat/TransportShip.js flies it (the NeutralShip escort/engage AI with the break-off window lapsed — it stays for the life of the contract). ART: assets/images/ships-transport.png, frames 0–3 at 180×320 (the art faces north, filling its frame — the long side is the ship's length). SIZE 135 reads as the hull's world LENGTH: ~2.5× a fighter's 54 (TransportShip scales by the sheet's long side, not the frameWidth the square enemy sheet assumes). ARMOR far above the escort roster (hull 1500 / shields 650 vs the roster's 60–150/0–60) — it soaks fire off the player; WEAPON a wide 'volley' fan (the roster's top pattern) whose per-bolt punch is honest (the transport is a bodyguard, not a gunboat). respawns: false — the convoy is spent when it's lost (the contract's reset, not a re-form).", "texture": "assets/images/ships-transport.png", "frameWidth": 180, "frameHeight": 320, "frame": 0, "artFacing": "north", "side": "player", "size": 135, "hitRadius": 46, "speed": 245, "turn": 2.6, "orbitRadius": 320, "orbitSpeed": 0.45, "escortRadius": 175, "hull": 1500, "shields": 650, "xp": 0, "weapon": { "pattern": "volley", "range": 660, "cooldown": 2.3, "bulletSpeed": 300, "damage": 14, "spread": 0.02, "bulletLife": 3.0, "bolts": 7, "fan": 1.5, "stagger": 0.3, "size": 22, "boltLength": 64, "boltWidth": 14, "color": "#7ce8a4" }, "ramDamage": 0, "ramDamageSelf": 0, "ramGrace": 0.8, "respawn": false }, "encounters": { "_comment": "PERIODIC RANDOM ENCOUNTERS (js/combat/Encounters.js → EncounterDirector; GameScene spawns on a hit) — NEUTRAL non-home space: pirates / neutrals / mixed; FACTION space (the war-zones — `encounters.faction` below): the local faction and one opposing faction, disposition off the player's standing with each (see that section's comment). The director rolls on the interval schedule (rollIntervalMin–rollIntervalMax s — `faction.rollIntervalMin..Max` in faction space; each attempt fires at chanceMin–chanceMax probability), and after an encounter ENDS it stays quiet for cooldownMin–cooldownMax s (or `faction.cooldownMin..Max` in faction space) before the next roll. KIND rolls at `weights` (shipped even — ~1/3 each; bypassed in faction space). SIZE: the player's combat level (js/combat/XpManager — a fresh run starts at 0) picks a band; mixed keeps both kinds present. Encounter hostiles are one-shots (no respawn); escorts carry the neutral.escortMin/escortMax window and break off on their own (js/combat/NeutralShip.js). Seconds + 0–1 probabilities — no code constants.", "enabled": true, "rollIntervalMin": 45, "rollIntervalMax": 90, "chanceMin": 0.5, "chanceMax": 0.6, "cooldownMin": 30, "cooldownMax": 60, "weights": { "pirates": 1, "neutrals": 1, "mixed": 1 }, "bands": [ { "minLevel": 0, "maxLevel": 2, "pirates": [ 1, 2 ], "neutrals": [ 1, 2 ], "mixedPirates": [ 1, 1 ], "mixedNeutrals": [ 1, 1 ] }, { "minLevel": 3, "maxLevel": 5, "pirates": [ 2, 3 ], "neutrals": [ 2, 3 ], "mixedPirates": [ 1, 2 ], "mixedNeutrals": [ 1, 2 ] }, { "minLevel": 6, "maxLevel": 8, "pirates": [ 5, 7 ], "neutrals": [ 5, 7 ], "mixedPirates": [ 3, 4 ], "mixedNeutrals": [ 2, 3 ], "roster": { "raider": 5, "spitter": 3, "bomber": 2 } }, { "minLevel": 9, "maxLevel": 12, "pirates": [ 7, 9 ], "neutrals": [ 7, 9 ], "mixedPirates": [ 4, 5 ], "mixedNeutrals": [ 3, 4 ], "roster": { "raider": 4, "spitter": 2, "stinger": 2, "gunner": 2, "bomber": 1 } }, { "minLevel": 13, "maxLevel": 99, "pirates": [ 9, 12 ], "neutrals": [ 9, 12 ], "mixedPirates": [ 4, 5 ], "mixedNeutrals": [ 4, 5 ], "roster": { "raider": 3, "spitter": 2, "stinger": 2, "gunner": 2, "hunter": 2, "barrage": 2 } } ], "_tiers": "THE LEVEL-6 JUMP (design): 1–2 → 2–3 → 5–7 (a ~2× step in the pack) → 7–9 → 9–12 (the cap — past level 13 the count holds and the roster's heavier types are the pressure). roster = the band's weighted pirate MIX (js/combat/Encounters.js → compose draws it into `pirateTypes`; a band without one leaves the scene's random pick standing — tiers 0–5 predate the rosters): raiders stay the anchor (the filler), spitter/bomber land at the jump, stinger/gunner at 9+, hunter (missiles) and barrage (rings) at 13+ — the heavy hitters, never the early pack.", "faction": { "_comment": "FACTION SPACE (the war-zones, js/combat/Encounters.js's compose context + GameScene._spawnFactionEncounter): in a faction-controlled system the ambient encounter is the LOCAL faction and one OPPOSING faction (js/combat/Encounters.js picks one of the other roster factions seeded; BOTH groups are sized off the `bands`' pirate counts, each drawn from its OWN combat.factions roster — no pirates/neutrals). DISPOSITION is per-faction, off the player's standing WITH each (js/reputation/Reputation.js → factionStanding, data/reputation.json): standing below `hostileBelow` → that faction targets the player and the player may return fire (the guns/skills' pools include it); at/above `hostileBelow` → it fights only the other faction, never the player (its bolts/seekers/rams can't touch the ship), and once its rival is spent it peels off (CombatSystem._peelOff) so the encounter ends on its own — the player observes. Factions ALWAYS fight each other while they're both up (CombatSystem.pickEnemyTarget). TIMERS: faction space rolls more often than neutral wild space (rollIntervalMin..Max, the director's roll schedule) with a longer quiet after each encounter (cooldownMin..Max, 40–60 s). The ships are the factions' OWN roster crafts (no new art).", "rollIntervalMin": 20, "rollIntervalMax": 40, "cooldownMin": 40, "cooldownMax": 60, "hostileBelow": -5 } }, "waves": { "_comment": "WAVE SCRIPTS — the quest-step combat (js/combat/Waves.js + CombatSystem.startWaves; the quest hook that FIRES one lands later, random encounters too — for now the console drives it: window.__ORBIT_COMBAT__.waves). A script is an ordered list of waves. KINDS: 'rolling' = maxConcurrent on screen at a time, each kill sending in replacePerKill more until the force is spent; 'fixed' = the whole force at once, no replacements until exhausted. COMPOSITION: `roster` = enemyTypes id → weight (the mix); `total` = the units deployed (default = the weight sum); `type` + `count` is the single-type shorthand. lead = s before wave 1; gap = s between waves (per-wave `gap` s overrides). The quests reference scripts BY NAME (e.g. {\"combat\": \"raid\"}) when the hook lands.", "lead": 1.5, "gap": 2.0, "defaults": { "maxConcurrent": 3, "replacePerKill": 1 }, "scripts": { "probe": [ { "kind": "fixed", "type": "raider", "count": 3 } ], "skirmish": [ { "kind": "rolling", "roster": { "raider": 2 }, "total": 8, "maxConcurrent": 3, "replacePerKill": 1 }, { "kind": "fixed", "roster": { "raider": 3, "bomber": 1 }, "total": 4 } ], "raid": [ { "kind": "fixed", "roster": { "raider": 4, "bomber": 2 }, "total": 6 }, { "kind": "rolling", "roster": { "raider": 3, "bomber": 1 }, "total": 12, "maxConcurrent": 4, "replacePerKill": 1 } ], "pirate_ambush": [ { "$comment": "'Pirate Problems' (data/quests.json → pirate_problems) — the quest's combat step ('Defend your ship from pirates'): ONE rolling wave of SIX frame-0 raiders (EnemyTypes: raider = the sheet's frame 0). Fired when the player ENTERS the Neutral Capital's system with the quest live (GameScene._maybeStartPirateAmbush — create() and the grant's start beats); the wave ships materialize OFF-SCREEN around the player, each from a random direction (CombatSystem._spawnWaveEnemy — the same off-screen materialization as the ?combat pack), streaming in on their own chase AI; three at a time (maxConcurrent), each kill sending in a replacement (replacePerKill) until all six are down; the completion marks the 'combatDone' ledger (GameScene._recordCombatDone — the step's monotonic witness, SaveData-captured).", "kind": "rolling", "type": "raider", "count": 6, "maxConcurrent": 3, "replacePerKill": 1 } ], "combat_demo": [ { "$comment": "?COMBAT DEMO (the tuning sandbox's boot — GameScene._wantsCombatDemo): a SEQUENCE of rolling waves, ONE per new hostile type in DIFFICULTY ORDER (bomber → warden, frames 1–7). Each wave is SIX of that single type, THREE on screen at a time (maxConcurrent), each kill sending in a replacement (replacePerKill) until the six are down — then the NEXT type's wave. This is the showcase for the seven new weapons (js/combat/Weapons.js).", "kind": "rolling", "type": "bomber", "count": 6, "maxConcurrent": 3, "replacePerKill": 1 }, { "kind": "rolling", "type": "spitter", "count": 6, "maxConcurrent": 3, "replacePerKill": 1 }, { "kind": "rolling", "type": "stinger", "count": 6, "maxConcurrent": 3, "replacePerKill": 1 }, { "kind": "rolling", "type": "hunter", "count": 6, "maxConcurrent": 3, "replacePerKill": 1 }, { "kind": "rolling", "type": "gunner", "count": 6, "maxConcurrent": 3, "replacePerKill": 1 }, { "kind": "rolling", "type": "barrage", "count": 6, "maxConcurrent": 3, "replacePerKill": 1 }, { "kind": "rolling", "type": "warden", "count": 6, "maxConcurrent": 3, "replacePerKill": 1 } ] } }, "playerShip": { "_comment": "Your armor (both contexts — the real ship's stats.hullIntegrity/shields replace this when combat ships for real). shields absorb before the hull; after shieldRegenDelay s without a hit, shields regrow at shieldRegenRate/s up to their max. respawn = when your hull breaks you come back (respawnDelay s) instead of the run ending. The MAIN GAME now runs the real death flow (js/scenes/GameScene.js → _playerWentDown: the Game Over beat ends the run — it forces playerRespawn off, overridable only by a skill), so here it only keeps the ?combat range's sandbox convenience.", "hull": 100, "shields": 40, "shieldRegenDelay": 4, "shieldRegenRate": 14, "respawn": true, "respawnDelay": 2.0 }, "fx": { "_comment": "The LOOK of combat (both contexts — js/combat/Fx.js + js/combat/CamShake.js). shake = the camera kick on the WORLD pass (the separate UI pass keeps the bar + feed still): each value is Phaser's shake intensity (a FRACTION of the viewport in px) and ms = the duration. hit = you take a hit, kill = an enemy goes down, break = the shield-collapse beat, ram = the Ramming Charge's impact (the heaviest beat). shield = the shield-sphere impact read: radius px = the sphere's world radius around the ship (the impact point sits this far off the ship), arcSpan rad = the bright arc's angular width (the highlighted hit region), ripple px = the shockwave that grows from the impact point, decayMs = how long the sphere flash lingers.", "shake": { "hit": 0.0045, "kill": 0.007, "break": 0.011, "ram": 0.014, "ms": 230 }, "shield": { "radius": 40, "arcSpan": 1.9, "ripple": 90, "decayMs": 520 } }, "arena": { "_comment": "The range walls. inset = px between the screen edge and the bounce wall; bounce = the fraction of speed a ship keeps off a wall (1 = full mirror bounce). wallColor = the rim line, wallGlow = its neon edge.", "inset": 26, "bounce": 0.9, "wallColor": "#1b2c4a", "wallGlow": "#00e5ff" }, "sandbox": { "_comment": "Sandbox conveniences — none of this ships with the real game. resetKey rebuilds the whole range (a fresh scene); pauseKey freezes the sim (handy for eyeballing positions via window.__COMBAT__ in the console); respawn = the player comes back (respawnDelay s) instead of dying permanently.", "resetKey": "R", "pauseKey": "P", "respawn": true, "respawnDelay": 1.6 }, "readability": { "_comment": "The READABILITY LAYER (js/combat/CombatHud.js) — the 'what do I need to see' side of combat, shared by both contexts. reticle = the target/LOCK indicator: rotating corner brackets on the current target (the one your guns are on — the range's nearest hostile, the main game's auto-fire target; the shared rule is Combat.js → pickTarget). margin = px outside the target's hitRadius, spin = rad/s bracket rotation, color = the lock's amber. threat = the hostile cue: a small pulsing marker above EVERY enemy (size px, pulse 1/s, above = px over the ship's center, color = hot red). offscreen = the edge ARROWS for hostiles outside the camera's view (main game — the range's arena is one screen, so they rarely fire there): pad = px inside the screen edge, size = arrow px, max = the most arrows drawn (nearest hostiles win), color. feed = the KILL FEED, top-right (entries = max lines, life ms per entry, gap px between lines, size px font, xInset = px in from the screen's RIGHT edge, y0 = px DOWN from the top (the first line), ink + hit colors). barFlash = the combat bar's GAUGE FLASH when YOU take a hit: life ms, alpha = the white overlay's start (the shield gauge flashes on shield hits, the hull gauge on hull hits — both when the shields pop through).", "reticle": { "enabled": true, "margin": 16, "spin": 1.6, "color": "#ffd84d" }, "threat": { "enabled": false, "size": 8, "pulse": 2.2, "above": 36, "color": "#ff2d6f" }, "offscreen": { "enabled": true, "pad": 28, "size": 13, "max": 4, "color": "#ff2d6f" }, "feed": { "enabled": true, "entries": 5, "life": 3000, "gap": 19, "size": 13, "xInset": 18, "y0": 64, "ink": "#cfe9ff", "kill": "#ffd84d" }, "barFlash": { "life": 340, "alpha": 0.85 } }, "hud": { "_comment": "The range console chrome (colors fall back to data/theme.json). title = the corner label; hints = the control line (top-left — the bottom-center is the combat bar's now).", "title": "COMBAT // SANDBOX", "hints": "WASD / arrows — fly · space — fire · R — reset range · P — pause" }, "bar": { "_comment": "The COMBAT BAR (js/ui/CombatBar.js) — shown in place of the action deck while combat is live (the deck hides, the bar takes its place; when the pack is wiped it's the other way around). geometry = a BOTTOM-ALIGNED, CENTERED strip sized to its contents (assets/bar.png is the storyboard — not a color ref). Left end = the hull ORB (red) with the SHIELD orb (cyan, semi-transparent) overlapping it — shields in front of hull; middle = slot.count skill seats wearing keys 1-8 (the skills land later — for now they are empty seats); right end = the ENERGY orb (yellow, the skills' resource). The orbs fill like a Diablo orb: the liquid drains as the value drops. circle.radius = the hull/energy orb radius; circle.shield.scale = the shield orb's radius as a fraction of the hull radius — the shield orb shares the hull's CENTER (offsetX/offsetY default 0) and its liquid is semi-transparent over the hull's; orbGap = orb-to-slot spacing; margin.edge = the strip's overhang past the orbs. Colors fall back to data/theme.json where sensible.", "strip": { "height": 64 }, "margin": { "bottom": 26, "edge": 10 }, "orbGap": 24, "circle": { "radius": 46, "hull": { "fillAlpha": 0.95 }, "shield": { "scale": 1.12, "offsetX": 0, "offsetY": 0, "fillAlpha": 0.55 } }, "colors": { "bg": "#070d18", "bgAlpha": 0.92, "edge": "#22405f", "shield": "#00e5ff", "hull": "#ff2d6f", "energy": "#ffd84d", "slotBg": "#0b1322", "slotBorder": "#22405f", "key": "#5c7196" }, "slot": { "count": 8, "size": 44, "gap": 10, "keySize": 12 } } }