orbit/js/ui/ExploreBar.js

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),
);
}