206 lines
14 KiB
JSON
206 lines
14 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); frames 2..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, the recharge sweep and the rank stamp. 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 two text sizes are 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, then the rank stamp in the corner. `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 rows the console draws, top to bottom); the graph is the skills' own `requires` edges. 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,
|
||
"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/skills/SkillEffects.js reads these; every key has a fallback): the streak drawn along the dash path, and the arrival shock ring. sfxEvent names a COMBAT_SFX EVENT (js/combat/CombatSfx.js) — 'hull' is the heavy armor-crack voice, the same one a hull hit plays.",
|
||
"trailColor": "#ff3a4d",
|
||
"trailAlpha": 0.55,
|
||
"ringColor": "#ffd84d",
|
||
"ringLifeMs": 320,
|
||
"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,
|
||
"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 — authored with the next install. The console opens on it and says so (window.emptyNote) instead of pretending there is a tree.",
|
||
"skills": []
|
||
}
|
||
},
|
||
|
||
"window": {
|
||
"_comment": "THE SKILL TREE CONSOLE (js/ui/SkillWindow.js, depth 85 — the class console's height, over the ship console's 80, because it is opened FROM that console's Skill Tree tab). Same cabinet as every other window: LEFT the looping feed, RIGHT the plate over the class's tree.",
|
||
"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",
|
||
"assignLabel": "ASSIGN SEAT",
|
||
"assignHint": "PICK A COMBAT BAR SEAT // 1-{maxSeat}",
|
||
"assignedToast": "{skill} → SEAT {seat}",
|
||
"unbindLabel": "CLEAR SEAT",
|
||
"refundNote": "POINTS SPENT ARE SPENT — RESPEC LANDS LATER",
|
||
"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",
|
||
"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], 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,
|
||
"description": "The full effect text the console prints.",
|
||
"shortDescription": "One line for the tooltip / bar seat.",
|
||
"effect": { "kind": "none" }
|
||
}
|
||
}
|