orbit/js/ui/ExploreBar.js

104 lines
4.6 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).
*
* 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.
*
* 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).
* @returns {{world: string, planetClass: string|null, faction: string}}
*/
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',
};
}
/**
* Does an item's optional `where` clause match the context? (AND semantics
* across keys — every key named must match.) No clause → shown everywhere.
*/
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;
}
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),
);
}