Compare commits
2 Commits
5e799bbd5a
...
8ade473479
| Author | SHA1 | Date |
|---|---|---|
|
|
8ade473479 | |
|
|
78593b1ea2 |
|
|
@ -57,7 +57,13 @@
|
|||
"toast": "JUMP — {dest}",
|
||||
"dormantToast": "JUMP GATE DORMANT — CHART {system} TO ACTIVATE ITS JUMP GATES",
|
||||
"miningToast": "CANNOT JUMP WHILE MINING",
|
||||
"combatToast": "CANNOT JUMP WHILE UNDER ATTACK"
|
||||
"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), 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"
|
||||
}
|
||||
},
|
||||
"route": {
|
||||
"_comment": "ROUTE (js/galaxy/Route.js + GameScene): the player's plotted course to a SET DESTINATION (SYSTEM tab of the MAP console). The run persists only the destination (systemId + objectId); the path from the current system is derived (planRoute — the jump network is a spanning tree, so it is the unique route). While a route is active, the compass marks the NEXT STOP in ORANGE (compassColor), replacing that object's ordinary arrow, and shows it even while still UNDISCOVERED (it is the thing to find): in an intermediate system the next stop is that system's jump gate toward the destination (typeLabel 'Route Gate'); in the DESTINATION system the next stop is the destination OBJECT itself (destTypeLabel) — the last hop is across the system to the world, and it stays orange until the ship actually reaches it (GameScene._checkDestinationReached fires reachedToast, then the route clears). The NEXT STOP in this system ALSO wears an in-world orange pulse the moment it is on screen (js/entities/RoutePulse.js — GameScene marks it each frame): the FULL pulse on the route GATE (pulse), the SAME pulse SUBDUED on the destination OBJECT in the destination system (objectPulse). A jump to any other system re-plots from the detour (replotToast). A destination with NO specific object clears the moment its system is entered. setToast fires when the destination is set ({dest}/{hops}). COLOR BY KIND (GameScene.setDestination stores destination.kind, _routeMarkColor resolves it): a CARGO delivery set by the PLAYER — the ship console's cargo manifest or the surface shop's scout (kind 'cargo') — wears the mark in BROWN (cargoColor), so the player's own delivery runs read apart from the story; a STORY-QUEST destination (the quest's own beat — kind 'quest') and the MAP console's manual pick (kind 'manual') keep the route's ORANGE (compassColor). The compass entry (arrow + chip) and the in-world pulse (RoutePulse) both follow the mark's color.",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,11 @@
|
|||
* destination's RETURN gate):
|
||||
* - config: data/gates.json → jump (the feature switch, the arrival
|
||||
* geometry, the toasts with their placeholders);
|
||||
* - the FACTION GATE LOCK (data/gates.json → jump.factionLock, the
|
||||
* pure rule js/galaxy/JumpTravel.js → factionGateLocked): a gate
|
||||
* from a NEUTRAL system into a FACTION-CONTROLLED one is locked
|
||||
* below the lock's minLevel (level 5) — and the REAL galaxy has
|
||||
* such gates at a fresh run's level, all open at level 5;
|
||||
* - the pure geometry (js/galaxy/JumpTravel.js): the return-gate
|
||||
* lookup (tree-edge jumps have one, one-way shortcut jumps don't),
|
||||
* the spawn point (just past the gate's keepout, on the side the
|
||||
|
|
@ -58,6 +63,17 @@ check('jump: the toast templates keep their placeholders',
|
|||
typeof jump?.miningToast === 'string' && jump.miningToast.length > 0);
|
||||
check('jump: combatToast is the live-fight denial (non-empty string)',
|
||||
typeof jump?.combatToast === 'string' && jump.combatToast.length > 0);
|
||||
|
||||
// The FACTION GATE LOCK (GameScene.openGateCommsPanel → the gate comm
|
||||
// window's grayed REQUEST JUMP + its message; GameScene.jumpThroughGate
|
||||
// keeps its own flat denial): the shipped contract — the feature on,
|
||||
// the level-5 rule, and the message the panel must show, verbatim.
|
||||
const flock = jump?.factionLock;
|
||||
check('factionLock: section present', !!flock && typeof flock === 'object');
|
||||
check('factionLock: enabled (feature switch, on)', flock?.enabled === true);
|
||||
check('factionLock: minLevel is the level that OPENS the lock (5)', Number(flock?.minLevel) === 5);
|
||||
check('factionLock: the message is the panel\'s verbatim copy',
|
||||
flock?.message === 'Jump Gate is under Faction Control. Status: Locked');
|
||||
check('jump: the gap keeps the ship outside the keepout with room to spare',
|
||||
Number(jump?.arrivalGap ?? -1) + Number(gates.size ?? 0) + Number(gates.shipClearance ?? 0) > Number(gates.size ?? 0) + Number(gates.shipClearance ?? 0));
|
||||
|
||||
|
|
@ -73,7 +89,7 @@ check('landing: combatDenyToast configured (glyph + text + color)',
|
|||
// ----------------------------------------------------------------------
|
||||
// The pure geometry (js/galaxy/JumpTravel.js)
|
||||
// ----------------------------------------------------------------------
|
||||
const { returnGateFor, arrivalPoint, jumpArrival, ambushTriggerDistance } = await import('../js/galaxy/JumpTravel.js');
|
||||
const { returnGateFor, arrivalPoint, jumpArrival, ambushTriggerDistance, factionGateLocked } = await import('../js/galaxy/JumpTravel.js');
|
||||
|
||||
const contentA = {
|
||||
jumps: [
|
||||
|
|
@ -129,6 +145,41 @@ check('ambushTriggerDistance: null on a missing point (no gate / no capital)',
|
|||
check('ambushTriggerDistance: null on a zero stretch (gate on the world)',
|
||||
ambushTriggerDistance({ x: 5, y: 5 }, { x: 5, y: 5 }) === null);
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// The FACTION GATE LOCK (js/galaxy/JumpTravel.js → factionGateLocked) —
|
||||
// a gate FROM a neutral system INTO a faction-controlled one, locked
|
||||
// while the player's level is below minLevel (data/gates.json →
|
||||
// jump.factionLock; the gate comm window's grayed REQUEST JUMP + the
|
||||
// "under Faction Control" message are the visible half).
|
||||
// ----------------------------------------------------------------------
|
||||
const NEUTRAL = { id: 'n', faction: null };
|
||||
const NEUTRAL_STR = { id: 'ns', faction: 'neutral' };
|
||||
const FACTION = { id: 'f', faction: 'apex' };
|
||||
const FACTION2 = { id: 'f2', faction: 'mindweave' };
|
||||
|
||||
check('factionGateLocked: neutral → faction, below minLevel, is LOCKED',
|
||||
factionGateLocked({ minLevel: 5, level: 0, from: NEUTRAL, dest: FACTION }) === true &&
|
||||
factionGateLocked({ minLevel: 5, level: 4, from: NEUTRAL, dest: FACTION }) === true);
|
||||
check('factionGateLocked: at/above minLevel the lock is OPEN (level 5)',
|
||||
factionGateLocked({ minLevel: 5, level: 5, from: NEUTRAL, dest: FACTION }) === false &&
|
||||
factionGateLocked({ minLevel: 5, level: 80, from: NEUTRAL, dest: FACTION }) === false);
|
||||
check('factionGateLocked: a missing level counts as 0 (a fresh run finds it locked)',
|
||||
factionGateLocked({ minLevel: 5, level: null, from: NEUTRAL, dest: FACTION }) === true &&
|
||||
factionGateLocked({ minLevel: 5, level: undefined, from: NEUTRAL, dest: FACTION }) === true);
|
||||
check('factionGateLocked: the neutral→neutral hop is never locked',
|
||||
factionGateLocked({ minLevel: 5, level: 0, from: NEUTRAL, dest: NEUTRAL }) === false &&
|
||||
factionGateLocked({ minLevel: 5, level: 0, from: NEUTRAL_STR, dest: NEUTRAL }) === false);
|
||||
check('factionGateLocked: gates FROM a faction system are never locked (either way)',
|
||||
factionGateLocked({ minLevel: 5, level: 0, from: FACTION, dest: NEUTRAL }) === false &&
|
||||
factionGateLocked({ minLevel: 5, level: 0, from: FACTION, dest: FACTION2 }) === false);
|
||||
check('factionGateLocked: an unknown destination is not faction-controlled (never locked)',
|
||||
factionGateLocked({ minLevel: 5, level: 0, from: NEUTRAL, dest: null }) === false);
|
||||
check('factionGateLocked: enabled:false lifts the lock entirely',
|
||||
factionGateLocked({ enabled: false, minLevel: 5, level: 0, from: NEUTRAL, dest: FACTION }) === false);
|
||||
check('factionGateLocked: a custom minLevel rules (10: level 5 is still locked, 10 open)',
|
||||
factionGateLocked({ minLevel: 10, level: 5, from: NEUTRAL, dest: FACTION }) === true &&
|
||||
factionGateLocked({ minLevel: 10, level: 10, from: NEUTRAL, dest: FACTION }) === false);
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// A REAL galaxy (bootstrap config the way main.js does, from the data dir)
|
||||
// ----------------------------------------------------------------------
|
||||
|
|
@ -172,7 +223,32 @@ for (const rec of g.records) {
|
|||
}
|
||||
}
|
||||
check('galaxy: every gate\'s destination is a known system (no dangling links)', badFb === 0);
|
||||
check(`galaxy: ${total} gates, ${withReturn} land at a return gate, the rest are one-way (origin fallback)`,
|
||||
|
||||
// The FACTION GATE LOCK against this galaxy: at a fresh run's level (0)
|
||||
// the neutral→faction gates stand LOCKED (the panel's grayed REQUEST
|
||||
// JUMP + its message); at level 5 every lock is open — and the galaxy
|
||||
// actually holds such gates (the lock has a target).
|
||||
const lockedAt = (level) => {
|
||||
let n = 0;
|
||||
for (const rec of g.records) {
|
||||
for (const j of g.ensureContent(rec.id).jumps ?? []) {
|
||||
const dest = g.byId.get(j.to) ?? null;
|
||||
if (factionGateLocked({
|
||||
enabled: flock.enabled !== false,
|
||||
minLevel: Number(flock.minLevel) || 5,
|
||||
level,
|
||||
from: rec,
|
||||
dest,
|
||||
})) n += 1;
|
||||
}
|
||||
}
|
||||
return n;
|
||||
};
|
||||
const lockedFresh = lockedAt(0);
|
||||
check(`galaxy: ${lockedFresh} neutral→faction gates stand at a fresh run's level (the lock has a target)`,
|
||||
lockedFresh > 0);
|
||||
check('galaxy: level 5 lifts every faction-gate lock', lockedFresh > 0 && lockedAt(5) === 0);
|
||||
check(`galaxy: ${withReturn} gates land at a return gate, the rest are one-way (origin fallback)`,
|
||||
withReturn > 0 && total - withReturn >= 0);
|
||||
check('galaxy: every landing is past the keepout and inside the gate tether', badLanding === 0);
|
||||
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -24,6 +24,11 @@
|
|||
* record). The scene falls back to the destination's origin (its
|
||||
* home-tether zone) when this is null.
|
||||
*
|
||||
* factionGateLocked(o)
|
||||
* The FACTION GATE LOCK (data/gates.json → jump.factionLock): a
|
||||
* gate FROM a neutral system INTO a faction-controlled one, locked
|
||||
* while the player's level is below minLevel.
|
||||
*
|
||||
* GameScene.jumpThroughGate supplies the live numbers (the gate's
|
||||
* radius/clearance, the ship's radius, data/gates.json → jump) and
|
||||
* does the actual transport (re-stage the run for the destination
|
||||
|
|
@ -105,3 +110,46 @@ export function ambushTriggerDistance(gate, world) {
|
|||
const d = Math.hypot(gx - wx, gy - wy);
|
||||
return d > 0 ? d / 2 : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The FACTION GATE LOCK (data/gates.json → jump.factionLock): a jump
|
||||
* gate FROM a NEUTRAL system INTO a FACTION-CONTROLLED one is locked
|
||||
* while the player's level is below `minLevel`.
|
||||
*
|
||||
* - A system is NEUTRAL when its record's `faction` is null, '' or the
|
||||
* string 'neutral' (the roster defaults to null — neutral wild space);
|
||||
* any other `faction` (apex/mindweave/zenith, data/factions.json) is
|
||||
* FACTION-CONTROLLED.
|
||||
* - Gates FROM a faction system are never locked (the run is already
|
||||
* inside faction space — leaving it, or moving within it, stays open),
|
||||
* and neutral→neutral hops are never locked.
|
||||
* - A missing level (null — no XP bank yet) counts as level 0, so a
|
||||
* fresh run finds the gates locked.
|
||||
* - A missing destination (unknown id) is not faction-controlled, so it
|
||||
* is never locked by this rule (the scene's own "unknown destination"
|
||||
* denial stands on its own).
|
||||
*
|
||||
* Pure (no Phaser, no config import) — the caller resolves the config
|
||||
* section and the system records and passes them in (Node-testable in
|
||||
* dev/jump-travel.test.mjs).
|
||||
*
|
||||
* @param {object} o { enabled (the feature switch, default true),
|
||||
* minLevel (the level that OPENS the lock, default 5), level (the
|
||||
* player's combat level — null/undefined = 0), from (the current
|
||||
* system's record), dest (the gate's DESTINATION system's record —
|
||||
* null when unknown) } — a system record only contributes `faction`
|
||||
* @returns {boolean} true while the gate's jump is blocked
|
||||
*/
|
||||
export function factionGateLocked(o = {}) {
|
||||
if (o.enabled === false) return false; // the feature switch
|
||||
const minLevel = Number.isFinite(Number(o.minLevel)) ? Number(o.minLevel) : 5;
|
||||
const level = Number.isFinite(Number(o.level)) ? Number(o.level) : 0;
|
||||
if (level >= minLevel) return false; // the lock is open at/above minLevel
|
||||
const neutral = (rec) => {
|
||||
const f = rec?.faction;
|
||||
return f == null || f === '' || f === 'neutral';
|
||||
};
|
||||
if (!neutral(o.from)) return false; // already inside faction space
|
||||
if (o.dest == null) return false; // an unknown destination is not faction-controlled
|
||||
return !neutral(o.dest);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import { Planet } from '../entities/Planet.js';
|
|||
import { AsteroidCluster } from '../entities/AsteroidCluster.js';
|
||||
import { Station } from '../entities/Station.js';
|
||||
import { JumpGate } from '../entities/JumpGate.js';
|
||||
import { jumpArrival, returnGateFor, ambushTriggerDistance } from '../galaxy/JumpTravel.js';
|
||||
import { jumpArrival, returnGateFor, ambushTriggerDistance, factionGateLocked } from '../galaxy/JumpTravel.js';
|
||||
import { Starfield } from '../visuals/Starfield.js';
|
||||
import { NebulaAtmosphere } from '../visuals/NebulaAtmosphere.js';
|
||||
import { NebulaLightning } from '../visuals/NebulaLightning.js';
|
||||
|
|
@ -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
|
||||
|
|
@ -1889,11 +1902,14 @@ export class GameScene extends Phaser.Scene {
|
|||
|
||||
// A JUMP GATE under the cursor (solid — the ship can't pass
|
||||
// through it): the click opens the GATE COMM WINDOW
|
||||
// (openGateCommsPanel) — the LINK line reads ACTIVE / DORMANT,
|
||||
// REQUEST JUMP initiates the transport (GameScene.jumpThroughGate,
|
||||
// (openGateCommsPanel) — the LINK line reads ACTIVE / DORMANT (or
|
||||
// the FACTION LOCK's message — the gate is from a neutral system
|
||||
// into a faction-controlled one and the player is below the lock's
|
||||
// minLevel — data/gates.json → jump.factionLock), REQUEST JUMP
|
||||
// initiates the transport (GameScene.jumpThroughGate,
|
||||
// data/gates.json → jump; a grayed ghost while the gate is
|
||||
// dormant) and CANCEL closes the window. Like a planet click it
|
||||
// is NOT a fly-here — the ship stays put.
|
||||
// dormant or locked) and CANCEL closes the window. Like a planet
|
||||
// click it is NOT a fly-here — the ship stays put.
|
||||
const gateObj = this.gateAt(pointer.worldX, pointer.worldY);
|
||||
if (gateObj) {
|
||||
this.hideHint();
|
||||
|
|
@ -3198,6 +3214,20 @@ export class GameScene extends Phaser.Scene {
|
|||
);
|
||||
return;
|
||||
}
|
||||
// FACTION GATE LOCK (data/gates.json → jump.factionLock) — the gate
|
||||
// comm window's grayed REQUEST JUMP + the "under Faction Control"
|
||||
// message are the visible half; this is the scene's own flat denial
|
||||
// on top (the same shape as the mining/combat guards above): a gate
|
||||
// from a neutral system into a faction-controlled one, below the
|
||||
// lock's minLevel.
|
||||
if (this.factionGateLockedFor(gt)) {
|
||||
const t = config.get('gates.jump.factionLock', {});
|
||||
this.consoleToast(
|
||||
String(t.message ?? 'JUMP GATE IS UNDER FACTION CONTROL. STATUS: LOCKED'),
|
||||
{ glyph: '⌁', glyphColor: toCss(themeColor('neon2', 0xff9b9b)) },
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Where we arrive: just past the return gate's keepout (JumpTravel).
|
||||
// Fallback — no return gate (a one-way shortcut, shortcuts are OFF in
|
||||
// the current config): the destination's origin (its center is empty —
|
||||
|
|
@ -3487,23 +3517,98 @@ export class GameScene extends Phaser.Scene {
|
|||
* A JUMP GATE was clicked: the comm window opens at the click — the
|
||||
* name decodes in, the LINK line reads ACTIVE / DORMANT, and the
|
||||
* buttons wait (REQUEST JUMP is a grayed ghost while the gate is
|
||||
* dormant or gates.jump.enabled is off). Same contract as a planet
|
||||
* click: NOT a fly-here (the ship stays put), world-anchored at the
|
||||
* click, side-picked to stay on screen (CommsPanel.pickSideAndPlace).
|
||||
* dormant or gates.jump.enabled is off). The FACTION GATE LOCK
|
||||
* (data/gates.json → jump.factionLock) replaces the LINK line with its
|
||||
* own message ("Jump Gate is under Faction Control. Status: Locked")
|
||||
* and grays REQUEST JUMP too — a gate from a NEUTRAL system into a
|
||||
* FACTION-CONTROLLED one, below the player's level 5. Same contract
|
||||
* as a planet click: NOT a fly-here (the ship stays put),
|
||||
* world-anchored at the click, side-picked to stay on screen
|
||||
* (CommsPanel.pickSideAndPlace).
|
||||
*/
|
||||
openGateCommsPanel(gt, pointer) {
|
||||
const fl = config.get('gates.jump.factionLock', {});
|
||||
const locked = this.factionGateLockedFor(gt);
|
||||
this.commsPanel.open(pointer.worldX, pointer.worldY, pointer.x, pointer.y, {
|
||||
name: gt.discoveryName,
|
||||
kindLabel: String(config.get('gates.typeLabel', 'Jump Gate')),
|
||||
gate: {
|
||||
active: gt.active,
|
||||
jumpEnabled: config.get('gates.jump.enabled', true) !== false,
|
||||
// FACTION GATE LOCK — the panel's gate variant shows `message`
|
||||
// (the LINK line's place) + the grayed REQUEST JUMP; the scene's
|
||||
// own flat denial sits in jumpThroughGate (defense in depth).
|
||||
locked,
|
||||
lockMessage: String(
|
||||
fl.message ?? 'Jump Gate is under Faction Control. Status: Locked',
|
||||
),
|
||||
// The JumpGate entity — commsAction hands it to jumpThroughGate.
|
||||
entity: gt,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The FACTION GATE LOCK (data/gates.json → jump.factionLock; the pure
|
||||
* rule in js/galaxy/JumpTravel.js → factionGateLocked): true while
|
||||
* this gate's jump is blocked — a gate FROM a NEUTRAL system INTO a
|
||||
* FACTION-CONTROLLED one, while the player's combat level
|
||||
* (js/combat/XpManager.js) is below the lock's minLevel. The gate
|
||||
* comm window's grayed REQUEST JUMP + the "under Faction Control"
|
||||
* message are the visible half; jumpThroughGate's flat denial is the
|
||||
* scene's own guard on top (the same shape as its mining/combat
|
||||
* guards).
|
||||
* @param {object} gt the gate entity (JumpGate — .gate record)
|
||||
*/
|
||||
factionGateLockedFor(gt) {
|
||||
const cfg = config.get('gates.jump.factionLock', {});
|
||||
if (cfg.enabled === false) return false; // the feature switch
|
||||
const destId = gt?.gate?.to;
|
||||
return factionGateLocked({
|
||||
enabled: true,
|
||||
minLevel: Number(cfg.minLevel) || 5,
|
||||
level: this.combatLevel(),
|
||||
from: this.systemRecord ?? null,
|
||||
dest: destId != null && this.galaxy ? (this.galaxy.byId.get(destId) ?? null) : null,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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:
|
||||
|
|
|
|||
|
|
@ -196,6 +196,18 @@ class TermButton extends Phaser.GameObjects.Container {
|
|||
* the gate is dormant, or when gates.jump.enabled is off). CANCEL is
|
||||
* the same in both variants.
|
||||
*
|
||||
* FACTION LOCK (the scene's gate.locked — a gate from a NEUTRAL system
|
||||
* into a FACTION-CONTROLLED one, while the player is below the lock's
|
||||
* minLevel; data/gates.json → jump.factionLock): the status line's
|
||||
* slot instead holds the lock's own message, in two phosphor lines —
|
||||
*
|
||||
* JUMP GATE IS UNDER FACTION CONTROL. (amber, verbatim data copy)
|
||||
* STATUS: LOCKED (red)
|
||||
*
|
||||
* — the case widens to fit the wider line, and REQUEST JUMP is a
|
||||
* grayed ghost (the scene's jumpThroughGate keeps a flat denial on
|
||||
* top). CANCEL is the same in both.
|
||||
*
|
||||
* WORLD-ANCHORED (like the mining menu): the panel lives at the click
|
||||
* point in the world and stays with it as the camera trails. It opens
|
||||
* on the side (up/down/left/right) that keeps it fully on screen — the
|
||||
|
|
@ -215,7 +227,7 @@ class TermButton extends Phaser.GameObjects.Container {
|
|||
* });
|
||||
* // …or the gate variant (the two are mutually exclusive):
|
||||
* cp.open(wx, wy, screenX, screenY, {
|
||||
* name, kindLabel, gate: { active, jumpEnabled },
|
||||
* name, kindLabel, gate: { active, jumpEnabled, locked?, lockMessage? },
|
||||
* });
|
||||
* cp.close(); cp.contains(wx, wy); cp.isOpen; cp.update(time); cp.destroy();
|
||||
*/
|
||||
|
|
@ -234,7 +246,10 @@ export class CommsPanel extends Phaser.GameObjects.Container {
|
|||
|
||||
// ---- Geometry (px). The case frame wraps a phosphor screen; the
|
||||
// settled and unsettled layouts differ only in the reputation block.
|
||||
this.W = 312;
|
||||
// The FACTION LOCK variant widens the case to fit its two message
|
||||
// lines (open() → _setLockLines); the buttons keep W_BASE's width.
|
||||
this.W_BASE = 312;
|
||||
this.W = this.W_BASE; // the lock variant widens (open())
|
||||
this.framePad = 20; // the metal case's border
|
||||
this.padTop = 14;
|
||||
this.padBottom = 14;
|
||||
|
|
@ -246,6 +261,7 @@ export class CommsPanel extends Phaser.GameObjects.Container {
|
|||
this.marksH = 14;
|
||||
this.gapBtns = 12; // reputation block → buttons
|
||||
this.gapOpen = 14; // name block → buttons (unsettled)
|
||||
this.lockGap = 4; // the lock message's two lines (the faction lock)
|
||||
this.btnH = 30;
|
||||
this.btnGap = 8;
|
||||
this.btnW = this.W - this.framePad * 2 - 28; // content width (14 px screen padding each side)
|
||||
|
|
@ -264,6 +280,9 @@ export class CommsPanel extends Phaser.GameObjects.Container {
|
|||
this.framePad * 2 + this.padTop + this.nameH + this.kindH +
|
||||
this.gapSettled + this.repLabelH + this.gapBtns +
|
||||
this.btnH + this.btnGap + this.btnH + this.padBottom;
|
||||
// The gate + FACTION LOCK variant: the LINK line's slot becomes the
|
||||
// lock's two message lines (one more line than H_GATE).
|
||||
this.H_GATE_LOCKED = this.H_GATE + this.repLabelH + this.lockGap;
|
||||
this.H_OPEN =
|
||||
this.framePad * 2 + this.padTop + this.nameH + this.kindH +
|
||||
this.gapOpen + this.btnH + this.btnGap + this.btnH + this.padBottom;
|
||||
|
|
@ -297,6 +316,28 @@ export class CommsPanel extends Phaser.GameObjects.Container {
|
|||
letterSpacing: 1,
|
||||
})
|
||||
.setOrigin(1, 0.5);
|
||||
// The FACTION LOCK's message lines (the gate variant's LINK slot —
|
||||
// data/gates.json → jump.factionLock.message, split at "Status:"):
|
||||
// the message in the case's warning amber, the status clause in the
|
||||
// panel's red. Hidden except in the locked gate variant.
|
||||
this.lockLine1 = scene.add
|
||||
.text(nameX, 0, '', {
|
||||
fontFamily: FONT,
|
||||
fontSize: '10px',
|
||||
color: toCss(AMBER),
|
||||
letterSpacing: 2,
|
||||
})
|
||||
.setOrigin(0, 0.5);
|
||||
this.lockLine2 = scene.add
|
||||
.text(nameX, 0, '', {
|
||||
fontFamily: FONT,
|
||||
fontSize: '10px',
|
||||
color: toCss(MARK_RED),
|
||||
letterSpacing: 2,
|
||||
})
|
||||
.setOrigin(0, 0.5);
|
||||
this.lockLine1.setVisible(false);
|
||||
this.lockLine2.setVisible(false);
|
||||
// btn1's action id is DYNAMIC — the world variants read
|
||||
// REQUEST LANDING / LAND, the gate variant reads REQUEST JUMP and
|
||||
// must fire 'request-jump' (open() switches this).
|
||||
|
|
@ -315,6 +356,8 @@ export class CommsPanel extends Phaser.GameObjects.Container {
|
|||
this.kindText,
|
||||
this.repLabel,
|
||||
this.repValue,
|
||||
this.lockLine1,
|
||||
this.lockLine2,
|
||||
this.btn1,
|
||||
this.btn2,
|
||||
this.flickG,
|
||||
|
|
@ -333,6 +376,8 @@ export class CommsPanel extends Phaser.GameObjects.Container {
|
|||
this.isGate = false;
|
||||
this.gateActive = false;
|
||||
this.gateJumpEnabled = true;
|
||||
this.gateLocked = false;
|
||||
this.gateLockText = '';
|
||||
this.H = this.H_OPEN;
|
||||
this.layout();
|
||||
this.drawFrame();
|
||||
|
|
@ -350,7 +395,9 @@ export class CommsPanel extends Phaser.GameObjects.Container {
|
|||
* @param {number} wx, wy — the click, WORLD coords (the panel anchors here)
|
||||
* @param {number} sx, sy — the same click in SCREEN coords (decides the side)
|
||||
* @param {object} o — { name, settled, reputation, litMarks?, canLand?, key?, kindLabel?,
|
||||
* gate?: { active, jumpEnabled } } (gate = the gate variant)
|
||||
* gate?: { active, jumpEnabled, locked?, lockMessage? } }
|
||||
* (gate = the gate variant; locked + lockMessage
|
||||
* = the faction gate lock)
|
||||
*/
|
||||
open(wx, wy, sx, sy, o = {}) {
|
||||
if (this.scene === null || this.active === false) return;
|
||||
|
|
@ -392,23 +439,44 @@ export class CommsPanel extends Phaser.GameObjects.Container {
|
|||
// primary button's action id (the gate variant switches it).
|
||||
this.repValue.setColor(toCss(GRN_MID));
|
||||
this.btn1Id = 'request-landing';
|
||||
this.gateLocked = false; // a world opening starts with the lock lifted
|
||||
|
||||
if (this.isGate) {
|
||||
// JUMP GATE — one LINK status line (ACTIVE lit / DORMANT amber) in
|
||||
// place of the reputation block, and the primary button is the
|
||||
// jump itself (a grayed ghost while the gate is dormant, or with
|
||||
// gates.jump.enabled off). CANCEL is the same in both.
|
||||
//
|
||||
// FACTION GATE LOCK (the scene's gate.locked — a gate from a
|
||||
// NEUTRAL system into a FACTION-CONTROLLED one, below the
|
||||
// player's minLevel): the LINK line is replaced by the lock's own
|
||||
// message (data/gates.json → jump.factionLock.message, verbatim
|
||||
// under the panel's uppercase convention) and REQUEST JUMP is a
|
||||
// grayed ghost too (the scene's jumpThroughGate keeps a flat
|
||||
// denial on top).
|
||||
this.repRevealT0 = null;
|
||||
this.repFinalLit = 0;
|
||||
this.marksG.setVisible(false);
|
||||
this.repLabel.setVisible(true);
|
||||
this.repValue.setVisible(true);
|
||||
this.repLabel.setText('LINK');
|
||||
this.repValue.setText(this.gateActive ? 'ACTIVE' : 'DORMANT');
|
||||
this.repValue.setColor(toCss(this.gateActive ? GRN_BRIGHT : AMBER));
|
||||
this.btn1Id = 'request-jump';
|
||||
this.btn1.setLabel('REQUEST JUMP');
|
||||
this.btn1.setDisabled(!this.gateActive || !this.gateJumpEnabled);
|
||||
this.gateLocked = o.gate.locked === true;
|
||||
this.gateLockText = String(o.gate.lockMessage ?? '');
|
||||
this.W = this.W_BASE; // the lock variant may widen (below)
|
||||
if (this.gateLocked) {
|
||||
this.repLabel.setVisible(false);
|
||||
this.repValue.setVisible(false);
|
||||
this._setLockLines();
|
||||
this.btn1.setDisabled(true);
|
||||
} else {
|
||||
this.lockLine1.setVisible(false);
|
||||
this.lockLine2.setVisible(false);
|
||||
this.repLabel.setVisible(true);
|
||||
this.repValue.setVisible(true);
|
||||
this.repLabel.setText('LINK');
|
||||
this.repValue.setText(this.gateActive ? 'ACTIVE' : 'DORMANT');
|
||||
this.repValue.setColor(toCss(this.gateActive ? GRN_BRIGHT : AMBER));
|
||||
this.btn1.setDisabled(!this.gateActive || !this.gateJumpEnabled);
|
||||
}
|
||||
} else if (this.settled) {
|
||||
// …then the bar draws itself to the standing (update() drives it).
|
||||
const repMin = config.get('reputation.min', -20);
|
||||
|
|
@ -420,6 +488,8 @@ export class CommsPanel extends Phaser.GameObjects.Container {
|
|||
this.repLabel.setVisible(true);
|
||||
this.repValue.setVisible(true);
|
||||
this.repValue.setText(this.rep > 0 ? `+${this.rep}` : String(this.rep));
|
||||
this.lockLine1.setVisible(false);
|
||||
this.lockLine2.setVisible(false);
|
||||
this.marksG.setVisible(true);
|
||||
this.btn1.setLabel('REQUEST LANDING');
|
||||
this.btn1.setDisabled(!this.canLand);
|
||||
|
|
@ -429,6 +499,9 @@ export class CommsPanel extends Phaser.GameObjects.Container {
|
|||
this.repLabel.setVisible(false);
|
||||
this.repValue.setVisible(false);
|
||||
this.marksG.setVisible(false);
|
||||
this.gateLocked = false; // a world variant (a stale lock line is gone)
|
||||
this.lockLine1.setVisible(false);
|
||||
this.lockLine2.setVisible(false);
|
||||
this.btn1.setLabel('LAND');
|
||||
this.btn1.setDisabled(false);
|
||||
}
|
||||
|
|
@ -436,7 +509,7 @@ export class CommsPanel extends Phaser.GameObjects.Container {
|
|||
this.btn2.setDisabled(false);
|
||||
|
||||
this.H = this.isGate
|
||||
? this.H_GATE
|
||||
? (this.gateLocked ? this.H_GATE_LOCKED : this.H_GATE)
|
||||
: this.settled
|
||||
? this.H_SETTLED
|
||||
: this.H_OPEN;
|
||||
|
|
@ -462,6 +535,33 @@ export class CommsPanel extends Phaser.GameObjects.Container {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Split the FACTION LOCK's message (data/gates.json →
|
||||
* jump.factionLock.message — "Jump Gate is under Faction Control.
|
||||
* Status: Locked") into the panel's two phosphor lines (the message
|
||||
* up to the "Status:" clause, then the status clause), and WIDEN THE
|
||||
* CASE to fit the wider line (the buttons keep W_BASE's width).
|
||||
*/
|
||||
_setLockLines() {
|
||||
const msg = String(
|
||||
this.gateLockText || 'Jump Gate is under Faction Control. Status: Locked',
|
||||
).trim();
|
||||
const m = /Status:/i.exec(msg);
|
||||
const l1 = m && m.index > 0 ? msg.slice(0, m.index).trim() : msg;
|
||||
const l2 = m && m.index > 0 ? msg.slice(m.index).trim() : '';
|
||||
this.lockLine1.setText(l1.toUpperCase());
|
||||
this.lockLine1.setVisible(l1.length > 0);
|
||||
this.lockLine2.setText(l2.toUpperCase());
|
||||
this.lockLine2.setVisible(l2.length > 0);
|
||||
// The case widens to the wider line (content = line + case + screen
|
||||
// padding + a little breathing room).
|
||||
const widest = Math.max(this.lockLine1.width, this.lockLine2.width);
|
||||
this.W = Math.max(
|
||||
this.W_BASE,
|
||||
Math.ceil(widest) + this.framePad * 2 + this.innerPad * 2 + 12,
|
||||
);
|
||||
}
|
||||
|
||||
/** Close (any click outside, a button press, or ESC). */
|
||||
close() {
|
||||
if (this.state !== 'open') return;
|
||||
|
|
@ -544,7 +644,16 @@ export class CommsPanel extends Phaser.GameObjects.Container {
|
|||
this.kindText.setPosition(lx, st + this.padTop + this.nameH + this.kindH / 2);
|
||||
|
||||
let next = st + this.padTop + this.nameH + this.kindH;
|
||||
if (this.settled || this.isGate) {
|
||||
if (this.isGate && this.gateLocked) {
|
||||
// The FACTION LOCK's message — two phosphor lines where the LINK
|
||||
// line sits (data/gates.json → jump.factionLock.message).
|
||||
const blockH = this.repLabelH * 2 + this.lockGap;
|
||||
const midY = next + this.gapSettled + blockH / 2;
|
||||
this.lockLine1.setPosition(lx, midY - this.repLabelH / 2 - this.lockGap / 2);
|
||||
this.lockLine2.setPosition(lx, midY + this.repLabelH / 2 + this.lockGap / 2);
|
||||
this.marksY = -1e9;
|
||||
next = midY + blockH / 2 + this.gapBtns;
|
||||
} else if (this.settled || this.isGate) {
|
||||
// The label line — REPUTATION (world) or LINK (gate).
|
||||
const labelY = next + this.gapSettled + this.repLabelH / 2;
|
||||
this.repLabel.setPosition(lx, labelY);
|
||||
|
|
@ -780,6 +889,8 @@ export class CommsPanel extends Phaser.GameObjects.Container {
|
|||
this.kindText?.destroy();
|
||||
this.repLabel?.destroy();
|
||||
this.repValue?.destroy();
|
||||
this.lockLine1?.destroy();
|
||||
this.lockLine2?.destroy();
|
||||
this.btn1?.destroy();
|
||||
this.btn2?.destroy();
|
||||
super.destroy();
|
||||
|
|
|
|||
Loading…
Reference in New Issue