351 lines
25 KiB
JSON
351 lines
25 KiB
JSON
{
|
||
"_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 8 = Boleadoras (the two snaring orbs), frame 9 = Blink (the watching eye); frames 2..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.",
|
||
"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": "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
|
||
}
|
||
}
|
||
]
|
||
},
|
||
|
||
"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); 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"
|
||
}
|
||
}
|
||
},
|
||
{
|
||
"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" }
|
||
}
|
||
}
|