orbit/data/combat.json

360 lines
20 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 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; 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": {
"frame": 0
},
"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"
}
}
},
"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. 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, "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 }
}
}