orbit/data/combat.json

553 lines
31 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"_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.10,
"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"
}
}
},
"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.10, "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) — every NEUTRAL non-home system, while flying, no combat live, no wave run in flight, one at a time. The director rolls on the interval schedule (rollIntervalMin–rollIntervalMax s; each attempt fires at chanceMin–chanceMax probability), and after an encounter ENDS it stays quiet for cooldownMin–cooldownMax s before the next roll. KIND rolls at `weights` (shipped even — ~1/3 each). 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": 99, "pirates": [4, 5], "neutrals": [4, 5], "mixedPirates": [2, 3], "mixedNeutrals": [1, 2] }
]
},
"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 — a stand-in until the real death flow lands.",
"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 }
}
}