{ "_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).", "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 gauge). The eight skills (keys 1-8, the bar's middle seats) will spend it; for now it just sits full. regen = energy/s recovered (0 = static).", "max": 100, "regen": 0 }, "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.", "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 } }, "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). weapon.* = their lance: range px, cooldown s, bulletSpeed px/s, damage, spread rad, bulletLife s. 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, "weapon": { "range": 560, "cooldown": 1.4, "bulletSpeed": 330, "damage": 8, "spread": 0.08, "bulletLife": 2.4, "color": "#ff2d6f", "size": 18 }, "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 (waves write it explicitly); `bomber` rides the same sheet on frame 1 — a slower, tougher, longer-reach striker. 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.", "raider": { "frame": 0 }, "bomber": { "frame": 1, "speed": 110, "turn": 2.2, "hull": 90, "shields": 20, "weapon": { "range": 720, "cooldown": 1.8, "bulletSpeed": 260, "damage": 12 } } }, "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 } ] } }, "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 }, "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": true, "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 at the bottom.", "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 = the bar's screen rect (margin/height mirror actionbar's, so it sits in the deck's place). Left = the vertical SHIELDS + HULL gauges; right = the vertical ENERGY gauge; middle = slot.count skill seats wearing keys 1-8 (the skills land later — for now they are empty seats). Colors fall back to data/theme.json where sensible.", "height": 92, "margin": { "left": 20, "right": 20, "bottom": 12 }, "colors": { "bg": "#070d18", "bgAlpha": 0.92, "edge": "#22405f", "shield": "#00e5ff", "hull": "#7dff8a", "energy": "#ffd84d", "slotBg": "#0b1322", "slotBorder": "#22405f", "key": "#5c7196" }, "gauge": { "width": 14, "gap": 8, "pad": 12, "labelSize": 11 }, "slot": { "count": 8, "size": 64, "gap": 10, "keySize": 12 } } }