Add faction-tinted swirl to locked jump gates
- Locked gates (neutral → faction, below minLevel) now render their swirling center in the controlling faction's hue as an in-world "that gate belongs to someone" tell, complementing the comm window's grayed REQUEST JUMP and lock message. - JumpGate.setFactionTint tints the active swirl (setTint) or recolors the procedural fallback field discs; null clears back to the ordinary swirl. Falsy-but-valid hues (e.g. 0x000000) are not treated as clear. - GameScene.gateFactionTintFor resolves the destination faction's color; _refreshGateFactionTints re-resolves all gates at build, once the XP ledger exists (save may have already lifted the lock), and on each level-up event to clear tints as the lock lifts. - Test harness extended with setTint/setFillStyle mocks; new checks cover tint apply/clear/falsy-hue on the sprite path and field-disc recolor + restore on the fallback path.
This commit is contained in:
parent
78593b1ea2
commit
8ade473479
|
|
@ -59,7 +59,7 @@
|
|||
"miningToast": "CANNOT JUMP WHILE MINING",
|
||||
"combatToast": "CANNOT JUMP WHILE UNDER ATTACK",
|
||||
"factionLock": {
|
||||
"_comment": "FACTION GATE LOCK (the pure rule is js/galaxy/JumpTravel.js → factionGateLocked; the gate comm window shows it — js/ui/CommsPanel.js's gate variant — and GameScene.jumpThroughGate keeps its own flat denial on top): a jump gate that leads FROM a NEUTRAL system (faction null/'neutral') INTO a FACTION-CONTROLLED one (faction = apex/mindweave/zenith, the roster's faction pass — data/factions.json) is LOCKED while the player's combat level (js/combat/XpManager.js) is below minLevel — the window's LINK line is replaced by `message` (rendered verbatim under the panel's uppercase display convention) and REQUEST JUMP is a grayed ghost. At level >= minLevel the lock lifts and the gate reads as ordinary again (LINK ACTIVE/DORMANT). Gates FROM a faction system (any destination — the run is already inside faction space) and neutral→neutral hops are never locked. `enabled` is the feature switch (false = the lock is off); `minLevel` is the level that OPENS the lock (a fresh run starts at 0).",
|
||||
"_comment": "FACTION GATE LOCK (the pure rule is js/galaxy/JumpTravel.js → factionGateLocked; the gate comm window shows it — js/ui/CommsPanel.js's gate variant — and GameScene.jumpThroughGate keeps its own flat denial on top): a jump gate that leads FROM a NEUTRAL system (faction null/'neutral') INTO a FACTION-CONTROLLED one (faction = apex/mindweave/zenith, the roster's faction pass — data/factions.json) is LOCKED while the player's combat level (js/combat/XpManager.js) is below minLevel — the window's LINK line is replaced by `message` (rendered verbatim under the panel's uppercase display convention), REQUEST JUMP is a grayed ghost, and the gate's SWIRLING CENTER reads in the controlling faction's hue (data/factions.json → factions.<id>.color — GameScene.gateFactionTintFor → JumpGate.setFactionTint, the in-world 'that gate belongs to someone' tell; it clears on a level-up that lifts the lock). At level >= minLevel the lock lifts and the gate reads as ordinary again (LINK ACTIVE/DORMANT, the ordinary swirl). Gates FROM a faction system (any destination — the run is already inside faction space) and neutral→neutral hops are never locked. `enabled` is the feature switch (false = the lock is off); `minLevel` is the level that OPENS the lock (a fresh run starts at 0).",
|
||||
"enabled": true,
|
||||
"minLevel": 5,
|
||||
"message": "Jump Gate is under Faction Control. Status: Locked"
|
||||
|
|
|
|||
|
|
@ -22,6 +22,11 @@
|
|||
* default; setRouteTarget(true) shows it, update() animates the
|
||||
* steady ring + two expanding/fading ripples outside the keepout
|
||||
* (even on a dormant gate), setRouteTarget(false) clears + hides it;
|
||||
* - the FACTION GATE LOCK's swirl tint (data/gates.json →
|
||||
* jump.factionLock; data/factions.json → factions.\u003cid\u003e.color):
|
||||
* setFactionTint(hue) tints the swirling center (sprite: the swirl
|
||||
* frame — fallback: the field discs), setFactionTint(null) clears it
|
||||
* back to the ordinary swirl;
|
||||
* - the SOLID contract (same as Planet/Station): minCenterDistance,
|
||||
* edgePoint exactly `gap` past the surface, aimPoint clamping, and
|
||||
* constrainShip pushing the ship out of the keepout circle
|
||||
|
|
@ -114,6 +119,7 @@ const scene = {
|
|||
setScale(s) { this.scale = s; return this; },
|
||||
setAlpha(a) { this.alpha = a; return this; },
|
||||
setVisible(v) { this.visible = v; return this; },
|
||||
setTint(c) { this.tint = c; return this; },
|
||||
};
|
||||
images.push(img);
|
||||
return img;
|
||||
|
|
@ -130,6 +136,7 @@ const scene = {
|
|||
}),
|
||||
circle: (x, y, r, fill, alpha) => {
|
||||
const c = { x, y, r, fill, alpha, setAlpha(a) { this.alpha = a; return this; } };
|
||||
c.setFillStyle = (f, a) => { c.fill = f; if (a != null) c.alpha = a; return c; };
|
||||
alphas.push(c);
|
||||
return c;
|
||||
},
|
||||
|
|
@ -209,6 +216,22 @@ gate.setRouteTarget(true);
|
|||
check('setRouteTarget(true) twice is a no-op (idempotent)', gate.routePulse.on === true && gate.routePulse.ring.visible === true);
|
||||
gate.setRouteTarget(false);
|
||||
|
||||
// --- The FACTION GATE LOCK's swirl tint (the in-world tell) -------------
|
||||
// A gate into FACTION-CONTROLLED space, below the player's level-5 lock
|
||||
// (GameScene.gateFactionTintFor → the lock's pure rule in JumpTravel),
|
||||
// swirls in the controlling faction's hue (data/factions.json);
|
||||
// null clears it (the lock lifted on a level-up).
|
||||
const APEX_HUE = hexToRgbInt(config.get('factions.factions.apex.color'));
|
||||
check('fresh gate: no faction tint parked (the ordinary swirl)', gateB.factionTint === null);
|
||||
gateB.setFactionTint(APEX_HUE);
|
||||
check('setFactionTint(faction hue): the swirl carries the faction tint', gateB.factionTint === APEX_HUE && swirlB.tint === APEX_HUE);
|
||||
gateB.update(1500); // the spin/breath keeps running tinted
|
||||
gateB.setFactionTint(null);
|
||||
check('setFactionTint(null): the tint clears (white = untinted)', gateB.factionTint === null && swirlB.tint === 0xffffff);
|
||||
gateB.setFactionTint(0); // a falsy-but-valid hue (e.g. #000000) is NOT 'clear'
|
||||
check('setFactionTint(0) parks 0x0 (falsy ≠ null — only null clears)', gateB.factionTint === 0 && swirlB.tint === 0);
|
||||
gateB.setFactionTint(null);
|
||||
|
||||
// --- The solid contract (same rules as Planet / Station) ------------------
|
||||
const shipRadius = (config.get('ship.size', 46) * config.get('ship.scale', 1)) / 2;
|
||||
const minDist = gate.minCenterDistance(shipRadius);
|
||||
|
|
@ -258,6 +281,16 @@ const d0 = circlesD.map((c) => c.alpha);
|
|||
gateD.update(873); // t ≈ π/2 of the pulse → maximum breathing
|
||||
check('fallback: an active gate breathes its field', circlesD.some((c, i) => Math.abs(c.alpha - d0[i]) > 1e-6));
|
||||
|
||||
// --- FACTION LOCK tint, fallback path: the field discs recolor -----------
|
||||
const MIND_HUE = hexToRgbInt(config.get('factions.factions.mindweave.color'));
|
||||
const baseFills = circlesD.map((c) => c.fill);
|
||||
gateD.setFactionTint(MIND_HUE);
|
||||
check('fallback: setFactionTint recolors the field discs (the faction hue)',
|
||||
circlesD.every((c) => c.fill === MIND_HUE));
|
||||
gateD.setFactionTint(null);
|
||||
check('fallback: clearing restores the base field hues',
|
||||
circlesD.every((c, i) => c.fill === baseFills[i]));
|
||||
|
||||
// --- Teardown -------------------------------------------------------------
|
||||
gate.update(1000); // must not throw (sprite path, dormant)
|
||||
check('sprite gate: update() runs without throwing', true);
|
||||
|
|
|
|||
|
|
@ -46,6 +46,16 @@ import { Planet } from './Planet.js';
|
|||
*
|
||||
* The collision API is circular and rotation-blind.
|
||||
*
|
||||
* FACTION GATE LOCK (data/gates.json → jump.factionLock): while the
|
||||
* gate's jump is LOCKED (its destination system is FACTION-CONTROLLED,
|
||||
* the player is below the lock's minLevel — GameScene.gateFactionTintFor),
|
||||
* the SWIRLING CENTER reads in the controlling faction's hue
|
||||
* (data/factions.json → factions.<id>.color — apex red / mindweave blue
|
||||
* / zenith gold; setFactionTint, cleared to null when the lock lifts on
|
||||
* a level-up). The in-world "that gate belongs to someone" tell; the
|
||||
* gate comm window (its grayed REQUEST JUMP + the lock's message) is
|
||||
* the primary read.
|
||||
*
|
||||
* update(time) spins the swirl (active gates) / breathes the fallback
|
||||
* art — driven by GameScene.update, like the stations and asteroid
|
||||
* clusters.
|
||||
|
|
@ -75,6 +85,7 @@ export class JumpGate extends Phaser.GameObjects.Container {
|
|||
this.ringBody = null;
|
||||
this.swirl = null;
|
||||
this.routePulse = null; // the route's next-stop mark (GameScene → setRouteTarget)
|
||||
this.factionTint = null; // the FACTION LOCK's swirl hue (GameScene → setFactionTint)
|
||||
this.setDepth(o.depth ?? 5);
|
||||
this.rotation = gate.rotation ?? 0; // face the destination star
|
||||
|
||||
|
|
@ -205,8 +216,14 @@ export class JumpGate extends Phaser.GameObjects.Container {
|
|||
arc(FIELD * 0.82, -0.5, 0.5, S * 0.035, 1, rgb(1.4, 1.6, 2));
|
||||
|
||||
// The energy field (the breathing discs — alpha driven in update()).
|
||||
this.field = scene.add.circle(0, 0, FIELD, rgb(0.5, 0.7, 1), 0.16);
|
||||
this.fieldCore = scene.add.circle(0, 0, FIELD * 0.5, rgb(0.75, 0.9, 1), 0.22);
|
||||
// The base hues are kept for setFactionTint (the FACTION LOCK's
|
||||
// swirl tint — a faction hue while locked, these when it clears).
|
||||
this._fieldBase = rgb(0.5, 0.7, 1);
|
||||
this._fieldBaseAlpha = 0.16;
|
||||
this._fieldCoreBase = rgb(0.75, 0.9, 1);
|
||||
this._fieldCoreBaseAlpha = 0.22;
|
||||
this.field = scene.add.circle(0, 0, FIELD, this._fieldBase, this._fieldBaseAlpha);
|
||||
this.fieldCore = scene.add.circle(0, 0, FIELD * 0.5, this._fieldCoreBase, this._fieldCoreBaseAlpha);
|
||||
this.add([this.field, this.fieldCore]);
|
||||
|
||||
// The chevrons — pointing at the destination star (+x), over the field.
|
||||
|
|
@ -239,6 +256,27 @@ export class JumpGate extends Phaser.GameObjects.Container {
|
|||
if (this.swirl) this.swirl.setVisible(true); // update() breathes it from the next frame
|
||||
}
|
||||
|
||||
/**
|
||||
* The FACTION GATE LOCK's swirl tint (GameScene.gateFactionTintFor —
|
||||
* this gate's destination is FACTION-CONTROLLED and the player is
|
||||
* below the lock's minLevel, data/gates.json → jump.factionLock): the
|
||||
* swirling center (the active swirl — frame 1; the procedural
|
||||
* fallback's field discs) reads in the controlling faction's hue
|
||||
* (data/factions.json → factions.<id>.color). `null` clears it (the
|
||||
* ordinary swirl — the lock lifted). On a DORMANT gate the swirl is
|
||||
* hidden — the tint is simply parked, so a later activate() reveals
|
||||
* the swirl already tinted.
|
||||
* @param {number|null} color — the faction's 0xRRGGBB hue (null = clear)
|
||||
*/
|
||||
setFactionTint(color) {
|
||||
this.factionTint = color == null ? null : color >>> 0;
|
||||
if (this.swirl) this.swirl.setTint(this.factionTint ?? 0xffffff); // white = untinted
|
||||
if (this.field && this.fieldCore) {
|
||||
this.field.setFillStyle(this.factionTint ?? this._fieldBase, this._fieldBaseAlpha);
|
||||
this.fieldCore.setFillStyle(this.factionTint ?? this._fieldCoreBase, this._fieldCoreBaseAlpha);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ROUTE-TARGET state (GameScene.update, each frame): true while this
|
||||
* gate is the route's NEXT STOP in the current system (routeNextGate).
|
||||
|
|
|
|||
|
|
@ -762,6 +762,13 @@ export class GameScene extends Phaser.Scene {
|
|||
this.systemGates.push(new JumpGate(this, j, { depth: 5 }));
|
||||
}
|
||||
}
|
||||
// FACTION GATE LOCK — the swirl tint (the in-world tell): a gate
|
||||
// into faction-controlled space, below the player's level-5 lock,
|
||||
// swirls in that faction's hue (gateFactionTintFor). With combat on
|
||||
// the XP ledger isn't stood up yet (it lands below with the save's
|
||||
// level), so _refreshGateFactionTints() re-resolves once the ledger
|
||||
// exists; a level-up that lifts the lock clears it again.
|
||||
this._refreshGateFactionTints();
|
||||
|
||||
// Every solid in the system — worlds first (their keep-out circles are
|
||||
// disjoint), then the clusters, then the stations, then the gates. The
|
||||
|
|
@ -1079,6 +1086,12 @@ export class GameScene extends Phaser.Scene {
|
|||
// `active` — the same freeze contract as the comms' ack pause.
|
||||
this.levelMarquee = new LevelUpMarquee(this);
|
||||
this.levelMarquee.bind(this.combat.xp);
|
||||
// FACTION GATE LOCK — the swirl tint follows the level: the ledger
|
||||
// now stands (the save's level restored), so re-resolve the gates'
|
||||
// tints (a level that already lifted the lock clears them), and
|
||||
// every level-up after re-resolves too (the lock lifts at minLevel).
|
||||
this._refreshGateFactionTints();
|
||||
this.combat.xp.on('levelup', () => this._refreshGateFactionTints());
|
||||
// ?combat boot (the tuning sandbox): the SEVEN-TYPE DEMO around the
|
||||
// ship (data/combat.json → waves.scripts.combat_demo) — a sequence
|
||||
// of rolling waves, one per new hostile type in difficulty order
|
||||
|
|
@ -3560,6 +3573,42 @@ export class GameScene extends Phaser.Scene {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The FACTION GATE LOCK's SWIRL TINT (data/gates.json →
|
||||
* jump.factionLock; the in-world half of the lock): while this gate's
|
||||
* jump is LOCKED — a gate from a NEUTRAL system into a
|
||||
* FACTION-CONTROLLED one, below the lock's minLevel — the gate's
|
||||
* swirling center reads in the CONTROLLING FACTION's hue: the
|
||||
* destination system's `faction` (data/factions.json →
|
||||
* factions.\<id\>.color — apex red / mindweave blue / zenith gold).
|
||||
* null when the lock doesn't hold (an ordinary swirl) — the gate comm
|
||||
* window's grayed REQUEST JUMP + its message stay the primary read;
|
||||
* this is the "that gate belongs to someone" tell on the gate itself.
|
||||
* @param {object} gt the gate entity (JumpGate — .gate record)
|
||||
* @returns {?number} the faction's 0xRRGGBB hue (ready for setTint), or null
|
||||
*/
|
||||
gateFactionTintFor(gt) {
|
||||
if (!this.factionGateLockedFor(gt)) return null; // unlocked → the ordinary swirl
|
||||
const destId = gt?.gate?.to;
|
||||
const faction =
|
||||
destId != null && this.galaxy ? (this.galaxy.byId.get(destId)?.faction ?? null) : null;
|
||||
if (!faction || faction === 'neutral') return null;
|
||||
const hex = config.get(`factions.factions.${faction}.color`, '');
|
||||
return typeof hex === 'string' && hex.length > 0 ? toColor(hex) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-resolve every gate's faction-lock tint (JumpGate.setFactionTint).
|
||||
* Called at build (the tint follows the lock as it stands — level 0
|
||||
* while the XP ledger is still to come), once the combat ledger exists
|
||||
* (the save's level may already have lifted the lock), and on each
|
||||
* level-up (the ledger's 'levelup' event — the same hook the level-up
|
||||
* marquee rides), when a locked gate's tint clears.
|
||||
*/
|
||||
_refreshGateFactionTints() {
|
||||
for (const gt of this.systemGates ?? []) gt.setFactionTint(this.gateFactionTintFor(gt));
|
||||
}
|
||||
|
||||
/**
|
||||
* The panel's button presses (CommsPanel → onAction). The panel is
|
||||
* already closed by the time this runs:
|
||||
|
|
|
|||
Loading…
Reference in New Issue