feat(@projects/@magic-civilization): add guide deployment and resource paths

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-04-17 15:36:01 -07:00
parent 94f6bc0489
commit c6bcc5ba91
7 changed files with 143 additions and 21 deletions

12
.env
View file

@ -19,3 +19,15 @@ AUTOPLAY_HOST=lilith@apricot.local
PROJECT_ROOT_REMOTE=~/Code/@projects/@magic-civilization
REMOTE_RUNNER=~/bin/run_ap3.sh
SCREENSHOT_HOST=natalie@plum.local
# ── Dev-guide deploy (p1-15, tourguide) ────────────────────────────
NEXT_DEPLOY_HOST=lilith@black.local
NEXT_DEPLOY_PATH=/bigdisk/next/mc/
# ── Guide resource / simulator paths (relative to repo root) ───────
# Consumed by: public/games/age-of-dwarves/guide/tools/bake-simcache.ts
# (and any other script that needs to read the climate-sim terrain /
# params inputs without hardcoding subdirectory layout).
GUIDE_RESOURCES_DIR=public/resources
GUIDE_TERRAIN_DIR=public/resources/tiles
GUIDE_CLIMATE_PARAMS=public/resources/worlds/earth/climate_params.json

View file

@ -26,6 +26,22 @@ PROJECT_ROOT_REMOTE=~/Code/@projects/@magic-civilization
REMOTE_RUNNER=~/bin/run_ap3.sh
SCREENSHOT_HOST=natalie@plum.local
# ── Dev-guide deploy (p1-15, tourguide) ────────────────────────────
# rsync target for `./run deploy:guide:next`. The host-nginx vhost for
# mc.next.black.local bind-mounts the deploy path read-only. See
# .project/objectives/p1-15-guide-next-deploy-infra.md for the runbook.
NEXT_DEPLOY_HOST=lilith@black.local
NEXT_DEPLOY_PATH=/bigdisk/next/mc/
# ── Guide resource / simulator paths (p2-21, tourguide) ────────────
# Relative to the repo root. Consumed by
# public/games/age-of-dwarves/guide/tools/bake-simcache.ts and any
# other script that needs the climate-sim inputs without hardcoding
# the subdirectory layout.
GUIDE_RESOURCES_DIR=public/resources
GUIDE_TERRAIN_DIR=public/resources/tiles
GUIDE_CLIMATE_PARAMS=public/resources/worlds/earth/climate_params.json
# ── Game runtime flags (read by Godot `EnvConfig` autoload) ────────
# These are tracked in `.env.development` / `.env.production` and
# deployed with the build via `scripts/run/remote.sh`. Override here

View file

@ -211,17 +211,20 @@ const FEATURES = [
},
]
// Game 1 scope: no /magic/* routes exist in App.tsx. The Magic Schools /
// Spells / Archons pages were purged during p2-09's scope-narrow pass.
// Those entries were previously here and redirected to `/` via the wildcard
// catch-all route — a broken UX reported by the user. When Game 2/3 routes
// return, add them back wrapped in an episode-gate filter so they only
// render in the dev bundle (VITE_DEV_GUIDE=1). Tracked by p1-14.
const SECTIONS = [
{ icon: '🗺', label: 'Terrain', to: '/map/terrain' },
{ icon: '💎', label: 'Resources', to: '/map/resources' },
{ icon: '🧝', label: 'Races', to: '/empire/races' },
{ icon: '🏛', label: 'Government', to: '/empire/government' },
{ icon: '🔬', label: 'Tech Tree', to: '/research/tech-tree' },
{ icon: '✦', label: 'Magic Schools', to: '/magic/schools' },
{ icon: '⚔', label: 'Units', to: '/military/units' },
{ icon: '🗡', label: 'Combat', to: '/military/combat' },
{ icon: '🔮', label: 'Spells', to: '/magic/spells' },
{ icon: '👁', label: 'Archons', to: '/magic/archons' },
{ icon: '🏗', label: 'Buildings', to: '/buildings/buildings' },
{ icon: '🌍', label: 'Climate', to: '/climate' },
]

View file

@ -72,20 +72,57 @@ const GUIDE_ROOT = path.resolve(HERE, '..')
// GUIDE_ROOT is <repo>/public/games/age-of-dwarves/guide — four `..` hops to repo root.
const REPO_ROOT = path.resolve(GUIDE_ROOT, '../../../..')
const DIST_DIR = path.join(GUIDE_ROOT, 'dist')
const TERRAIN_DIR = path.join(REPO_ROOT, 'public', 'resources', 'tiles')
const PARAMS_PATH = path.join(REPO_ROOT, 'public', 'resources', 'worlds', 'earth', 'climate_params.json')
// Paths come from the repo-root .env (loaded by scripts/run/common.sh before
// `./run` dispatches to this script). .env defines:
// GUIDE_TERRAIN_DIR — dir containing terrain JSON definitions
// GUIDE_CLIMATE_PARAMS — path to the earth climate_params.json
// Both are relative to the repo root. Falling back to hardcoded defaults
// keeps the script runnable ad-hoc (e.g. under vitest or from an IDE), but
// the canonical source of truth is .env.
function envPath(envKey: string, fallback: string): string {
const raw = process.env[envKey]
const rel = raw && raw.trim().length > 0 ? raw : fallback
return path.isAbsolute(rel) ? rel : path.join(REPO_ROOT, rel)
}
const TERRAIN_DIR = envPath('GUIDE_TERRAIN_DIR', 'public/resources/tiles')
const PARAMS_PATH = envPath('GUIDE_CLIMATE_PARAMS', 'public/resources/worlds/earth/climate_params.json')
// Mirror `simCachePlugin.PREWARM_IDS` + fixed query-string defaults the
// frontend sends (`seed=42&turns=2000`). Keep these two in sync if the
// plugin's list changes.
const BAKE_SPECS: readonly BakeSpec[] = [
{ scenarioId: 'base_no_magic', seed: 42, turns: 2000 },
{ scenarioId: 'hadean_earth', seed: 42, turns: 2000 },
{ scenarioId: 'ice_age', seed: 42, turns: 2000 },
{ scenarioId: 'desertification', seed: 42, turns: 2000 },
{ scenarioId: 'ecological_collapse', seed: 42, turns: 2000 },
{ scenarioId: 'volcanic_winter', seed: 42, turns: 2000 },
] as const
const ALL_SCENARIO_IDS = [
'base_no_magic',
'hadean_earth',
'ice_age',
'desertification',
'ecological_collapse',
'volcanic_winter',
] as const satisfies readonly string[]
type ScenarioId = typeof ALL_SCENARIO_IDS[number]
function parseCliScenarios(argv: readonly string[]): readonly ScenarioId[] {
// Supported forms:
// node bake-simcache.ts → all scenarios
// node bake-simcache.ts base_no_magic → one scenario
// node bake-simcache.ts a,b,c → multiple (comma-separated)
// BAKE_SCENARIOS="a,b" node bake-simcache.ts → env var override
const fromArgv = argv.slice(2).flatMap(arg => arg.split(',')).filter(Boolean)
const fromEnv = (process.env.BAKE_SCENARIOS ?? '').split(',').map(s => s.trim()).filter(Boolean)
const requested = fromArgv.length > 0 ? fromArgv : fromEnv
if (requested.length === 0) return ALL_SCENARIO_IDS
const known = new Set<string>(ALL_SCENARIO_IDS)
const unknown = requested.filter(id => !known.has(id))
if (unknown.length > 0) {
throw new Error(`bake-simcache: unknown scenario ids: ${unknown.join(', ')}. Known: ${ALL_SCENARIO_IDS.join(', ')}`)
}
return requested as ScenarioId[]
}
const DEFAULT_SEED = 42
const DEFAULT_TURNS = 2000
// CLI progress writer. Build-time script; structured logging framework
// is overkill for a stdout progress trail that only a human reads.
@ -209,19 +246,26 @@ async function main(): Promise<void> {
process.exit(1)
}
const scenarios = parseCliScenarios(process.argv)
const specs: readonly BakeSpec[] = scenarios.map(id => ({
scenarioId: id,
seed: DEFAULT_SEED,
turns: DEFAULT_TURNS,
}))
log('[bake] loading terrain + climate params')
const terrainData = loadTerrainData()
const climateParams = loadClimateParams()
const terrainCache = buildTerrainCacheFromData(terrainData)
log(`[bake] ${BAKE_SPECS.length} scenarios queued, seed=42, turns=2000 each`)
log(`[bake] ${specs.length} scenario(s) queued — ${scenarios.join(', ')} — seed=${DEFAULT_SEED}, turns=${DEFAULT_TURNS}`)
const t0 = Date.now()
let totalBytes = 0
let totalFrames = 0
// Serial rather than Promise.all: WASM is CPU-bound single-threaded,
// parallel just thrashes the scheduler. 6 × ~1 min each is the budget.
for (const spec of BAKE_SPECS) {
// parallel just thrashes the scheduler. ~3 min per scenario.
for (const spec of specs) {
const { bytes, frames } = await bake(spec, terrainCache, climateParams)
totalBytes += bytes
totalFrames += frames
@ -229,7 +273,7 @@ async function main(): Promise<void> {
const elapsedS = ((Date.now() - t0) / 1000).toFixed(1)
const totalMiB = (totalBytes / (1024 * 1024)).toFixed(1)
log(`[bake] done — ${BAKE_SPECS.length} scenarios · ${totalFrames} frames · ${totalMiB} MiB · ${elapsedS}s`)
log(`[bake] done — ${specs.length} scenario(s) · ${totalFrames} frames · ${totalMiB} MiB · ${elapsedS}s`)
}
main().catch((err: unknown) => {

2
run
View file

@ -76,6 +76,8 @@ usage() {
echo ""
echo -e "${YELLOW}Deploy${NC}"
echo " deploy:guide:next Build dev guide (all episodes) + rsync to mc.next.black.local"
echo " DEPLOY_BAKE_SCENARIOS=base_no_magic|all bakes sim-cache before rsync"
echo " bake:simcache [ids|all] Pre-compute sim-cache frames into dist/__sim-cache/"
}
# ── Install args parser (shared by install:* targets) ────────────────

View file

@ -16,6 +16,31 @@
: "${NEXT_DEPLOY_HOST:=lilith@black.local}"
: "${NEXT_DEPLOY_PATH:=/bigdisk/next/mc/}"
# Bake-simcache scenarios. Empty = skip bake step entirely in `deploy:guide:next`.
# Comma-separated list = bake those scenarios after `pnpm build`. `all` = every
# canonical scenario (1.1 GB × 6 ≈ 6.6 GB; minutes to bake). Default: empty
# (deploy ships only the client-WASM fallback path).
: "${DEPLOY_BAKE_SCENARIOS:=}"
cmd_bake_simcache() {
# `./run bake:simcache [ids…]` — pre-compute sim-cache frames into dist/.
# Pass scenario ids (space- or comma-separated) or the string `all`.
# Used standalone (post-`pnpm build`) or invoked from cmd_deploy_guide_next.
local scenarios="${1:-all}"; shift || true
if [ "$scenarios" = "all" ]; then
scenarios="" # empty arg list = bake-simcache defaults to ALL_SCENARIO_IDS
else
scenarios="${scenarios//,/ }"
fi
if [ ! -d "$GUIDE_DIR/dist" ]; then
echo -e "${RED}$GUIDE_DIR/dist missing — run \`pnpm build\` first, then \`./run bake:simcache\`.${NC}"
return 1
fi
echo -e "${BLUE}Baking sim-cache frames into $GUIDE_DIR/dist/__sim-cache/${NC}"
# shellcheck disable=SC2086
(cd "$GUIDE_DIR" && node --import tsx/esm tools/bake-simcache.ts $scenarios)
}
cmd_deploy_guide_next() {
# Build the dev bundle (all EpisodeGate subtrees visible) + rsync to black.
# Safe to run repeatedly — rsync --delete replaces the target dir with dist/.
@ -30,7 +55,7 @@ cmd_deploy_guide_next() {
return 1
fi
echo -e "${BLUE}[1/4] Building dev bundle (VITE_DEV_GUIDE=1 pnpm build)...${NC}"
echo -e "${BLUE}[1/5] Building dev bundle (VITE_DEV_GUIDE=1 pnpm build)...${NC}"
if ! (cd "$GUIDE_DIR" && VITE_DEV_GUIDE=1 pnpm build 2>&1); then
echo -e "${RED}✗ pnpm build failed${NC}"
return 1
@ -45,21 +70,33 @@ cmd_deploy_guide_next() {
size="$(du -sh "$dist" | cut -f1)"
echo -e "${GREEN}✓ dist/ ready ($size)${NC}"
echo -e "${BLUE}[2/4] Verifying SSH to $NEXT_DEPLOY_HOST...${NC}"
if [ -n "$DEPLOY_BAKE_SCENARIOS" ]; then
echo -e "${BLUE}[2/5] Baking sim-cache scenarios: $DEPLOY_BAKE_SCENARIOS${NC}"
if ! cmd_bake_simcache "$DEPLOY_BAKE_SCENARIOS"; then
echo -e "${RED}✗ bake-simcache failed${NC}"
return 1
fi
size="$(du -sh "$dist" | cut -f1)"
echo -e "${GREEN}✓ dist/ with baked frames ($size)${NC}"
else
echo -e "${YELLOW}[2/5] DEPLOY_BAKE_SCENARIOS unset — skipping sim-cache bake (client-WASM fallback in browser).${NC}"
fi
echo -e "${BLUE}[3/5] Verifying SSH to $NEXT_DEPLOY_HOST...${NC}"
if ! ssh -o ConnectTimeout=5 -o BatchMode=yes "$NEXT_DEPLOY_HOST" "test -d $NEXT_DEPLOY_PATH && echo ok" >/dev/null 2>&1; then
echo -e "${RED}✗ can't reach $NEXT_DEPLOY_HOST:$NEXT_DEPLOY_PATH (VPN or permissions).${NC}"
return 1
fi
echo -e "${GREEN}✓ reachable${NC}"
echo -e "${BLUE}[3/4] Rsyncing dist/ → $NEXT_DEPLOY_HOST:$NEXT_DEPLOY_PATH${NC}"
echo -e "${BLUE}[4/5] Rsyncing dist/ → $NEXT_DEPLOY_HOST:$NEXT_DEPLOY_PATH${NC}"
if ! rsync -az --delete "$dist/" "$NEXT_DEPLOY_HOST:$NEXT_DEPLOY_PATH"; then
echo -e "${RED}✗ rsync failed${NC}"
return 1
fi
echo -e "${GREEN}✓ deployed${NC}"
echo -e "${BLUE}[4/4] Probing https://mc.next.black.local ...${NC}"
echo -e "${BLUE}[5/5] Probing https://mc.next.black.local ...${NC}"
local http_status
http_status="$(curl -sk -o /dev/null -w "%{http_code}" --max-time 10 https://mc.next.black.local)"
if [ "$http_status" = "200" ]; then

View file

@ -79,6 +79,14 @@ if ! [[ "$SEED_OFFSET" =~ ^[0-9]+$ ]]; then
exit 2
fi
# Flatpak's sandboxed Godot resolves AUTO_PLAY_DIR against an unspecified CWD,
# not the caller's shell CWD — a relative path silently produces 0-byte
# meta.json / turn_stats.jsonl even when the game itself completes (game.log
# is fine because it's redirected host-side). realpath -m tolerates the path
# not existing yet; it will be mkdir'd just below. Also ensures the /tmp
# reject check that follows catches all forms (./tmp, ../tmp, etc).
RESULTS_DIR="$(realpath -m "$RESULTS_DIR")"
# Flatpak sandbox can't write to /tmp. Reject /tmp paths outright instead of
# silently redirecting — persistent output belongs under the repo.
if [[ "$RESULTS_DIR" == /tmp/* ]] || [[ "$RESULTS_DIR" == /private/tmp/* ]]; then