Skip to content

Ship Classes (EVE-style) — design

Date: 2026-07-04 Status: approved (design), pre-implementation Determinism baseline at design time: snapshot_string().hash()=2730172591, state_checksum()=4075578713 → read the literal pinned assertion in tests/test_determinism_checksum.gd / test_determinism_crystals.gd, not this note.

Naming is provisional. Chris wants a later pass on overall themes/names for everything, and there is still a large amount of balancing to do. Every name, number, and gold price below is a placeholder to try out and tune, not a final decision. The architecture is the durable part; the content (the “Obsidian” identity, the exact stat numbers) is expected to change.

Introduce an EVE-style class dimension to hulls. The 6 existing hulls (manta, cobalt, aurum, prism, amethyst, fuchsia) become frigate-class: 3 weapon slots, 1 drone slot, at today’s stats (100 HP / 260 speed / 16px hitbox).

Add one cruiser hull with a full stat block — more slots, tankier, slower, bigger — unlocked with gold in the meta-shop. A cruiser is a genuine trade (bigger guns, harder to dodge), not a free upgrade.

  • Retag the existing 6 hulls as frigate class with weapon_slots=3, drone_slots=1.
  • One cruiser hull with a full stat block + a real baked sprite.
  • Sim.max_weapon_slots as per-run state (currently a hardcoded const).
  • Gold unlock for the cruiser via the existing meta-shop unlock pattern.
  • UI wiring so slot counts, the picker, and the config panel reflect the hull.
  • TDD for every unit; determinism re-verified.
  • More cruisers, or destroyer / battlecruiser / battleship tiers.
  • Per-pilot hulls in co-opmax_weapon_slots stays Sim-global this pass, matching the current P1-centric reality (P2 progression is already gated/incomplete). Flagged inline where it would need to become per-pilot.
  • EVE high/mid/low-slot distinctions — we model only weapon slots and drone slots.
  • A weapon-slot meta-shop upgrade (drones already have one; weapons stay hull-fixed for now).

2. Data model — enrich sim/ship_bonuses.gd

Section titled “2. Data model — enrich sim/ship_bonuses.gd”

Each TABLE entry grows from a bare bonus into a full hull spec. Keep the ShipBonuses class_name — renaming a class referenced in 5 files (meta_state, start_menu, ship_config_panel, player_renderer, main) invites the stale-class-cache trap for no real gain. The name is now a light misnomer (it’s a hull registry, not just bonuses); a comment notes it.

Frigate entries keep their existing bonus, gaining class + slots + explicit base stats equal to today’s defaults (a behavioural no-op except for the weapon-slot cap):

"cobalt": { class="frigate", weapon_slots=3, drone_slots=1,
base_hp=100, base_speed=260, base_radius=16,
stat_effect="move_speed", magnitude=1.15, name="Cobalt", label="+15% speed" },
# manta / aurum / prism / amethyst / fuchsia identically retagged, each keeping its own bonus

The cruiser (provisional identity — “Obsidian”, heavy dark hull, fits Dark Cosmos; frigates are gem/metal names, this is heavier stone):

"obsidian": { class="cruiser", weapon_slots=5, drone_slots=2,
base_hp=170, base_speed=210, base_radius=24,
stat_effect="armor", magnitude=10.0, name="Obsidian", label="Cruiser · +10 armor" },

New static accessors (all pure, unknown id → frigate defaults for stale saves): class_of(id), weapon_slots_for(id), drone_slots_for(id), base_stats_for(id).

sim/sim.gd
var max_weapon_slots: int = UpgradeSystem.MAX_WEAPONS # per-run active cap; hull sets it at run start
  • UpgradeSystem.MAX_WEAPONS (6) stays as the absolute ceiling: weapon-instance count, UI slot-array size, hard cap. Frigate (3) and cruiser (5) are both <= 6.
  • max_weapon_slots is the per-run soft cap. UpgradeSystem.roll_upgrade_choices and grant_weapon gate weapon grants on sim.max_weapon_slots instead of the const.
  • Default equals the const so any Sim built without a hull (every headless test, the determinism baseline) behaves exactly as today.
  • This mirrors the existing Sim.max_drone_slots field one-for-one — the pattern already exists.

4. Run-start application (render-side, main.gd:474-475)

Section titled “4. Run-start application (render-side, main.gd:474-475)”

Today: meta.apply_to(...) then ShipBonuses.apply_to(selected_ship, sim.player, sim.mods), and sim.max_drone_slots = meta.drone_slots() at main.gd:485.

Replace with a single ShipBonuses.apply_hull(selected_ship, sim, meta) that:

  • sim.max_weapon_slots = weapon_slots_for(id)
  • sim.max_drone_slots = drone_slots_for(id) + meta.level_of("drone-slots") — hull base plus the existing drone-slots shop upgrade (additive). Frigate base 1 makes this a perfect no-op versus today’s 1 + level_of("drone-slots").
  • overrides player.max_hp / player.hp / player.speed / player.radius from the base block
  • applies the headline bonus via the existing StatEffects / SimMods path (unchanged mechanism)

apply_to() is kept as a thin wrapper (bonus-only) if any other caller needs it, or folded in — decided during implementation after grepping callers.

Determinism boundary: this is run config, applied once before tick 0 — the same boundary max_drone_slots already uses. Both co-op peers must agree on the hull (as they already agree on the seed and run config). max_weapon_slots being sim state read by the deterministic upgrade roller is fine because it is fixed before ticking and identical across peers.

Add a bible entry unlock-hull-obsidian (type:"unlock", target:"hull:obsidian", a gold cost) — mirrors the existing unlock-drone-<class> / unlock-decoy-<type> pattern the shop already renders (ui/shop_categories.gd, ui/shop_icons.gd already special-case unlock-* ids; the new id needs minor additions to those maps).

# sim/meta_state.gd — mirrors owns_decoy / owns_class
func owns_ship(ship_id: String, defs: Array) -> bool:
if ShipBonuses.class_of(ship_id) == "frigate":
return true # all frigates always owned
return level_of("unlock-hull-" + ship_id) >= 1
File Line Change
ui/start_menu.gd ~129 List the cruiser too; render it locked with its gold price until owns_ship; selecting it (when owned) sets selected_ship.
ui/ship_config_panel.gd ~95 Draw weapon_slots_for(id) cards, not MAX_WEAPONS; show class name + stat block (slots / HP / speed).
ui/weapon_panel.gd ~176 Draw sim.max_weapon_slots slots, not UpgradeSystem.MAX_WEAPONS.
ui/drone_dock.gd Already reads sim.max_drone_slots. No change.
render/player_renderer.gd 731 Already data-driven on selected_ship. Just needs the sprite asset to exist.

Bake a distinct, chunkier silhouette (EVE cruiser = more mass than a frigate) via tools/ship_preview (SHIP_VARIANT=bake godot --path . res://tools/ship_preview/preview.tscn --rendering-method forward_plus) → render/ship_sprites/ship3d_obsidian.png (+ any hull texture the renderer expects, matching the existing ship3d_* assets).

Per memory bullet-heaven-ui-look: a capture that “looks plausible” is not proof — diff against the frigate baseline and get Chris’s on-device eye before calling the art done. This is its own iteration sub-task, sequenced after the mechanical system works with a placeholder.

Unit tests:

  • ShipBonuses: class_of / weapon_slots_for / drone_slots_for / base_stats_for for a frigate, the cruiser, and an unknown id (→ frigate defaults).
  • apply_hull: frigate sets max_weapon_slots=3, max_drone_slots=1 (+shop), base stats unchanged; cruiser sets 5 / 2 / 170 / 210 / 24 + +10 armor.
  • UpgradeSystem: weapon grants stop at max_weapon_slots (a frigate run cannot exceed 3 weapons; roll_upgrade_choices offers no weapon grant when full).
  • MetaState.owns_ship: frigate always true; cruiser false until unlock-hull-obsidian >= 1; purchase flow flips it.

Determinism: the baseline test builds Sim with no hull → default max_weapon_slots=6, and frigate numbers equal today’s, so expect 2730172591 / 4075578713 unchanged (read the literal pinned assertion, not this note). Re-verify after the change. If it moves — it should not — treat it as a real break and investigate, do not blindly re-pin.

Ritual: every chunk goes through bh-dev-chunk (build → TDD → import → boot-check → test-count → determinism → commit → tvOS-sync). New class/dir additions run godot --headless --import to dodge the stale-class-cache parse error.

  • Frigate 6→3 weapon slots is a real shift to the only shipping mode: builds go deep (more mods/evolutions on fewer weapons) instead of wide. Intended and arguably better build-craft, but changes late-run upgrade pacing (once 3 slots fill, offers are mods/evos/stats only). Worth a playtest.
  • All cruiser numbers (5W/2D, 170/210/24, +10 armor, gold price) are consts in the TABLE.
  • Cruiser must stay a genuine trade: the bigger hitbox + lower speed are the cost of the extra slots + HP. If it reads as strictly-better in playtest, widen the downside, don’t shrink the slots.
  1. Data model — enrich ShipBonuses.TABLE + accessors + tests (no behaviour change yet).
  2. Sim fieldSim.max_weapon_slots; re-point UpgradeSystem gates; determinism re-verify.
  3. Run-start applyapply_hull wiring in main.gd; frigate proves the no-op path.
  4. UI slot countsweapon_panel / ship_config_panel read the hull’s counts.
  5. Cruiser content + unlockobsidian entry, owns_ship, shop unlock def, start-menu lock/price.
  6. Cruiser sprite — bake, diff vs baseline, on-device review (art sub-task, last).