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:
Brian Fertig 2026-09-30 15:23:53 -06:00
parent f9f5dca694
commit 20d8fcb0ef
7 changed files with 164 additions and 33 deletions

View File

@ -1057,7 +1057,7 @@
"color": "#ff5c77", "color": "#ff5c77",
"text": "ACCESS DENIED — THE CONVOY'S DESTINATION HOLDS UNTIL THE TRANSPORT LANDS" "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}", "bountyLockBadge": "LOCKED — REQUIRES LEVEL {level}",
"bountyLockSub": "BRING YOUR RECORD UP TO LEVEL {level} AND THE BOARD WILL LIST YOU", "bountyLockSub": "BRING YOUR RECORD UP TO LEVEL {level} AND THE BOARD WILL LIST YOU",
"bountyAcceptDenyToast": { "bountyAcceptDenyToast": {
@ -1095,19 +1095,19 @@
"color": "#67e863", "color": "#67e863",
"text": "BOUNTY HUNT COMPLETE — {name} IS DOWN, ITS REWARD IS READY TO CLAIM" "text": "BOUNTY HUNT COMPLETE — {name} IS DOWN, ITS REWARD IS READY TO CLAIM"
}, },
"bountyGateLockBadge": "LOCKED — GATE ACCESS LEVEL {level}", "bountyGateLockBadge": "LOCKED — ACCESS TO {system} REQUIRED",
"bountyGateLockSub": "THE TARGET SYSTEM BEHIND THE GATE OPENS AT COMBAT LEVEL {level}", "bountyGateLockSub": "ACCESS TO {system} OPENS AT COMBAT LEVEL {level}",
"bountyGateDenyToast": { "bountyGateDenyToast": {
"glyph": "✕", "glyph": "✕",
"color": "#ff2d6f", "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}", "escortGateLockBadge": "LOCKED — ACCESS TO {system} REQUIRED",
"escortGateLockSub": "THE TRANSPORT'S GROUND BEHIND THE GATE OPENS AT COMBAT LEVEL {level}", "escortGateLockSub": "ACCESS TO {system} OPENS AT COMBAT LEVEL {level}",
"escortGateDenyToast": { "escortGateDenyToast": {
"glyph": "✕", "glyph": "✕",
"color": "#ff2d6f", "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}"
} }
} }
} }

View File

@ -200,6 +200,22 @@ section('THE BOARD — rows on Neutral boards, LOCKED below level 2');
const at = S.sideQuestAcceptable(fb, gl); 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 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}`); 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)));
} }
} }
} }

View File

@ -45,6 +45,8 @@ const {
sideQuestAcceptable, sideQuestAcceptable,
sideQuestPostingsFor, sideQuestPostingsFor,
gateAccessLevel, gateAccessLevel,
gateLockCopy,
postingGateLocked,
escortDestinationFor, escortDestinationFor,
escortWaveRoster, escortWaveRoster,
escortEncounterXp, 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.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 - 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'); 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');
} }
// =========================================================================== // ===========================================================================

View File

@ -1475,6 +1475,69 @@ export function bountyDenyCopy() {
return bountyUiLine('bountyAcceptDenyToast', { level: bountyMinLevel() }, 'COMMISSION REFUSED — LEVEL {level} REQUIRED'); 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). */ /** A hunt line's copy (a leg logged / pickets inbound / the mark sighted / down / complete). */
export function bountyText(key, vars = {}) { export function bountyText(key, vars = {}) {
return bountyUiLine(key, vars, ''); return bountyUiLine(key, vars, '');
@ -1637,6 +1700,12 @@ export function sideQuestPostingsFor(seed, rec, worldKey, galaxy = null, level =
const gate = sideQuestAcceptable(posting, level); const gate = sideQuestAcceptable(posting, level);
posting.locked = gate.locked === true; posting.locked = gate.locked === true;
posting.minLevel = gate.minLevel; 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); out.push(posting);
} }
return out; return out;

View File

@ -77,6 +77,7 @@ import {
patrolRouteComplete, patrolRouteComplete,
sideQuestAcceptable, sideQuestAcceptable,
bountyDenyCopy, bountyDenyCopy,
gateLockCopy,
deriveBountyPlan, deriveBountyPlan,
bountySkirmishScript, bountySkirmishScript,
bountyEngagementScript, bountyEngagementScript,
@ -5686,19 +5687,27 @@ export class GameScene extends Phaser.Scene {
if (!this.sideQuestState) return { ok: false }; if (!this.sideQuestState) return { ok: false };
const posting = this.sideQuestPostings(worldName, isStation).find((p) => p.id === jobId) ?? null; const posting = this.sideQuestPostings(worldName, isStation).find((p) => p.id === jobId) ?? null;
if (!posting) return { ok: false }; if (!posting) return { ok: false };
// The LEVEL GATE (data/sidequests.json → categories.bounty.minLevel — the // The ACCEPT gates: the LEVEL gate (data/sidequests.json →
// hunt's 2): a locked contract is a REFUSAL, never a record. The board // categories.bounty.minLevel — the hunt's 2) and the GATE ACCESS gate
// renders the row LOCKED; pressing it anyway says why in the data's own // (the posting's gateLevel — its target system behind a faction gate). A
// words (`bountyAcceptDenyToast`) and hands the caller `locked` so the // locked contract is a REFUSAL, never a record. The board renders the
// surface console can say the same thing out loud. // 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()); const gate = sideQuestAcceptable(posting, this.combatLevel());
if (!gate.ok) { 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'), { this.consoleToast(String(deny?.text ?? 'COMMISSION REFUSED'), {
glyph: deny?.glyph || '\u2717', glyph: deny?.glyph || '\u2717',
glyphColor: toCss(deny?.color || '#ff5c77'), 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 }; if (!this.sideQuestState.accept(jobId)) return { ok: false };
// The PATROL's ROUTE stands WITH the accept — the route is the // The PATROL's ROUTE stands WITH the accept — the route is the

View File

@ -22,6 +22,7 @@ import { CreditsHud } from '../ui/CreditsHud.js';
import { MineralHud } from '../ui/MineralHud.js'; import { MineralHud } from '../ui/MineralHud.js';
import * as Economy from '../economy/Economy.js'; import * as Economy from '../economy/Economy.js';
import { buildDefs } from '../research/ResearchModel.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 * 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 }, { glyph: t.glyph ?? '◆', glyphColor: t.color ? toCss(t.color) : undefined },
); );
} else if (res?.locked) { } else if (res?.locked) {
// The LEVEL GATE's refusal (a BOUNTY HUNT under categories.bounty.minLevel // The GATE ACCESS lock's refusal (a faction board's hunt / rescue
// — data → ui.bountyAcceptDenyToast): the board greyed the row, so this is // behind a hostile neighbor) names the system the player needs access
// the line that says WHY, in the level's own number off the posting. The // to FIRST (data → ui.{bounty,escort}GateDenyToast — the gate's own
// game scene's own toast lands behind this sleeping one. // words, the target system filled); the LEVEL GATE's refusal is the
const d = config.get('sidequests.ui.bountyAcceptDenyToast', {}); // 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; 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 ?? '✗', glyph: d.glyph ?? '✗',
glyphColor: d.color ? toCss(d.color) : undefined, glyphColor: d.color ? toCss(d.color) : undefined,
}); });

View File

@ -51,6 +51,7 @@ import { fontStack, themeColor } from '../utils/Theme.js';
import { ScrambleDecode } from '../utils/Decode.js'; import { ScrambleDecode } from '../utils/Decode.js';
import { playSfxOn } from '../utils/Sfx.js'; import { playSfxOn } from '../utils/Sfx.js';
import { CyberShape } from './CyberShape.js'; import { CyberShape } from './CyberShape.js';
import { gateLockCopy } from '../quests/SideQuests.js';
const HEADER = fontStack('header'); const HEADER = fontStack('header');
const BODY = fontStack('body'); const BODY = fontStack('body');
@ -602,11 +603,16 @@ export class JobBoardWindow extends Phaser.GameObjects.Container {
let rightTxt; let rightTxt;
let rightColor; let rightColor;
if (locked) { if (locked) {
// The gate reads on the row itself: LOCKED — REQUIRES LEVEL 2 (data → // The gate reads on the row itself: the LEVEL gate reads LOCKED —
// ui.bountyLockBadge), greyed in place of the payout. The row still // REQUIRES LEVEL 2 (data → ui.bountyLockBadge); the GATE ACCESS lock
// SELECTS (the dossier is readable before the level lands); the // (the faction boards' hunt / rescue behind a hostile neighbor) names
// scene's accept is the door that refuses. // the system the player needs access to FIRST (data →
rightTxt = fillLevel(String(ui.bountyLockBadge ?? 'LOCKED — REQUIRES LEVEL {level}'), p); // 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; rightColor = C.faint;
rightSize = '9px'; rightSize = '9px';
} else if (state === 'claimed') { } 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 }); 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): // 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)) { if (sel.locked === true && !this.isAccepted(sel.id)) {
const gateLine = sel.gateLocked === true ? gateLockCopy(sel, 'sub') : null;
const lock = s 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), { .text(
fontFamily: BODY, rightX + 14,
fontSize: '9px', lineY,
color: toCss(0xff5c77), gateLine?.text ?? fillLevel(String(ui.bountyLockSub ?? 'BRING YOUR RECORD UP TO LEVEL {level} AND THE BOARD WILL LIST YOU'), sel),
letterSpacing: 2, { fontFamily: BODY, fontSize: '9px', color: toCss(0xff5c77), letterSpacing: 2 },
}) )
.setScrollFactor(0); .setScrollFactor(0);
this.add(lock); this.add(lock);
this.detailSection.content.push(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 // pressing it is how the player learns the rule (the scene's accept
// refuses with data/sidequests.json → ui.bountyAcceptDenyToast, which // refuses with data/sidequests.json → ui.bountyAcceptDenyToast, which
// the surface console speaks out). No contract is taken, ever. // 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; color = 0xff5c77;
action = 'accept'; action = 'accept';
} else if (state === 'open') { } else if (state === 'open') {