orbit/data/skills.json

494 lines
32 KiB
JSON
Raw Permalink 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": "THE SKILL LAYER — what a class can actually DO with the skill points its lock-in banks (data/classes.json → bonuses.skillPoints, the ledger js/player/ClassState.js). THE SHAPE: code owns behavior, this file owns every number, string, colour and icon frame (js/skills/SkillModel.js reads it and normalizes; js/skills/SkillState.js spends it; js/ui/SkillWindow.js draws it; js/combat/CombatSystem.js executes the effects). Keys starting with `_` are documentation, not data. THE RULE: adding skill #3 is an edit to this file plus, at most, one new effect `kind` handler in js/skills/SkillEffects.js — never a re-plumb of the tree, the bar or the ledger.",
"enabled": true,
"_timeUnit": "cooldown is SECONDS (recharge time after a cast); duration/durationMs inside an effect are MILLISECONDS so they match the combat loop's clock.",
"icons": {
"_comment": "The skill-insignia sheet — assets/images/skills.png is 1024×512 = eight 128×128 columns × four rows = thirty-two frames, numbered row-major (frame 0 = top-left, frame 1 = the cell right of it). Frame 0 = Ramming Charge (the ram's head), frame 1 = Reinforced Plating (the armour plate), frame 2 = Inferno (the arcs of flame), frame 3 = Nanobots (the mending swarm), frame 8 = Boleadoras (the two snaring orbs), frame 9 = Blink (the watching eye); frames 4..7 and 10..31 are unassigned seams waiting for the next installs. The tree, the tooltip and the combat bar's seats all read a skill's `frame` off this ONE sheet.",
"texture": "assets/images/skills.png",
"frameWidth": 128,
"frameHeight": 128
},
"types": {
"_comment": "THE TYPE TAXONOMY. A skill's `type` names an entry here; the type carries the accent the tree/badge wears, the label it prints, and `passive` — the ONLY thing that decides whether a skill is CASTABLE (an active skill can hold a combat-bar seat and spend energy; a passive is always-on at rank ≥1 and can never take a seat). Types are first-class so later skills can reference them ('+10% to all Offense skills'): reference by the SAME key. Add a type = add an entry here.",
"Offense": {
"label": "OFFENSE",
"accent": "#ff3a4d",
"passive": false
},
"Defense": {
"label": "DEFENSE",
"accent": "#5fd4ff",
"passive": false
},
"Utility": {
"label": "UTILITY",
"accent": "#7ce8a4",
"passive": false
},
"Mobility": {
"label": "MOBILITY",
"accent": "#ffc94d",
"passive": false
},
"Passive": {
"label": "PASSIVE",
"accent": "#c084fc",
"passive": true
}
},
"seats": {
"_comment": "THE COMBAT BAR'S SEATS. `count` is the fallback when data/combat.json → bar.slot.count is unreadable (the bar owns the real number — eight seats wearing keys 1-8). A skill's `seat` below is its DEFAULT binding (1-based); the player reassigns by clicking a skill in the tree and then a seat, and the run's bindings live in the ledger (js/skills/SkillState.js → record.playerSkills.seats), so this table is only ever the first-run suggestion. seatOutOfRangePolicy: 'ignore' drops a default that points past the last seat, 'clamp' pins it to the last seat.",
"count": 8,
"seatOutOfRangePolicy": "ignore"
},
"bar": {
"_comment": "How a SEATED skill is DRAWN on the combat bar (js/ui/CombatBar.js): the icon size, the state veils and the recharge sweep. Behaviour lives in code — js/combat/CombatSystem.js → skillSeats() decides WHICH state a seat is in (ready / recharging / not affordable / inert / empty) and this block says what each one LOOKS like. Sizes: the icon is a FRACTION of the seat (the bar scales its seats down on narrow viewports, so absolute pixels would break at 1024px wide); the text size is px inside a ~44px seat. Colours stay CSS strings — the bar converts them at draw time (js/skills/SkillEffects.js → hexToInt).",
"iconScale": 0.76,
"rankTextPx": 9,
"secondsTextPx": 13,
"assignPulseHz": 1.6,
"states": {
"_comment": "Draw order: the icon, then a veil over it (an unranked skill is buried, a too-expensive one is dimmed blue), then the recharge SWEEP — a dark pie that unwinds clockwise as the skill recharges — with the SECONDS LEFT printed over it. `readyEdge` is the outline a live-and-charged seat wears; `assignEdge` outlines every seat while the tree is holding a skill up waiting to be placed.",
"readyEdge": "#7ce8ff",
"inertVeil": "#050a13",
"inertVeilAlpha": 0.74,
"noEnergyVeil": "#0d1526",
"noEnergyVeilAlpha": 0.58,
"cooldownVeil": "#03080f",
"cooldownVeilAlpha": 0.66,
"sweepColor": "#9fd8ff",
"sweepAlpha": 0.16,
"secondsColor": "#eaf6ff",
"rankColor": "#cfe3f7",
"assignEdge": "#ffd84d"
},
"copy": {
"_comment": "The bar's own toasts (the seat pick/clear path, js/scenes/GameScene.js). The SUCCESS line is the window's `assignedToast` — one copy string, two doors into the same binding.",
"assignBlocked": "{skill} CANNOT TAKE A SEAT — ONLY ACTIVE SKILLS SIT IN THE BAR",
"pickFromTree": "PICK A SKILL IN THE SKILL TREE FIRST",
"clearEmpty": "SEAT {seat} IS ALREADY EMPTY",
"clearedToast": "SEAT {seat} CLEARED — THE SKILL KEEPS ITS RANKS",
"hintUnbind": "RIGHT-CLICK A SEAT TO CLEAR IT"
}
},
"classes": {
"_comment": "ONE TREE PER CLASS, keyed by the class id in data/classes.json. `skills` is the tree in DISPLAY order (the order the console selects through); the GRAPH is the skills' own `requires` edges. Where each skill SITS is its own `node: { col, row }` — a grid cell, row 0 at the top of the console's tree pane and one row further down for every generation below it (`window.tree`). Leave `node` out and the console lays it out itself: roots across the top, each child one row under its deepest parent (js/skills/SkillModel.js → treeLayout). A class with no authored skills shows the console's empty-trees plate rather than a dead window.",
"vanguard": {
"_comment": "THE VANGUARD TREE — install 1 holds the two doctrines the class ships with: Ramming Charge (the opening move) and Reinforced Plating (the reason it survives one). Both are ROOTS: no `requires`, so a Vanguard that just locked in can spend into either immediately. Install 2 grows a child under each: Inferno (the fire that stays on the target — 3 points in Ramming Charge before it unlocks) and Nanobots (the hull that mends itself — 3 points in Reinforced Plating before it unlocks).",
"skills": [
{
"id": "ramming_charge",
"label": "Ramming Charge",
"_comment": "THE HEAD-ON OPENING. Type Offense (the type other skills will reference), 60 energy off the right orb, a 6 second recharge, five points to spend. Rank 0 = inert (no seat lights); rank ≥1 = castable. `seat: 1` is the default binding on a fresh run — keys 1-8 press the seat, not the skill.",
"type": "Offense",
"frame": 0,
"energy": 60,
"cooldown": 6,
"maxPoints": 5,
"requires": [],
"seat": 1,
"node": {
"col": 1,
"row": 0
},
"description": "Charge the nearest enemy at 3x speed over 1 second at ramming speed. Inflict 4x regular damage upon hit, 2x additional damage per additional skill point.",
"shortDescription": "Ram the nearest hostile for 4x weapon damage.",
"effect": {
"_comment": "kind = the handler in js/skills/SkillEffects.js. dashBlast: lock the flight controls, drive the hull at speedMul × the ship's own max speed along the line to the aim point, and detonate on ARRIVAL (the moment it reaches the point, or when durationMs runs out — whichever lands first). The aim point is the nearest hostile's position AT CAST plus overshootPx PAST it, so the ship ends up through the enemy rather than inside it. blastRadiusPx = how far the arrival shock reaches from the ship (every hostile inside takes the hit once). damageMul is a multiple of the cannon's own damage (data/combat.json → player.weapon.damage) at rank 1; damageMulPerRank adds to it for every skill point AFTER the first — 4x at 1/5, 6x at 2/5 … 12x at 5/5.",
"kind": "dashBlast",
"speedMul": 3,
"durationMs": 1000,
"overshootPx": 90,
"blastRadiusPx": 170,
"damageMul": 4,
"damageMulPerRank": 2,
"fx": {
"_comment": "The charge's own dressing (js/combat/Fx.js reads these; every key has a fallback): trailColor = the comet head's shed embers AND the arrival's ember ring, ringColor = the DAMAGE-RADIUS ring + the shock ripples, ringLifeMs = how long that radius ring lingers, sfxEvent = a COMBAT_SFX EVENT (js/combat/CombatSfx.js) played with the thunder boom ('kill') under the impact — 'hull' is the heavy armor-crack voice. The comet head itself, the lingering ~1 s ember trail and the fireball are Fx's fixed craft; these colors skin them.",
"trailColor": "#ff3a4d",
"trailAlpha": 0.55,
"ringColor": "#ffd84d",
"ringLifeMs": 480,
"sfxEvent": "hull"
}
}
},
{
"id": "inferno",
"_comment": "THE FIRE THAT STAYS ON THE TARGET. Type Offense (the type the other Vanguard skills wear), 50 energy off the right orb, a 7 second recharge, five points to spend. `requires` is the tree's spends-through-unlock edge: 3 points must sit in Ramming Charge before a point may go here. `seat: 2` is the default binding on a fresh run — the Vanguard's second seat, under the ram. THE MECHANIC lives in js/combat/CombatSystem.js → launchFlames / _stepFlames / _igniteFlames / _stepBurns: one wide arc of flame per nearest hostile (effect.targetBase of them — effect.extraTargets more from effect.extraTargetAtRank on), each a fast wave that homes on its assigned craft and, on arrival, ignites everything its splash touches. The burn each caught craft wears is STAMPED on it (burnDurationMs of it, ticking burnTickDamage every effect.burnTickMs starting at t=0) and stepped in the combat loop — the damage outlives the arc, and a stronger cast overwrites a weaker burn on the same hull.",
"label": "Inferno",
"type": "Offense",
"frame": 2,
"energy": 50,
"cooldown": 7,
"maxPoints": 5,
"requires": [
{
"skill": "ramming_charge",
"points": 3
}
],
"seat": 2,
"node": {
"col": 1,
"row": 1
},
"description": "Send wide arcs of flame at the nearest 2 enemies. When an arc impacts it lights the enemy's ship on fire for 0.5 seconds, dealing 1x regular damage every 0.5 seconds (a hit at 0 seconds and another at 0.5 seconds to begin with). Each additional skill point adds 0.5 seconds to the burn and 0.1 extra damage to every tick, and the 4th point sets one additional enemy alight (3 total).",
"shortDescription": "Flame the nearest 2 — 0.5s of burns, 1x per tick, growing per point.",
"effect": {
"_comment": "kind = the handler in js/skills/SkillEffects.js. targetBase = the NEAREST hostiles that each get their own arc (2); from extraTargetAtRank (the 4th point) extraTargets more are targeted (3 total). speed = the arcs' flight (px/s); splashRadiusPx = on impact, every live hostile whose hull touches this radius is ignited (the arc's own target plus any it sweeps in). THE BURN, per caught craft, in MS (the combat loop's clock): it lasts burnBaseMs + burnPerRankMs × (rank − 1) — 500 at 1/5 … 2500 at 5/5 — and ticks every burnTickMs starting at t=0 (a 0.5s burn deals TWO ticks: 0s and 0.5s), each for (burnDamageMul + burnDamagePerRank × (rank − 1)) × the cannon's own damage — 1x at 1/5 … 1.4x at 5/5. fx skins the arcs (flameColor = the wave and the ignition's read, emberColor = the embers the wave sheds); sfxEvent is a COMBAT_SFX EVENT (js/combat/CombatSfx.js) played when an arc lands.",
"kind": "flameArc",
"targetBase": 2,
"extraTargetAtRank": 4,
"extraTargets": 1,
"speed": 950,
"splashRadiusPx": 64,
"burnBaseMs": 500,
"burnPerRankMs": 500,
"burnTickMs": 500,
"burnDamageMul": 1,
"burnDamagePerRank": 0.1,
"fx": {
"flameColor": "#ff9d3a",
"emberColor": "#ffd84d",
"sfxEvent": "hull"
}
}
},
{
"id": "reinforced_plating",
"_comment": "THE HULL THAT WILL NOT QUIT. Type Passive — no cast, no energy, no recharge, and it can NEVER hold a bar seat: it works from rank 1 onward. `seat` is null to say so out loud in the data.",
"label": "Reinforced Plating",
"type": "Passive",
"frame": 1,
"energy": 0,
"cooldown": 0,
"maxPoints": 5,
"requires": [],
"seat": null,
"node": {
"col": 3,
"row": 0
},
"description": "Strengthen your ship's hull, adding a 5% increase in hull strength, with an additional 5% increase per additional skill point.",
"shortDescription": "+5% hull. +5% more per point.",
"effect": {
"_comment": "hullPool: the armor's base hull (the ship's own stats.hullIntegrity, or data/combat.json → playerShip.hull on the range) is multiplied by 1 + hullBonusPerRank × rank — 5% at 1/5 … 25% at 5/5. THE MULTIPLIER IS RE-ROLLED FROM BASE EVERY TIME (never compounded on last frame's max), so rank-ups, level-up armor bumps (data/progression.json → reward), combat restarts and save/load can never stack it twice. The hull orb shows the bigger number.",
"kind": "hullPool",
"hullBonusPerRank": 0.05
}
},
{
"id": "nanobots",
"_comment": "THE HULL THAT MENDS ITSELF. Type Passive — no cast, no energy, no recharge, and it can NEVER hold a bar seat: it works from rank 1 onward. `requires` is the tree's spends-through-unlock edge: 3 points must sit in Reinforced Plating before a point may go here. THE MECHANIC lives in js/combat/CombatSystem.js → hullRegen() + the update loop's armor pass: on the combat clock, every effect.intervalMs (and only while the hull is up and not already full) the hull regrows the ledger's share of its MAX — regenPercentPerRank × rank of it: 1% at 1/5 … 5% at 5/5 — so a 200-hull ship gains 2 hull every 2 seconds at 1/5 and 10 at 5/5. A full hull banks nothing: the mend clock stays spent, never queued.",
"label": "Nanobots",
"type": "Passive",
"frame": 3,
"energy": 0,
"cooldown": 0,
"maxPoints": 5,
"requires": [
{
"skill": "reinforced_plating",
"points": 3
}
],
"seat": null,
"node": {
"col": 3,
"row": 1
},
"description": "Mend your ship's hull: repair 1% of maximum hull every 2 seconds, with an additional 1% of maximum hull per additional skill point.",
"shortDescription": "Repair 1% max hull every 2s. +1% per point.",
"effect": {
"_comment": "hullRegen: the combat loop owns the tick (effect.intervalMs between mends — the data's number, read off the ledger, never restated in code). regenPercentPerRank is a RATIO of the hull's MAX (0.01 = 1%), summed across the skill's ranked points by the ledger (js/skills/SkillState.js → effectTotal) exactly like Reinforced Plating's hullBonusPerRank — rank 5 mends 5% of max per tick. fx.mendColor is the color each mend's beat wears (js/combat/Fx.js).",
"kind": "hullRegen",
"regenPercentPerRank": 0.01,
"intervalMs": 2000,
"fx": {
"mendColor": "#7ce8a4"
}
}
}
]
},
"wraith": {
"_comment": "THE WRAITH TREE — install 2 holds the class's two tricks: Boleadoras (the SNARE — the trap the targeting computer exists to lead) and Blink (the ESCAPE — the reason the trickery is survivable). Both are ROOTS: no `requires`, so a Wraith that just locked in can spend into either immediately. Both spend the right orb (60 / 70 off a 100) and sit in seats 1 / 2 on a fresh run, the same as the Vanguard's defaults.",
"skills": [
{
"id": "boleadoras",
"label": "Boleadoras",
"_comment": "THE SNARE. Type Utility (the type other skills reference), 60 energy off the right orb, a 6 second recharge, five points to spend. Rank 0 = inert; rank ≥1 = castable. `seat: 1` is the default binding on a fresh run. THE MECHANIC lives in js/combat/CombatSystem.js → launchBoleadoras / _stepBoleadoras / _snagBola: one set per nearest hostile (targetCount of them), each hunting its assigned craft on the ADVANCED TARGETING's intercept solve (js/combat/Aiming.js, capped by perks.targetingComputer.maxLead when the ledger carries the Wraith's computer). The STUN the impact drops (disableBaseMs + disablePerRankMs × rank, stamped on each caught craft's `stunUntil`) is what the pack AI's gate reads — a strung craft flies nowhere and fires nothing.",
"type": "Utility",
"frame": 8,
"energy": 60,
"cooldown": 6,
"maxPoints": 5,
"requires": [],
"seat": 1,
"node": {
"col": 1,
"row": 0
},
"description": "Hurl a spinning set of digital Boleadoras (two orbs on a string) at each of the nearest 3 enemies. On impact the enemies caught are disabled — unable to fly or shoot — for a period of 0.5 seconds, with an additional 0.5 seconds of disable time per skill point.",
"shortDescription": "String up to 3 enemies: no flight, no fire, 0.5s +0.5s per point.",
"effect": {
"_comment": "kind = the handler in js/skills/SkillEffects.js. targetCount = the number of NEAREST hostiles that each get their own set; speed = the sets' flight (px/s); captureRadiusPx = on arrival, every live hostile whose hull touches this radius is strung (the set's own target plus any it sweeps in). disableBaseMs + disablePerRankMs × rank = the stun a strung craft wears, in MS (the combat loop's clock): 0.5s base + 0.5s per point — 1.0s at 1/5 … 3.0s at 5/5. fx skins the sets (ballColor = the orbs, stringColor = the line between them, ballSize/stringRadius in px) and the STRUNG STATE while the stun holds (stunAlpha = the strung craft's alpha — the dimmed/disabled read; the crackling discharge's look lives in js/combat/Fx.js → stunArms); sfxEvent is a COMBAT_SFX EVENT (js/combat/CombatSfx.js) played when a set lands.",
"kind": "boleadoras",
"targetCount": 3,
"speed": 700,
"captureRadiusPx": 56,
"disableBaseMs": 500,
"disablePerRankMs": 500,
"fx": {
"ballColor": "#b489ff",
"stringColor": "#9fd8ff",
"ballSize": 14,
"stringRadius": 16,
"sfxEvent": "bola_snag",
"stunAlpha": 0.42
}
}
},
{
"id": "blink",
"label": "Blink",
"_comment": "THE ESCAPE. Type Utility, 70 energy off the right orb, a 15 second recharge, five points to spend. Rank 0 = inert; rank ≥1 = castable. `seat: 2` is the default binding on a fresh run. THE MECHANIC lives in js/combat/CombatSystem.js → blink / _blinkDestination / _stepDecoys: on activation the hull is SWAPPED for a decoy copy of the player's own ship (decoyFrame of the player's own sheet) left at the old position, and the Wraith stands at a random point of the visible screen. While it stands (decoyBaseMs + decoyPerRankMs × rank), every pirate tracks and lances the decoy instead (pickEnemyTarget's first rule) and enemy bolts that reach it fizzle on the shell (collide()'s decoy pass).",
"type": "Utility",
"frame": 9,
"energy": 70,
"cooldown": 15,
"maxPoints": 5,
"requires": [],
"seat": 2,
"node": {
"col": 3,
"row": 0
},
"description": "Leave a decoy copy of your ship at your old position — it draws all enemy aggro while it stands — and teleport to a random location on the visible screen. The decoy lasts 0.4 seconds, with an additional 0.4 seconds per skill point.",
"shortDescription": "Decoy behind, hop across the screen. 0.4s +0.4s per point.",
"effect": {
"_comment": "kind = the handler in js/skills/SkillEffects.js. decoyFrame = the decoy's frame of the PLAYER'S OWN sheet (assets/images/ships-player.png — frame 1, the frame right of the starter ship). decoyBaseMs + decoyPerRankMs × rank = the decoy's stand, in MS (the combat loop's clock): 0.4s base + 0.4s per point — 0.8s at 1/5 … 2.4s at 5/5. insetFraction = how far the destination stays off the visible screen's edges (a fraction of the shorter side); minDistancePx = the hop's minimum distance from the old position (a blink that lands in place is not a blink); fallbackRadiusPx = the hop's radius when the scene has no camera (the headless range/tests). fx.decoyColor tints the shell, fx.color the teleport beats; sfxEvent is a COMBAT_SFX EVENT (js/combat/CombatSfx.js).",
"kind": "blink",
"decoyFrame": 1,
"decoyBaseMs": 400,
"decoyPerRankMs": 400,
"insetFraction": 0.16,
"minDistancePx": 180,
"fallbackRadiusPx": 340,
"fx": {
"decoyColor": "#b489ff",
"color": "#a764ff",
"sfxEvent": "blink"
}
}
}
]
}
},
"window": {
"_comment": "THE SKILL TREE CONSOLE (js/ui/SkillWindow.js, depth 80). Same cabinet as every other window, but a three-region body: the class PLATE upper-left, the TREE upper-right (a scrollable grid of icon tiles wearing n/m rank badges and unlock lines — see `tree` below), and the DETAIL drawer along the bottom third for whichever tile is selected (big icon left, copy right, ACTIONS lower-right).",
"title": "SKILL TREE",
"meta": "{class} // {points} SKILL POINTS",
"feedLine": "COMBAT DOCTRINE // {class}",
"video": {
"_comment": "The left feed — a muted 2:3 loop, authored separately. A missing clip leaves the NO SIGNAL plate up and the console still works (the ship/class consoles' rule).",
"file": "assets/videos/skills.mp4",
"aspect": [
2,
3
],
"tag": "TRAINING SIM // OPEN CHANNEL",
"noSignalTag": "FEED OFFLINE"
},
"treeTitle": "AVAILABLE DOCTRINE",
"pointsLine": "{n} SKILL POINTS AVAILABLE",
"pointsLineOne": "{n} SKILL POINT AVAILABLE",
"rankLine": "{rank}/{max}",
"costLine": "{energy} ENERGY · {cooldown}s RECHARGE",
"passiveCostLine": "PASSIVE · NO ENERGY · ALWAYS ON",
"typeLabel": "TYPE",
"seatLabel": "SEAT",
"states": {
"_comment": "The state stamp a skill row wears, and the copy a refused SPEND toasts. `available` is a root/parent satisfied but rank 0 (the first point), `upgradable` takes the next point, `locked` names the parent via lockedNote, `maxed` is 5/5, `noPoints` is spendable-but-broke.",
"locked": "LOCKED",
"available": "RANK UP",
"upgradable": "RANK UP",
"noPoints": "NO POINTS",
"maxed": "MASTERED",
"passiveOn": "ACTIVE"
},
"_commentStates": "One line per reason a seat press can be REFUSED (js/combat/CombatSystem.js → cast() returns the reason; js/scenes/GameScene.js → castSkillSeat says it). The seat is always named, because on the bar a number IS the skill — keys bind seats, not skills. {skill}, {seat} and {left} (seconds left) fill in.",
"castRefusals": {
"empty": "SEAT {seat}: NOTHING ASSIGNED",
"inert": "{skill} IS NOT RANKED YET",
"energy": "NOT ENOUGH ENERGY FOR {skill}",
"cooldown": "{skill} RECHARGING — {left}s",
"passive": "{skill} IS ALWAYS ON",
"down": "HULL OFFLINE",
"dashing": "ALREADY CHARGING",
"noLedger": "NO SKILLS ON FILE",
"disabled": "SKILL SYSTEM OFFLINE",
"unhandled": "{skill} IS NOT WIRED"
},
"lockedNote": "REQUIRES {parent}",
"spendLabel": "+ RANK UP",
"_commentDetail": "THE DETAIL DRAWER (bottom third): what a selected tile says about itself, and the three actions in its lower-right corner. `detailPrompt` is the drawer with nothing selected; `addPointLabel` spends one point from the bank into the selected skill, `resetLabel` gives back ONLY what was ranked since this console opened, `assignLabel`/`unbindLabel` hand the skill to a combat-bar seat. Each action goes inert (grayed) when it has nothing to do — and `{points}` on the add button is the bank, so the player sees what the click costs.",
"detailPrompt": "SELECT A SKILL",
"detailPromptSub": "CLICK A TILE IN THE TREE ABOVE",
"addPointLabel": "+ ADD A SKILL POINT",
"addPointNoPoints": "NO POINTS LEFT",
"resetLabel": "RESET",
"_commentChip": "The two tallies the console keeps beside it: the bank in the title bar and the session's own pending spend in the drawer. Both are counts, so they live here as templates.",
"pointsChip": "{n} PTS",
"pendingNote": "+{n} THIS SESSION",
"resetToast": "{skill} — {n} {points} RETURNED TO THE BANK",
"cascadeToast": "{child} UNLOCKED WITH IT TOO",
"resetRefusals": {
"_comment": "Why RESET does nothing (and what the console says). `nothing` = this skill holds no point from THIS session; `noSession` = the ledger is not inside a console session (a save/load mid-window); `blocked` = giving these back would strand a skill that was bought under it.",
"nothing": "NOTHING TO RESET — {skill} WAS ALREADY AT {rank}",
"noSession": "NO OPEN SESSION TO RESET",
"blocked": "{child} IS SPOKEN FOR BY {skill} — RESET {child} FIRST"
},
"sessionNote": "POINTS YOU SPEND HERE ARE HELD UNTIL THIS CONSOLE CLOSES — RESET GIVES BACK ONLY WHAT YOU RANKED IN THIS SESSION, AND CLOSING COMMITS THE REST",
"commitToast": "{n} {points} COMMITTED TO THE TREE",
"assignLabel": "ASSIGN SEAT",
"assignHint": "PICK A COMBAT BAR SEAT // 1-{maxSeat}",
"assignedToast": "{skill} → SEAT {seat}",
"unbindLabel": "CLEAR SEAT",
"tree": {
"_comment": "THE TREE PANE (upper-right region): a GRID of icon tiles. `cols` is the authored grid width (a skill's own `node: {col,row}` places it; an unplaced skill falls into the auto-layout); the console fits `cols` cells across whatever room it has and clamps the tile between minPx and maxPx. Every look-knob below is the data's — the code restates none of them.",
"cols": 5,
"_commentRegions": "How the console's body is cut up: the tree pane takes the upper `splitFraction` of the body and the class plate `plateFraction` of its width (the drawer lives under both). The remaining third is the detail drawer.",
"regions": {
"splitFraction": 0.66,
"plateFraction": 0.27,
"gapPx": 14
},
"tile": {
"minPx": 46,
"maxPx": 92
},
"gap": {
"colPx": 34,
"rowPx": 44,
"padPx": 18
},
"edge": {
"_comment": "The unlock lines between a parent tile and its child. `spent` = the requirement is met AND the child has a rank (the chosen path — the bright gold), `open` = the requirement is met but the child is still unranked, `locked` = the edge is not satisfied yet. Lines are drawn as an elbow: straight down out of the parent, across, straight into the child.",
"spentColor": "#ffd84d",
"spentAlpha": 0.95,
"openColor": "#7d92c4",
"openAlpha": 0.5,
"lockedColor": "#3d4c74",
"lockedAlpha": 0.42,
"widthPx": 4,
"notchPx": 8
},
"frame": {
"_comment": "A tile's frame, four states — the same idiom as the screenshot this console is dressed after: a lit bevel for the affordable, a gold bevel on the path, dead iron for locked, and a pulsing ring around the selected tile.",
"idleEdge": "#5f7099",
"availableEdge": "#cfe3f7",
"spentEdge": "#ffd84d",
"maxedEdge": "#7ce8a4",
"lockedEdge": "#2c3a5c",
"selectedEdge": "#00e5ff",
"faceColor": "#0b1424",
"faceAlpha": 0.92,
"bevelPx": 3,
"selectedPulseHz": 1.8
},
"badge": {
"_comment": "The n/m rank plate on a tile's lower-right corner (the screenshot's own badge). Drawn only when the skill holds more than one point.",
"color": "#eaf6ff",
"spentColor": "#ffd84d",
"bgColor": "#04060d",
"bgAlpha": 0.88,
"edgeColor": "#3d4c74",
"fontSizePx": 12
},
"_commentSocket": "Every cell inside the tree's bounds that holds NO skill is plated as a dim socket (the reference sheet's empty nodes): the authored grid stays legible when a tree is young instead of leaving icons adrift in an empty pane.",
"socket": {
"enabled": true,
"faceColor": "#080f1c",
"faceAlpha": 0.5,
"edgeColor": "#3d4c74",
"edgeAlpha": 0.35,
"tickPx": 7
},
"veil": {
"lockedColor": "#050a13",
"lockedAlpha": 0.62,
"pendingGlow": "#ffd84d",
"pendingAlpha": 0.5
},
"scrollbar": {
"_comment": "The tree scrolls (wheel, drag, or the thumb) whenever the laid-out content is taller than the pane — a long install's tree must never be cut off.",
"wPx": 8,
"trackColor": "#0b1424",
"thumbColor": "#3f5a86",
"thumbHotColor": "#00e5ff",
"wheelPxPerNotch": 64
},
"hintLine": "WHEEL OR DRAG TO SCROLL THE TREE"
},
"emptyNote": "NO SKILLS ON FILE FOR THIS CLASS",
"noClassNote": "PICK A CLASS AT COMBAT LEVEL {classAt} — THE TREE GROWS FROM THERE",
"closeLabel": "◂ CLOSE",
"sfx": {
"_comment": "Rank-up + window sfx ids (data/sfx.json). spend = the point lands (the discovery chime), denied = a refused spend (the deconstruct voice — it has to sound like a step backwards), assign = a seat bound, open/close = the cabinet.",
"spend": "discovery",
"denied": "deconstruct",
"_commentRefund": "RESET is a step backwards, so it wears the same voice as a refusal — its own key only so the two decisions stay separable.",
"refund": "deconstruct",
"assign": "ui_click",
"open": "ui_window",
"close": "ui_window"
},
"fx": {
"_comment": "Window dressing, the research console's recipe: occasional RGB-split slice bursts over the plate and a scan band crawling the feed.",
"glitch": {
"enabled": true,
"intervalMs": [
6000,
13000
],
"durationMs": [
220,
420
]
},
"sweep": {
"enabled": true,
"everyMs": [
4200,
8600
],
"durationMs": 1500
}
}
},
"_skillTemplate": {
"_comment": "THE CONTRACT one skill record owes the model (defaults in brackets when a key is missing/wrong): id [required — dropped without it], label [id], type ['Utility' → see types], frame [0], energy [0], cooldown [0], maxPoints [1], requires [[] — ids, or {skill, points} for a 'put N points into X first' edge], seat [null = no default binding], node [{col,row} on the console's tree grid — auto-laid-out when absent], description [''], shortDescription [''], effect [{kind:'none'}].",
"id": "skill-id",
"label": "Display Name",
"type": "Offense",
"frame": 2,
"energy": 40,
"cooldown": 8,
"maxPoints": 5,
"requires": [
"ramming_charge"
],
"seat": null,
"node": {
"col": 1,
"row": 1
},
"description": "The full effect text the console prints.",
"shortDescription": "One line for the tooltip / bar seat.",
"effect": {
"kind": "none"
}
}
}