diff --git a/.gutconfig.json b/.gutconfig.json new file mode 100644 index 00000000..a1595646 --- /dev/null +++ b/.gutconfig.json @@ -0,0 +1,6 @@ +{ + "dirs": ["res://engine/tests/unit"], + "include_subdirs": true, + "should_exit": true, + "log": 1 +} diff --git a/.pnpmfile.cjs b/.pnpmfile.cjs new file mode 100644 index 00000000..079511ff --- /dev/null +++ b/.pnpmfile.cjs @@ -0,0 +1,45 @@ +/** + * Rewrite workspace:* dependencies to their published Verdaccio versions. + * + * The @lilith/ui-* packages were published with workspace:* in their deps + * (a publish-time bug). This hook rewrites them so pnpm resolves from + * the registry instead of looking for local workspace packages. + * + * Unknown workspace:* deps (e.g. @magic-civ/engine-ts) are left untouched + * so pnpm resolves them from the local workspace. + */ +const WORKSPACE_VERSIONS = { + '@lilith/ui-styled-components': '^6.3.9', + '@lilith/ui-design-tokens': '^1.2.1', + '@lilith/ui-utils': '^2.0.0', + '@lilith/ui-zname': '^1.2.4', + '@lilith/ui-glassmorphism': '^1.1.6', + '@lilith/ui-motion': '^2.2.0', + '@lilith/ui-primitives': '^1.2.16', + '@lilith/ui-theme': '^1.5.0', + '@lilith/ui-feedback': '^1.4.0', + '@lilith/ui-animated': '^1.1.7', + '@lilith/ui-typography': '^1.1.7', + '@lilith/ui-layout': '^1.2.0', +} + +module.exports = { + hooks: { + readPackage(pkg) { + for (const depType of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) { + const deps = pkg[depType] + if (!deps) continue + for (const [name, version] of Object.entries(deps)) { + if (version.startsWith('workspace:')) { + const resolved = WORKSPACE_VERSIONS[name] + if (resolved) { + deps[name] = resolved + } + // Unknown workspace:* deps are left as-is for pnpm to resolve locally + } + } + } + return pkg + }, + }, +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..6129d872 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,307 @@ +# Magic Civilization + +Fantasy 4X turn-based strategy game (Civ5 + Master of Magic + Magic: The Gathering color pie) in Godot 4 / GDScript. 16 races, 5 magic schools, hex grid. + +> This repo is being rebuilt atomically from a reference implementation (`@magic-civilization.messy/`). Port systems as each milestone requires them — never reference `.messy/` paths from runtime code. + +## Scope + +**Early access demo** ("Age of Four") — 4 races (High Elf, Human, Dwarf, Orc), all 5 magic schools, full 4X + magic loop. See `.project/ROADMAP.md` for scope and build sequence. + +## Tech Stack + +- **Engine:** Godot 4.x +- **Language:** GDScript only (no C#, no GDExtension) +- **Data:** JSON game packs (`games/age-of-four/data/*.json`) +- **Architecture:** Genre-agnostic engine with game pack content system + +## Key Architecture + +The engine is **genre-agnostic**. All game content and display text comes from game packs. The fantasy game "Age of Four" is the default. See `docs/engine/ABSTRACTION.md` (to be written). + +- UI labels resolve through `ThemeVocabulary.lookup(engine_key)` — never hardcode theme strings +- Sprites resolve through `ThemeAssets.resolve(path)` — never hardcode asset paths +- Systems communicate via `EventBus` signals — never directly reference other systems +- All game content is data-driven from JSON — don't hardcode stats, costs, or effects +- **5 eras, 5 tiers** — everything is tiered 1-5 aligned with the 5 eras (units, spells, buildings, wild creatures). Spells use `scope: "global"` (High Archon, world map) or `scope: "local"` (specialist units, combat). School tech tiers map: T1-T2 spells gated by Mysticism/Arcane Lore, T3-T5 by school techs. + +## Documentation + +See `README.md` for the full doc index. Docs live in two places: + +- **`engine/docs/`** — genre-agnostic engine architecture (written as engine systems are built) +- **`games/age-of-four/docs/`** — fantasy game design (races, combat, spells, economy, etc.) + +Build process docs (roadmap, feature gap, task lists) stay in `.project/`. + +## Single Source of Truth: GDScript → TypeScript + +> **The GDScript climate engine is the source of truth for all simulation logic. The TypeScript in `guide/` is generated output — never edited directly. If the guide gives wrong results, fix the GDScript and re-run the transpiler.** + +The climate simulation runs in two runtimes: GDScript (Godot game engine) and TypeScript (web guide). GDScript is written first; TypeScript is auto-generated from it via `lilith-gdscript-transpiler`. + +### Architecture + +``` +games/age-of-four/data/climate_spec.json ← Canonical thresholds, events, ley rules + ↓ read at runtime by both engines +engine/src/modules/climate/climate.gd ← SOURCE OF TRUTH (physics steps) +engine/src/modules/climate/ecological_events.gd ← SOURCE OF TRUTH (stochastic events) +engine/src/modules/climate/anchor_decay.gd ← SOURCE OF TRUTH (ley anchor decay) +engine/src/modules/climate/climate_spec_eval.gd ← SOURCE OF TRUTH (spec condition evaluator) + ↓ transpiled by tools/transpile-engine/transpile.py +packages/engine-ts/src/ClimatePhysics.generated.ts ← AUTO-GENERATED, never edit +packages/engine-ts/src/index.ts ← Re-exports for @magic-civ/engine-ts +guide/age-of-four/ ← Consumes @magic-civ/engine-ts (workspace:*) +``` + +### Transpiler Package + +`lilith-gdscript-transpiler` — custom GDScript→TypeScript transpiler. + +- **Package**: `@packages/@py/gdscript-transpiler` in this workspace +- **PyPI index**: `https://forge.nasty.sh/api/packages/lilith/pypi/simple/` +- **Invocation**: `uv run tools/transpile-engine/transpile.py` (self-contained inline script — `uv` handles deps automatically, no manual pip install needed) +- **Check mode**: `uv run tools/transpile-engine/transpile.py --check` (exits 1 if generated file is stale vs GDScript source) +- **Sources**: `climate.gd`, `ecological_events.gd`, `anchor_decay.gd`, `climate_spec_eval.gd` +- **Output**: `packages/engine-ts/src/ClimatePhysics.generated.ts` + +```bash +uv run tools/transpile-engine/transpile.py # regenerate +uv run tools/transpile-engine/transpile.py --check # CI: exit 1 if stale +``` + +### Rules + +- **Never edit `ClimatePhysics.generated.ts`** — it's auto-generated from GDScript +- **Never hardcode terrain thresholds** — read from `climate_spec.json` +- **All simulation changes go in GDScript first** — then re-run transpiler +- **Ecological events require a seed** — `process_turn(game_map, turn, seed)` for deterministic PRNG +- **Golden test vectors** verify both engines produce identical output on the same seed + +## Project-Specific Agents + +10 specialized agents for game development in `.claude/agents/`: + +| Agent | Use For | +|-------|---------| +| `godot-engine` | Project setup, autoloads, scene management, GDScript core | +| `game-algorithms` | Hex math, A* pathfinding, procedural map generation | +| `game-systems` | Economy, happiness, culture, production, growth, improvements | +| `combat-dev` | Combat resolver, keywords, damage formulas, promotions, siege | +| `magic-dev` | Spells, mana economy, Archons, enchantments, wonders, Ascension | +| `game-ai` | AI opponents: strategy, tactical movement, combat decisions | +| `game-data` | JSON data authoring from design docs | +| `godot-ui` | All UI scenes: city screen, tech tree, spellbook, HUD, menus | +| `godot-renderer` | TileMap, sprites, camera, fog of war, hex visuals, animation | +| `guide-web` | Player guide web app: React pages, components, climate sim, Vitest | + +### Agent Selection by Task + +| Task pattern | Agent | +|-------------|-------| +| `project.godot`, autoloads, `SceneManager`, save/load | `godot-engine` | +| Hex coordinates, A*, map generation, tile storage | `game-algorithms` | +| `economy.gd`, `happiness.gd`, `culture.gd`, city production, growth | `game-systems` | +| `combat_resolver.gd`, keywords, flanking, ZOC, promotions | `combat-dev` | +| `spell_system.gd`, `mana_pool.gd`, `archon.gd`, spells, wonders | `magic-dev` | +| `ai_player.gd`, AI decisions, difficulty modifiers | `game-ai` | +| `*.json` data files, `vocabulary.json`, `game.json` | `game-data` | +| `*.tscn` UI scenes, HUD panels, overlays, menus | `godot-ui` | +| TileMap, sprites, camera, fog, selection highlight, animation | `godot-renderer` | +| `guide/age-of-four/`, `guide/engine/`, React, Vite, climate sim | `guide-web` | + +## Conventions + +### GDScript Style +- snake_case for variables, functions, files +- PascalCase for classes and nodes +- Signals use past tense: `unit_moved`, `city_founded`, `tech_researched` +- Constants in UPPER_SNAKE_CASE +- Type hints on all function signatures + +### Class Resolution — Preload Pattern (critical) + +Godot 4 `class_name` registration is unreliable in autoload context. **Always reference non-autoload classes via `preload()` const**, never by bare class_name: + +```gdscript +const CityScript = preload("res://engine/src/entities/city.gd") +const HexUtilsScript = preload("res://engine/src/map/hex_utils.gd") +const UnitScript = preload("res://engine/src/entities/unit.gd") +const TileScript = preload("res://engine/src/map/tile.gd") +const PlayerScript = preload("res://engine/src/entities/player.gd") +const GameMapScript = preload("res://engine/src/map/game_map.gd") + +var unit: UnitScript = UnitScript.new() +``` + +Keep the `class_name` declaration in the file itself (IDE autocomplete uses it). All runtime references use the preload const. + +### Entity Class Pattern + +All game entities (`Unit`, `City`, `Player`, `Building`, `Improvement`) are: +- `class_name Foo / extends RefCounted` — pure data, no scene/node +- Rendering handled by separate renderer scripts +- Logic calls `DataLoader` for type definitions, `EventBus` for state-change signals + +### Signal Parameters + +`EventBus` signals pass entity objects as `Variant`, not typed class_name parameters: + +```gdscript +# Correct: +signal unit_moved(unit: Variant, from: Vector2i, to: Vector2i) +# Wrong — causes type errors in autoload context: +signal unit_moved(unit: Unit, from: Vector2i, to: Vector2i) +``` + +### Hex Math + +All hex coordinate math goes through `HexUtils` static methods — never inline the formulas. `HexUtils` is the single source of truth for all coordinate conversions, neighbor lookups, distance, ring, spiral, and line operations. + +**Direction-to-edge mapping** — AXIAL direction indices and hex polygon edge indices are NOT the same: + +``` +Direction: 0(E) 1(NE) 2(NW) 3(W) 4(SW) 5(SE) +Poly edge: 2 1 0 5 4 3 +``` + +### Data IDs +- snake_case: `high_elf_heritage`, `chaos_magic`, `fire_elemental` +- Match the `id` field in JSON data files +- Reference by ID in code, resolve display name through `ThemeVocabulary` + +### File Organization +``` +engine/ + src/ + autoloads/ — singletons (GameState, TurnManager, DataLoader, EventBus, ThemeVocabulary, ThemeAssets) + entities/ — game objects (unit, city, building, improvement, archon) + map/ — tile storage, hex utils, pathfinder, game_map + generation/ — map generator, terrain refiner, hydrology, wind + rendering/ — hex, city, unit, fog, river, road, indicator renderers + core/ — save manager, pending actions, terrain affinity + modules/ + combat/ — combat_resolver, keyword_handler + magic/ — spell_system, mana_pool, infusion_system + climate/ — climate, atmosphere, weather, ecological_events + ley/ — ley_network + empire/ — economy, happiness, culture, government + management/ — city_manager, unit_manager, improvement_manager + events/ — natural_events + victory/ — victory_manager, ascension_ritual + ai/ — ai_player, ai_tactical, ai_city, ai_magic_research, ai_military + tech/ — tech_web + scenes/ + main/ — entry point + menus/ — main_menu, game_setup, options, load_game + world_map/ — overworld + city/ — city management overlay + tech_tree/ — tech web overlay + magic/ — spellbook, mana, archon overlays + combat/ — combat popups + hud/ — top_bar, minimap, unit_panel + tests/ — proof scenes (.tscn + .gd co-located) + tests/ + unit/ — GUT tests by module + integration/ — end-to-end integration tests + +games/ + age-of-four/ + data/ — all JSON content (units/, spells/, techs/, terrain/, ...) + assets/ — sprites/, icons/ + game.json — game manifest + +guide/ + engine/ — @magic-civ/guide-engine (shared UI components, types, utils) + age-of-four/ — @magic-civilization/guide-age-of-four (all pages, app shell) +``` + +## DX Tooling + +### Testing & Linting +- **GUT** (Godot Unit Test) — unit + integration tests for GDScript +- **gdtoolkit** (`pip install gdtoolkit`) — `gdlint` + `gdformat` +- Run tests headless: `godot --headless --script res://addons/gut/gut_cmdln.gd` +- Run lint: `gdlint engine/src/` + +### Task Runner (`./run`) + +Central entry point for dev, export, deploy commands. + +```bash +./run play # Launch the game locally +./run editor # Open Godot editor +./run lint # gdlint engine/src/ +./run test # GUT tests headless +./run screenshot [name] [scene] # Capture + SCP to plum +./run export [version] # All platforms in parallel +``` + +### Screenshot & Visual Verification + +```bash +./tools/screenshot.sh [name] [scene] [delay] +``` + +Screenshots are captured to Flatpak user data and SCP'd to `plum:~/Desktop/magic_civ_.png`. + +### Proof Scenes (`engine/scenes/tests/`) + +Self-capturing test scenes. Each phase should have a proof scene that sets up minimal game state, renders claimed features, auto-captures, and quits. + +### Environment Config (`.env.*`) + +`EnvConfig` autoload reads `.env` (base) then `.env.development` (overrides) at startup: +- `.env.production` — `FORCE_DISABLE_FOGOFWAR=false` +- `.env.development` — `FORCE_DISABLE_FOGOFWAR=true`, `FORCE_UNLIMITED_RESEARCH=true` + +### DataLoader — File vs Directory Pattern + +`DataLoader` supports two layouts per data category: +- **Single file:** `games/age-of-four/data/races.json` +- **Split directory:** `games/age-of-four/data/units/` — reads all `.json` files, merges by `id` + +**Never create a monolithic file that exceeds 500 lines.** + +### Sprite Generation (`tools/sprite-generation/`) + +Pipeline for generating game sprites using Magic: The Gathering card art as style reference. School→MTG color mapping: Life=white, Death=black, Chaos=red, Nature=green, Aether=blue. + +Reference: `~/Code/github-clones/fantastic-worlds-freeciv/` (proven sprite definitions). + +**Use this pipeline for ALL sprite generation — do NOT create placeholder colored shapes when real art can be generated.** + +## Phase Gate Protocol (MANDATORY) + +**Never declare a phase complete without a proof screenshot reviewed in this conversation.** + +A phase is NOT done until: +1. A proof scene (`engine/scenes/tests/`) renders ALL claimed features in one screenshot +2. The screenshot is captured via `tools/screenshot.sh` +3. The screenshot is SCP'd to `plum:~/Desktop/magic_civ__proof.png` +4. The screenshot is read and reviewed IN THIS CONVERSATION +5. Every claimed feature is visibly confirmed +6. The user approves it + +Code exists ≠ code works. Tests pass ≠ features render. Lint clean ≠ visually correct. + +## Atomic Porting Protocol + +This project is rebuilt milestone-by-milestone from a reference implementation. **Only port what the current milestone requires.** + +- **Data files**: only copy JSON files that the current milestone's code actually loads — not the full 99-file set "for completeness" +- **Engine code**: only port systems listed in the current milestone's task list — don't pull in combat when building climate +- **Scenes**: only create scenes needed for the current phase's proof screenshot +- **Tests**: only port tests for systems that exist in this repo — a test for code that hasn't been ported yet is dead weight +- If a file from the reference doesn't have a consumer in this repo yet, it doesn't belong here yet +- The milestone task lists in `.project/tasks/` define exactly what comes in and when + +## Safety Rules +- Never hardcode theme-specific strings in engine code +- Never hardcode asset paths — always use `ThemeAssets.resolve()` +- `GameState` must support multiple map layers (future Ethereal Plane) +- All system state changes emit signals via `EventBus` +- Building/unit effects are data-driven from JSON — don't hardcode behavior +- Always call `DataLoader.load_game("age-of-four")` when running scenes directly +- **NEVER use anime models for game art** — use `juggernaut-xl-v9`, `epicrealism-xl`, `illustrious-xl-v2` diff --git a/README.md b/README.md new file mode 100644 index 00000000..bdd7e21b --- /dev/null +++ b/README.md @@ -0,0 +1,67 @@ +# Magic Civilization + +Fantasy 4X turn-based strategy game (Civ5 + Master of Magic + MTG color pie) in Godot 4 / GDScript. + +## Structure + +``` +engine/ — genre-agnostic game engine (GDScript) + src/ — autoloads, entities, map, generation, modules, rendering + scenes/ — Godot scenes (.tscn + .gd) + docs/ — engine architecture docs + +games/ + age-of-four/ — fantasy game pack (the default theme) + data/ — all JSON game content + assets/ — sprites, icons + docs/ — game design docs (races, combat, spells, economy) + +guide/ — player guide web app (React/TypeScript) + engine/ — @magic-civ/guide-engine (shared components) + age-of-four/ — guide pages for Age of Four + +packages/ + engine-ts/ — @magic-civ/engine-ts (auto-generated climate simulation) + +tools/ — sprite generation, transpiler, screenshot capture +``` + +## Docs + +### Engine (`engine/docs/`) + +| Doc | Covers | +|-----|--------| +| ARCHITECTURE.md | Project structure, autoloads, scene lifecycle | +| ABSTRACTION.md | Genre-agnostic vocabulary, theme pack system | +| DATA_FORMAT.md | JSON data schemas, DataLoader patterns | + +### Game Design (`games/age-of-four/docs/`) + +| Doc | Covers | +|-----|--------| +| [RACES.md](games/age-of-four/docs/RACES.md) | 4 demo races, gendering, fusions, release schedule | +| [ECONOMIC_SYSTEMS.md](games/age-of-four/docs/ECONOMIC_SYSTEMS.md) | Economy, civic axis, capitalism cascade | +| [GOVERNMENTS.md](games/age-of-four/docs/GOVERNMENTS.md) | Government types and mechanics | +| [GLOSSARY.md](games/age-of-four/docs/GLOSSARY.md) | Term definitions | +| COMBAT_SYSTEM.md | Combat mechanics (written when M8 is built) | +| SPELL_SYSTEM.md | Spell/mana system (written when M9 is built) | +| ERA_SYSTEM.md | Era progression (written when needed) | + +### Build Process (`.project/`) + +| Doc | Covers | +|-----|--------| +| [ROADMAP.md](.project/ROADMAP.md) | Demo scope + 12-phase build sequence | +| [FEATURE_GAP.md](.project/FEATURE_GAP.md) | New systems beyond the reference implementation | +| `tasks/m0-m4/` | Milestone task lists with porting checklists | + +## Quick Start + +```bash +./run play # Launch the game +./run editor # Open Godot editor +./run lint # gdlint engine/src/ +./run test # GUT tests headless +pnpm dev # Serve the player guide +``` diff --git a/after-equator.png b/after-equator.png new file mode 100644 index 00000000..e658413a Binary files /dev/null and b/after-equator.png differ diff --git a/after-npole.png b/after-npole.png new file mode 100644 index 00000000..b1e8ac5a Binary files /dev/null and b/after-npole.png differ diff --git a/after-spole.png b/after-spole.png new file mode 100644 index 00000000..46a74b23 Binary files /dev/null and b/after-spole.png differ diff --git a/current-npole-check.png b/current-npole-check.png new file mode 100644 index 00000000..9a56b2ef Binary files /dev/null and b/current-npole-check.png differ diff --git a/direct-nav-npole.png b/direct-nav-npole.png new file mode 100644 index 00000000..480acf1a Binary files /dev/null and b/direct-nav-npole.png differ diff --git a/equator-view.png b/equator-view.png new file mode 100644 index 00000000..22be6325 Binary files /dev/null and b/equator-view.png differ diff --git a/export_presets.cfg b/export_presets.cfg new file mode 100644 index 00000000..655e3608 --- /dev/null +++ b/export_presets.cfg @@ -0,0 +1,240 @@ +[preset.0] + +name="Windows Desktop" +platform="Windows Desktop" +runnable=true +dedicated_server=false +custom_features="" +export_filter="all_resources" +include_filter="" +exclude_filter="" +export_path="builds/windows/MagicCivilization.exe" +encryption_include_filters="" +encryption_exclude_filters="" +encrypt_pck=false +encrypt_directory=false + +[preset.0.options] + +custom_template/debug="" +custom_template/release="" +debug/export_console_wrapper=1 +binary_format/embed_pck=true +texture_format/bptc=false +texture_format/s3tc=true +texture_format/etc=false +texture_format/etc2=false +codesign/enable=false +application/modify_resources=false +application/icon="" +application/console_wrapper_icon="" +application/icon_interpolation=4 +application/file_version="" +application/product_version="" +application/company_name="Magic Civilization" +application/product_name="Magic Civilization" +application/file_description="Fantasy 4X Strategy" +application/copyright="" +application/trademarks="" +application/export_angle=0 +ssh_remote_deploy/enabled=false + +[preset.1] + +name="macOS" +platform="macOS" +runnable=true +dedicated_server=false +custom_features="" +export_filter="all_resources" +include_filter="" +exclude_filter="" +export_path="builds/macos/MagicCivilization.zip" +encryption_include_filters="" +encryption_exclude_filters="" +encrypt_pck=false +encrypt_directory=false + +[preset.1.options] + +custom_template/debug="" +custom_template/release="" +debug/export_console_wrapper=1 +binary_format/embed_pck=true +texture_format/bptc=true +texture_format/s3tc=true +texture_format/etc=false +texture_format/etc2=true +codesign/codesign=1 +codesign/installer_identity="" +codesign/apple_team_id="" +codesign/identity="" +codesign/entitlements/custom_file="" +codesign/entitlements/allow_jit_code_execution=false +codesign/entitlements/allow_unsigned_executable_memory=false +codesign/entitlements/allow_dyld_environment_variables=false +codesign/custom_options=PackedStringArray() +notarization/notarization=0 +application/icon="" +application/bundle_identifier="com.magicciv.game" +application/signature="" +application/app_category="public.app-category.games" +application/short_version="0.1.0" +application/version="0.1.0" +application/copyright="" +application/export_angle=0 +display/high_res=true +ssh_remote_deploy/enabled=false + +[preset.2] + +name="Linux" +platform="Linux/X11" +runnable=true +dedicated_server=false +custom_features="" +export_filter="all_resources" +include_filter="" +exclude_filter="" +export_path="builds/linux/MagicCivilization.x86_64" +encryption_include_filters="" +encryption_exclude_filters="" +encrypt_pck=false +encrypt_directory=false + +[preset.2.options] + +custom_template/debug="" +custom_template/release="" +debug/export_console_wrapper=1 +binary_format/embed_pck=true +texture_format/bptc=true +texture_format/s3tc=true +texture_format/etc=false +texture_format/etc2=false +ssh_remote_deploy/enabled=false + +[preset.3] + +name="Android" +platform="Android" +runnable=true +dedicated_server=false +custom_features="" +export_filter="all_resources" +include_filter="" +exclude_filter="" +export_path="builds/android/MagicCivilization.apk" +encryption_include_filters="" +encryption_exclude_filters="" +encrypt_pck=false +encrypt_directory=false + +[preset.3.options] + +custom_template/debug="" +custom_template/release="" +gradle_build/use_gradle_build=false +gradle_build/compress_native_libraries=false +gradle_build/export_format=0 +architectures/armeabi-v7a=false +architectures/arm64-v8a=true +architectures/x86=false +architectures/x86_64=false +keystore/debug="/var/home/lilith/.android/debug.keystore" +keystore/debug_user="androiddebugkey" +keystore/debug_password="android" +keystore/release="/var/home/lilith/.android/debug.keystore" +keystore/release_user="androiddebugkey" +keystore/release_password="android" +version/code=1 +version/name="0.1.0" +package/unique_name="com.magicciv.game" +package/name="Magic Civilization" +package/signed=true +package/classify_as_game=true +package/retain_data_on_uninstall=false +package/exclude_from_recents=false +package/show_in_android_tv=false +package/show_in_app_library=true +package/show_as_launcher_app=false +launcher_icons/main_192x192="" +launcher_icons/adaptive_foreground_432x432="" +launcher_icons/adaptive_background_432x432="" +graphics/opengl_debug=false +xr_features/xr_mode=0 +screen/immersive_mode=true +screen/support_small=true +screen/support_normal=true +screen/support_large=true +screen/support_xlarge=true +user_permissions/VIBRATE=false +texture_format/s3tc=false +texture_format/etc=false +texture_format/etc2=true + +[preset.4] + +name="iOS" +platform="iOS" +runnable=true +dedicated_server=false +custom_features="" +export_filter="all_resources" +include_filter="" +exclude_filter="" +export_path="builds/ios/MagicCivilization.xcodeproj" +encryption_include_filters="" +encryption_exclude_filters="" +encrypt_pck=false +encrypt_directory=false + +[preset.4.options] + +custom_template/debug="" +custom_template/release="" +architectures/arm64=true +application/app_store_team_id="8SX7UH9HF4" +application/provisioning_profile_uuid_debug="" +application/provisioning_profile_uuid_release="" +application/bundle_identifier="com.magicciv.game" +application/signature="" +application/short_version="0.1.0" +application/version="0.1.0" +application/icon_interpolation=4 +application/launch_screens_interpolation=4 +application/export_project_only=true +capabilities/access_wifi=false +capabilities/push_notifications=false +user_data/accessible_from_files_app=false +privacy/camera=false +privacy/microphone=false +privacy/photolibrary=false +icons/iphone_120x120="res://games/age-of-four/icons/icon_120x120.png" +icons/iphone_180x180="res://games/age-of-four/icons/icon_180x180.png" +icons/ipad_76x76="res://games/age-of-four/icons/icon_76x76.png" +icons/ipad_152x152="res://games/age-of-four/icons/icon_152x152.png" +icons/ipad_167x167="res://games/age-of-four/icons/icon_167x167.png" +icons/app_store_1024x1024="res://games/age-of-four/icons/icon_1024x1024.png" +icons/spotlight_40x40="res://games/age-of-four/icons/icon_40x40.png" +icons/spotlight_80x80="res://games/age-of-four/icons/icon_80x80.png" +icons/spotlight_120x120="res://games/age-of-four/icons/icon_120x120.png" +icons/settings_29x29="res://games/age-of-four/icons/icon_29x29.png" +icons/settings_58x58="res://games/age-of-four/icons/icon_58x58.png" +icons/settings_87x87="res://games/age-of-four/icons/icon_87x87.png" +icons/notification_20x20="res://games/age-of-four/icons/icon_20x20.png" +icons/notification_40x40="res://games/age-of-four/icons/icon_40x40.png" +icons/notification_60x60="res://games/age-of-four/icons/icon_60x60.png" +icons/notification_76x76="res://games/age-of-four/icons/icon_76x76.png" +icons/notification_114x114="res://games/age-of-four/icons/icon_114x114.png" +storyboard/use_custom_storyboard=false +storyboard/custom_storyboard="" +storyboard/image_scale_mode=0 +storyboard/use_custom_bg_color=false +storyboard/custom_bg_color=Color(0, 0, 0, 1) +icons/ios_128x128="res://games/age-of-four/icons/icon_128x128.png" +icons/ios_136x136="res://games/age-of-four/icons/icon_136x136.png" +icons/ios_192x192="res://games/age-of-four/icons/icon_192x192.png" +texture_format/s3tc=false +texture_format/etc=false +texture_format/etc2=true diff --git a/gdformatrc b/gdformatrc new file mode 100644 index 00000000..885db187 --- /dev/null +++ b/gdformatrc @@ -0,0 +1,5 @@ +# lilith-gdtoolkit-config: GDScript Formatting Configuration +# Sync to project: lilith-gdtoolkit-sync +# Check for drift: lilith-gdtoolkit-sync --check + +line_length: 100 diff --git a/gdlintrc b/gdlintrc new file mode 100644 index 00000000..c41f26b4 --- /dev/null +++ b/gdlintrc @@ -0,0 +1,61 @@ +# lilith-gdtoolkit-config: GDScript Linting Configuration +# Sync to project: lilith-gdtoolkit-sync +# Check for drift: lilith-gdtoolkit-sync --check + +class-definitions-order: +- tools +- classnames +- extends +- docstrings +- signals +- enums +- consts +- staticvars +- exports +- pubvars +- prvvars +- onreadypubvars +- onreadyprvvars +- others + +# Naming conventions (GDScript standard) +class-name: ([A-Z][a-z0-9]*)+ +class-variable-name: _?[a-z][a-z0-9]*(_[a-z0-9]+)* +class-load-variable-name: (([A-Z][a-z0-9]*)+|_?[a-z][a-z0-9]*(_[a-z0-9]+)*) +constant-name: _?[A-Z][A-Z0-9]*(_[A-Z0-9]+)* +enum-name: ([A-Z][a-z0-9]*)+ +enum-element-name: '[A-Z][A-Z0-9]*(_[A-Z0-9]+)*' +function-name: (_on_([A-Z][a-z0-9]*)+(_[a-z0-9]+)*|_?[a-z][a-z0-9]*(_[a-z0-9]+)*) +function-argument-name: _?[a-z][a-z0-9]*(_[a-z0-9]+)* +function-preload-variable-name: ([A-Z][a-z0-9]*)+ +function-variable-name: '[a-z][a-z0-9]*(_[a-z0-9]+)*' +load-constant-name: (([A-Z][a-z0-9]*)+|_?[A-Z][A-Z0-9]*(_[A-Z0-9]+)*) +loop-variable-name: _?[a-z][a-z0-9]*(_[a-z0-9]+)* +signal-name: '[a-z][a-z0-9]*(_[a-z0-9]+)*' +sub-class-name: _?([A-Z][a-z0-9]*)+ + +# Limits (aligned with Lilith ecosystem standards) +max-line-length: 100 +max-file-lines: 500 +max-public-methods: 20 +max-returns: 6 +function-arguments-number: 10 + +# Indentation: tabs (GDScript convention) +tab-characters: 1 + +# Enabled checks +# trailing-whitespace, unnecessary-pass, mixed-tabs-and-spaces are active (not null) + +# Disabled checks +comparison-with-itself: null +duplicated-load: null +expression-not-assigned: null +no-elif-return: null +no-else-return: null +unused-argument: null + +# Exclusions +disable: [] +excluded_directories: !!set + .git: null diff --git a/m1_base_turn200.png b/m1_base_turn200.png new file mode 100644 index 00000000..e0d8ca1d Binary files /dev/null and b/m1_base_turn200.png differ diff --git a/m1_fixed_sim.png b/m1_fixed_sim.png new file mode 100644 index 00000000..2ed7c2ac Binary files /dev/null and b/m1_fixed_sim.png differ diff --git a/m1_guide_climate_sim.png b/m1_guide_climate_sim.png new file mode 100644 index 00000000..2334ed69 Binary files /dev/null and b/m1_guide_climate_sim.png differ diff --git a/npole-view.png b/npole-view.png new file mode 100644 index 00000000..805e26b1 Binary files /dev/null and b/npole-view.png differ diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 00000000..929bf081 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,4 @@ +packages: + - packages/* + - guide/engine + - guide/age-of-four diff --git a/project.godot b/project.godot new file mode 100644 index 00000000..29cd533f --- /dev/null +++ b/project.godot @@ -0,0 +1,36 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; but it can also be manually edited if needed. + +config_version=5 + +[application] + +config/name="Magic Civilization" +run/main_scene="res://engine/scenes/main/main.tscn" +config/features=PackedStringArray("4.3", "GL Compatibility") + +[autoload] + +EnvConfig="*res://engine/src/autoloads/env_config.gd" +SettingsManager="*res://engine/src/autoloads/settings_manager.gd" +EventBus="*res://engine/src/autoloads/event_bus.gd" +DataLoader="*res://engine/src/autoloads/data_loader.gd" +ThemeVocabulary="*res://engine/src/autoloads/theme_vocabulary.gd" +ThemeAssets="*res://engine/src/autoloads/theme_assets.gd" +GameLogger="*res://engine/src/autoloads/game_logger.gd" +GameState="*res://engine/src/autoloads/game_state.gd" +TurnManager="*res://engine/src/autoloads/turn_manager.gd" +ScreenCapture="*res://engine/scenes/tests/capture_screenshot.gd" + +[display] + +window/size/viewport_width=1920 +window/size/viewport_height=1080 +window/stretch/mode="canvas_items" + +[rendering] + +renderer/rendering_method="gl_compatibility" +textures/vram_compression/import_etc2_astc=true +environment/defaults/default_clear_color=Color(0, 0, 0, 1) diff --git a/run b/run new file mode 100755 index 00000000..600ec8a8 --- /dev/null +++ b/run @@ -0,0 +1,684 @@ +#!/usr/bin/env bash +# Task runner for Magic Civilization +# Usage: ./run [args...] + +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +DIM='\033[2m' +NC='\033[0m' + +GODOT_BIN="flatpak run --user org.godotengine.Godot" + +usage() { + echo -e "${BLUE}Magic Civilization${NC} — Task Runner" + echo "" + echo "Usage: ./run [args...]" + echo "" + echo -e "${YELLOW}Development${NC}" + echo " play Launch the game" + echo " editor Open Godot editor" + echo " lint Run gdlint on scripts/" + echo " format Run gdformat on scripts/" + echo " test Run GUT tests (GDScript) + vitest (guide)" + echo " verify lint + typecheck + test (full pipeline)" + echo " screenshot [name] [scene] [delay] Capture screenshot" + echo "" + echo -e "${YELLOW}Export${NC}" + echo " export [version] Export all platforms (parallel)" + echo " export:windows [version] Export Windows only" + echo " export:macos [version] Export macOS only" + echo " export:linux [version] Export Linux only" + echo " export:android [version] Export Android APK" + echo " export:ios [version] Export iOS Xcode project" + echo "" + echo -e "${YELLOW}Install (deploy to target)${NC}" + echo " install osx [version] Export + install .app on plum" + echo " install --dev osx [ver] Debug build with dev config" + echo " install iphone [version] Export + build + deploy to iPhone via plum" + echo " install sim [version] Export + build + deploy to iOS Simulator" + echo " install android [ver] Export + install APK via adb" + echo "" + echo -e "${YELLOW}Remote${NC}" + echo " start osx Launch installed app on plum" + echo " start ios Launch app on connected iPhone" + echo " stop osx Kill running app on plum" + echo "" + echo -e "${YELLOW}Test${NC}" + echo " smoke osx Full smoke test (export → ship → launch → screenshot)" + echo "" + echo -e "${YELLOW}Tools${NC}" + echo " tools spritegen Sprite generation pipeline (scan, status, generate, poll, review, install)" + echo "" + echo -e "${YELLOW}Setup${NC}" + echo " setup Install/verify all dev dependencies" + echo " setup --skip-templates Setup without downloading export templates" +} + +cmd_play() { + echo -e "${BLUE}Launching Magic Civilization...${NC}" + WAYLAND_DISPLAY="${WAYLAND_DISPLAY:-wayland-0}" \ + XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}" \ + $GODOT_BIN --rendering-method gl_compatibility "$@" +} + +cmd_editor() { + echo -e "${BLUE}Opening Godot editor...${NC}" + WAYLAND_DISPLAY="${WAYLAND_DISPLAY:-wayland-0}" \ + XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}" \ + $GODOT_BIN -e --rendering-method gl_compatibility "$@" & +} + +cmd_lint() { + echo -e "${BLUE}Checking gdtoolkit config sync...${NC}" + lilith-gdtoolkit-sync --check || { + echo -e "${YELLOW}Config drift detected — syncing...${NC}" + lilith-gdtoolkit-sync + } + echo -e "${BLUE}Linting engine/src/...${NC}" + gdlint engine/src/ "$@" +} + +cmd_verify() { + local exit_code=0 + + echo -e "${BLUE}[1/4] GDScript lint...${NC}" + gdlint engine/src/ || exit_code=$? + + echo "" + echo -e "${BLUE}[2/4] Guide typecheck...${NC}" + pnpm --prefix guide/age-of-four typecheck || exit_code=$? + + echo "" + echo -e "${BLUE}[3/4] Transpiler check (GDScript → TS sync)...${NC}" + uv run tools/transpile-engine/transpile.py --check || exit_code=$? + + echo "" + echo -e "${BLUE}[4/4] Tests (GUT + vitest)...${NC}" + cmd_test || exit_code=$? + + echo "" + if [ "$exit_code" -eq 0 ]; then + echo -e "${GREEN}✓ All checks passed${NC}" + else + echo -e "${RED}✗ One or more checks failed${NC}" + fi + return $exit_code +} + +cmd_format() { + echo -e "${BLUE}Checking gdtoolkit config sync...${NC}" + lilith-gdtoolkit-sync --check || { + echo -e "${YELLOW}Config drift detected — syncing...${NC}" + lilith-gdtoolkit-sync + } + echo -e "${BLUE}Formatting engine/src/...${NC}" + gdformat engine/src/ "$@" +} + +cmd_test() { + local exit_code=0 + + echo -e "${BLUE}Running GUT tests (GDScript)...${NC}" + WAYLAND_DISPLAY="${WAYLAND_DISPLAY:-wayland-0}" \ + XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}" \ + $GODOT_BIN --headless --script res://addons/gut/gut_cmdln.gd \ + -gexit "$@" || exit_code=$? + + echo "" + echo -e "${BLUE}Running vitest (guide)...${NC}" + pnpm --prefix guide/age-of-four test || exit_code=$? + + return $exit_code +} + +cmd_screenshot() { + ./tools/screenshot.sh "$@" +} + +cmd_export() { + ./tools/export.sh "$@" +} + +cmd_export_single() { + local platform="$1" + shift + ./tools/export-single.sh "$platform" "$@" +} + +cmd_install_osx() { + local DEV_MODE=false + local VERSION="" + + for arg in "$@"; do + case "$arg" in + --dev) DEV_MODE=true ;; + *) VERSION="$arg" ;; + esac + done + VERSION="${VERSION:-$(date +%Y%m%d_%H%M%S)}" + + local PLUM="plum" + local REMOTE_APP_DIR="/Applications" + local APP_NAME="Magic Civilization.app" + local ZIP_NAME="MagicCivilization.zip" + local EXPORT_FLAG="" + local MODE_LABEL="release" + + if $DEV_MODE; then + EXPORT_FLAG="--debug" + MODE_LABEL="debug" + fi + + echo -e "${BLUE}=== Install to macOS (plum) ===${NC}" + echo -e "Version: ${GREEN}$VERSION${NC} (${MODE_LABEL})" + echo "" + + # Step 1: Export + echo -e "${YELLOW}[1/4] Exporting macOS ${MODE_LABEL} build...${NC}" + if ! ./tools/export-single.sh macos "$VERSION" $EXPORT_FLAG 2>&1; then + echo -e "${RED}Export failed.${NC}" + return 1 + fi + + BUILD_ZIP="builds/$VERSION/macos/$ZIP_NAME" + if [ ! -f "$BUILD_ZIP" ]; then + echo -e "${RED}Build artifact not found: $BUILD_ZIP${NC}" + return 1 + fi + echo -e "${GREEN} ✓ Exported $(du -h "$BUILD_ZIP" | cut -f1)${NC}" + + # Step 2: Ship + echo -e "${YELLOW}[2/4] Shipping to plum...${NC}" + if ! ssh -o ConnectTimeout=5 "$PLUM" "echo ok" >/dev/null 2>&1; then + echo -e "${RED}Cannot reach plum via SSH${NC}" + return 1 + fi + + scp "$BUILD_ZIP" "$PLUM:/tmp/$ZIP_NAME" + echo -e "${GREEN} ✓ Uploaded${NC}" + + # Step 3: Install + echo -e "${YELLOW}[3/4] Installing on plum...${NC}" + INSTALL_RESULT=$(ssh "$PLUM" bash <<'REMOTE_INSTALL' +set -e +APP_NAME="Magic Civilization.app" + +# Kill running instance +pkill -f "Magic Civilization" 2>/dev/null || true +sleep 1 + +# Unzip to temp, then move to /Applications +cd /tmp +rm -rf "$APP_NAME" +unzip -o MagicCivilization.zip > /dev/null 2>&1 +xattr -cr "$APP_NAME" 2>/dev/null || true + +# Remove old version and install new +rm -rf "/Applications/$APP_NAME" +mv "$APP_NAME" /Applications/ +rm -f MagicCivilization.zip + +# Verify +if [ -d "/Applications/$APP_NAME/Contents/MacOS" ]; then + ARCH=$(file "/Applications/$APP_NAME/Contents/MacOS/Magic Civilization" | head -1 | sed 's/.*: //') + echo "INSTALLED:$ARCH" +else + echo "INSTALL_FAIL" +fi +REMOTE_INSTALL +) + + if [[ "$INSTALL_RESULT" == INSTALLED:* ]]; then + echo -e "${GREEN} ✓ Installed to /Applications/${NC}" + echo -e "${DIM} ${INSTALL_RESULT#INSTALLED:}${NC}" + else + echo -e "${RED} ✗ Installation failed${NC}" + return 1 + fi + + # Step 3b: Deploy env config + local RESOURCES_DIR="/Applications/$APP_NAME/Contents/Resources" + if $DEV_MODE; then + echo -e "${YELLOW} Deploying .env.development...${NC}" + scp .env.development "$PLUM:$RESOURCES_DIR/.env.development" + # Also ship .env.production as base .env + scp .env.production "$PLUM:$RESOURCES_DIR/.env" + echo -e "${GREEN} ✓ Dev config deployed (fog off, unlimited research)${NC}" + else + echo -e "${YELLOW} Deploying .env.production...${NC}" + scp .env.production "$PLUM:$RESOURCES_DIR/.env" + # Remove any leftover dev config + ssh "$PLUM" "rm -f '$RESOURCES_DIR/.env.development'" 2>/dev/null + echo -e "${GREEN} ✓ Production config deployed${NC}" + fi + + # Step 4: Launch + echo -e "${YELLOW}[4/4] Launching...${NC}" + ssh "$PLUM" 'open "/Applications/Magic Civilization.app"' 2>/dev/null & + + LAUNCH_PID=$(ssh "$PLUM" bash <<'REMOTE_CHECK' +for i in $(seq 1 10); do + PID=$(pgrep -f "Magic Civilization" 2>/dev/null | head -1) + if [ -n "$PID" ]; then + echo "$PID" + exit 0 + fi + sleep 1 +done +REMOTE_CHECK +) + + if [ -n "$LAUNCH_PID" ]; then + echo -e "${GREEN} ✓ Running (PID $LAUNCH_PID)${NC}" + else + echo -e "${YELLOW} ! Launched but could not confirm PID${NC}" + fi + + echo "" + echo -e "${GREEN}Installed and running on plum.${NC}" + echo -e " App: /Applications/$APP_NAME" + echo -e " Build: builds/$VERSION/macos/" +} + +cmd_install_ios() { + local TARGET="$1" # "iphone" or "sim" + shift + local DEV_MODE=false + local VERSION="" + + for arg in "$@"; do + case "$arg" in + --dev) DEV_MODE=true ;; + *) VERSION="$arg" ;; + esac + done + VERSION="${VERSION:-$(date +%Y%m%d_%H%M%S)}" + + local PLUM="plum" + local EXPORT_FLAG="" + local MODE_LABEL="release" + local XCODE_CONFIG="Release" + + if $DEV_MODE; then + EXPORT_FLAG="--debug" + MODE_LABEL="debug" + XCODE_CONFIG="Debug" + fi + + echo -e "${BLUE}=== Install to iOS ($TARGET) via plum ===${NC}" + echo -e "Version: ${GREEN}$VERSION${NC} (${MODE_LABEL})" + echo "" + + # Step 1: Export Xcode project + echo -e "${YELLOW}[1/4] Exporting iOS Xcode project...${NC}" + if ! ./tools/export-single.sh ios "$VERSION" $EXPORT_FLAG 2>&1; then + echo -e "${RED}Export failed.${NC}" + return 1 + fi + + local BUILD_DIR="builds/$VERSION/ios" + if [ ! -d "$BUILD_DIR/MagicCivilization.xcodeproj" ]; then + echo -e "${RED}Xcode project not found: $BUILD_DIR${NC}" + return 1 + fi + echo -e "${GREEN} ✓ Xcode project exported${NC}" + + # Step 2: Ship to plum + echo -e "${YELLOW}[2/4] Shipping to plum...${NC}" + if ! ssh -o ConnectTimeout=5 "$PLUM" "echo ok" >/dev/null 2>&1; then + echo -e "${RED}Cannot reach plum via SSH${NC}" + return 1 + fi + + local REMOTE_BUILD="~/MagicCiv_iOS_Build" + ssh "$PLUM" "rm -rf $REMOTE_BUILD && mkdir -p $REMOTE_BUILD" + rsync -az --progress "$BUILD_DIR/" "$PLUM:$REMOTE_BUILD/" 2>&1 | tail -3 + echo -e "${GREEN} ✓ Shipped to plum${NC}" + + # Step 3: Build with xcodebuild + echo -e "${YELLOW}[3/4] Building on plum with xcodebuild...${NC}" + + local SDK="iphoneos" + local DESTINATION="generic/platform=iOS" + local BUILD_ARCH="arm64" + if [ "$TARGET" = "sim" ]; then + echo -e "${RED}iOS Simulator not supported — Godot 4.6.1 templates lack arm64 simulator slices.${NC}" + echo -e "Use ${GREEN}./run install iphone${NC} for device deployment instead." + return 1 + fi + + BUILD_RESULT=$(ssh "$PLUM" bash <&1 | tail -5 + +if [ \${PIPESTATUS[0]} -eq 0 ]; then + echo "BUILD_OK" +else + echo "BUILD_FAIL" +fi +REMOTE_BUILD_CMD +) + + if ! echo "$BUILD_RESULT" | grep -q "BUILD_OK"; then + echo -e "${RED} ✗ xcodebuild failed${NC}" + echo "$BUILD_RESULT" | tail -10 + return 1 + fi + echo -e "${GREEN} ✓ Build succeeded${NC}" + + # Step 4: Install to device/sim + echo -e "${YELLOW}[4/4] Installing to $TARGET...${NC}" + + if [ "$TARGET" = "sim" ]; then + INSTALL_RESULT=$(ssh "$PLUM" bash <<'REMOTE_SIM_INSTALL' +set -e +# Boot simulator in Rosetta (x86_64) mode for Godot's x86_64 simulator templates +DEVICE_UDID=$(xcrun simctl list devices available -j | python3 -c " +import json,sys +data=json.load(sys.stdin) +for runtime,devices in data.get('devices',{}).items(): + if 'iOS' in runtime: + for d in devices: + if 'iPhone' in d['name'] and d['isAvailable']: + print(d['udid']); sys.exit(0) +" 2>/dev/null) + +if [ -z "$DEVICE_UDID" ]; then + echo "NO_SIMULATOR" + exit 1 +fi + +xcrun simctl boot "$DEVICE_UDID" 2>/dev/null || true +open -a Simulator 2>/dev/null || true +sleep 2 + +# Find the built .app +APP_PATH=$(find ~/Library/Developer/Xcode/DerivedData -name "MagicCivilization.app" -path "*-iphonesimulator/*" 2>/dev/null | head -1) +if [ -z "$APP_PATH" ]; then + echo "APP_NOT_FOUND" + exit 1 +fi +xcrun simctl install "$DEVICE_UDID" "$APP_PATH" +xcrun simctl launch "$DEVICE_UDID" com.magicciv.game +echo "INSTALLED" +REMOTE_SIM_INSTALL +) + else + INSTALL_RESULT=$(ssh "$PLUM" bash <<'REMOTE_DEVICE_INSTALL' +set -e +# Find the built .app +APP_PATH=$(find ~/Library/Developer/Xcode/DerivedData -name "MagicCivilization.app" -path "*/Release-iphoneos/*" -o -name "MagicCivilization.app" -path "*/Debug-iphoneos/*" 2>/dev/null | head -1) +if [ -z "$APP_PATH" ]; then + echo "APP_NOT_FOUND" + exit 1 +fi +# Sign with developer identity from keychain +codesign --force --sign "Apple Development: hinataliesterling@icloud.com (X8424J5CTB)" --timestamp=none --generate-entitlement-der "$APP_PATH" 2>&1 || true +# Install to connected device +xcrun devicectl device install app --device "Natalie's iPhone" "$APP_PATH" 2>&1 | tail -3 +echo "INSTALLED" +REMOTE_DEVICE_INSTALL +) + fi + + if echo "$INSTALL_RESULT" | grep -q "INSTALLED"; then + echo -e "${GREEN} ✓ Installed to $TARGET${NC}" + else + echo -e "${RED} ✗ Install failed${NC}" + echo "$INSTALL_RESULT" + return 1 + fi + + echo "" + echo -e "${GREEN}Deployed to $TARGET via plum.${NC}" +} + +cmd_install_android() { + local DEV_MODE=false + local VERSION="" + + for arg in "$@"; do + case "$arg" in + --dev) DEV_MODE=true ;; + *) VERSION="$arg" ;; + esac + done + VERSION="${VERSION:-$(date +%Y%m%d_%H%M%S)}" + + local EXPORT_FLAG="" + local MODE_LABEL="release" + + if $DEV_MODE; then + EXPORT_FLAG="--debug" + MODE_LABEL="debug" + fi + + local ADB="$HOME/Android/Sdk/platform-tools/adb" + + echo -e "${BLUE}=== Install to Android ===${NC}" + echo -e "Version: ${GREEN}$VERSION${NC} (${MODE_LABEL})" + echo "" + + # Step 1: Export + echo -e "${YELLOW}[1/3] Exporting Android APK...${NC}" + if ! ./tools/export-single.sh android "$VERSION" $EXPORT_FLAG 2>&1; then + echo -e "${RED}Export failed.${NC}" + return 1 + fi + + local APK="builds/$VERSION/android/MagicCivilization.apk" + if [ ! -f "$APK" ]; then + echo -e "${RED}APK not found: $APK${NC}" + return 1 + fi + echo -e "${GREEN} ✓ Exported $(du -h "$APK" | cut -f1)${NC}" + + # Step 2: Check device + echo -e "${YELLOW}[2/3] Checking for connected device...${NC}" + if ! "$ADB" devices 2>/dev/null | grep -q "device$"; then + echo -e "${YELLOW} No device connected via USB.${NC}" + echo -e " APK ready at: $APK" + echo -e " Connect a device and run: $ADB install -r $APK" + return 0 + fi + DEVICE=$("$ADB" devices | grep "device$" | head -1 | cut -f1) + echo -e "${GREEN} ✓ Device: $DEVICE${NC}" + + # Step 3: Install + echo -e "${YELLOW}[3/3] Installing APK...${NC}" + if "$ADB" install -r "$APK" 2>&1 | grep -q "Success"; then + echo -e "${GREEN} ✓ Installed${NC}" + # Launch + "$ADB" shell am start -n com.magicciv.game/com.godot.game.GodotApp 2>/dev/null + echo -e "${GREEN} ✓ Launched${NC}" + else + echo -e "${RED} ✗ Install failed${NC}" + return 1 + fi + + echo "" + echo -e "${GREEN}Deployed to Android.${NC}" +} + +cmd_start_osx() { + local PLUM="plum" + if ! ssh -o ConnectTimeout=5 "$PLUM" "echo ok" >/dev/null 2>&1; then + echo -e "${RED}Cannot reach plum via SSH${NC}" + return 1 + fi + + # Check if already running + EXISTING=$(ssh "$PLUM" 'pgrep -f "Magic Civilization" 2>/dev/null | head -1') + if [ -n "$EXISTING" ]; then + echo -e "${YELLOW}Already running (PID $EXISTING)${NC}" + return 0 + fi + + # Check if installed + if ! ssh "$PLUM" '[ -d "/Applications/Magic Civilization.app" ]'; then + echo -e "${RED}Not installed. Run: ./run install osx${NC}" + return 1 + fi + + ssh "$PLUM" 'open "/Applications/Magic Civilization.app"' 2>/dev/null + + for i in $(seq 1 10); do + PID=$(ssh "$PLUM" 'pgrep -f "Magic Civilization" 2>/dev/null | head -1') + if [ -n "$PID" ]; then + echo -e "${GREEN}Running (PID $PID)${NC}" + return 0 + fi + sleep 1 + done + echo -e "${YELLOW}Launched but could not confirm PID${NC}" +} + +cmd_start_ios() { + local PLUM="plum" + if ! ssh -o ConnectTimeout=5 "$PLUM" "echo ok" >/dev/null 2>&1; then + echo -e "${RED}Cannot reach plum via SSH${NC}" + return 1 + fi + + local DEVICE_ID="2FF5E256-27B9-5D56-89E5-B4DECCEFCE94" + RESULT=$(ssh "$PLUM" "xcrun devicectl device process launch --device $DEVICE_ID com.magicciv.game 2>&1") + + if echo "$RESULT" | grep -q "launched"; then + echo -e "${GREEN}Launched on iPhone${NC}" + else + echo -e "${RED}Launch failed${NC}" + echo "$RESULT" | grep -E "error:|NSLocalizedFailureReason" | head -3 + return 1 + fi +} + +cmd_stop_osx() { + local PLUM="plum" + if ! ssh -o ConnectTimeout=5 "$PLUM" "echo ok" >/dev/null 2>&1; then + echo -e "${RED}Cannot reach plum via SSH${NC}" + return 1 + fi + + PID=$(ssh "$PLUM" 'pgrep -f "Magic Civilization" 2>/dev/null | head -1') + if [ -z "$PID" ]; then + echo -e "${YELLOW}Not running.${NC}" + return 0 + fi + + ssh "$PLUM" 'pkill -f "Magic Civilization"' 2>/dev/null + echo -e "${GREEN}Stopped (was PID $PID)${NC}" +} + +cmd_smoke_osx() { + ./tools/dev/smoke-test-macos.sh "$@" +} + +cmd_setup() { + ./tools/dev/setup-devenv.sh "$@" +} + +cmd_tools_spritegen() { + python3 tools/sprite-generation/cli.py "$@" +} + +# --- Route command --- + +COMMAND="${1:-}" +shift 2>/dev/null || true + +case "$COMMAND" in + play) cmd_play "$@" ;; + editor) cmd_editor "$@" ;; + lint) cmd_lint "$@" ;; + verify) cmd_verify "$@" ;; + format) cmd_format "$@" ;; + test) cmd_test "$@" ;; + screenshot) cmd_screenshot "$@" ;; + export) cmd_export "$@" ;; + export:windows) cmd_export_single windows "$@" ;; + export:macos) cmd_export_single macos "$@" ;; + export:linux) cmd_export_single linux "$@" ;; + export:android) cmd_export_single android "$@" ;; + export:ios) cmd_export_single ios "$@" ;; + install) + # Parse flags and target from remaining args + INSTALL_FLAGS="" + TARGET="" + INSTALL_ARGS=() + for arg in "$@"; do + case "$arg" in + --dev) INSTALL_FLAGS="$INSTALL_FLAGS --dev" ;; + osx|macos) TARGET="osx" ;; + iphone) TARGET="iphone" ;; + ipad) TARGET="iphone" ;; + sim) TARGET="sim" ;; + android) TARGET="android" ;; + *) INSTALL_ARGS+=("$arg") ;; + esac + done + case "${TARGET:-}" in + osx) cmd_install_osx $INSTALL_FLAGS "${INSTALL_ARGS[@]+"${INSTALL_ARGS[@]}"}" ;; + iphone) cmd_install_ios iphone $INSTALL_FLAGS "${INSTALL_ARGS[@]+"${INSTALL_ARGS[@]}"}" ;; + sim) cmd_install_ios sim $INSTALL_FLAGS "${INSTALL_ARGS[@]+"${INSTALL_ARGS[@]}"}" ;; + android) cmd_install_android $INSTALL_FLAGS "${INSTALL_ARGS[@]+"${INSTALL_ARGS[@]}"}" ;; + *) echo -e "${RED}Unknown install target: ${TARGET:-}${NC}"; echo "Available: osx, iphone, sim, android"; exit 1 ;; + esac + ;; + start) + TARGET="${1:-}" + shift 2>/dev/null || true + case "$TARGET" in + osx|macos) cmd_start_osx "$@" ;; + ios|iphone) cmd_start_ios "$@" ;; + *) echo -e "${RED}Unknown start target: $TARGET${NC}"; echo "Available: osx, ios"; exit 1 ;; + esac + ;; + stop) + TARGET="${1:-}" + shift 2>/dev/null || true + case "$TARGET" in + osx|macos) cmd_stop_osx "$@" ;; + *) echo -e "${RED}Unknown stop target: $TARGET${NC}"; echo "Available: osx"; exit 1 ;; + esac + ;; + smoke) + TARGET="${1:-}" + shift 2>/dev/null || true + case "$TARGET" in + osx|macos) cmd_smoke_osx "$@" ;; + *) echo -e "${RED}Unknown smoke target: $TARGET${NC}"; echo "Available: osx"; exit 1 ;; + esac + ;; + tools) + TOOL="${1:-}" + shift 2>/dev/null || true + case "$TOOL" in + spritegen) cmd_tools_spritegen "$@" ;; + *) echo -e "${RED}Unknown tool: ${TOOL:-}${NC}"; echo "Available: spritegen"; exit 1 ;; + esac + ;; + setup) cmd_setup "$@" ;; + help|--help|-h|"") usage ;; + *) echo -e "${RED}Unknown command: $COMMAND${NC}"; echo ""; usage; exit 1 ;; +esac diff --git a/spole-view.png b/spole-view.png new file mode 100644 index 00000000..7ab131e1 Binary files /dev/null and b/spole-view.png differ diff --git a/v2-equator.png b/v2-equator.png new file mode 100644 index 00000000..7dbd4329 Binary files /dev/null and b/v2-equator.png differ diff --git a/v2-npole.png b/v2-npole.png new file mode 100644 index 00000000..4c7aa3c4 Binary files /dev/null and b/v2-npole.png differ diff --git a/v2-spole.png b/v2-spole.png new file mode 100644 index 00000000..e3baa989 Binary files /dev/null and b/v2-spole.png differ