Skip to content

Weekly tech debt audit: windowstead - 2026-07-01 #225

Description

@itsmiso-ai

Weekly Tech Debt Audit — misospace/windowstead

Date: 2026-07-01
Overall Risk Level: Low-Medium

Summary

Windowstead is a well-structured Godot 4.2 desktop companion game that has been carefully maintained with a recent push to extract pure-data modules from the monolithic main.gd. The extraction is mid-progress — goal_progression.gd, goal_reward.gd, rotating_goal.gd, colony_stance.gd, worker_cap_logic.gd, layout_math.gd, constants.gd, and game_state.gd have been split out successfully. However, main.gd remains at 2622 lines and still contains significant logic that should move to dedicated modules. Test coverage is good with 15+ test files, but some tests duplicate effort and several game-tick code paths lack direct verification.


Top Findings

P0 — Critical

  1. Monolithic main.gd (2622 lines) — mid-extraction with high coupling

    • Evidence: main.gd still contains worker_texture() (pixel rendering), apply_anchor_layout() (full UI tree wiring), choose_task() (AI logic), _process() (edge snapping + overlay rendering), and push_event() mixed alongside game-state mutations.
    • Risk: Every new feature requires touching this file. Testability suffers because half the logic is coupled to scene node references ( @onready var nodes, %BuildButtons, etc.).
    • File: scripts/main.gd
  2. Worker texture rendering is pixel-pushing at runtime

    • Evidence: worker_texture() in main.gd creates a new Image object per worker per frame (12x14 RGBA8 image), fills it pixel-by-pixel, and creates an ImageTexture. Cached by worker_texture_cache keyed on name+frame+carrying, but _process calls render_worker_overlay() every frame.
    • Risk: CPU cost per frame grows with worker count. Each texture is tiny, but the Image.create() + pixel fill + ImageTexture.create_from_image() path is not GPU-friendly.
    • File: scripts/main.gd lines ~2000-2100
  3. Edge-snapping rechecks apply_dock_position on every process frame (with cooldown)

    • Evidence: _process() re-runs apply_dock_position() every DOCK_RECHECK_COOLDOWN (0.5s). This calls DisplayServer.window_set_size(), DisplayServer.window_set_position(), and recalculates all tile metrics.
    • Risk: Unnecessary display server calls even when nothing changed (only current_usable != _last_usable_rect check guards the worst case, but calling it every 0.5s when idle still adds overhead).
    • File: scripts/main.gd

P1 — High

  1. Stance food-bias logic duplicated between main.gd and colony_stance.gd

    • Evidence: choose_task() in main.gd has two separate food-sorting branches: one for "gather" with should_bias_to_food_gathering() and another for "gather_food" with ColonyStance.is_food_gather_task(). Both do virtually the same food-prioritization sort but via different code paths. The ColonyStance module already defines the food stance — the extra bias in choose_task() for "gather" kind duplicates this intent.
    • File: scripts/main.gd, scripts/colony_stance.gd
  2. Save schema validation is thorough but not versioned against actual state shape

    • Evidence: validate_save_schema() in game_state.gd checks field types and bounds but does not validate that active_goal, completed_goal_ids, active_rewards, colony_stance have valid values or that the state dictionary is internally consistent (e.g., tile count matches grid_w * grid_h). The expected_sizes array hard-codes specific grid dimensions but doesn't derive them from anchor family logic.
    • File: scripts/game_state.gd
  3. No integration test for the _on_tick full cycle

    • Evidence: test_e2e.gd simulates ticks by incrementing counters manually. _on_tick() in main.gd calls maybe_fire_event(), _clean_stale_reservations(), apply_food_upkeep(), choose_task() per worker, GoalProgression.process_tick(), GoalReward.tick_rewards(), persist(), and render_all(). None of the test files instantiate a full Control and run _on_tick() with a real timer. The e2e tests test state persistence, not real-time tick orchestration.
    • Files: tests/test_e2e.gd, scripts/main.gd
  4. No tests for worker_cap_logic.gd integration with recruit_worker

    • Evidence: worker_cap_logic.gd has pure calculate_worker_cap() and can_recruit() but tests/test_worker_cap.gd tests the module in isolation. The actual recruit_worker() in main.gd uses can_recruit_worker() which calls get_worker_cap() — a slightly different implementation that iterates state.builds directly. This is a parallel implementation of the same logic.
    • Files: scripts/worker_cap_logic.gd, scripts/main.gd

P2 — Medium

  1. Duplicate worker cap calculation logic

    • Evidence: worker_cap_logic.gd:calculate_worker_cap() computes cap from a builds array. main.gd:get_worker_cap() computes the same formula iterating state.builds. Two implementations of the same logic — test drift risk.
    • Files: scripts/worker_cap_logic.gd, scripts/main.gd
  2. recruit_worker doesn't use worker_cap_logic module

    • Evidence: recruit_worker() calls get_worker_cap() which is a local main.gd method, not the extracted worker_cap_logic.calculate_worker_cap(). The extracted module was clearly intended to replace this.
    • Files: scripts/main.gd, scripts/worker_cap_logic.gd
  3. constant WORKER_NAMES has only 2 entries — cycles early

    • Evidence: WORKER_NAMES := ["Jun", "Mara"]. With BASE_WORKER_CAP=2 and huts adding +2 each, even with one hut the cap is 4. Workers cycle through 2 names (Jun, Mara, Jun, Mara). No collision handling for same-named workers.
    • File: scripts/constants.gd
  4. Event log bounded to 8 entries — events push_front and pop_back

    • Evidence: push_event() pushes front and pops back when size > 8. The event drawer shows last 6. This means 2 events are hidden between the collapsed label and the expanded log. The game loses event history rapidly — 8 events is very tight.
    • File: scripts/main.gd
  5. Reservation system is not persisted

    • Evidence: reserve_resource() / release_resource() mutate state.reserved_resources but persist() does not include it in the save. On load, reservations reset to 0, which can cause two workers to double-book on the same resource tile.
    • File: scripts/main.gd
  6. Builder do_build progress uses structure_build_speed without reservation check

    • Evidence: Two workers can both be assigned to build the same foundation. do_build() increments progress independently for each. If both have the build task, progress advances at double speed. No mutex or worker-cap-per-build limit exists.
    • File: scripts/main.gd
  7. tile_accent uses pending_build_kind hover logic with floating-panel references

    • Evidence: tile_accent() accesses pending_build_kind and hover_tile_index — both mutable state interleaved with rendering. This is called from tile_style() which is called from render_world(). This coupling makes testing tile rendering without scene setup impossible.
    • File: scripts/main.gd
  8. Tests import GameState as Node but use use_local_storage = false hack

    • Evidence: Every test file creates var gs = load_game_state()gs_script.new() → sets gs.use_local_storage = false. This is a test-time workaround for the JavaScriptBridge reference in _ready(). If _ready() runs, it will crash without OS.has_feature("web").
    • Files: All test files
  9. export_presets.cfg not exported via CI

    • Evidence: The release.yml workflow does not reference export presets. There is no pck/export build step in CI. The game only tests via --script headless mode, never verifies the export configuration works.
    • Files: .github/workflows/release.yml, export_presets.cfg
  10. MILESTONE_CATALOG in milestone_manager.gd is defined but never called from main.gd

    • Evidence: MilestoneManager class is fully defined at scripts/milestone_manager.gd but main.gd has zero references to it. No import, no preload, no usage in bootstrap_state() or _on_tick(). The milestone system is dead code.
    • File: scripts/milestone_manager.gd

Recommended Issue Breakdown

  1. P1 — Extract remaining main.gd subsystems into modules (choose_task AI, render_*, worker_texture, _process edge snap)
  2. P1 — Deduplicate worker cap logic: make main.gd:get_worker_cap() delegate to worker_cap_logic.gd
  3. P1 — Deduplicate food-bias sort logic: unify gather/gather_food sort paths in choose_task
  4. P2 — Persist reserved_resources in save data to prevent double-booking across reloads
  5. P2 — Add worker-per-build cap to prevent double-speed construction
  6. P2 — Expand WORKER_NAMES or add unique seed to worker names
  7. P2 — Increase event log capacity (8→20+)
  8. P2 — Add full-cycle integration test for _on_tick with real timer mock
  9. P2 — Wire MilestoneManager into main.gd bootstrap_state() or remove dead code
  10. P3 — Move tile_accent / tile_style rendering helpers into a render_module.gd
  11. P3 — Derive expected tile grid sizes from LayoutMath in save schema validation instead of hard-coded list
  12. P3 — Remove use_local_storage = false test workaround with proper DI or sandbox
  13. P3 — Add export build step to CI

Not Worth Doing Yet

  • Full pixel-art asset pipeline: The runtime pixel-pushed textures are charming and part of the aesthetic. An actual sprite system would be a major refactor for a desktop-companion game. Keep as-is.
  • Network/web integration: Godot 4.2's web export is experimental and the game has no multiplayer use case. The JavaScriptBridge code is minimal.
  • Save encryption/obfuscation: The game saves plain JSON to user://. For a single-player desktop companion, this is fine.
  • Phaser-like state machine for workers: The current task→step→complete pattern is simple and works. A full FSM would be overengineering.
  • UI toolkit abstraction (MVC): Godot's scene tree IS the view. Extracting a separate view layer adds complexity without benefit for this scale.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions