Add isHomeSystem where-clause to hide Job Board in the home system
- The player's home system is safe space (no enemies), so no work should be posted on its board.
- worldContext now exposes isHomeSystem from GameScene; matchesWhere handles boolean values with list any-match.
- data/actionbar.json gives jobBoard a { isHomeSystem: false } clause instead of showing everywhere.
- explore.test.mjs covers home/non-home worlds, stations, and boolean-clause matching.
- Added the jobboard.mp4 bar clip asset; updated SurfaceScene/GameScene comments to reflect the new rule.
This commit is contained in:
parent
5890e93cde
commit
919bcc4c4e
Binary file not shown.
|
|
@ -30,9 +30,9 @@
|
|||
{ "id": "menu", "label": "Menu", "accent": "#ff2d6f" }
|
||||
],
|
||||
"explore": {
|
||||
"_comment": "The EXPLORE drawer — the full-width secondary bar (the deck's own left→right span, resting on its top rail — the shop tab bar's recipe) that folds up from the surface deck's Explore slot. SurfaceScene renders it with js/ui/MenuSubBar.js in span mode; the item LOGIC is js/ui/ExploreBar.js (worldContext / matchesWhere / resolveItems). `items` is the ordered button list: `id` (fired via onAction — the behavior lands later), `label`, `labelActive` (the label while the service is up — BAR relabels to LEAVE BAR while its clip loops, and is the only live button on the surface until pressed), `accent`, and an optional `where` clause — EVERY key it names must match the world the player is standing on for the button to appear. Keys: `world` ('planet' | 'station'); `planetClass` (the planet's class — 'terran' | 'rocky' | 'gas' | 'ice' | 'lava' | 'arid' | 'capitalNeutral' | 'capitalApex' | 'capitalMindweave' | 'capitalZenith', per data/planets.json → frames; 'none' on stations); `faction` (the faction controlling this system — 'apex' | 'mindweave' | 'zenith', or 'neutral' = wild space; per data/factions.json). A value may be a single entry or a list (any-match). No `where` = every planet and station (Job Board). The bar's clip is data/landing.json → videos[frame].bar (the neutral capital's capital-bar-neutral.mp4).",
|
||||
"_comment": "The EXPLORE drawer — the full-width secondary bar (the deck's own left→right span, resting on its top rail — the shop tab bar's recipe) that folds up from the surface deck's Explore slot. SurfaceScene renders it with js/ui/MenuSubBar.js in span mode; the item LOGIC is js/ui/ExploreBar.js (worldContext / matchesWhere / resolveItems). `items` is the ordered button list: `id` (fired via onAction — the behavior lands later), `label`, `labelActive` (the label while the service is up — BAR relabels to LEAVE BAR while its clip loops, and is the only live button on the surface until pressed), `accent`, and an optional `where` clause — EVERY key it names must match the world the player is standing on for the button to appear. Keys: `world` ('planet' | 'station'); `planetClass` (the planet's class — 'terran' | 'rocky' | 'gas' | 'ice' | 'lava' | 'arid' | 'capitalNeutral' | 'capitalApex' | 'capitalMindweave' | 'capitalZenith', per data/planets.json → frames; 'none' on stations); `faction` (the faction controlling this system — 'apex' | 'mindweave' | 'zenith', or 'neutral' = wild space; per data/factions.json); `isHomeSystem` (boolean — true in the player's home / starting system; `{ isHomeSystem: false }` = every system EXCEPT the home). A value may be a single entry or a list (any-match). No `where` = every planet and station; the Job Board carries `{ isHomeSystem: false }` — it's on every planet and station EXCEPT the player's home system. DESIGN NOTE: the home system is safe space — enemies should NEVER be placed there, so there is no work to post on its board. The bar's clip is data/landing.json → videos[frame].bar (the neutral capital's capital-bar-neutral.mp4).",
|
||||
"items": [
|
||||
{ "id": "jobBoard", "label": "Job Board", "accent": "#ffc94d" },
|
||||
{ "id": "jobBoard", "label": "Job Board", "accent": "#ffc94d", "where": { "isHomeSystem": false } },
|
||||
{ "id": "bar", "label": "Bar", "labelActive": "Leave Bar", "accent": "#00e5ff", "where": { "planetClass": "capitalNeutral" } }
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,12 +10,14 @@
|
|||
* - DECK — the surface deck swapped SHIP for EXPLORE (six slots, the
|
||||
* flight deck's ship slot untouched).
|
||||
* - ITEMS — data/actionbar.json → surface.explore.items: Job Board on
|
||||
* EVERY planet and station (no `where`), Bar only on the neutral
|
||||
* capital (`planetClass: capitalNeutral`), and nothing extra on a
|
||||
* rocky world or a station.
|
||||
* EVERY planet and station EXCEPT the player's home system
|
||||
* (`isHomeSystem: false` — safe space, no work posted there), Bar only
|
||||
* on the neutral capital (`planetClass: capitalNeutral`), and nothing
|
||||
* extra on a rocky world or a station.
|
||||
* - WHERE — the clause matcher: world / planetClass / faction keys,
|
||||
* single + list values, case-insensitive; stations have no class
|
||||
* (`none`); neutral wild space reads `faction: 'neutral'`.
|
||||
* (`none`); neutral wild space reads `faction: 'neutral'`;
|
||||
* boolean keys (isHomeSystem) compare directly (a list any-matches).
|
||||
* - SPAN — MenuSubBar in span mode stretches across the deck's
|
||||
* left→right edges, resting on its top rail (the shop tab bar's
|
||||
* span), buttons packed at the left; without span the classic
|
||||
|
|
@ -82,6 +84,7 @@ console.log('== CONTEXT — world / planetClass / faction resolve from the run =
|
|||
assert.equal(rocky.world, 'planet');
|
||||
assert.equal(rocky.planetClass, 'rocky');
|
||||
assert.equal(rocky.faction, 'neutral', 'a null faction reads neutral wild space');
|
||||
assert.equal(rocky.isHomeSystem, false, 'a plain world is not the home system');
|
||||
|
||||
// The same world inside an APEX-controlled system.
|
||||
const apex = worldContext(
|
||||
|
|
@ -111,25 +114,33 @@ console.log('== CONTEXT — world / planetClass / faction resolve from the run =
|
|||
// The HOME world (gameScene.planet — not in systemPlanets).
|
||||
const home = worldContext(
|
||||
{ isStation: false, name: 'Terra' },
|
||||
{ systemPlanets: [], planet: { discoveryName: 'Terra', name: 'terran' }, systemRecord: { faction: null } },
|
||||
{
|
||||
systemPlanets: [],
|
||||
planet: { discoveryName: 'Terra', name: 'terran' },
|
||||
systemRecord: { faction: null },
|
||||
isHomeSystem: true,
|
||||
},
|
||||
);
|
||||
assert.equal(home.planetClass, 'terran', 'the home world resolves via gameScene.planet');
|
||||
assert.equal(home.isHomeSystem, true, 'the home system reads isHomeSystem true');
|
||||
|
||||
// No game scene at all (defensive) — the context still forms.
|
||||
const bare = worldContext({ isStation: false, name: 'X' }, null);
|
||||
assert.equal(bare.planetClass, null);
|
||||
assert.equal(bare.faction, 'neutral');
|
||||
assert.equal(bare.isHomeSystem, false, 'no scene → not the home system');
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
console.log('== ITEMS — Job Board everywhere, Bar on the neutral capital ==');
|
||||
console.log('== ITEMS — Job Board everywhere except home, Bar on the neutral capital ==');
|
||||
{
|
||||
const mk = (cls, { isStation = false, faction = null } = {}) =>
|
||||
const mk = (cls, { isStation = false, faction = null, home = false } = {}) =>
|
||||
worldContext(
|
||||
{ isStation, name: 'World' },
|
||||
{
|
||||
systemPlanets: isStation ? [] : [{ discoveryName: 'World', name: cls }],
|
||||
systemRecord: { faction },
|
||||
isHomeSystem: home,
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -142,6 +153,12 @@ console.log('== ITEMS — Job Board everywhere, Bar on the neutral capital ==');
|
|||
assert.ok(ids(resolveItems(mk(null, { isStation: true }))).includes('jobBoard'), 'job board on a station');
|
||||
// …and inside faction space.
|
||||
assert.ok(ids(resolveItems(mk('rocky', { faction: 'zenith' }))).includes('jobBoard'), 'job board in zenith space');
|
||||
// …but NEVER in the player's home system (safe space — the Job Board's
|
||||
// `isHomeSystem: false` clause): the home world AND any other world in
|
||||
// the home system (a second planet or a station there).
|
||||
assert.ok(!ids(resolveItems(mk('terran', { home: true }))).includes('jobBoard'), 'no job board on the home world');
|
||||
assert.ok(!ids(resolveItems(mk('rocky', { home: true }))).includes('jobBoard'), 'no job board on a home-system planet');
|
||||
assert.ok(!ids(resolveItems(mk(null, { isStation: true, home: true }))).includes('jobBoard'), 'no job board on a home-system station');
|
||||
|
||||
// Bar: the neutral capital ONLY.
|
||||
assert.deepEqual(ids(resolveItems(mk('capitalNeutral'))), ['jobBoard', 'bar'], 'bar on the neutral capital');
|
||||
|
|
@ -191,6 +208,20 @@ console.log('== WHERE — clause matching (single + list values, case-insensitiv
|
|||
assert.ok(matchesWhere({ faction: ['apex', 'zenith'] }, apex), 'faction lists any-match');
|
||||
assert.ok(matchesWhere({ world: 'planet', faction: 'apex', planetClass: 'rocky' }, apex), 'AND across keys');
|
||||
assert.ok(!matchesWhere({ world: 'planet', faction: 'apex', planetClass: 'ice' }, apex));
|
||||
|
||||
// Boolean keys (isHomeSystem): direct comparison, a list any-matches,
|
||||
// AND-able with string keys.
|
||||
const home = worldContext(
|
||||
{ isStation: false, name: 'H' },
|
||||
{ systemPlanets: [{ discoveryName: 'H', name: 'terran' }], systemRecord: { faction: null }, isHomeSystem: true },
|
||||
);
|
||||
assert.ok(matchesWhere({ isHomeSystem: false }, rocky), 'isHomeSystem false matches a non-home world');
|
||||
assert.ok(!matchesWhere({ isHomeSystem: false }, home), '…but not the home system (the Job Board\u2019s clause)');
|
||||
assert.ok(matchesWhere({ isHomeSystem: true }, home), 'isHomeSystem true matches the home system');
|
||||
assert.ok(!matchesWhere({ isHomeSystem: true }, rocky), '…and not elsewhere');
|
||||
assert.ok(matchesWhere({ isHomeSystem: [true, false] }, home), 'boolean lists any-match');
|
||||
assert.ok(matchesWhere({ world: 'planet', isHomeSystem: true }, home), 'AND across string + boolean keys');
|
||||
assert.ok(!matchesWhere({ world: 'station', isHomeSystem: true }, home), '…one failing key fails the clause');
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
|
|
|
|||
|
|
@ -966,6 +966,11 @@ export class GameScene extends Phaser.Scene {
|
|||
// ?combat boot (the tuning sandbox): the hostile pack around the
|
||||
// ship. The main game's combat arrives with the quest's ambush
|
||||
// (the wave run above) — the boot just adds the practice pack.
|
||||
// NOTE — the player's home system is SAFE SPACE: enemies should
|
||||
// NEVER be placed there. This dev boot is the one place they could
|
||||
// land in it, so don't run ?combat at home, and keep that rule for
|
||||
// any new enemy placement (the quest ambush already targets the
|
||||
// Neutral Capital, never home).
|
||||
if (this._wantsCombatDemo()) {
|
||||
this.combat.spawnPack(this.ship.x, this.ship.y);
|
||||
// SETTLE WINDOW — the ship's start position is finalized AFTER
|
||||
|
|
|
|||
|
|
@ -1104,9 +1104,10 @@ export class SurfaceScene extends Phaser.Scene {
|
|||
// deck's own left→right edges, resting on its top rail) the same
|
||||
// MenuSubBar drawer carries in span mode, with this world's service
|
||||
// buttons: data/actionbar.json → surface.explore.items, each item's
|
||||
// optional `where` clause (world / planetClass / faction) resolved
|
||||
// against THIS world by js/ui/ExploreBar.js — Job Board on every
|
||||
// planet and station, Bar on the neutral capital, and whatever the
|
||||
// optional `where` clause (world / planetClass / faction / isHomeSystem)
|
||||
// resolved against THIS world by js/ui/ExploreBar.js — Job Board on
|
||||
// every planet and station EXCEPT the player's home system (safe space),
|
||||
// Bar on the neutral capital, and whatever the
|
||||
// config adds per class / faction later. The buttons' behavior lands
|
||||
// later (exploreAction is the seam — BAR is live, the rest land later). A
|
||||
// world with no matching items
|
||||
|
|
@ -1286,8 +1287,8 @@ export class SurfaceScene extends Phaser.Scene {
|
|||
/**
|
||||
* The EXPLORE drawer's button presses (MenuSubBar → onAction). The
|
||||
* items are the world's service points (data/actionbar.json →
|
||||
* surface.explore.items — Job Board everywhere, Bar on the neutral
|
||||
* capital). BAR swaps the surface loop for the world's bar clip and
|
||||
* surface.explore.items — Job Board everywhere except the home system,
|
||||
* Bar on the neutral capital). BAR swaps the surface loop for the world's bar clip and
|
||||
* back (toggleBar — the drawer relabels to LEAVE BAR, and every other
|
||||
* button on the deck + drawer goes standby until pressed). While the
|
||||
* bar clip is up a press on any OTHER item is refused (belt-and-braces
|
||||
|
|
|
|||
|
|
@ -22,10 +22,16 @@ import { config } from '../config/Config.js';
|
|||
* on a station).
|
||||
* faction the faction controlling this system — 'apex' | 'mindweave' |
|
||||
* 'zenith', or 'neutral' (wild space; data/factions.json).
|
||||
* isHomeSystem boolean — true in the player's home (starting) system
|
||||
* (GameScene.isHomeSystem). A clause of booleans compares
|
||||
* directly (a list any-matches): `{ isHomeSystem: false }`
|
||||
* = every system EXCEPT the home.
|
||||
*
|
||||
* A value may be a single entry or a list (any-match, case-insensitive).
|
||||
* An item with no `where` shows everywhere — the Job Board, which is on
|
||||
* every planet and station.
|
||||
* A value may be a single entry or a list (any-match; strings are
|
||||
* case-insensitive). No `where` = every world. The Job Board carries
|
||||
* `{ isHomeSystem: false }` — every planet and station EXCEPT the
|
||||
* player's home system (safe space — enemies should never be placed
|
||||
* there, so there's no work to post on its board).
|
||||
*
|
||||
* const ctx = worldContext(surface, gameScene);
|
||||
* const items = resolveItems(ctx); // the drawer's live buttons
|
||||
|
|
@ -39,8 +45,9 @@ import { config } from '../config/Config.js';
|
|||
* (the same name the surface HUD decodes in).
|
||||
* @param {object|null} gameScene — the paused flight scene beneath: its
|
||||
* systemPlanets / planet (home world) carry the class, its systemRecord
|
||||
* carries the faction (Galaxy's faction pass — data/factions.json).
|
||||
* @returns {{world: string, planetClass: string|null, faction: string}}
|
||||
* carries the faction (Galaxy's faction pass — data/factions.json), and
|
||||
* its isHomeSystem marks the player's home (starting) system.
|
||||
* @returns {{world: string, planetClass: string|null, faction: string, isHomeSystem: boolean}}
|
||||
*/
|
||||
export function worldContext(surface = {}, gameScene = null) {
|
||||
const name = String(surface.name ?? '').trim();
|
||||
|
|
@ -69,21 +76,30 @@ export function worldContext(surface = {}, gameScene = null) {
|
|||
// null = neutral wild space (the faction pass leaves it null) — the
|
||||
// clause reads it as 'neutral'.
|
||||
faction: faction != null ? String(faction).trim().toLowerCase() : 'neutral',
|
||||
// The player's home (starting) system — safe space (enemies should
|
||||
// never be placed there); the Job Board's clause keeps it out.
|
||||
isHomeSystem: gameScene?.isHomeSystem === true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Does an item's optional `where` clause match the context? (AND semantics
|
||||
* across keys — every key named must match.) No clause → shown everywhere.
|
||||
* String values normalize case-insensitively; boolean values (the
|
||||
* isHomeSystem key) compare directly, a list any-matching.
|
||||
*/
|
||||
export function matchesWhere(where, ctx) {
|
||||
if (where == null || typeof where !== 'object') return true;
|
||||
const norm = (v) => (v == null ? 'none' : String(v).trim().toLowerCase());
|
||||
const c = ctx ?? {};
|
||||
for (const key of Object.keys(where)) {
|
||||
const have = norm(c[key]);
|
||||
const want = (Array.isArray(where[key]) ? where[key] : [where[key]]).map(norm);
|
||||
if (!want.includes(have)) return false;
|
||||
const want = Array.isArray(where[key]) ? where[key] : [where[key]];
|
||||
if (want.every((v) => typeof v === 'boolean')) {
|
||||
if (!want.includes(Boolean(c[key]))) return false;
|
||||
} else {
|
||||
const have = norm(c[key]);
|
||||
if (!want.map(norm).includes(have)) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue