Rework gate-access lock copy to name the target system
- Gate locks (bounty + escort) now show "ACCESS TO {system}" instead of a generic level plate, so the player sees which system they need access to first.
- Add `gateLockSystemName`, `postingGateLocked`, and `gateLockCopy` helpers in SideQuests.js with badge/sub/deny forms; fall back to level-gate copy when no system is named.
- Stamp `gateLocked` on postings so JobBoardWindow, GameScene, and SurfaceScene can branch between gate words and the existing level-gate words.
- Update bounty/escort tests to assert the gate lock names the target system (and only that system) plus the opening level.
This commit is contained in:
parent
f9f5dca694
commit
20d8fcb0ef
|
|
@ -1057,7 +1057,7 @@
|
|||
"color": "#ff5c77",
|
||||
"text": "ACCESS DENIED — THE CONVOY'S DESTINATION HOLDS UNTIL THE TRANSPORT LANDS"
|
||||
},
|
||||
"_commentBounty": "The BOUNTY HUNT's face (js/ui/JobBoardWindow.js + GameScene.updateBountyHunt). NOTE THE SILENCE: nothing here names the leg that holds the mark or a picket wave — the chain is hidden until it fires, so these lines only ever speak of what the player has already flown into. bountyLockBadge = the greyed row's plate (and the detail's lock line) under categories.bounty.minLevel; bountyAcceptDenyToast = the scene's refusal when a locked contract is pressed anyway.",
|
||||
"_commentBounty": "The BOUNTY HUNT's face (js/ui/JobBoardWindow.js + GameScene.updateBountyHunt). NOTE THE SILENCE: nothing here names the leg that holds the mark or a picket wave — the chain is hidden until it fires, so these lines only ever speak of what the player has already flown into. bountyLockBadge = the greyed row's plate (and the detail's lock line) under categories.bounty.minLevel; bountyAcceptDenyToast = the scene's refusal when a locked contract is pressed anyway. {bounty,escort}GateLockBadge / …GateLockSub / …GateDenyToast = the GATE ACCESS lock's words (the faction boards' hunts + rescues behind a hostile neighbor — the posting's gateLevel): the greyed row's badge, the detail's lock line, and the scene's refusal, each naming the target system the player needs access to FIRST ({system}) and the combat level that access opens at ({level}).",
|
||||
"bountyLockBadge": "LOCKED — REQUIRES LEVEL {level}",
|
||||
"bountyLockSub": "BRING YOUR RECORD UP TO LEVEL {level} AND THE BOARD WILL LIST YOU",
|
||||
"bountyAcceptDenyToast": {
|
||||
|
|
@ -1095,19 +1095,19 @@
|
|||
"color": "#67e863",
|
||||
"text": "BOUNTY HUNT COMPLETE — {name} IS DOWN, ITS REWARD IS READY TO CLAIM"
|
||||
},
|
||||
"bountyGateLockBadge": "LOCKED — GATE ACCESS LEVEL {level}",
|
||||
"bountyGateLockSub": "THE TARGET SYSTEM BEHIND THE GATE OPENS AT COMBAT LEVEL {level}",
|
||||
"bountyGateLockBadge": "LOCKED — ACCESS TO {system} REQUIRED",
|
||||
"bountyGateLockSub": "ACCESS TO {system} OPENS AT COMBAT LEVEL {level}",
|
||||
"bountyGateDenyToast": {
|
||||
"glyph": "✕",
|
||||
"color": "#ff2d6f",
|
||||
"text": "GATE LOCKED — THE TARGET SYSTEM OPENS AT COMBAT LEVEL {level}"
|
||||
"text": "COMMISSION REFUSED — ACCESS TO {system} OPENS AT COMBAT LEVEL {level}"
|
||||
},
|
||||
"escortGateLockBadge": "LOCKED — GATE ACCESS LEVEL {level}",
|
||||
"escortGateLockSub": "THE TRANSPORT'S GROUND BEHIND THE GATE OPENS AT COMBAT LEVEL {level}",
|
||||
"escortGateLockBadge": "LOCKED — ACCESS TO {system} REQUIRED",
|
||||
"escortGateLockSub": "ACCESS TO {system} OPENS AT COMBAT LEVEL {level}",
|
||||
"escortGateDenyToast": {
|
||||
"glyph": "✕",
|
||||
"color": "#ff2d6f",
|
||||
"text": "GATE LOCKED — THE TRANSPORT'S GROUND OPENS AT COMBAT LEVEL {level}"
|
||||
"text": "COMMISSION REFUSED — ACCESS TO {system} OPENS AT COMBAT LEVEL {level}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -200,6 +200,22 @@ section('THE BOARD — rows on Neutral boards, LOCKED below level 2');
|
|||
const at = S.sideQuestAcceptable(fb, gl);
|
||||
check(`${f} board's hunt is LOCKED below the gate level`, below.locked === true, `lvl=${gl - 1} locked=${below.locked}`);
|
||||
check(`${f} board's hunt OPENS at the gate level`, at.ok === true, `lvl=${gl} ok=${at.ok}`);
|
||||
// The gate lock speaks the GATE's own words — naming the system the
|
||||
// player needs access to FIRST (the row's badge, the detail's line,
|
||||
// and the scene's refusal — data → ui.{bounty,escort}Gate*), not the
|
||||
// level gate's generic "REQUIRES LEVEL" plate.
|
||||
check(`${f} board's hunt's gate lock is the binding door`, S.postingGateLocked(fb) === true, `gateLevel=${fb.gateLevel} minLevel=${fb.minLevel}`);
|
||||
for (const w of ['badge', 'sub', 'deny']) {
|
||||
const copy = S.gateLockCopy(fb, w);
|
||||
check(`${f} board's gate ${w} names the target system`, new RegExp(`ACCESS TO RIVAL ${f.toUpperCase()}`.replace(/\s+/g, '\\s+')).test(copy?.text ?? ''), copy?.text);
|
||||
}
|
||||
// The badge carries the system; the sub + the refusal add the level that opens it.
|
||||
for (const w of ['sub', 'deny']) {
|
||||
const copy = S.gateLockCopy(fb, w);
|
||||
check(`${f} board's gate ${w} names the level that opens it`, new RegExp(`LEVEL ${gl}`).test(copy?.text ?? ''), copy?.text);
|
||||
}
|
||||
check('the gate-locked stamp rides the posting below the gate level', S.sideQuestPostingsFor(99, rec, 'p1', g, gl - 1).some((p) => p.category === 'bounty' && p.locked === true && p.gateLocked === true));
|
||||
check('the stamp lifts at the gate level', S.sideQuestPostingsFor(99, rec, 'p1', g, gl).every((p) => !(p.category === 'bounty' && p.gateLocked === true)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,8 @@ const {
|
|||
sideQuestAcceptable,
|
||||
sideQuestPostingsFor,
|
||||
gateAccessLevel,
|
||||
gateLockCopy,
|
||||
postingGateLocked,
|
||||
escortDestinationFor,
|
||||
escortWaveRoster,
|
||||
escortEncounterXp,
|
||||
|
|
@ -160,6 +162,22 @@ console.log('== the posting — neutral boards stand the classic escort; faction
|
|||
assert.ok(typeof fe.gateLevel === 'number' && fe.gateLevel >= gl && gl >= 1, `the rescue carries a GATE lock (target is faction-held) — gateLevel=${fe.gateLevel} rule=${gl}`);
|
||||
assert.equal(sideQuestAcceptable(fe, gl - 1).locked, true, 'the rescue is LOCKED below the gate level');
|
||||
assert.equal(sideQuestAcceptable(fe, gl).ok, true, 'the rescue OPENS at the gate level');
|
||||
// The gate lock speaks the GATE's own words — naming the system the
|
||||
// player needs access to FIRST (the transport's ground here, not the
|
||||
// delivery's friendly destination), in the row's badge, the detail's
|
||||
// line, and the scene's refusal (data → ui.escortGate*).
|
||||
assert.equal(postingGateLocked(fe), true, 'the escort’s gate lock is the binding door');
|
||||
for (const w of ['badge', 'sub', 'deny']) {
|
||||
const copy = gateLockCopy(fe, w);
|
||||
assert.ok(/ACCESS TO APEX VERGE/.test(copy?.text ?? ''), `the escort gate ${w} names the transport's system`, copy?.text);
|
||||
assert.ok(!/ZENITH VERGE/.test(copy?.text ?? ''), `the escort gate ${w} never names the delivery's own system`, copy?.text);
|
||||
}
|
||||
// The badge carries the system; the sub + the refusal add the level that opens it.
|
||||
for (const w of ['sub', 'deny']) {
|
||||
const copy = gateLockCopy(fe, w);
|
||||
assert.ok(new RegExp(`LEVEL ${gl}`).test(copy?.text ?? ''), `the escort gate ${w} names the opening level`, copy?.text);
|
||||
}
|
||||
assert.ok(gateLockCopy(fe, 'deny')?.glyph !== '', 'the escort gate refusal carries its own glyph');
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
|
|
|
|||
|
|
@ -1475,6 +1475,69 @@ export function bountyDenyCopy() {
|
|||
return bountyUiLine('bountyAcceptDenyToast', { level: bountyMinLevel() }, 'COMMISSION REFUSED — LEVEL {level} REQUIRED');
|
||||
}
|
||||
|
||||
/**
|
||||
* The system name the posting's GATE ACCESS lock stands on — the system the
|
||||
* player needs access to FIRST: the BOUNTY's mark's home (targetSystemName)
|
||||
or the ESCORT's trapped transport's ground (enemySystemName). Null when the
|
||||
* posting names none (a neutral posting, a data gap — the copy then falls
|
||||
* to the level gate's own words).
|
||||
* @param {object} posting a sideQuestPostingsFor posting
|
||||
* @returns {string|null}
|
||||
*/
|
||||
export function gateLockSystemName(posting) {
|
||||
const p = posting && typeof posting === 'object' ? posting : {};
|
||||
const raw = p.category === 'escort' ? p.enemySystemName : p.targetSystemName;
|
||||
const name = String(raw ?? '').trim();
|
||||
return name !== '' ? name : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the posting's binding door the GATE ACCESS lock (its target system sits
|
||||
* behind a faction gate) rather than the category's own level gate? The
|
||||
* gate level must stand AND outrun the level gate — then the lock's words
|
||||
* are the gate's, with the system named (gateLockCopy).
|
||||
* @param {object} posting a sideQuestPostingsFor posting
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function postingGateLocked(posting) {
|
||||
const gl = Math.max(0, Math.floor(Number(posting?.gateLevel) || 0));
|
||||
if (gl <= 0) return false;
|
||||
const minLevel = sideQuestCategoryMeta(posting?.category)?.minLevel ?? 0;
|
||||
return gl >= minLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* The GATE ACCESS lock's copy — the faction board's hunt / rescue stands
|
||||
* behind a gate into the hostile neighbor's system, and the player is told
|
||||
* WHICH system it needs access to before the contract stands open (the
|
||||
* data's {system} placeholder, resolved against the posting: bounty →
|
||||
* targetSystemName, escort → enemySystemName; the {level} is the posting's
|
||||
* gateLevel — the capability that opens the hop). The `which` forms: 'badge'
|
||||
* (the row's greyed plate + the door's label), 'sub' (the detail's lock
|
||||
* line), 'deny' (the scene's refusal when a locked contract is pressed).
|
||||
* A posting with no named system (a data gap) degrades to the level gate's
|
||||
* own words rather than shipping an empty brace pair.
|
||||
* @param {object} posting a sideQuestPostingsFor posting
|
||||
* @param {'badge'|'sub'|'deny'} [which] the form
|
||||
* @returns {{glyph:string,color:string,text:string}}
|
||||
*/
|
||||
export function gateLockCopy(posting, which = 'badge') {
|
||||
const isEscort = String(posting?.category ?? '') === 'escort';
|
||||
const key = {
|
||||
badge: isEscort ? 'escortGateLockBadge' : 'bountyGateLockBadge',
|
||||
sub: isEscort ? 'escortGateLockSub' : 'bountyGateLockSub',
|
||||
deny: isEscort ? 'escortGateDenyToast' : 'bountyGateDenyToast',
|
||||
}[which] ?? (isEscort ? 'escortGateLockBadge' : 'bountyGateLockBadge');
|
||||
const level =
|
||||
Math.max(0, Math.floor(Number(posting?.gateLevel) || 0)) ||
|
||||
Math.max(0, Math.floor(numOr(config.get('gates.jump.factionLock.minLevel', 5), 5)));
|
||||
const name = gateLockSystemName(posting);
|
||||
if (name === null) return which === 'deny' ? bountyDenyCopy() : bountyLockCopy(which === 'sub');
|
||||
const fb =
|
||||
which === 'deny' ? 'COMMISSION REFUSED — ACCESS TO {system} OPENS AT COMBAT LEVEL {level}' : 'LOCKED — ACCESS TO {system} REQUIRED';
|
||||
return bountyUiLine(key, { system: String(name).toUpperCase(), level }, fb);
|
||||
}
|
||||
|
||||
/** A hunt line's copy (a leg logged / pickets inbound / the mark sighted / down / complete). */
|
||||
export function bountyText(key, vars = {}) {
|
||||
return bountyUiLine(key, vars, '');
|
||||
|
|
@ -1637,6 +1700,12 @@ export function sideQuestPostingsFor(seed, rec, worldKey, galaxy = null, level =
|
|||
const gate = sideQuestAcceptable(posting, level);
|
||||
posting.locked = gate.locked === true;
|
||||
posting.minLevel = gate.minLevel;
|
||||
// The GATE ACCESS lock's mark (the posting's target system sits behind a
|
||||
// faction gate and it is the binding door — the board's row / detail +
|
||||
// the scene's refusal then speak in the gate's words, the system named,
|
||||
// not the level gate's).
|
||||
posting.gateLocked =
|
||||
gate.locked === true && gate.gateLevel > 0 && gate.gateLevel >= gate.minLevel;
|
||||
out.push(posting);
|
||||
}
|
||||
return out;
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ import {
|
|||
patrolRouteComplete,
|
||||
sideQuestAcceptable,
|
||||
bountyDenyCopy,
|
||||
gateLockCopy,
|
||||
deriveBountyPlan,
|
||||
bountySkirmishScript,
|
||||
bountyEngagementScript,
|
||||
|
|
@ -5686,19 +5687,27 @@ export class GameScene extends Phaser.Scene {
|
|||
if (!this.sideQuestState) return { ok: false };
|
||||
const posting = this.sideQuestPostings(worldName, isStation).find((p) => p.id === jobId) ?? null;
|
||||
if (!posting) return { ok: false };
|
||||
// The LEVEL GATE (data/sidequests.json → categories.bounty.minLevel — the
|
||||
// hunt's 2): a locked contract is a REFUSAL, never a record. The board
|
||||
// renders the row LOCKED; pressing it anyway says why in the data's own
|
||||
// words (`bountyAcceptDenyToast`) and hands the caller `locked` so the
|
||||
// surface console can say the same thing out loud.
|
||||
// The ACCEPT gates: the LEVEL gate (data/sidequests.json →
|
||||
// categories.bounty.minLevel — the hunt's 2) and the GATE ACCESS gate
|
||||
// (the posting's gateLevel — its target system behind a faction gate). A
|
||||
// locked contract is a REFUSAL, never a record. The board renders the
|
||||
// row LOCKED; pressing it anyway says why in the data's own words — the
|
||||
// level gate's (bountyAcceptDenyToast) or the gate's (the system named —
|
||||
// *GateDenyToast) — and hands the caller `locked` so the surface
|
||||
// console can say the same thing out loud.
|
||||
const gate = sideQuestAcceptable(posting, this.combatLevel());
|
||||
if (!gate.ok) {
|
||||
const deny = bountyDenyCopy();
|
||||
// The GATE ACCESS lock (a faction board's hunt / rescue behind a
|
||||
// hostile neighbor — the posting's gateLevel) speaks the GATE's own
|
||||
// words, naming the system the player needs access to FIRST (data →
|
||||
// ui.{bounty,escort}GateDenyToast); the level gate keeps its own.
|
||||
const gateLocked = gate.gateLevel > 0 && gate.gateLevel >= gate.minLevel;
|
||||
const deny = gateLocked ? gateLockCopy(posting, 'deny') : bountyDenyCopy();
|
||||
this.consoleToast(String(deny?.text ?? 'COMMISSION REFUSED'), {
|
||||
glyph: deny?.glyph || '\u2717',
|
||||
glyphColor: toCss(deny?.color || '#ff5c77'),
|
||||
});
|
||||
return { ok: false, locked: true, posting };
|
||||
return { ok: false, locked: true, gateLocked, posting };
|
||||
}
|
||||
if (!this.sideQuestState.accept(jobId)) return { ok: false };
|
||||
// The PATROL's ROUTE stands WITH the accept — the route is the
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import { CreditsHud } from '../ui/CreditsHud.js';
|
|||
import { MineralHud } from '../ui/MineralHud.js';
|
||||
import * as Economy from '../economy/Economy.js';
|
||||
import { buildDefs } from '../research/ResearchModel.js';
|
||||
import { gateLockCopy } from '../quests/SideQuests.js';
|
||||
|
||||
/**
|
||||
* The double-click window (ms): two presses this close together during a
|
||||
|
|
@ -1617,13 +1618,17 @@ export class SurfaceScene extends Phaser.Scene {
|
|||
{ glyph: t.glyph ?? '◆', glyphColor: t.color ? toCss(t.color) : undefined },
|
||||
);
|
||||
} else if (res?.locked) {
|
||||
// The LEVEL GATE's refusal (a BOUNTY HUNT under categories.bounty.minLevel
|
||||
// — data → ui.bountyAcceptDenyToast): the board greyed the row, so this is
|
||||
// the line that says WHY, in the level's own number off the posting. The
|
||||
// game scene's own toast lands behind this sleeping one.
|
||||
const d = config.get('sidequests.ui.bountyAcceptDenyToast', {});
|
||||
// The GATE ACCESS lock's refusal (a faction board's hunt / rescue
|
||||
// behind a hostile neighbor) names the system the player needs access
|
||||
// to FIRST (data → ui.{bounty,escort}GateDenyToast — the gate's own
|
||||
// words, the target system filled); the LEVEL GATE's refusal is the
|
||||
// generic line, its level off the posting. The game scene's own toast
|
||||
// lands behind this sleeping one.
|
||||
const d = res?.gateLocked
|
||||
? gateLockCopy(res.posting, 'deny')
|
||||
: config.get('sidequests.ui.bountyAcceptDenyToast', {});
|
||||
const level = Number.isFinite(Number(res?.posting?.minLevel)) ? Math.floor(Number(res.posting.minLevel)) : 2;
|
||||
this.consoleNote(String(d.text ?? 'COMMISSION REFUSED — LEVEL {level} REQUIRED').replace(/\{level\}/g, String(level)), {
|
||||
this.consoleNote(String(d.text ?? 'COMMISSION REFUSED').replace(/\{level\}/g, String(level)), {
|
||||
glyph: d.glyph ?? '✗',
|
||||
glyphColor: d.color ? toCss(d.color) : undefined,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ import { fontStack, themeColor } from '../utils/Theme.js';
|
|||
import { ScrambleDecode } from '../utils/Decode.js';
|
||||
import { playSfxOn } from '../utils/Sfx.js';
|
||||
import { CyberShape } from './CyberShape.js';
|
||||
import { gateLockCopy } from '../quests/SideQuests.js';
|
||||
|
||||
const HEADER = fontStack('header');
|
||||
const BODY = fontStack('body');
|
||||
|
|
@ -602,11 +603,16 @@ export class JobBoardWindow extends Phaser.GameObjects.Container {
|
|||
let rightTxt;
|
||||
let rightColor;
|
||||
if (locked) {
|
||||
// The gate reads on the row itself: LOCKED — REQUIRES LEVEL 2 (data →
|
||||
// ui.bountyLockBadge), greyed in place of the payout. The row still
|
||||
// SELECTS (the dossier is readable before the level lands); the
|
||||
// scene's accept is the door that refuses.
|
||||
rightTxt = fillLevel(String(ui.bountyLockBadge ?? 'LOCKED — REQUIRES LEVEL {level}'), p);
|
||||
// The gate reads on the row itself: the LEVEL gate reads LOCKED —
|
||||
// REQUIRES LEVEL 2 (data → ui.bountyLockBadge); the GATE ACCESS lock
|
||||
// (the faction boards' hunt / rescue behind a hostile neighbor) names
|
||||
// the system the player needs access to FIRST (data →
|
||||
// ui.{bounty,escort}GateLockBadge). The row still SELECTS (the
|
||||
// dossier is readable before the level lands); the scene's accept
|
||||
// is the door that refuses.
|
||||
rightTxt = p.gateLocked === true
|
||||
? gateLockCopy(p, 'badge')?.text ?? 'LOCKED'
|
||||
: fillLevel(String(ui.bountyLockBadge ?? 'LOCKED — REQUIRES LEVEL {level}'), p);
|
||||
rightColor = C.faint;
|
||||
rightSize = '9px';
|
||||
} else if (state === 'claimed') {
|
||||
|
|
@ -797,15 +803,18 @@ export class JobBoardWindow extends Phaser.GameObjects.Container {
|
|||
if (dossier.craftLabel) dline(ui.dossierCraft ?? 'CRAFT — {craft}', { craft: String(dossier.craftLabel).toUpperCase() }, { color: C.faint });
|
||||
}
|
||||
// The level gate's own line under the dossier (data → ui.bountyLockSub):
|
||||
// what the record needs, not what the contract hides.
|
||||
// what the record needs, not what the contract hides — or, under the
|
||||
// GATE ACCESS lock, the GATE's own words naming the system the player
|
||||
// needs access to first (data → ui.{bounty,escort}GateLockSub).
|
||||
if (sel.locked === true && !this.isAccepted(sel.id)) {
|
||||
const gateLine = sel.gateLocked === true ? gateLockCopy(sel, 'sub') : null;
|
||||
const lock = s
|
||||
.text(rightX + 14, lineY, fillLevel(String(ui.bountyLockSub ?? 'BRING YOUR RECORD UP TO LEVEL {level} AND THE BOARD WILL LIST YOU'), sel), {
|
||||
fontFamily: BODY,
|
||||
fontSize: '9px',
|
||||
color: toCss(0xff5c77),
|
||||
letterSpacing: 2,
|
||||
})
|
||||
.text(
|
||||
rightX + 14,
|
||||
lineY,
|
||||
gateLine?.text ?? fillLevel(String(ui.bountyLockSub ?? 'BRING YOUR RECORD UP TO LEVEL {level} AND THE BOARD WILL LIST YOU'), sel),
|
||||
{ fontFamily: BODY, fontSize: '9px', color: toCss(0xff5c77), letterSpacing: 2 },
|
||||
)
|
||||
.setScrollFactor(0);
|
||||
this.add(lock);
|
||||
this.detailSection.content.push(lock);
|
||||
|
|
@ -924,7 +933,12 @@ export class JobBoardWindow extends Phaser.GameObjects.Container {
|
|||
// pressing it is how the player learns the rule (the scene's accept
|
||||
// refuses with data/sidequests.json → ui.bountyAcceptDenyToast, which
|
||||
// the surface console speaks out). No contract is taken, ever.
|
||||
label = fillLevel(String(ui.bountyLockBadge ?? 'LOCKED — REQUIRES LEVEL {level}'), sel);
|
||||
// Under the GATE ACCESS lock the door reads the gate's own badge,
|
||||
// the system named (data → ui.{bounty,escort}GateLockBadge).
|
||||
label =
|
||||
sel.gateLocked === true
|
||||
? gateLockCopy(sel, 'badge')?.text ?? 'LOCKED'
|
||||
: fillLevel(String(ui.bountyLockBadge ?? 'LOCKED — REQUIRES LEVEL {level}'), sel);
|
||||
color = 0xff5c77;
|
||||
action = 'accept';
|
||||
} else if (state === 'open') {
|
||||
|
|
|
|||
Loading…
Reference in New Issue