Compare commits
No commits in common. "b325a19c82e987f9dc093566eac59857f32694df" and "ec7671d0cb7fc77577e6e24e4a19f4cd0ace3e46" have entirely different histories.
b325a19c82
...
ec7671d0cb
|
|
@ -146,7 +146,7 @@ layers now, with the rules and panels to come next:
|
|||
Six evenly spaced slots: **Research, Build, Ship, ·, ·, Menu**. The slots
|
||||
fire `onAction` and are otherwise inert; two slots are reserved. Dressed
|
||||
with the menu's CRT language — scanlines over the strip and the GlitchText
|
||||
RGB pull-apart on labels and the panel outline during bursts. All
|
||||
RGB pull-apart on icons, labels, and the panel outline during bursts. All
|
||||
layout, palette, and motion (boot flicker, rail comet, glitch bursts, RGB
|
||||
split, scanlines) live in the config.
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,13 +1,14 @@
|
|||
{
|
||||
"_comment": "COMMAND DECK — the cyberpunk bar across the bottom of the screen (rendered by js/ui/ActionBar.js). Six evenly spaced slots: `buttons` is the ORDERED list — give a slot an `id` (the action it will fire) + `label` + `accent`, or leave both `null` for a reserved slot. Buttons currently fire `onAction` (see GameScene); the behavior behind them comes next. Everything visual is procedural (no image assets) and tunable here: layout in `height`/`margin`/`padding`/`button`, palette in `colors`, CRT scanlines in `scanline`, motion in `animation` (boot flicker, the rail comet, the glitch bursts, the RGB pull-apart).",
|
||||
"enabled": true,
|
||||
"height": 56,
|
||||
"height": 92,
|
||||
"margin": { "left": 20, "right": 20, "bottom": 12 },
|
||||
"padding": 16,
|
||||
"button": {
|
||||
"widthFactor": 0.72,
|
||||
"maxWidth": 190,
|
||||
"notch": 10,
|
||||
"iconSize": 26,
|
||||
"fontSize": 14,
|
||||
"letterSpacing": 2.5
|
||||
},
|
||||
|
|
@ -38,7 +39,7 @@
|
|||
"animation": {
|
||||
"bootStagger": 70,
|
||||
"rgb": {
|
||||
"_comment": "RGB pull-apart on labels/panel (the menu title's GlitchText technique): idleOffset px of constant chromatic fringe, burstOffset extra px at full burst, idleAlpha/burstAlpha ghost strength.",
|
||||
"_comment": "RGB pull-apart on icons/labels/panel (the menu title's GlitchText technique): idleOffset px of constant chromatic fringe, burstOffset extra px at full burst, idleAlpha/burstAlpha ghost strength.",
|
||||
"idleOffset": 1.8,
|
||||
"burstOffset": 6,
|
||||
"idleAlpha": 0.4,
|
||||
|
|
|
|||
|
|
@ -1,19 +1,17 @@
|
|||
{
|
||||
"_comment": "ASTEROID CLUSTERS — loose groups of slowly tumbling rocks drifting in the system's void. texture = spritesheet of frameWidth×frameHeight asteroid frames (frameCount frames). cluster = how a group looks & moves: groupSize = rocks per cluster (minFullSize guarantees at least one full-size rock), sizes = per-rock diameter px, spread = how far a rock may sit from the cluster center, gapFactor = the small edge-gap between ANY two rocks, as a multiple of their combined radii (1.0 = touching, 1.12 = a subtle gap — the generator pushes apart just the overlapping pairs, each by half, so groups stay compact), spin = each rock's OWN very slow tumble (deg/s, direction per-rock random), groupSpin = the whole loose group drifts around its center (deg/s), tint = subtle warm/cool starlight per cluster (anchors blended toward white by strength), halo = soft glow behind the group, debris = a halo of fine dust motes orbiting just outside the rocks. distribution = how many clusters a system gets: targetObjects − planetCount, ±jitter, clamped to [minClusters, maxClusters] (more planets ⇒ fewer clusters); the starting system always gets at least startingSystemMinClusters, and those first ones are placed INSIDE the player's initial tether so they're reachable from the spawn. placement = where clusters may sit: minObjectSpacing = no cluster may be closer (center-to-center) than this to ANY other object (home world, planets, free-space stations, other clusters); minRadius/maxRadius = the annulus around the origin random clusters live in; tetherMargin = how far inside the tether rim a starting-system cluster must sit (whole group stays reachable). shipClearance = how close (edge-to-edge) the ship may get to a rock — clusters are solid, like worlds.",
|
||||
"_comment": "ASTEROID CLUSTERS — loose groups of slowly tumbling rocks drifting in the system's void. texture = spritesheet of frameWidth×frameHeight asteroid frames (frameCount frames). cluster = how a group looks & moves: groupSize = rocks per cluster (minFullSize guarantees at least one full-size rock), sizes = per-rock diameter px, spread = how far a rock may sit from the cluster center, spin = each rock's OWN very slow tumble (deg/s, direction per-rock random), groupSpin = the whole loose group drifts around its center (deg/s), tint = subtle warm/cool starlight per cluster (anchors blended toward white by strength), halo = soft glow behind the group, debris = a halo of fine dust motes orbiting just outside the rocks. distribution = how many clusters a system gets: targetObjects − planetCount, ±jitter, clamped to [minClusters, maxClusters] (more planets ⇒ fewer clusters); the starting system always gets at least startingSystemMinClusters, and those first ones are placed INSIDE the player's initial tether so they're reachable from the spawn. placement = where clusters may sit: minObjectSpacing = no cluster may be closer (center-to-center) than this to ANY other object (home world, planets, free-space stations, other clusters); minRadius/maxRadius = the annulus around the origin random clusters live in; tetherMargin = how far inside the tether rim a starting-system cluster must sit (whole group stays reachable). shipClearance = how close (edge-to-edge) the ship may get to a rock — clusters are solid, like worlds.",
|
||||
"enabled": true,
|
||||
"texture": "assets/images/asteroids.png",
|
||||
"frameWidth": 128,
|
||||
"frameHeight": 128,
|
||||
"frameCount": 10,
|
||||
"typeLabel": "Asteroid Cluster",
|
||||
"compassColor": "#c8d2e0",
|
||||
"shipClearance": 50,
|
||||
"cluster": {
|
||||
"groupSize": { "min": 4, "max": 8 },
|
||||
"sizes": { "min": 64, "max": 128 },
|
||||
"minFullSize": 1,
|
||||
"spread": 140,
|
||||
"gapFactor": 1.12,
|
||||
"spin": { "minDegPerSec": 0.3, "maxDegPerSec": 1.6 },
|
||||
"groupSpin": { "minDegPerSec": 0.12, "maxDegPerSec": 0.4 },
|
||||
"tint": {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@
|
|||
* minus placement.tetherMargin);
|
||||
* - GROUP SHAPE: 4–8 rocks, sizes in [64, 128], at least one full-size
|
||||
* rock, frames in [0, frameCount) with NO frame twice in a cluster,
|
||||
* every rock pair keeps a gapFactor × (r1+r2) gap (no interpenetration),
|
||||
* bound = max(offset + size/2);
|
||||
* - SPACING: no cluster within 1024 px (center-to-center) of ANY other
|
||||
* object — home world (origin), planets, free-space stations, other
|
||||
|
|
@ -164,18 +163,6 @@ for (const rec of sample) {
|
|||
if (Math.abs(bound - cl.bound) > 1e-9) {
|
||||
shapeOk = false; shapeWhy = `${rec.id}#${i}: bound ${cl.bound} ≠ max(offset + size/2) ${bound}`; break;
|
||||
}
|
||||
// Rocks keep a small gap from each other: no pair closer than
|
||||
// gapFactor × (r1 + r2) centers (the generator's relaxation contract).
|
||||
const gap = Math.max(1, CL.gapFactor ?? 1.12);
|
||||
outer: for (let ai = 0; ai < rocks.length; ai++) {
|
||||
for (let bi = ai + 1; bi < rocks.length; bi++) {
|
||||
const d = Math.hypot(rocks[ai].x - rocks[bi].x, rocks[ai].y - rocks[bi].y);
|
||||
const need = gap * (rocks[ai].size / 2 + rocks[bi].size / 2);
|
||||
if (d < need - 1e-6) {
|
||||
shapeOk = false; shapeWhy = `${rec.id}#${i}: rocks ${ai}/${bi} ${d.toFixed(1)} px apart (< ${need.toFixed(1)} gap)`; break outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Motion ----------------------------------------------------------
|
||||
if (!rocks.every((r) => Math.abs(r.spin) >= spinMin - 1e-12 && Math.abs(r.spin) <= spinMax + 1e-12)) {
|
||||
|
|
@ -225,7 +212,7 @@ for (const rec of sample) {
|
|||
}
|
||||
|
||||
check(`count: ${sample.length} systems obey targetObjects−planets (±jitter), clamped ${D.minClusters}–${D.maxClusters}${countOk ? '' : ' — ' + countWhy}`, countOk);
|
||||
check(`shape: groups of ${CL.groupSize.min}–${CL.groupSize.max}, sizes ${CL.sizes.min}–${CL.sizes.max}, ≥1 full-size, frames unique per cluster, rocks keep a ${CL.gapFactor ?? 1.12}× gap, bound correct${shapeOk ? '' : ' — ' + shapeWhy}`, shapeOk);
|
||||
check(`shape: groups of ${CL.groupSize.min}–${CL.groupSize.max}, sizes ${CL.sizes.min}–${CL.sizes.max}, ≥1 full-size, frames unique per cluster, bound correct${shapeOk ? '' : ' — ' + shapeWhy}`, shapeOk);
|
||||
check(`spacing: no cluster within ${P.minObjectSpacing} px (center-to-center) of ANY object (home, planets, stations, clusters)${spacingOk ? '' : ' — ' + spacingWhy}`, spacingOk);
|
||||
check(`scatter: every cluster inside the ${P.minRadius}–${P.maxRadius} annulus around the origin${annulusOk ? '' : ' — ' + annulusWhy}`, annulusOk);
|
||||
check(`motion: per-rock spins & group drifts within the slow-spin ranges${spinOk ? '' : ' — ' + spinWhy}`, spinOk);
|
||||
|
|
|
|||
|
|
@ -280,9 +280,7 @@ function layoutSystem(seed, systemId, planets, freeSpace) {
|
|||
*
|
||||
* A cluster is a GROUP of 4–8 rocks (data → cluster.groupSize), each 64–128
|
||||
* px (cluster.sizes) with at least one full-size rock, sitting within
|
||||
* `cluster.spread` of the cluster center, and kept a small gap apart from
|
||||
* each other (cluster.gapFactor — the generator relaxes any overlap so no
|
||||
* two rocks interpenetrate). Every rock tumbles on its own
|
||||
* `cluster.spread` of the cluster center. Every rock tumbles on its own
|
||||
* slow spin (its own speed and direction — cluster.spin), and the whole
|
||||
* group drifts slowly around its center (cluster.groupSpin) — the render
|
||||
* (js/entities/AsteroidCluster.js) reads these straight off the record.
|
||||
|
|
@ -399,19 +397,22 @@ function generateAsteroidClusters(galaxy, record, planets, freeSpace) {
|
|||
const fullRocks = Math.min(Math.max(0, Math.floor(clusterCfg.minFullSize ?? 1)), memberCount);
|
||||
for (let k = 0; k < fullRocks; k++) sizes[rng.int(0, memberCount - 1)] = sizeMax;
|
||||
|
||||
// Offsets: a dense random blob around the center (uniform in area),
|
||||
// then a relaxation pass enforces a small gap between EVERY pair of
|
||||
// rocks (cluster.gapFactor — 1.12 ≈ a subtle gap): only pairs that
|
||||
// would overlap move, each by half the shortfall, so the group keeps
|
||||
// its random look and stays compact while no rocks interpenetrate.
|
||||
const gapFactor = Math.max(1, clusterCfg.gapFactor ?? 1.12);
|
||||
// Offsets: a dense random blob around the center (slight overlap is
|
||||
// allowed — collision clusters look organic; solids never fully
|
||||
// interpenetrate, see the 0.75 factor).
|
||||
const members = [];
|
||||
for (let k = 0; k < memberCount; k++) {
|
||||
let ox = 0;
|
||||
let oy = 0;
|
||||
for (let attempt = 0; attempt < 24; attempt++) {
|
||||
const a = rng.range(0, TAU);
|
||||
const r = spread * Math.sqrt(rng.next()); // uniform in area
|
||||
members.push({ x: Math.cos(a) * r, y: Math.sin(a) * r, size: sizes[k] });
|
||||
ox = Math.cos(a) * r;
|
||||
oy = Math.sin(a) * r;
|
||||
if (members.every((m) => Math.hypot(ox - m.x, oy - m.y) >= 0.75 * (sizes[k] / 2 + m.size / 2))) break;
|
||||
}
|
||||
members.push({ x: ox, y: oy, size: sizes[k] });
|
||||
}
|
||||
relaxRockGaps(members, gapFactor);
|
||||
const bound = Math.max(...members.map((m) => Math.hypot(m.x, m.y) + m.size / 2));
|
||||
|
||||
// Spins: each rock tumbles on its own — its own slow speed, its own
|
||||
|
|
@ -511,39 +512,6 @@ function placeInAnnulus(rng, rMin, rMax, placed, minSep, maxAttempts) {
|
|||
return { x: best.x, y: best.y };
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforce the generator's gap rule: after the pass, every pair of rocks is
|
||||
* ≥ gapFactor × (r1 + r2) apart (centers) — a small visible edge-gap
|
||||
* (1.0 = touching). Iterative pairwise separation: each violator moves half
|
||||
* the shortfall, only violating pairs move (minimal displacement), and a
|
||||
* few dozen sweeps settle any rock count. Fully deterministic (fixed pair
|
||||
* order, fixed iteration cap).
|
||||
*/
|
||||
function relaxRockGaps(members, gapFactor, maxIter = 96) {
|
||||
for (let it = 0; it < maxIter; it++) {
|
||||
let worst = 0;
|
||||
for (let i = 0; i < members.length; i++) {
|
||||
for (let j = i + 1; j < members.length; j++) {
|
||||
const a = members[i];
|
||||
const b = members[j];
|
||||
let dx = b.x - a.x;
|
||||
let dy = b.y - a.y;
|
||||
let d = Math.hypot(dx, dy);
|
||||
const need = gapFactor * (a.size / 2 + b.size / 2);
|
||||
if (d >= need) continue;
|
||||
if (d < 1e-9) { dx = 1; dy = 0; d = 1; } // coincident: deterministic axis
|
||||
const push = (need - d) / 2;
|
||||
a.x -= (dx / d) * push;
|
||||
a.y -= (dy / d) * push;
|
||||
b.x += (dx / d) * push;
|
||||
b.y += (dy / d) * push;
|
||||
worst = Math.max(worst, need - d);
|
||||
}
|
||||
}
|
||||
if (worst <= 1e-6) break;
|
||||
}
|
||||
}
|
||||
|
||||
/** The starting system's initial tether radius (home world's level). */
|
||||
function homeTetherRadius() {
|
||||
const level = Math.max(1, Math.floor(config.get('tether.homeLevel', 1)));
|
||||
|
|
|
|||
|
|
@ -544,9 +544,6 @@ export class GameScene extends Phaser.Scene {
|
|||
y: c.y,
|
||||
radius: c.bound,
|
||||
typeLabel: config.get('asteroids.typeLabel', 'Asteroid Cluster'),
|
||||
// The compass shows clusters in their own light gray (not the
|
||||
// worlds' neon cyan) — the arrow + name tag pick it up.
|
||||
color: config.get('asteroids.compassColor', '#c8d2e0'),
|
||||
name: c.discoveryName,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,16 +6,17 @@ import { canvasTexture } from '../utils/Textures.js';
|
|||
import { CyberShape } from './CyberShape.js';
|
||||
|
||||
const FONT_FALLBACK = "'Segoe UI', 'Helvetica Neue', Arial, sans-serif";
|
||||
const DEG = Math.PI / 180;
|
||||
|
||||
/**
|
||||
* COMMAND DECK — the cyberpunk action bar across the bottom of the screen:
|
||||
* a cut-corner console panel with an energy rail, six evenly spaced slot
|
||||
* buttons (label-only, per-slot accent rail on top, RGB-split labels),
|
||||
* reserved "standby" slots, a rail comet that streaks past on a loop, periodic glitch
|
||||
* buttons (drawn icons, RGB-split labels, per-slot accent colors), reserved
|
||||
* "standby" slots, a rail comet that streaks past on a loop, periodic glitch
|
||||
* bursts (slice bars + label jitter), and a boot flicker-in.
|
||||
* Dressed with the menu's CRT language: scanlines over the whole bar (the
|
||||
* same 1px-dark-row tile as CyberOverlay) and the GlitchText RGB pull-apart
|
||||
* on labels and the panel outline itself on bursts.
|
||||
* on everything — icons, labels, and the panel outline itself on bursts.
|
||||
*
|
||||
* Everything is procedural — no image assets — and fully config-driven
|
||||
* (data/actionbar.json): slots, labels, accents, palette, and every
|
||||
|
|
@ -68,7 +69,9 @@ export class ActionBar extends Phaser.GameObjects.Container {
|
|||
slotBorder: toColor(ab.colors?.slotBorder, 0x22405f),
|
||||
reserved: toColor(ab.colors?.reserved, 0x3d4c74),
|
||||
inkCss: toCss(ab.colors?.text ?? '#eaf6ff'),
|
||||
labelY: 0, // label-only buttons: the text is centred vertically
|
||||
iconY: -12,
|
||||
labelY: 16,
|
||||
iconSize: bc.iconSize ?? 26,
|
||||
};
|
||||
// The menu's CRT scanline tuning (data/theme.json) is the default, so
|
||||
// the bar matches the menu unless actionbar.json overrides it.
|
||||
|
|
@ -209,6 +212,8 @@ export class ActionBar extends Phaser.GameObjects.Container {
|
|||
const letterSpacing = bc.letterSpacing ?? 2.5;
|
||||
const cyan = toCss('#00e5ff');
|
||||
const magenta = toCss('#ff2d6f');
|
||||
const iconSize = this.style.iconSize;
|
||||
const iconY = this.style.iconY;
|
||||
|
||||
this.slots = buttons.map((b, i) => {
|
||||
const live = typeof b.id === 'string' && b.id.length > 0;
|
||||
|
|
@ -239,7 +244,31 @@ export class ActionBar extends Phaser.GameObjects.Container {
|
|||
_scaleTw: null,
|
||||
};
|
||||
s.panel = scene.add.graphics().setScrollFactor(0);
|
||||
slot.add(s.panel);
|
||||
// RGB-split icon ghosts (the GlitchText technique on the menu title):
|
||||
// additive magenta/cyan copies of the drawn icon, moved per frame.
|
||||
s.iconGhostM = scene
|
||||
.add.graphics()
|
||||
.setPosition(0, iconY)
|
||||
.setScrollFactor(0)
|
||||
.setBlendMode(Phaser.BlendModes.ADD)
|
||||
.setAlpha(0);
|
||||
s.iconGhostC = scene
|
||||
.add.graphics()
|
||||
.setPosition(0, iconY)
|
||||
.setScrollFactor(0)
|
||||
.setBlendMode(Phaser.BlendModes.ADD)
|
||||
.setAlpha(0);
|
||||
s.icon = scene.add.graphics().setPosition(0, iconY).setScrollFactor(0);
|
||||
slot.add([s.panel, s.iconGhostM, s.iconGhostC, s.icon]);
|
||||
|
||||
// Drawn icon (accent for live slots, dim for reserved sockets) —
|
||||
// live slots also get their ghost copies in the split colors.
|
||||
const iconKind = live ? b.id : 'reserved';
|
||||
drawIcon(s.icon, iconKind, iconSize, live ? s.accent : this.style.reserved);
|
||||
if (live) {
|
||||
drawIcon(s.iconGhostM, iconKind, iconSize, 0xff2d6f);
|
||||
drawIcon(s.iconGhostC, iconKind, iconSize, 0x00e5ff);
|
||||
}
|
||||
|
||||
if (live) {
|
||||
const labelText = String(b.label ?? '').toUpperCase();
|
||||
|
|
@ -283,8 +312,8 @@ export class ActionBar extends Phaser.GameObjects.Container {
|
|||
s.panel.on('pointerout', () => this.setHover(s, false));
|
||||
s.panel.on('pointerdown', () => this.press(s));
|
||||
} else {
|
||||
// Reserved socket: a standby dot (centred) that breathes in update().
|
||||
s.resDot = scene.add.circle(0, 0, 1.6, this.style.reserved, 0.35).setScrollFactor(0);
|
||||
// Reserved socket: a standby dot that breathes in update().
|
||||
s.resDot = scene.add.circle(0, iconY, 1.6, this.style.reserved, 0.35).setScrollFactor(0);
|
||||
slot.add(s.resDot);
|
||||
}
|
||||
|
||||
|
|
@ -480,7 +509,7 @@ export class ActionBar extends Phaser.GameObjects.Container {
|
|||
|
||||
/**
|
||||
* Fire a glitch burst right now: slice bars across the deck, and the RGB
|
||||
* split (level → 1, decays in update) on labels and the panel.
|
||||
* split (level → 1, decays in update) on icons, labels, and the panel.
|
||||
*/
|
||||
triggerBurst(duration = 300, intensity = 1) {
|
||||
const now = this.lastTime ?? this.scene.time.now;
|
||||
|
|
@ -591,7 +620,7 @@ export class ActionBar extends Phaser.GameObjects.Container {
|
|||
const flick = 0.6 + 0.4 * Math.sin(t * 3.3);
|
||||
const baseA = Math.min(1, rgb.idleAlpha * flick + L * rgb.burstAlpha);
|
||||
|
||||
// Slots: the menu's RGB pull-apart on the label — magenta left,
|
||||
// Slots: the menu's RGB pull-apart on icon + label — magenta left,
|
||||
// cyan right (GlitchText's exact offsets), always a faint fringe at
|
||||
// idle, pulled apart + jittered during bursts; per-slot phase so the
|
||||
// six buttons don't shimmer in lockstep.
|
||||
|
|
@ -607,6 +636,8 @@ export class ActionBar extends Phaser.GameObjects.Container {
|
|||
const a = s.hoverOn ? Math.min(1, baseA + 0.25) : baseA;
|
||||
s.ghostMagenta.setAlpha(a).setPosition(-off + jx, this.style.labelY + jy * 0.5);
|
||||
s.ghostCyan.setAlpha(a * 0.95).setPosition(off * 0.85, this.style.labelY - off * 0.3 + jy);
|
||||
s.iconGhostM.setAlpha(a * 0.8).setPosition(-off + jx, this.style.iconY + jy * 0.5);
|
||||
s.iconGhostC.setAlpha(a * 0.75).setPosition(off * 0.85, this.style.iconY - off * 0.3 + jy);
|
||||
}
|
||||
|
||||
// The bar itself: the panel outline pulled apart in magenta/cyan while
|
||||
|
|
@ -804,3 +835,85 @@ function drawBarBody(ctx, W, H, P, c) {
|
|||
corner(x - 3, y + h + 3, 1, -1);
|
||||
corner(x + w + 3, y + h + 3, -1, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* The drawn slot icons — small, geometric, in the console's language.
|
||||
* Each icon is drawn centered on the icon Graphics' origin.
|
||||
*/
|
||||
function drawIcon(g, id, size, color) {
|
||||
const r = size * 0.44;
|
||||
const glow = (lw) => g.lineStyle(lw, color, 0.25);
|
||||
|
||||
switch (id) {
|
||||
case 'research': {
|
||||
// Orbit: a ring with a lit trail, a satellite riding it, a core.
|
||||
glow(4);
|
||||
g.strokeCircle(0, 0, r);
|
||||
g.lineStyle(1.5, color, 0.95);
|
||||
g.strokeCircle(0, 0, r);
|
||||
g.lineStyle(2, color, 0.5); // trail behind the satellite
|
||||
g.beginPath();
|
||||
g.arc(0, 0, r, -110 * DEG, -25 * DEG);
|
||||
g.stroke();
|
||||
const sa = -35 * DEG;
|
||||
g.fillStyle(color, 1);
|
||||
g.fillCircle(Math.cos(sa) * r, Math.sin(sa) * r, 2.6);
|
||||
g.fillStyle(color, 0.9);
|
||||
g.fillCircle(0, 0, 1.7);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'build': {
|
||||
// Isometric cube — construction.
|
||||
const v = [0, -30, 30, 90, 150, 210].map((deg) => ({
|
||||
x: Math.cos(deg * DEG) * r,
|
||||
y: Math.sin(deg * DEG) * r,
|
||||
}));
|
||||
glow(4);
|
||||
g.strokePoints(v, true);
|
||||
g.lineStyle(1.5, color, 0.95);
|
||||
g.strokePoints(v, true);
|
||||
g.lineStyle(1.5, color, 0.8); // inner edges of the cube
|
||||
g.lineBetween(0, 0, v[0].x, v[0].y);
|
||||
g.lineBetween(0, 0, v[2].x, v[2].y);
|
||||
g.lineBetween(0, 0, v[4].x, v[4].y);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'ship': {
|
||||
// Chevron arrow, nose up.
|
||||
const pts = [
|
||||
{ x: 0, y: -r },
|
||||
{ x: r * 0.78, y: r * 0.62 },
|
||||
{ x: 0, y: r * 0.18 },
|
||||
{ x: -r * 0.78, y: r * 0.62 },
|
||||
];
|
||||
glow(4);
|
||||
g.strokePoints(pts, true);
|
||||
g.lineStyle(1.5, color, 0.95);
|
||||
g.strokePoints(pts, true);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'menu': {
|
||||
// Signal bars — slightly ragged widths, left-aligned.
|
||||
g.fillStyle(color, 0.9);
|
||||
g.fillRect(-r, -r * 0.75, r * 2, 2.4);
|
||||
g.fillRect(-r, -1.2, r * 1.5, 2.4);
|
||||
g.fillRect(-r, r * 0.75 - 2.4, r * 1.85, 2.4);
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
// Reserved socket: a dim diamond — a slot waiting for a command.
|
||||
const d = [
|
||||
{ x: 0, y: -7 },
|
||||
{ x: 7, y: 0 },
|
||||
{ x: 0, y: 7 },
|
||||
{ x: -7, y: 0 },
|
||||
];
|
||||
g.lineStyle(1.2, color, 0.45);
|
||||
g.strokePoints(d, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,10 +131,7 @@ export class DiscoveryCompass extends Phaser.GameObjects.Container {
|
|||
makeEntry(t) {
|
||||
const scene = this.scene;
|
||||
const fam = fontStack('body', FONT_FALLBACK);
|
||||
// Per-target accent: a target may carry its own color (asteroid
|
||||
// clusters — data/asteroids.json → compassColor, light gray) and the
|
||||
// arrow + chip use it; worlds keep the theme's neon cyan.
|
||||
const neon = t.color ? toColor(t.color) : themeColor('neon', 0x00e5ff);
|
||||
const neon = themeColor('neon', 0x00e5ff);
|
||||
const ink = themeColor('ink', 0xeaf6ff);
|
||||
const fill = toColor(config.get('theme.colors.panel', '#0a1120'));
|
||||
const typeLabel = (t.typeLabel ?? 'OBJECT').toUpperCase();
|
||||
|
|
@ -189,7 +186,6 @@ export class DiscoveryCompass extends Phaser.GameObjects.Container {
|
|||
// the __MISSING texture forever, even after the key is generated.
|
||||
ensureArrowTexture(scene);
|
||||
const arrow = scene.add.image(0, 0, ARROW_KEY);
|
||||
arrow.setTint(neon); // the texture is baked white; tint per target
|
||||
arrow.setScrollFactor(0); // UI — pinned to the screen
|
||||
arrow.setDepth(40);
|
||||
|
||||
|
|
@ -345,15 +341,13 @@ function separateAngles(entries, w, h, inset, minSep, cx, cy) {
|
|||
|
||||
/**
|
||||
* The arrow glyph, generated once per game (procedural, no assets —
|
||||
* same pattern as Ship.ensureTexture). A chevron head with a soft glow
|
||||
* pass over a dark fill, plus speed ticks streaming behind it. Points +x
|
||||
* (angle 0); the tip sits 25 px right of the texture center. Baked WHITE
|
||||
* — each arrow image is tinted per target (worlds = theme neon, asteroid
|
||||
* clusters = their light gray).
|
||||
* same pattern as Ship.ensureTexture). A neon chevron head with a soft
|
||||
* glow pass over a dark fill, plus speed ticks streaming behind it.
|
||||
* Points +x (angle 0); the tip sits 25 px right of the texture center.
|
||||
*/
|
||||
function ensureArrowTexture(scene) {
|
||||
if (scene.textures.exists(ARROW_KEY)) return;
|
||||
const neon = 0xffffff; // white source; tinted per arrow (see makeEntry)
|
||||
const neon = toColor(config.get('theme.colors.neon', '#00e5ff'));
|
||||
const fill = toColor(config.get('theme.colors.panel', '#0a1120'));
|
||||
const W = 56;
|
||||
const H = 36;
|
||||
|
|
|
|||
Loading…
Reference in New Issue