Auto Warz — Technical Architecture Document¶
Document Type: Technical Architecture Version: 0.3 Status: In Progress Last Updated: 2026-04-13 Vault destination:
02_Technical DocumentsAuthor: Solo Developer Parent Document:[01_Master GDD](<../01_Design Documents/01_Master GDD.md>)Related Documents:[02_Scope & MVP Document](<../01_Design Documents/02_Scope & MVP Document.md>)·[02_Server Architecture Document](<./02_Server Architecture Document.md>)·[[03_Cross-Platform Strategy]]
(Sections unchanged from v0.1 are preserved in full. Only sections with new content are marked with [UPDATED].)
Engine Decision¶
✅ Godot 4 — Confirmed¶
(Unchanged from v0.1)
Core Architecture Overview¶
(Unchanged from v0.1)
Rendering Architecture¶
(Unchanged from v0.1)
Game World & Scene Structure¶
(Unchanged from v0.1)
Factory Simulation System [UPDATED]¶
Simulation Architecture: Tick-Based¶
The factory runs at 20 ticks per second, decoupled from the render frame rate. (Unchanged.)
Simulation Tick Phase Order¶
As of M2.3, the tick phases are:
P1 — Buildings produce (including fluid outputs)
P2 — IoConnectors tick_pull
P2b — Pipe Connectors tick_pull
P3 — IoConnectors tick_push
P3b — Pipe Connectors tick_push
P3c — Pipe segments propagate pressure (Standard/Full pipe_complexity modes)
P3d — Pipe segments equalise fluid (pair-equalisation — all modes)
P3e — Pipe segments update temperature (Full mode only — deferred to M2.3.1)
P4 — Belts advance
P5 — Genesis simulation tick (mining and building timers)
Logistics Systems¶
Belt System¶
Belts connect buildings and connectors using the IoConnector directional pass-through pattern. Key implementation notes:
- Connection-aware visuals: BeltSegment visuals compute L/T/straight/cross shapes based on flow connections (neighbour belt pointing toward this cell = connected input arm).
update_visual(belt_system)called after every placement/removal;refresh_all_visuals()after load. - Reverse-flow guard: A belt will not push an item to a neighbour whose
direction == opposite_dir(this.direction)— prevents items flowing backward into a belt's output face. - Drag placement: Belts support click-and-drag runs. Direction is fixed to the active
_belt_directionfor the whole drag. See #Placement System below.
Pipe System¶
Pipes transport fluids between buildings. Three core classes:
PipeSegment (Node3D)
- Auto-connects to all adjacent pipe cells — no explicit direction required
- Tracks fluid_id and fluid_amount per segment
- Visual: dark hub + one arm per connected face (arm-based, not single mesh rotation)
- Registered in PipeSystem._pipes: Dictionary (Vector2i → PipeSegment)
PipeSystem (Node3D, scene-level)
- Registry and tick coordinator for all pipe segments and pipe connectors
- _rebuild_networks() — BFS that groups connected segments into PipeNetwork objects
- Bridge logic: when BFS encounters an adjacent PipeConnector, it checks both input_face and output_face sides for pipe segments to bridge across
- get_pipe_at(pos), get_pipe_connector_at(pos) — used by connectors and buildings
PipeNetwork (RefCounted)
- Owns a list of PipeSegment references sharing the same fluid
- Fluid simulation via pair equalisation (not greedy pressure-sort):
- Iterates all connected segment pairs exactly once per tick using _pair_key() deduplication
- diff = seg_a.fluid_amount - seg_b.fluid_amount
- Dead-band: skip if |diff| < 0.5 (prevents micro-oscillation at equilibrium)
- Transfer: diff × 0.4 clamped to available fluid and remaining capacity
- Result: stable on looping networks, O(edges) per tick, no sort step
- See [07_Pipe & Fluid System Design](<../01_Design Documents/07_Pipe & Fluid System Design.md>) for full algorithm spec
PipeConnector (Node3D)
- Mirrors IoConnector pattern: blue input_face, green output_face (always opposite in Phase 1)
- 10-unit internal buffer
- tick_pull() — pulls from pipe or building on input face
- tick_push() — pushes to adjacent same-type connector (if faces align), then pipe, then building on output face
- Supports output→input chaining with adjacent PipeConnectors
IoConnector System¶
IoConnector is the belt-equivalent pass-through for solid items:
- tick_pull() — pulls one item from the building on input_face each tick
- tick_push() — pushes held item to belt or building on output_face
- Checks adjacent IoConnector first (chaining)
- Falls back to BeltSegment, then BuildingInstance
- Dynamic building discovery every tick — no re-registration needed when buildings are added/removed
Placement System [NEW]¶
All placement tools stay active after a placement — never auto-clear. Escape or right-click cancels. This allows click-and-drag runs.
Drag runs (belts and pipes):
- Click begins drag, release completes it
- Path computed using Manhattan routing (longer axis first)
- Ghost pool: pre-allocated MeshInstance3D nodes reused between drag updates
- Camera pan suppressed while drag is active: CameraRig.placement_drag_active = true
Sentinel pattern:
- When a belt/pipe/connector is queued as a Genesis task, its grid cell is immediately reserved in GridSystem with a null sentinel value
- _is_cell_free_for_ghost() checks both GridSystem and earlier cells in the current drag path to prevent double-queuing
- On Genesis task completion, the real node replaces the sentinel
- On task cancellation, the sentinel is cleared
Approach position for buildings:
- Genesis walks to the nearest cell just outside the building footprint edge, not the building centre
- _get_build_approach_pos() checks all edge cells of the footprint, picks the closest to Genesis's current position
- Prevents Genesis from walking into the footprint and getting stuck
Genesis Navigation [UPDATED]¶
Genesis uses NavigationAgent3D with a runtime-baked NavigationRegion3D:
- Buildings add a
StaticBody3D+CollisionShape3D(collision_layer=1) insetup() NavigationRegion3Dis baked viabake_navigation_mesh()at runtime- Nav mesh is rebaked (via
call_deferred) after every_place_building_at()and_remove_building_at()call - Genesis
collision_mask = 0— physical collision with buildings is disabled; the nav mesh handles avoidance - Physical collision caused Genesis to become trapped inside building footprints
- Genesis reads
NavigationAgent3D.get_next_path_position()each_physics_processframe
Save & Load System [UPDATED]¶
What Gets Saved (as of M2.3)¶
WorldScene.serialize() → {
planet_inventory: Dictionary,
metal_plates_produced: int,
genesis_pos_x: float,
genesis_pos_z: float,
genesis_task_queue: Array, # NEW — all queued tasks serialized
buildings: Array [
{ def_path, origin_x, origin_y, inventory }
],
belts: Array [
{ x, y, dir, held }
],
connectors: Array [
{ x, y, in_face, out_face, held }
],
pipes: Array [ # NEW
{ x, y, fluid_id, fluid_amount }
],
pipe_connectors: Array [ # NEW
{ x, y, in_face, out_face, held_fluid, held_amount }
],
galaxy_seed: int # added M2.1
}
Genesis Task Queue Serialization¶
All task types are serialized and restored on load:
| Task Type | Serialized Fields |
|---|---|
MOVE_TO |
target_x, target_z |
MINE |
target_x, target_z (node found by position at load time via _find_resource_node_at()) |
BUILD |
def_path, cell_x, cell_y, target_x, target_z |
PLACE_BELT |
belt_cell_x, belt_cell_y, dir, target_x, target_z |
PLACE_CONNECTOR |
connector_cell_x, connector_cell_y, input_face, target_x, target_z |
PLACE_PIPE |
pipe_cell_x, pipe_cell_y, target_x, target_z |
PLACE_PIPE_CONNECTOR |
pc_cell_x, pc_cell_y, input_face, target_x, target_z |
Null Sentinel Guard¶
serialize() skips any dictionary entry whose value is null (queued-but-not-yet-built items). Ghost nodes are never serialized.
Multiplayer & Networking Architecture¶
(Unchanged from v0.1)
Input System¶
(Unchanged from v0.1 with the following additions:)
| Action | Default | Notes |
|---|---|---|
| Place / drag start | Left Click | Hold and drag for belt/pipe runs |
| Cancel tool | Right Click / Escape | Cancels active tool; no auto-cancel after placement |
| Rotate belt/connector/pipe connector | R | Cycles input face through 4 directions |
| Output face rotation | E | Stub — research unlock (M3.x) |
Audio Architecture¶
(Unchanged from v0.1)
UI Architecture [UPDATED — M2.7]¶
Side Panel Flap System¶
Both screen-edge flaps share a common GDScript base class. This avoids duplication and ensures both flaps always have the same stage/attention/animation behaviour.
src/ui/panels/
FlapBase.gd ← shared base class (class_name FlapBase extends Control)
LeftFlap.gd ← extends FlapBase (left-side session panel)
RightFlap.gd ← extends FlapBase (right-side factory info panel)
FlapBase (src/ui/panels/FlapBase.gd)¶
Contains everything both flaps share:
- Constants — all colours, sizes (HANDLE_SECTION_HEIGHT = 36, TAB_STRIP_WIDTH = 36, TAB_BUTTON_HEIGHT = 48, PANEL_DEFAULT_WIDTH = 260, PANEL_MIN_WIDTH = 180).
- @onready refs —
_strip_wrapper,_tab_strip,_tab_vbox,_handle_section,_handle_arrow,_content_panel,_resize_handle,_content_area,_tooltip_label, twoTimernodes. - Stage system — three stages:
0(collapsed, handle only or handle + alerted tabs),1(tab strip visible, no content panel),2(full panel open). Set viaset_stage(n). Persisted touser://ui_prefs.cfg. - Attention map —
_tab_attention: Dictionarymaps tab_id → level (0/1/2). Updated byset_tab_attention()/clear_tab_attention(). - All tween logic —
_update_tab_attention_tween,_update_tab_bounce_tween,_start_handle_bounce,_stop_handle_bounce,_start_handle_pulse,_stop_handle_pulse. _refresh_attention_visuals()— called whenever attention state changes. Stage-0 special case: shows only alerted tabs, sizes strip to exact alerted-tab count via_set_strip_height_for_count(n)(prevents black gap).- NotificationCenter wiring — acquires
/root/NotificationCenter, connectstab_level_changedsignal, callsget_levels_snapshot()on init. 5-second pollingTimeras a self-healing fallback. - Preferences (
_load_preferences/_save_preferences) — reads/writes stage, active tab, panel width fromui_prefs.cfg. Uses virtual pref-key methods so both flaps write to different keys in the same file. - Resize drag —
_input()hit-tests_get_resize_strip_rect()(virtual); calls_compute_resize_width()(virtual) to convert mouse X to panel width. - Virtual hook —
_post_deferred_init()called at end of_deferred_init()so subclasses can do their own wiring after base setup is complete.
Virtual methods subclasses must override:
| Method | Purpose |
|---|---|
_get_tabs() -> Array |
Return the TABS constant for this flap |
_get_pref_key_stage/tab/width() -> String |
Preference file keys |
_get_default_stage() -> int |
Default stage when no prefs file exists |
_get_default_tab_id() -> String |
Default active tab when no prefs file exists |
_relayout() |
Full layout pass — positions all nodes for this flap's side |
_x_strip() -> float |
X offset for the strip wrapper (0 for left, viewport_w - 36 for right) |
_x_panel_visible() -> float |
X of content panel when open |
_x_panel_hidden() -> float |
X of content panel when off-screen |
_update_handle_arrow() |
Sets arrow glyph text per stage |
_get_resize_strip_rect() -> Rect2 |
Hit rect for resize drag |
_compute_resize_width(mouse_x) -> float |
Converts drag position to panel width |
_build_strip_style() -> StyleBoxFlat |
Strip panel style (border side differs L vs R) |
_build_content_style() -> StyleBoxFlat |
Content panel style (border side differs L vs R) |
_place_tooltip(rect, size) |
Tooltip position (right of strip for left flap, left for right) |
_setup_active_strip(cr) |
Anchors active-strip indicator to correct edge |
LeftFlap (src/ui/panels/LeftFlap.gd)¶
Extends FlapBase. Left-specific content:
- 6 TABS — Save, Load, Settings, Session, Exit, Debug (debug hidden in release builds via
_post_deferred_init()). - Layout — strip at x=0, content opens to the right (
_x_panel_visible() = TAB_STRIP_WIDTH). Resize handle on right edge of content panel. - Arrow direction — ◀ (stage 0), ▶ (stage 1), ▶▶ (stage 2).
- ESC handling —
_unhandled_input()toggles stage on ESC key. - Signal passthrough — wires SaveTab, LoadTab, SettingsTab, ExitTab child signals up to LeftFlap signals for WorldScene consumption.
- Threshold forwarding —
SettingsTab.save_attention_thresholds_changed→NotificationCenter.set_save_attention_thresholds(). wire_debug_tab()— called by WorldScene in debug builds to inject GenesisBot / SimulationManager references into DebugTab.
RightFlap (src/ui/panels/RightFlap.gd)¶
Extends FlapBase. Right-specific content:
- 6 TABS — Inventory, Power, Analytics, Production, Contributions, Log.
- Layout — strip at
viewport_width - 36, content opens to the left. Resize handle on left edge of content panel. - Arrow direction — ▶ (stage 0), ◀ (stage 1), ◀◀ (stage 2).
- Default stage — 2 (open). Written to prefs, remembered across sessions.
- Attention visuals (simplified) — RightFlap overrides
_update_tab_attention_tween,_refresh_attention_visuals,_start/stop_handle_pulse,_start/stop_handle_bounce,_animate_transition, and_apply_stage_immediateto preserve its original simpler behaviour: alpha-only icon pulse (no colour flash), alpha-only handle pulse, level-2-only handle bounce (6px left), no stage-0 alert tab expansion. This will be upgraded to full LeftFlap-style alerts in a later milestone.
Attention Animation System (LeftFlap)¶
Stage 0 — Collapsed with Alerts¶
When max_level >= 1 in stage 0:
- Strip expands to show only alerted tabs (no black gap — _set_strip_height_for_count(alerted_count)).
- Alerted tab buttons bounce half the strip width (18 px) outward using tween_method + offset_left = offset_right = x translation (not position.x, which is overridden by the layout engine on anchor-layout nodes).
- Handle bounces position.x by 14 px (warning) or 20 px (critical) and springs back, looping.
- Handle colour pulses between neutral and WARNING / CRITICAL colour.
Stage 1 — Strip Visible¶
- Alerted tabs bounce full strip width (36 px) outward.
- Handle animations stopped (tabs are visible; they carry the signal).
Stage 2 — Full Panel¶
- No bouncing. Icon colour pulse only (WARNING/CRITICAL flash, 0.50 s / 0.28 s period).
Escalation Guard¶
_handle_bounce_level and _handle_pulse_level track the level the running tween was built for. When attention escalates (warn → critical), the tween is killed and rebuilt immediately. The if tween != null: return guard was deliberately replaced with a level-comparison check.
NotificationCenter (src/autoloads/NotificationCenter.gd)¶
Autoload singleton. Tab-id agnostic — any system can alert any tab.
Key facts:
- set_save_attention_thresholds(warn_min, crit_min) — called by SettingsTab when the player adjusts sliders. Values stored and immediately applied.
- _load_save_preferences() — always initialises defaults first (warn_seconds, crit_seconds), then overrides from ui_prefs.cfg if the file exists. Fixes the prior bug where a missing prefs file left thresholds at 0. Debug defaults: 1 min warn / 2 min crit. Release defaults: 20 min / 40 min.
- Evaluation Timer fires every 15 seconds (was 60 s — too coarse for 1-minute thresholds).
- Emits tab_level_changed(tab_id: String, level: int) — LeftFlap and RightFlap connect to this signal.
- get_levels_snapshot() -> Dictionary — returns full tab_id → level map for init-time sync.
SaveTab (src/ui/panels/tabs/SaveTab.gd)¶
- Displays Session time (
HH:MM:SScounting from session start) and Last saved (HH:MM:SS ago). - Both clocks are initialised to the same
Time.get_unix_time_from_system()epoch in_ready(), keeping them synchronised with NotificationCenter's session-start reference. - Uses a 1-second autostart Timer (not
_process) to call_update_labels()— avoids per-frame processing for a label that only needs second-level accuracy. _on_save_completed(slot)— connected toSaveManager.save_completed; updates_last_save_timeand sets_has_saved = true(removes the "not yet" suffix).
Preferences File (user://ui_prefs.cfg)¶
Single ConfigFile used by both flaps and NotificationCenter. Keys written:
| Key | Written by |
|---|---|
ui/left_flap_stage |
LeftFlap |
ui/left_flap_tab |
LeftFlap |
ui/left_flap_width |
LeftFlap |
ui/right_flap_stage |
RightFlap |
ui/right_flap_tab |
RightFlap |
ui/right_flap_width |
RightFlap |
ui/save_warn_minutes |
NotificationCenter (written by SettingsTab via threshold signal) |
ui/save_crit_minutes |
NotificationCenter |
Asset Pipeline¶
(Unchanged from v0.1)
Performance Targets & Constraints¶
(Unchanged from v0.1)
Development Environment¶
(Unchanged from v0.1)
Phase-by-Phase Technical Scope¶
(Unchanged from v0.1)
Technical Risk Register¶
(Unchanged from v0.1)
Open Technical Questions¶
- What is the exact grid cell size in world units? → 1.0 world units per cell (resolved in M1.1)
- Should belt items have actual physics? → Visual-only (resolved in M1.3)
- What is the simulation tick rate? → 20 TPS (resolved in M1.2)
- Orthographic vs. perspective camera? (Test both in prototype)
- How are multiplayer sessions discovered? (Manual IP in Phase 1, Steam lobbies in Phase 3)
- How does the game handle host migration if the host disconnects?
- What Godot 4 version to pin for development start?
Revision History¶
| Version | Date | Changes |
|---|---|---|
| 0.1 | 2026-02-22 | Initial draft |
| 0.2 | 2026-03-13 | M2.3 additions: PipeSystem/PipeNetwork/PipeSegment/PipeConnector architecture, pair-equalisation fluid sim, sentinel placement pattern, connection-aware belt visuals, NavigationAgent3D Genesis pathfinding (collision_mask=0, edge-cell approach target), simulation tick phase order updated, save/load extended (genesis task queue, pipes, pipe connectors), null sentinel guard, open questions updated |