diff --git a/.env b/.env index c0d5ff4f..3026d662 100644 --- a/.env +++ b/.env @@ -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 diff --git a/.env.example b/.env.example index d3cd95c5..ac4e5b1b 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/public/games/age-of-dwarves/guide/src/pages/HomePage.tsx b/public/games/age-of-dwarves/guide/src/pages/HomePage.tsx index 36633dfa..fd0fa5a2 100644 --- a/public/games/age-of-dwarves/guide/src/pages/HomePage.tsx +++ b/public/games/age-of-dwarves/guide/src/pages/HomePage.tsx @@ -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' }, ] diff --git a/public/games/age-of-dwarves/guide/tools/bake-simcache.ts b/public/games/age-of-dwarves/guide/tools/bake-simcache.ts index e3e75bf0..311c724b 100644 --- a/public/games/age-of-dwarves/guide/tools/bake-simcache.ts +++ b/public/games/age-of-dwarves/guide/tools/bake-simcache.ts @@ -72,20 +72,57 @@ const GUIDE_ROOT = path.resolve(HERE, '..') // GUIDE_ROOT is /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(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 { 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 { 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) => { diff --git a/run b/run index 4a219336..a59d6de0 100755 --- a/run +++ b/run @@ -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) ──────────────── diff --git a/scripts/run/deploy.sh b/scripts/run/deploy.sh index c8bb01f6..e66295a2 100644 --- a/scripts/run/deploy.sh +++ b/scripts/run/deploy.sh @@ -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 diff --git a/tools/autoplay-batch.sh b/tools/autoplay-batch.sh index e6c83b6f..7d216fd9 100755 --- a/tools/autoplay-batch.sh +++ b/tools/autoplay-batch.sh @@ -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