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