diff --git a/data/skills.json b/data/skills.json index 857a928..b336c9a 100644 --- a/data/skills.json +++ b/data/skills.json @@ -25,13 +25,13 @@ "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).", + "_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, 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.", + "_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, diff --git a/dev/shots/skills-bar-assign.png b/dev/shots/skills-bar-assign.png index 13081ab..f3595bc 100644 Binary files a/dev/shots/skills-bar-assign.png and b/dev/shots/skills-bar-assign.png differ diff --git a/dev/shots/skills-bar-no-energy.png b/dev/shots/skills-bar-no-energy.png index e538399..d41f015 100644 Binary files a/dev/shots/skills-bar-no-energy.png and b/dev/shots/skills-bar-no-energy.png differ diff --git a/dev/shots/skills-bar-ready.png b/dev/shots/skills-bar-ready.png index 974d9e6..126e681 100644 Binary files a/dev/shots/skills-bar-ready.png and b/dev/shots/skills-bar-ready.png differ diff --git a/dev/shots/skills-bar-recharging.png b/dev/shots/skills-bar-recharging.png index 80b75dd..e77179f 100644 Binary files a/dev/shots/skills-bar-recharging.png and b/dev/shots/skills-bar-recharging.png differ diff --git a/dev/shots/skills-bar.json b/dev/shots/skills-bar.json index 68a4493..f070ffc 100644 --- a/dev/shots/skills-bar.json +++ b/dev/shots/skills-bar.json @@ -229,56 +229,48 @@ "state": "ready", "icon": true, "frame": "0", - "rank": "5/5", "secs": "" }, { "state": "empty", "icon": false, "frame": null, - "rank": "", "secs": "" }, { "state": "empty", "icon": false, "frame": null, - "rank": "", "secs": "" }, { "state": "empty", "icon": false, "frame": null, - "rank": "", "secs": "" }, { "state": "empty", "icon": false, "frame": null, - "rank": "", "secs": "" }, { "state": "empty", "icon": false, "frame": null, - "rank": "", "secs": "" }, { "state": "empty", "icon": false, "frame": null, - "rank": "", "secs": "" }, { "state": "empty", "icon": false, "frame": null, - "rank": "", "secs": "" } ], @@ -290,8 +282,8 @@ "dashing": false, "down": false, "ship": { - "x": 3810.5218340666174, - "y": 4088.361289263925 + "x": 5909.759469383378, + "y": -988.8723592216014 }, "ledgerSeats": [ "ramming_charge", @@ -337,8 +329,8 @@ "maxPoints": 5, "energy": 60, "cooldown": 6, - "cooldownLeft": 5.722706666666667, - "cooldownFrac": 0.046215555555555454, + "cooldownLeft": 5.733383333333334, + "cooldownFrac": 0.04443611111111101, "affordable": false, "ready": false, "inert": false @@ -489,60 +481,52 @@ "state": "cooldown", "icon": true, "frame": "0", - "rank": "5/5", "secs": "6" }, { "state": "empty", "icon": false, "frame": null, - "rank": "", "secs": "" }, { "state": "empty", "icon": false, "frame": null, - "rank": "", "secs": "" }, { "state": "empty", "icon": false, "frame": null, - "rank": "", "secs": "" }, { "state": "empty", "icon": false, "frame": null, - "rank": "", "secs": "" }, { "state": "empty", "icon": false, "frame": null, - "rank": "", "secs": "" }, { "state": "empty", "icon": false, "frame": null, - "rank": "", "secs": "" }, { "state": "empty", "icon": false, "frame": null, - "rank": "", "secs": "" } ], - "energy": 41.66376000000001, + "energy": 41.59970000000001, "maxEnergy": 100, "hull": 6000, "maxHull": 6000, @@ -550,8 +534,8 @@ "dashing": true, "down": false, "ship": { - "x": 3870.629477981919, - "y": 4033.226599552123 + "x": 5972.397742948351, + "y": -675.1068097485484 }, "ledgerSeats": [ "ramming_charge", @@ -749,69 +733,61 @@ "state": "noEnergy", "icon": true, "frame": "0", - "rank": "5/5", "secs": "" }, { "state": "empty", "icon": false, "frame": null, - "rank": "", "secs": "" }, { "state": "empty", "icon": false, "frame": null, - "rank": "", "secs": "" }, { "state": "empty", "icon": false, "frame": null, - "rank": "", "secs": "" }, { "state": "empty", "icon": false, "frame": null, - "rank": "", "secs": "" }, { "state": "empty", "icon": false, "frame": null, - "rank": "", "secs": "" }, { "state": "empty", "icon": false, "frame": null, - "rank": "", "secs": "" }, { "state": "empty", "icon": false, "frame": null, - "rank": "", "secs": "" } ], "energy": 59.384, "maxEnergy": 100, - "hull": 129, + "hull": 101.896, "maxHull": 129, "level": 1, "dashing": false, "down": false, "ship": { - "x": 3893.832344512312, - "y": 4012.409104914982 + "x": 5967.269013350007, + "y": -697.4064976194823 }, "ledgerSeats": [ "ramming_charge", @@ -1009,69 +985,61 @@ "state": "empty", "icon": false, "frame": "0", - "rank": "", "secs": "" }, { "state": "empty", "icon": false, "frame": null, - "rank": "", "secs": "" }, { "state": "empty", "icon": false, "frame": null, - "rank": "", "secs": "" }, { "state": "ready", "icon": true, "frame": "0", - "rank": "5/5", "secs": "" }, { "state": "empty", "icon": false, "frame": null, - "rank": "", "secs": "" }, { "state": "empty", "icon": false, "frame": null, - "rank": "", "secs": "" }, { "state": "empty", "icon": false, "frame": null, - "rank": "", "secs": "" }, { "state": "empty", "icon": false, "frame": null, - "rank": "", "secs": "" } ], "energy": 68.21600000000001, "maxEnergy": 100, - "hull": 121, + "hull": 71.896, "maxHull": 129, "level": 1, "dashing": false, "down": false, "ship": { - "x": 3893.832344512312, - "y": 4012.409104914982 + "x": 5967.269013350007, + "y": -697.4064976194823 }, "ledgerSeats": [ null, @@ -1269,69 +1237,61 @@ "state": "empty", "icon": false, "frame": "0", - "rank": "", "secs": "" }, { "state": "empty", "icon": false, "frame": null, - "rank": "", "secs": "" }, { "state": "empty", "icon": false, "frame": null, - "rank": "", "secs": "" }, { "state": "empty", "icon": false, "frame": "0", - "rank": "", "secs": "" }, { "state": "empty", "icon": false, "frame": null, - "rank": "", "secs": "" }, { "state": "empty", "icon": false, "frame": null, - "rank": "", "secs": "" }, { "state": "empty", "icon": false, "frame": null, - "rank": "", "secs": "" }, { "state": "empty", "icon": false, "frame": null, - "rank": "", "secs": "" } ], "energy": 71.28800000000001, "maxEnergy": 100, - "hull": 81, + "hull": 61.896, "maxHull": 129, "level": 1, "dashing": false, "down": false, "ship": { - "x": 3893.832344512312, - "y": 4012.409104914982 + "x": 5967.269013350007, + "y": -697.4064976194823 }, "ledgerSeats": [ null, @@ -1529,69 +1489,61 @@ "state": "ready", "icon": true, "frame": "0", - "rank": "5/5", "secs": "" }, { "state": "empty", "icon": false, "frame": null, - "rank": "", "secs": "" }, { "state": "empty", "icon": false, "frame": null, - "rank": "", "secs": "" }, { "state": "empty", "icon": false, "frame": "0", - "rank": "", "secs": "" }, { "state": "empty", "icon": false, "frame": null, - "rank": "", "secs": "" }, { "state": "empty", "icon": false, "frame": null, - "rank": "", "secs": "" }, { "state": "empty", "icon": false, "frame": null, - "rank": "", "secs": "" }, { "state": "empty", "icon": false, "frame": null, - "rank": "", "secs": "" } ], "energy": 73.97600000000001, "maxEnergy": 100, - "hull": 41, + "hull": 61.896, "maxHull": 129, "level": 1, "dashing": false, "down": false, "ship": { - "x": 3893.832344512312, - "y": 4012.409104914982 + "x": 5967.269013350007, + "y": -697.4064976194823 }, "ledgerSeats": [ "ramming_charge", diff --git a/dev/shots/skills-seat.png b/dev/shots/skills-seat.png index 618ff11..2bfc318 100644 Binary files a/dev/shots/skills-seat.png and b/dev/shots/skills-seat.png differ diff --git a/dev/skills-bar-browser.test.mjs b/dev/skills-bar-browser.test.mjs index 8bfbf36..103b699 100644 --- a/dev/skills-bar-browser.test.mjs +++ b/dev/skills-bar-browser.test.mjs @@ -11,8 +11,8 @@ * is the face and the hands): * · a `?skill=max&combat` run boots with the class filed, both skills 5/5 and * the seat Ramming Charge was GIVEN by data filled (nothing to grind); - * · the bound seat draws its ICON off the sheet, wears its RANK stamp and reads - * ready; an unbound seat keeps its reserved empty look; + * · the bound seat draws its ICON off the sheet and reads ready; an unbound + * seat keeps its reserved empty look; * · pressing 1 dashes: the orb drops by the skill's cost, the seat goes to its * recharge face with the SECONDS printed on it and the sweep unwinds toward * full charge (evidence in dev/shots/); @@ -81,7 +81,6 @@ const snap = () => state: x.state, icon: !!x.icon && x.icon.visible, frame: x.icon ? String(x.icon.frame?.name ?? '') : null, - rank: x.rankT.visible ? x.rankT.text : '', secs: x.secs.visible ? x.secs.text : '', })), energy: g.combat.energy, @@ -152,11 +151,12 @@ const seatClip = () => const r = document.querySelector('canvas').getBoundingClientRect(); const sx = r.width / g.scale.width; const sy = r.height / g.scale.height; - const m = art.rankT.getWorldTransformMatrix(); + const cx = art.lx + bar.seatBox.size / 2; + const cy = art.ly + bar.seatBox.size / 2; const S = (bar.seatBox?.size ?? 44) * 1.2; return { - x: Math.max(0, Math.round(r.left + (m.tx - S / 2) * sx)), - y: Math.max(0, Math.round(r.top + (m.ty - S / 2) * sy)), + x: Math.max(0, Math.round(r.left + (cx - S / 2) * sx)), + y: Math.max(0, Math.round(r.top + (cy - S / 2) * sy)), width: Math.max(8, Math.round(S * sx)), height: Math.max(8, Math.round(S * sy)), }; @@ -247,17 +247,14 @@ const ready = await page const s0 = ready.seats[0]; check(s0.id === RAM.id && s0.rank === RAM.maxPoints, `seat 1 holds ${RAM.label} ${s0.rank}/${s0.maxPoints} — the DEFAULT binding came from data/skills.json`); check(ready.art[0].icon === true && ready.art[0].frame === String(RAM.frame), `the seat draws icon frame ${RAM.frame} off the sheet (got "${ready.art[0].frame}")`); -check(ready.art[0].rank === `${RAM.maxPoints}/${RAM.maxPoints}`, `the seat wears its rank stamp "${ready.art[0].rank}"`); -// RENDERED PROOF: the rank stamp and the icon each contribute pixels ON TOP of -// each other (an icon added lazily over the state layers buries them while every -// property above still reads correct — that regression is what this catches). +// RENDERED PROOF: the icon contributes pixels ON TOP of the state layers (an +// icon added lazily over the state layers buries them while every property +// above still reads correct — that regression is what this catches). await clearScreen(); const seatBase = await shootSeat([]); -const noRank = await shootSeat(['rankT']); const noIcon = await shootSeat(['icon']); -check(Buffer.compare(seatBase, noRank) !== 0, 'the rank stamp actually reaches the pixels (hiding it changes the seat render, so it is not buried under the icon)'); -check(Buffer.compare(seatBase, noIcon) !== 0, 'the icon reaches the pixels too (so the two differ because of stacking, not an empty seat)'); +check(Buffer.compare(seatBase, noIcon) !== 0, 'the icon reaches the pixels too (so it is not buried under the state layers)'); fs.writeFileSync(new URL('./shots/skills-seat.png', import.meta.url), seatBase); await page.screenshot({ path: `${SHOTS}skills-bar-ready.png`, timeout: 60000 }); check(ready.art[0].state === 'ready', `a live, charged, affordable seat reads ready (got ${ready.art[0].state})`); diff --git a/js/scenes/GameScene.js b/js/scenes/GameScene.js index 8b3a2aa..b53b223 100644 --- a/js/scenes/GameScene.js +++ b/js/scenes/GameScene.js @@ -8431,14 +8431,14 @@ export class GameScene extends Phaser.Scene { /** * The skill icons the bar and the console draw (loaded once, on demand, the way * every other UI sheet in the game is). A missing file is not an error: the seats - * keep their frame, veil, sweep and rank stamp and simply draw no icon. + * keep their frame, veil and sweep and simply draw no icon. */ _ensureSkillIcons() { if (this._skillIconsTried) return; const sheet = skillIconSheet(); // The path is the data's (data/skills.json → icons.texture). No path, or a // sheet that never loads, is not an error — the seats keep their frame, veil, - // sweep and rank stamp and simply draw no icon. + // sweep and seconds and simply draw no icon. if (!sheet.texture) { this._skillIconsTried = true; return; @@ -8451,7 +8451,7 @@ export class GameScene extends Phaser.Scene { // issued while another load runs (the deferred comms/media pass, the scene // restart's own queue) is dropped — so this waits for the idle beat instead of // losing the sheet. A handful of beats, then it gives up and the bar draws - // seats without icons (frame, veil, sweep and rank stamp still tell the story). + // seats without icons (frame, veil and sweep still tell the story). if (this.load.isLoading?.() === true) { this._skillIconBeats = (this._skillIconBeats ?? 0) + 1; if (this._skillIconBeats <= 40) this.time.delayedCall(250, () => this._ensureSkillIcons()); diff --git a/js/ui/CombatBar.js b/js/ui/CombatBar.js index 23cbd90..a46becf 100644 --- a/js/ui/CombatBar.js +++ b/js/ui/CombatBar.js @@ -71,9 +71,9 @@ function orbDotKey(scene) { * the run's bindings live in the skill ledger (js/skills/SkillState.js), * a press lands on o.onCast(index) → GameScene.castSkillSeat → the * fight's cast(), and the fight answers every press with a reason. A - * seat draws its skill's icon (assets/images/skills.png), its rank - * stamp, a recharge sweep with the seconds left, a dimmed "cannot - * afford it" veil and an inert veil for a skill nobody has ranked; + * seat draws its skill's icon (assets/images/skills.png), a recharge + * sweep with the seconds left, a dimmed "cannot afford it" veil and + * an inert veil for a skill nobody has ranked; * unbound seats keep their reserved look. RIGHT-pressing a seat clears * it (making room), and while the skill tree is holding a skill up the * seats breathe and a press PLACES the skill instead of casting. @@ -274,8 +274,8 @@ export class CombatBar extends Phaser.GameObjects.Container { // layer only draws what each one looks like, and every colour, size and pulse // rate is data-owned (data/skills.json → bar). Draw order per seat: the icon, // a VEIL over it (an unranked skill is buried, a too-expensive one dimmed), - // the recharge SWEEP with the SECONDS left printed over that, and the RANK - // stamp in the corner. An empty seat draws nothing here — its reserved frame + // the recharge SWEEP with the SECONDS left printed over that. An empty seat + // draws nothing here — its reserved frame // (above) with its key number IS the empty look. this.seatArt = []; for (let i = 0; i < n; i++) { @@ -300,26 +300,12 @@ export class CombatBar extends Phaser.GameObjects.Container { .setScrollFactor(0) .setDepth(DEPTH + 1) .setVisible(false); - const rankT = scene.add - .text(lx + S - 3, ly + S - 3, '', { - fontFamily: fontStack('body', FONT_FALLBACK), - fontSize: `${this._barSpec.rankTextPx}px`, - fontStyle: 'bold', - color: this._barSpec.states.rankColor, - stroke: '#02070d', - strokeThickness: 2.5, - }) - .setOrigin(1, 1) - .setScrollFactor(0) - .setDepth(DEPTH + 1) - .setVisible(false); this.add(veil); this.add(sweep); this.add(edge); this.add(secs); - this.add(rankT); this.seatArt.push({ - lx, ly, veil, sweep, edge, secs, rankT, + lx, ly, veil, sweep, edge, secs, state: 'empty', // what the seat is drawing (see _drawSeat) icon: null, // created on the first seat that carries a skill sig: '', // last draw signature — unchanged seats are skipped @@ -622,9 +608,8 @@ export class CombatBar extends Phaser.GameObjects.Container { edge.strokeRoundedRect(...boxIn, rad); } - // 5 — the printed seconds + the rank stamp. + // 5 — the printed seconds. art.secs.setVisible(cooling).setText(secs); - art.rankT.setVisible(bound).setText(`${row.rank}/${row.maxPoints}`); } /** The seat's icon image, made on first use (the sheet is loaded once). */ @@ -646,10 +631,10 @@ export class CombatBar extends Phaser.GameObjects.Container { this.add(art.icon); // The icon is made LAZILY (mid-frame, on the first seat that needs it), so it // lands on top of the seat's state layers and an opaque sheet would bury the - // veil, the sweep, the seconds and the rank stamp. Put those back above it — - // veil → sweep → edge → seconds → rank, the order the data describes + // veil, the sweep and the seconds. Put those back above it — + // veil → sweep → edge → seconds, the order the data describes // (data/skills.json → bar._comment). The icon never moves again afterwards. - for (const go of [art.veil, art.sweep, art.edge, art.secs, art.rankT]) { + for (const go of [art.veil, art.sweep, art.edge, art.secs]) { if (!go) continue; if (typeof this.bringToTop === 'function') this.bringToTop(go); else { @@ -801,7 +786,7 @@ export class CombatBar extends Phaser.GameObjects.Container { this.onSeatUnbind = null; for (const art of this.seatArt ?? []) { art.icon = null; - art.veil = art.sweep = art.edge = art.secs = art.rankT = null; + art.veil = art.sweep = art.edge = art.secs = null; } this.seatArt = null; this.xpLabel = null;