Skip to content

Auto Warz — Technical Architecture Document

Document Type: Technical Architecture Version: 0.3 Status: In Progress Last Updated: 2026-04-13 Vault destination: 02_Technical Documents Author: 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_direction for 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) in setup()
  • NavigationRegion3D is baked via bake_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_process frame

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, two Timer nodes.
  • Stage system — three stages: 0 (collapsed, handle only or handle + alerted tabs), 1 (tab strip visible, no content panel), 2 (full panel open). Set via set_stage(n). Persisted to user://ui_prefs.cfg.
  • Attention map_tab_attention: Dictionary maps tab_id → level (0/1/2). Updated by set_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, connects tab_level_changed signal, calls get_levels_snapshot() on init. 5-second polling Timer as a self-healing fallback.
  • Preferences (_load_preferences / _save_preferences) — reads/writes stage, active tab, panel width from ui_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 forwardingSettingsTab.save_attention_thresholds_changedNotificationCenter.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_immediate to 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:SS counting 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 to SaveManager.save_completed; updates _last_save_time and 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