Light up BUILT rows and name the world each build was installed on
- BUILT rows are now lit — accent fill, glow ring, icon halo, and a stamped seal — so installed upgrades read as done at a glance. - The readout now names the world a build was actually installed on (new BuildState.builtOn): ship upgrades show "SHIP-WIDE · INSTALLED FROM KEPLER-186F" instead of the planet the console sits on. - BUILT outranks BUSY, so an installed upgrade no longer reads as waiting while another build is in flight; its cost line reads PAID. - Category tabs gain progress pips: filled = built, pulsing = in progress, hollow = not yet. - Dev-only headless harness (dev/build-shot.html/.mjs) boots the game and stages the full row-state matrix for one-frame screenshots. - Refresh the planet sheet artwork (planets.png/.psd).
This commit is contained in:
parent
831670c8ca
commit
63ea50928d
Binary file not shown.
|
Before Width: | Height: | Size: 4.7 MiB After Width: | Height: | Size: 5.1 MiB |
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,17 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<base href="../" />
|
||||
<title>Orbit — Build console (dev shot)</title>
|
||||
<style>
|
||||
html, body { margin: 0; height: 100%; background: #04060d; overflow: hidden; }
|
||||
#game { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; }
|
||||
</style>
|
||||
<script src="lib/phaser.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="game"></div>
|
||||
<script type="module" src="dev/build-shot.mjs"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
/**
|
||||
* Dev-only: boot the game (menu → new game → land on the home world),
|
||||
* open the BUILD console and stage the full row-state matrix, so a
|
||||
* headless screenshot shows every indicator in one frame:
|
||||
*
|
||||
* PLANET tab — tether-l1 BUILT (the lit row: soft accent fill, glow
|
||||
* ring, icon halo, the stamped seal, "INSTALLED ON …"),
|
||||
* tether-l2 ACTIVE (progress bar + the pulsing tab pip),
|
||||
* tether-l3 LOCKED (missing requirements);
|
||||
* MINING tab — improved arm BUILT ship-wide (installed on ANOTHER
|
||||
* world: "SHIP-WIDE · INSTALLED FROM KEPLER-186F"),
|
||||
* advanced arm READY, storage LOCKED.
|
||||
*
|
||||
* node dev/server.mjs 8080
|
||||
* chrome --headless --disable-gpu --remote-debugging-port=9333 \
|
||||
* --window-size=1280,720 about:blank
|
||||
* # planet tab (tether-l1 selected — the BUILT detail readout):
|
||||
* node dev/cdp-shot.mjs "http://127.0.0.1:8080/dev/build-shot.html" out.png \
|
||||
* "!!window.__BUILD_SHOT?.ready" 25000 \
|
||||
* "document.getElementById('report').style.display='none'; true"
|
||||
* # mining tab (improved arm selected — the SHIP-WIDE built readout):
|
||||
* node dev/cdp-shot.mjs "http://127.0.0.1:8080/dev/build-shot.html" out-mining.png \
|
||||
* "!!window.__BUILD_SHOT?.ready" 25000 \
|
||||
* "window.__buildShotMining(); document.getElementById('report').style.display='none'; true"
|
||||
*/
|
||||
import Phaser from '../js/vendor/phaser.js';
|
||||
import { config } from '../js/config/Config.js';
|
||||
import { ConfigLoader } from '../js/config/ConfigLoader.js';
|
||||
import { createGameConfig } from '../js/config/GameConfig.js';
|
||||
import { MenuScene } from '../js/scenes/MenuScene.js';
|
||||
import { GameScene } from '../js/scenes/GameScene.js';
|
||||
import { SurfaceScene } from '../js/scenes/SurfaceScene.js';
|
||||
|
||||
const data = await ConfigLoader.load();
|
||||
config.init(data);
|
||||
|
||||
// Capture console errors + uncaught exceptions for the report.
|
||||
const errors = [];
|
||||
const origErr = console.error.bind(console);
|
||||
console.error = (...a) => { errors.push(a.map(String).join(' ')); origErr(...a); };
|
||||
window.addEventListener('error', (e) => errors.push(String(e.message)));
|
||||
window.addEventListener('unhandledrejection', (e) => errors.push(`rejection: ${e.reason}`));
|
||||
|
||||
const gameConfig = createGameConfig();
|
||||
gameConfig.scene = [MenuScene, GameScene, SurfaceScene];
|
||||
const game = new Phaser.Game(gameConfig);
|
||||
window.game = game;
|
||||
|
||||
// The report lives OUTSIDE the canvas — a DOM <pre> the screenshot can
|
||||
// always read (headless canvases don't have to cooperate).
|
||||
const report = document.createElement('pre');
|
||||
report.id = 'report';
|
||||
report.style.cssText = 'position:fixed;left:10px;top:10px;z-index:9999;max-width:70%;margin:0;padding:8px 12px;font:13px/1.5 monospace;color:#eaf6ff;background:rgba(6,20,16,0.92);border:1px solid #1b3a5a;white-space:pre-wrap;';
|
||||
document.body.appendChild(report);
|
||||
const setReport = (lines) => { report.textContent = (Array.isArray(lines) ? lines : [lines]).join('\n'); };
|
||||
setReport('booting… (menu → new game → home world → build console)');
|
||||
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
async function waitFor(fn, tries = 150, everyMs = 100) {
|
||||
for (let i = 0; i < tries; i++) {
|
||||
const v = fn();
|
||||
if (v) return v;
|
||||
await sleep(everyMs);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// 1) The real menu flow: NEW GAME (fresh seed, registry reset).
|
||||
const menu = await waitFor(() => { const m = game.scene.getScene('MenuScene'); return m?.newGameBtn ? m : null; });
|
||||
if (!menu) throw new Error('menu never booted');
|
||||
menu.startNewGame();
|
||||
const gs = await waitFor(() => { const g = game.scene.getScene('GameScene'); return g?.ship && g?.buildState ? g : null; }, 200);
|
||||
if (!gs) throw new Error('GameScene never booted');
|
||||
await sleep(800);
|
||||
|
||||
// 2) Land on the home world the game's own way.
|
||||
gs.startLanding(gs.commsTargetFor(gs.planet));
|
||||
const ss = await waitFor(() => { const c = game.scene.getScene('SurfaceScene'); return c?.buildWindow ? c : null; });
|
||||
if (!ss) throw new Error('surface never booted');
|
||||
await sleep(700);
|
||||
const win = ss.buildWindow;
|
||||
const home = win.planetName;
|
||||
|
||||
// 3) Stage the state matrix (the scene's own seams — the same calls a
|
||||
// player's run makes): blueprints researched, a ship upgrade
|
||||
// installed on ANOTHER world, the level-2 tether build in flight.
|
||||
gs.ship.setMinerals(1000);
|
||||
gs.researchState.unlock('exploration', 'tether_l2');
|
||||
gs.researchState.unlock('mining', 'improved_arm');
|
||||
gs.researchState.unlock('mining', 'advanced_arm');
|
||||
gs.buildState.markBuilt('Kepler-186f', 'mining-arm-improved'); // ship-wide, from elsewhere
|
||||
const began = gs.beginBuild(home, 'tether-l2');
|
||||
if (!began.ok) throw new Error(`beginBuild refused: ${began.reason}`);
|
||||
|
||||
// 4) Open the console and settle the boot reveal.
|
||||
win.open();
|
||||
await waitFor(() => win.openState === 'open' && !win.reveal.length, 60, 100);
|
||||
await sleep(300);
|
||||
win.refresh();
|
||||
win.select('planet', 'tether-l1'); // the BUILT row's detail readout
|
||||
await sleep(300);
|
||||
|
||||
// The report: row states, pip states, the installed lines, errors.
|
||||
const { rowState } = await import('../js/build/BuildModel.js');
|
||||
const planetList = win.lists.get('planet');
|
||||
const miningList = win.lists.get('mining');
|
||||
const st = (list, id) =>
|
||||
`${id}=${rowState(list.entry.builds[id], win.ctxFor(id), win._activeOnPlanet(), id).toUpperCase()}`;
|
||||
const pips = (tab) => tab.pips.map((p) => `${p.id.slice(0, 6)}:${p.state}`).join(' ');
|
||||
const lines = [
|
||||
errors.length === 0 ? 'SMOKE OK — no console errors' : `ERRORS:\n${errors.slice(0, 3).join('\n')}`,
|
||||
`window: ${win.openState} · cat=${win.activeCat} · sel=${win.selected ? win.selected.category + '/' + win.selected.id : '—'}`,
|
||||
`planet rows: ${planetList.entry.order.map((id) => st(planetList, id)).join(' ')}`,
|
||||
`mining rows: ${miningList.entry.order.map((id) => st(miningList, id)).join(' ')}`,
|
||||
`planet pips: ${pips(win.tabs.find((t) => t.id === 'planet'))}`,
|
||||
`mining pips: ${pips(win.tabs.find((t) => t.id === 'mining'))}`,
|
||||
`builtOn(mining-arm-improved): ${gs.buildState.builtOn('mining-arm-improved')}`,
|
||||
`builtOn(tether-l1): ${gs.buildState.builtOn('tether-l1')}`,
|
||||
`installed line (arm): ${win._installedLine('mining-arm-improved')}`,
|
||||
`installed line (l1): ${win._installedLine('tether-l1')}`,
|
||||
];
|
||||
setReport(lines);
|
||||
window.__BUILD_SHOT = { ready: true, lines, errors };
|
||||
// second shot: flip to the MINING tab with the ship-wide BUILT arm selected
|
||||
window.__buildShotMining = () => {
|
||||
win.switchCategory('mining');
|
||||
win.select('mining', 'mining-arm-improved');
|
||||
win.refresh();
|
||||
};
|
||||
console.info('build-shot: report painted');
|
||||
} catch (err) {
|
||||
errors.push(`FATAL: ${err.message}`);
|
||||
setReport(['FATAL: ' + err.message, ...errors.slice(0, 3)]);
|
||||
window.__BUILD_SHOT = { ready: true, fatal: String(err.message), errors };
|
||||
console.error('build-shot: fatal', err);
|
||||
}
|
||||
|
|
@ -167,6 +167,13 @@ check('state: isBuiltAnywhere sees a ship-scoped build installed on ANY planet',
|
|||
check('state: isBuiltAnywhere is false for an uninstalled build', shipS.isBuiltAnywhere('mining-arm-advanced') === false);
|
||||
check('state: the per-planet isBuilt is unchanged (the record still keys the planet)', shipS.isBuilt('Kepler-186f', 'mining-arm-improved') === true && shipS.isBuilt('Home World', 'mining-arm-improved') === false);
|
||||
|
||||
// builtOn — the world a build was installed on (the console's
|
||||
// "INSTALLED ON …" / "SHIP-WIDE · INSTALLED FROM …" lines name this
|
||||
// world, never the planet the console happens to sit on)
|
||||
check('state: builtOn names the world the build was installed on', shipS.builtOn('mining-arm-improved') === 'Kepler-186f');
|
||||
check('state: builtOn is null for an uninstalled build', shipS.builtOn('mining-arm-advanced') === null);
|
||||
check('state: builtOn is null for unknown ids', shipS.builtOn(null) === null);
|
||||
|
||||
check('model: rowState reads built for a mining build (ship-scoped ctx)', rowState(armImproved, ctx({ miningResearch: ['improved_arm'], isBuilt: () => true }), null, 'mining-arm-improved') === 'built');
|
||||
check('model: rowState reads available for a researched mining build', rowState(armImproved, ctx({ miningResearch: ['improved_arm'] }), null, 'mining-arm-improved') === 'available');
|
||||
check('model: rowState reads locked before the research', rowState(armImproved, ctx(), null, 'mining-arm-improved') === 'locked');
|
||||
|
|
|
|||
|
|
@ -62,6 +62,20 @@ export class BuildState {
|
|||
return [...(this.built.get(planet) ?? [])];
|
||||
}
|
||||
|
||||
/**
|
||||
* The world a build was installed on (the record holding it; null when
|
||||
* never built). One-off builds land on exactly one world, so the first
|
||||
* hit is the record. The console's "INSTALLED ON …" / "INSTALLED FROM …"
|
||||
* lines name THIS world (BuildWindow._installedLine) — a ship upgrade
|
||||
* built on Kepler shows "FROM KEPLER" wherever the ship lands, not the
|
||||
* planet the console happens to sit on.
|
||||
*/
|
||||
builtOn(build) {
|
||||
if (!build) return null;
|
||||
for (const [planet, set] of this.built) if (set.has(build)) return planet;
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── the single in-progress build ─────────────────────────────────────────
|
||||
getActive() {
|
||||
return this.active;
|
||||
|
|
|
|||
|
|
@ -77,7 +77,8 @@ const C = {
|
|||
/**
|
||||
* Draw a cut-corner plate with its top-left at (x, y) into an existing
|
||||
* Graphics (CyberShape.points is centred on (0,0) — translate by the
|
||||
* plate centre). Mirrors ResearchWindow.panel().
|
||||
* plate centre). Mirrors ResearchWindow.panel(), including the wide,
|
||||
* low-alpha `glow` stroke pass (the "powered" ring under a lit plate).
|
||||
*/
|
||||
function panel(g, x, y, w, h, o = {}) {
|
||||
const cx = x + w / 2;
|
||||
|
|
@ -93,6 +94,10 @@ function panel(g, x, y, w, h, o = {}) {
|
|||
g.lineStyle(o.lineWidth ?? 1.5, o.stroke, o.strokeAlpha ?? 1);
|
||||
g.strokePoints(pts, true);
|
||||
}
|
||||
if (o.glow !== undefined) {
|
||||
g.lineStyle((o.lineWidth ?? 1.5) + 4, o.glow, o.glowAlpha ?? 0.22);
|
||||
g.strokePoints(pts, true);
|
||||
}
|
||||
}
|
||||
|
||||
/** Corner brackets framing a rect (x, y = top-left). */
|
||||
|
|
@ -113,6 +118,29 @@ function brackets(g, x, y, w, h, o = {}) {
|
|||
g.lineBetween(x2, y2, x2, y2 - len);
|
||||
}
|
||||
|
||||
/**
|
||||
* Radial halo texture (white → tintable per row) — the "powered" glow
|
||||
* behind a built row's icon. Mirrors ResearchWindow's halo (same radial
|
||||
* recipe, own key — the texture registry is game-global, so both
|
||||
* windows coexist). Built once per scene.
|
||||
*/
|
||||
function ensureHaloTexture(scene) {
|
||||
const key = 'build_halo';
|
||||
if (scene.textures?.exists(key)) return key;
|
||||
const c = document.createElement('canvas');
|
||||
c.width = 64;
|
||||
c.height = 64;
|
||||
const ctx = c.getContext('2d');
|
||||
const grad = ctx.createRadialGradient(32, 32, 2, 32, 32, 32);
|
||||
grad.addColorStop(0, 'rgba(255,255,255,0.9)');
|
||||
grad.addColorStop(0.4, 'rgba(255,255,255,0.28)');
|
||||
grad.addColorStop(1, 'rgba(255,255,255,0)');
|
||||
ctx.fillStyle = grad;
|
||||
ctx.fillRect(0, 0, 64, 64);
|
||||
scene.textures.addCanvas(key, c);
|
||||
return key;
|
||||
}
|
||||
|
||||
export class BuildWindow extends Phaser.GameObjects.Container {
|
||||
static VIDEO_KEY = 'build_console';
|
||||
|
||||
|
|
@ -209,6 +237,17 @@ export class BuildWindow extends Phaser.GameObjects.Container {
|
|||
return a && a.planet === this.planetName ? a : null;
|
||||
}
|
||||
|
||||
/** The "where is it" line for an installed build — scope-aware:
|
||||
* planet-scoped → INSTALLED ON <world>; ship-scoped → SHIP-WIDE,
|
||||
* named after the world it was installed FROM (BuildState.builtOn),
|
||||
* never the planet the console happens to sit on (the old line said
|
||||
* "INSTALLED ON <here>" for a ship upgrade built on another world). */
|
||||
_installedLine(id) {
|
||||
const world = String(this.state?.builtOn(id) ?? this.planetName ?? '');
|
||||
if (isShipScoped(defById(id))) return `SHIP-WIDE · INSTALLED FROM ${world.toUpperCase()}`;
|
||||
return `INSTALLED ON ${world.toUpperCase()}`;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ geometry
|
||||
_measure() {
|
||||
const W = this.scene.scale.width;
|
||||
|
|
@ -544,10 +583,19 @@ export class BuildWindow extends Phaser.GameObjects.Container {
|
|||
letterSpacing: 2,
|
||||
})
|
||||
.setScrollFactor(0);
|
||||
const w = txt.width + 44;
|
||||
// progress pips — one per build in this category: filled = built,
|
||||
// pulsing = in progress, hollow = not yet (the research tab idiom).
|
||||
// loadBuilds mirrors what _buildLists paints, so the pip count
|
||||
// tracks the row list even though the lists are built after the tabs.
|
||||
const list = loadBuilds(cat.id);
|
||||
const nPips = list.order.length;
|
||||
const pipS = 5;
|
||||
const pipGap = 4;
|
||||
const pipSpan = nPips ? nPips * pipS + (nPips - 1) * pipGap : 0;
|
||||
const w = txt.width + 44 + (pipSpan ? pipSpan + 14 : 0);
|
||||
const y = bodyY + tabH / 2;
|
||||
const g = this.scene.add.graphics().setScrollFactor(0);
|
||||
const tab = { id: cat.id, x, y, w, h: tabH, txt, g, accent, hover: false };
|
||||
const tab = { id: cat.id, x, y, w, h: tabH, txt, g, accent, hover: false, pips: [] };
|
||||
const tabRect = new Phaser.Geom.Rectangle(x, y - tabH / 2, w, tabH);
|
||||
g.setInteractive({
|
||||
useHandCursor: true,
|
||||
|
|
@ -568,6 +616,21 @@ export class BuildWindow extends Phaser.GameObjects.Container {
|
|||
this.switchCategory(cat.id);
|
||||
});
|
||||
txt.setPosition(x + 26, y - 4);
|
||||
const pipX0 = x + 26 + txt.width + 14;
|
||||
const pipY = bodyY + tabH / 2 - 1;
|
||||
for (let i = 0; i < nPips; i++) {
|
||||
const pg = this.scene.add.graphics().setScrollFactor(0).setDepth(2);
|
||||
this.add(pg);
|
||||
tab.pips.push({
|
||||
g: pg,
|
||||
id: list.order[i],
|
||||
def: list.builds[list.order[i]],
|
||||
x: pipX0 + i * (pipS + pipGap),
|
||||
y: pipY,
|
||||
s: pipS,
|
||||
state: 'todo',
|
||||
});
|
||||
}
|
||||
g.setDepth(2);
|
||||
txt.setDepth(3);
|
||||
this.add(g);
|
||||
|
|
@ -606,6 +669,33 @@ export class BuildWindow extends Phaser.GameObjects.Container {
|
|||
t.g.fillTriangle(dx, dy - 4.5, dx + 4.5, dy, dx, dy + 4.5, dx - 4.5, dy);
|
||||
t.txt.setColor(active ? toCss(t.accent) : toCss(C.ink));
|
||||
t.txt.setAlpha(active ? 1 : t.hover ? 0.9 : 0.62);
|
||||
|
||||
// progress pips — state from BuildState (a cheap repaint: a few
|
||||
// 5 px squares each; update() only pulses the in-progress one)
|
||||
const run = this._activeOnPlanet();
|
||||
for (const p of t.pips) {
|
||||
p.g.clear();
|
||||
// ship-scoped builds count as built wherever the ship lands;
|
||||
// planet-scoped ones only on this world (the row rule, mirrored)
|
||||
const built = p.def && isShipScoped(p.def)
|
||||
? (this.state?.isBuiltAnywhere(p.id) ?? false)
|
||||
: (this.state?.isBuilt(this.planetName, p.id) ?? false);
|
||||
const st = built ? 'done' : run && run.build === p.id ? 'progress' : 'todo';
|
||||
p.state = st;
|
||||
if (st === 'done') {
|
||||
p.g.fillStyle(t.accent, 1);
|
||||
p.g.fillRect(p.x, p.y - p.s / 2, p.s, p.s);
|
||||
p.g.setAlpha(active ? 1 : 0.8);
|
||||
} else if (st === 'progress') {
|
||||
p.g.fillStyle(t.accent, 1);
|
||||
p.g.fillRect(p.x, p.y - p.s / 2, p.s, p.s);
|
||||
p.g.setAlpha(0.35); // update() drives the pulse
|
||||
} else {
|
||||
p.g.lineStyle(1, active ? 0x6284b0 : 0x3d4c74, active ? 0.95 : 0.6);
|
||||
p.g.strokeRect(p.x, p.y - p.s / 2, p.s, p.s);
|
||||
p.g.setAlpha(active ? 1 : 0.65);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -635,6 +725,14 @@ export class BuildWindow extends Phaser.GameObjects.Container {
|
|||
const y = startY + i * (rowH + rowGap);
|
||||
const w = rightW - 16;
|
||||
const g = this.scene.add.graphics().setScrollFactor(0);
|
||||
// the powered halo behind the icon — "built" rows drive its
|
||||
// alpha in _paintRow (the research lit-node idiom)
|
||||
const halo = this.scene.add
|
||||
.image(x + 34, y + rowH / 2, ensureHaloTexture(this.scene))
|
||||
.setDisplaySize(56, 56)
|
||||
.setScrollFactor(0)
|
||||
.setBlendMode(Phaser.BlendModes.ADD)
|
||||
.setAlpha(0);
|
||||
const icon = this.scene.add
|
||||
.image(x + 34, y + rowH / 2, ensureIcon(this.scene, def.icon ?? 'diamond', accent))
|
||||
.setDisplaySize(40, 40)
|
||||
|
|
@ -668,7 +766,7 @@ export class BuildWindow extends Phaser.GameObjects.Container {
|
|||
.setScrollFactor(0);
|
||||
tag.setOrigin(1, 0);
|
||||
const bar = this.scene.add.graphics().setScrollFactor(0);
|
||||
const row = { id, def, x, y, w, h: rowH, g, icon, label, sub, tag, bar, hover: false, accent };
|
||||
const row = { id, def, x, y, w, h: rowH, g, halo, icon, label, sub, tag, bar, hover: false, accent };
|
||||
const hit = new Phaser.Geom.Rectangle(x, y, w, rowH);
|
||||
g.setInteractive({
|
||||
useHandCursor: true,
|
||||
|
|
@ -689,6 +787,7 @@ export class BuildWindow extends Phaser.GameObjects.Container {
|
|||
this.select(cat.id, id);
|
||||
});
|
||||
g.setDepth(2);
|
||||
halo.setDepth(2.5); // over the plate (2), under the icon (3)
|
||||
icon.setDepth(3);
|
||||
label.setDepth(3);
|
||||
sub.setDepth(3);
|
||||
|
|
@ -696,7 +795,7 @@ export class BuildWindow extends Phaser.GameObjects.Container {
|
|||
bar.setDepth(3);
|
||||
// v4 Container.add(child, index) — multi-add is the ARRAY form
|
||||
// (add(a, b, c) would silently drop every child after `a`).
|
||||
cont.add([g, icon, label, sub, tag, bar]);
|
||||
cont.add([g, halo, icon, label, sub, tag, bar]);
|
||||
rows.push(row);
|
||||
}
|
||||
|
||||
|
|
@ -736,9 +835,11 @@ export class BuildWindow extends Phaser.GameObjects.Container {
|
|||
const w = row.w;
|
||||
const h = row.h;
|
||||
|
||||
let fill = C.panel;
|
||||
let fillA = 0.4;
|
||||
let stroke = 0x22405f;
|
||||
let strokeA = 0.55;
|
||||
let glowA = 0;
|
||||
let labelColor = toCss(C.faint);
|
||||
let subColor = toCss(C.faint);
|
||||
let tagColor = toCss(C.faint);
|
||||
|
|
@ -781,6 +882,7 @@ export class BuildWindow extends Phaser.GameObjects.Container {
|
|||
fillA = 0.2;
|
||||
stroke = row.accent;
|
||||
strokeA = 1;
|
||||
glowA = 0.25;
|
||||
labelColor = toCss(0xffffff);
|
||||
subColor = toCss(row.accent);
|
||||
tagColor = toCss(row.accent);
|
||||
|
|
@ -791,16 +893,21 @@ export class BuildWindow extends Phaser.GameObjects.Container {
|
|||
sub = `IN PROGRESS · ${pct}% · ${Math.ceil((p?.remainingMs ?? 0) / 1000)}S LEFT`;
|
||||
tag = `${pct}%`;
|
||||
} else if (st === 'built') {
|
||||
fillA = 0.18;
|
||||
// LIT — the research "unlocked" idiom: a soft accent fill, a glow
|
||||
// ring, the icon halo and the stamped seal. Reads "installed" at a
|
||||
// glance; the sub line names the scope (planet vs ship-wide).
|
||||
fill = row.accent;
|
||||
fillA = 0.13;
|
||||
stroke = row.accent;
|
||||
strokeA = 0.55;
|
||||
strokeA = 0.95;
|
||||
glowA = 0.3;
|
||||
labelColor = toCss(C.ink);
|
||||
subColor = toCss(C.faint);
|
||||
tagColor = toCss(C.dim);
|
||||
tag = 'BUILT ✓';
|
||||
tagColor = toCss(row.accent);
|
||||
tag = 'BUILT';
|
||||
iconTint = 0xffffff;
|
||||
iconAlpha = 0.85;
|
||||
sub = `INSTALLED ON ${String(this.planetName ?? '').toUpperCase()}`;
|
||||
iconAlpha = 1;
|
||||
sub = this._installedLine(row.id);
|
||||
} else {
|
||||
// locked — grayed out, with the missing requirements
|
||||
const missing = missingRequirements(row.def, this.ctxFor(row.id));
|
||||
|
|
@ -808,7 +915,19 @@ export class BuildWindow extends Phaser.GameObjects.Container {
|
|||
}
|
||||
if (row.hover) fillA = Math.min(0.9, fillA + 0.12);
|
||||
|
||||
panel(g, row.x, row.y, w, h, { notch: 10, fill: C.panel, fillAlpha: fillA, stroke, strokeAlpha: strokeA });
|
||||
panel(g, row.x, row.y, w, h, {
|
||||
notch: 10,
|
||||
fill,
|
||||
fillAlpha: fillA,
|
||||
stroke,
|
||||
strokeAlpha: strokeA,
|
||||
...(glowA ? { glow: row.accent, glowAlpha: glowA } : {}),
|
||||
});
|
||||
// the powered halo behind the icon (research: 0.5 lit, 0.32 active)
|
||||
if (row.halo) {
|
||||
row.halo.setTint(row.accent);
|
||||
row.halo.setAlpha(st === 'built' ? 0.5 : st === 'active' ? 0.32 : 0);
|
||||
}
|
||||
row.icon.setTint(iconTint);
|
||||
row.icon.setAlpha(iconAlpha);
|
||||
row.label.setColor(labelColor);
|
||||
|
|
@ -817,6 +936,19 @@ export class BuildWindow extends Phaser.GameObjects.Container {
|
|||
row.tag.setText(tag);
|
||||
row.tag.setColor(tagColor);
|
||||
|
||||
// the stamped seal (built rows) — the research "certified" glyph:
|
||||
// an accent diamond with the check knocked out, left of the BUILT tag
|
||||
if (st === 'built') {
|
||||
const { rightX, rightW } = this.geo;
|
||||
const gx = rightX + rightW - 26 - row.tag.width - 12;
|
||||
const gy = row.y + h / 2;
|
||||
g.fillStyle(row.accent, 1);
|
||||
g.fillTriangle(gx, gy - 6, gx + 6, gy, gx, gy + 6, gx - 6, gy);
|
||||
g.lineStyle(1.8, 0x050b14, 1);
|
||||
g.lineBetween(gx - 3.2, gy + 0.8, gx - 0.9, gy + 3.2);
|
||||
g.lineBetween(gx - 0.9, gy + 3.2, gx + 3.2, gy - 2.8);
|
||||
}
|
||||
|
||||
// selection brackets
|
||||
const sel = this.selected;
|
||||
if (sel && sel.category === this.activeCat && sel.id === row.id) {
|
||||
|
|
@ -850,6 +982,7 @@ export class BuildWindow extends Phaser.GameObjects.Container {
|
|||
_paintAll(catId) {
|
||||
const list = this.lists.get(catId);
|
||||
if (!list) return;
|
||||
this._paintTabs(); // the tab pips track the same state (a build flipping to BUILT fills one)
|
||||
for (const row of list.rows) this._paintRow(row);
|
||||
}
|
||||
|
||||
|
|
@ -1080,12 +1213,15 @@ export class BuildWindow extends Phaser.GameObjects.Container {
|
|||
const state = this.state;
|
||||
if (!sel || !state) return 'locked';
|
||||
const active = state.getActive();
|
||||
const def = this._selDef();
|
||||
// BUILT outranks BUSY: an installed build isn't waiting on the
|
||||
// console — it's done (the research console reads OWNED the same
|
||||
// way, mid-project; the deck bar carries the in-flight status).
|
||||
if (def && (isShipScoped(def) ? state.isBuiltAnywhere(sel.id) : state.isBuilt(this.planetName, sel.id))) return 'built';
|
||||
if (active) {
|
||||
if (active.planet === this.planetName && active.build === sel.id) return 'active';
|
||||
return 'busy';
|
||||
}
|
||||
const def = this._selDef();
|
||||
if (isShipScoped(def) ? state.isBuiltAnywhere(sel.id) : state.isBuilt(this.planetName, sel.id)) return 'built';
|
||||
if (def && isAvailable(def, this.ctxFor(sel.id))) {
|
||||
return canAfford(def, this.minerals()) ? 'build' : 'unaffordable';
|
||||
}
|
||||
|
|
@ -1178,7 +1314,7 @@ export class BuildWindow extends Phaser.GameObjects.Container {
|
|||
meta = `IN PROGRESS · ${Math.round((p?.fraction ?? 0) * 100)}% · ${Math.ceil((p?.remainingMs ?? 0) / 1000)}S LEFT · DURATION ${durStr}`;
|
||||
metaColor = accent;
|
||||
} else if (st === 'built') {
|
||||
meta = `INSTALLED ON ${String(this.planetName ?? '').toUpperCase()} · DURATION ${durStr}`;
|
||||
meta = `${this._installedLine(sel.id)} · DURATION ${durStr}`;
|
||||
metaColor = accent;
|
||||
} else if (st === 'busy') {
|
||||
meta = `BUILD CONSOLE BUSY — ONE BUILD AT A TIME · DURATION ${durStr}`;
|
||||
|
|
@ -1193,7 +1329,8 @@ export class BuildWindow extends Phaser.GameObjects.Container {
|
|||
this.dMeta.setText(meta);
|
||||
this.dMeta.setColor(toCss(metaColor));
|
||||
|
||||
// cost line — the highlighted resource requirement (minerals)
|
||||
// cost line — the highlighted resource requirement (minerals); a
|
||||
// BUILT build shows it dimmed + PAID (the cost already landed)
|
||||
const lines = costLines(def);
|
||||
if (!lines.length) {
|
||||
this.dCost.setText('COST — NO COST · STARTING EQUIPMENT');
|
||||
|
|
@ -1202,9 +1339,14 @@ export class BuildWindow extends Phaser.GameObjects.Container {
|
|||
const costStr = lines
|
||||
.map((l) => `${l.amount} ${String(config.get(`builds.resources.${l.res}.label`, l.res))}`)
|
||||
.join(' + ');
|
||||
const afford = canAfford(def, this.minerals());
|
||||
this.dCost.setText(`COST ◆ ${costStr}${afford ? '' : ' · INSUFFICIENT MINERALS'}`);
|
||||
this.dCost.setColor(toCss(afford ? C.neon : C.amber));
|
||||
if (st === 'built') {
|
||||
this.dCost.setText(`COST ◆ ${costStr} · PAID`);
|
||||
this.dCost.setColor(toCss(C.faint));
|
||||
} else {
|
||||
const afford = canAfford(def, this.minerals());
|
||||
this.dCost.setText(`COST ◆ ${costStr}${afford ? '' : ' · INSUFFICIENT MINERALS'}`);
|
||||
this.dCost.setColor(toCss(afford ? C.neon : C.amber));
|
||||
}
|
||||
}
|
||||
|
||||
this._paintDetailBtn();
|
||||
|
|
@ -1323,6 +1465,7 @@ export class BuildWindow extends Phaser.GameObjects.Container {
|
|||
this.tabs.forEach((t, i) => {
|
||||
push(t.g, 300 + i * 70, 240, 'fade');
|
||||
push(t.txt, 300 + i * 70, 240, 'fade');
|
||||
for (const p of t.pips) push(p.g, 300 + i * 70, 240, 'fade');
|
||||
});
|
||||
const entry = this.activeCat ? this.lists.get(this.activeCat) : null;
|
||||
if (entry) {
|
||||
|
|
@ -1330,6 +1473,7 @@ export class BuildWindow extends Phaser.GameObjects.Container {
|
|||
// baseY = o.y (0) — the panel shape is drawn at absolute coords,
|
||||
// so the object itself must not be repositioned.
|
||||
push(row.g, 380 + i * 90, 260, 'rise');
|
||||
push(row.halo, 400 + i * 90, 240, 'fade');
|
||||
push(row.icon, 400 + i * 90, 240, 'fade');
|
||||
push(row.label, 400 + i * 90, 240, 'fade');
|
||||
push(row.sub, 400 + i * 90, 240, 'fade');
|
||||
|
|
@ -1469,6 +1613,17 @@ export class BuildWindow extends Phaser.GameObjects.Container {
|
|||
this._lastMinerals = m;
|
||||
if (this.selected) this._paintDetail(false);
|
||||
}
|
||||
|
||||
// tab pips: the in-progress one pulses (the rest are static — painted
|
||||
// in _paintTabs on state flips). Last in update(): a _paintAll above
|
||||
// may have just reset the pip's alpha, and the pulse must win the frame.
|
||||
const run = this._activeOnPlanet();
|
||||
if (run) {
|
||||
for (const t of this.tabs) {
|
||||
const pip = t.pips.find((p) => p.id === run.build && p.state === 'progress');
|
||||
if (pip) pip.g.setAlpha(0.3 + 0.45 * Math.abs(Math.sin(time * 0.005)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── teardown ───────────────────────────────────────────────────────────────
|
||||
|
|
|
|||
Loading…
Reference in New Issue