120 lines
5.5 KiB
JavaScript
120 lines
5.5 KiB
JavaScript
import { config } from '../config/Config.js';
|
|
|
|
/**
|
|
* The surface deck's EXPLORE drawer — its item logic (pure data, headless-
|
|
* testable: dev/explore.test.mjs). SurfaceScene renders the drawer with the
|
|
* shared MenuSubBar in full-span mode (js/ui/MenuSubBar.js — the full-width
|
|
* secondary bar, the shop tab bar's span); this module decides WHICH items
|
|
* that drawer carries on a given world.
|
|
*
|
|
* data/actionbar.json → surface.explore.items is the ordered button list.
|
|
* Each item may carry an optional `where` clause — EVERY key it names must
|
|
* match the world the player is standing on for the button to appear:
|
|
*
|
|
* world 'planet' | 'station' — what the surface is (a deep-space
|
|
* station vs a planet).
|
|
* planetClass the planet's class — 'terran' | 'rocky' | 'gas' | 'ice' |
|
|
* 'lava' | 'arid' | 'capitalneutral' | 'capitalapex' |
|
|
* 'capitalmindweave' | 'capitalzenith' (data/planets.json →
|
|
* frames, lowercased by the planet entity — matching is
|
|
* case-insensitive); stations carry none ('none' — so a
|
|
* clause like `planetClass: 'capitalneutral'` never fires
|
|
* 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; 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
|
|
*/
|
|
|
|
/**
|
|
* The world context a `where` clause matches against.
|
|
*
|
|
* @param {object} surface — the landing handoff (SurfaceScene.init):
|
|
* { isStation: boolean, name: string } — the world's kind + proper name
|
|
* (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), 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();
|
|
const same = (o) => o && String(o.discoveryName ?? '').trim() === name;
|
|
const match = (list) => (Array.isArray(list) ? list : []).find(same);
|
|
|
|
let planetClass = null;
|
|
if (surface.isStation !== true) {
|
|
// A PLANTED world: the matched entity (a system planet, or the home
|
|
// world) carries its kind — the Planet entity's `name` arg IS the
|
|
// class ('terran'/'rocky'/…/'capitalneutral', data/planets.json →
|
|
// frames, lowercased), `.class` when a future entity prefers that
|
|
// property (matching is case-insensitive).
|
|
const p =
|
|
match(gameScene?.systemPlanets) ??
|
|
(gameScene?.planet && same(gameScene.planet) ? gameScene.planet : null);
|
|
if (p) {
|
|
const raw = p.class ?? p.name ?? null;
|
|
if (raw != null && String(raw).trim() !== '') planetClass = String(raw).trim().toLowerCase();
|
|
}
|
|
}
|
|
const faction = gameScene?.systemRecord?.faction ?? null;
|
|
return {
|
|
world: surface.isStation === true ? 'station' : 'planet',
|
|
planetClass,
|
|
// 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 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;
|
|
}
|
|
|
|
/**
|
|
* The drawer's live items for a context: the config's ordered list with
|
|
* every item whose `where` clause doesn't match dropped (order kept).
|
|
* `items` defaults to data/actionbar.json → surface.explore.items.
|
|
*/
|
|
export function resolveItems(ctx, items) {
|
|
const list = Array.isArray(items)
|
|
? items
|
|
: config.get('actionbar.surface.explore.items');
|
|
return (Array.isArray(list) ? list : []).filter(
|
|
(it) => it && typeof it === 'object' && matchesWhere(it.where, ctx),
|
|
);
|
|
}
|