Godot port is non-playable: missing in-mission UI, map 4, ghostcell/aegis research, playbook callsites, and team-broadcast binding #23

Open
opened 2026-10-04 04:00:54 +00:00 by Mark · 0 comments
Owner

Context

README.md lines 206–218 describe the Godot port as system-for-system parity
with the web prototype: "TimeDirector = semi-realtime clock + command phases +
snap-focus events, unit.gd = order queue + stances, playbook_library.gd,
abilities.gd, grid_map.gd = AStarGrid2D + Bresenham LOS, scenery.gd".
The verification line says "Verified on import with Godot 4.7.2 headless
(--import compiles all scripts clean; a 120-frame headless run of the main
scene raises no runtime errors)."

That bar — compiles + 120 frames without runtime errors — is true. It is also
the only verification. Open godot/scenes/main.tscn in the editor and
DEPLOY from the title screen: the mission starts, units spawn on the grid,
the hostiles patrol, abilities fire on timers, and either the squad gets wiped
or all hostiles die. Nothing else happens. The README's claim of parity
does not match what the port actually does.

AGENTS.md line 9 calls make ci the contract; make ci runs
tools/godot_import_check.sh, which only verifies scripts compile (parse +
import). There is no Godot-side runtime test that exercises the title →
mission → objective flow with assertions on player inputs.

This issue enumerates the unimplemented surfaces in the Godot port. The
parallel web build is the reference contract; closing a gap here means
mirroring the web feature end-to-end (not just adding lines to a function).

What's unimplemented

1. In-mission UI is empty

godot/scenes/main.tscn (the entire scene — 9 lines):

[gd_scene load_steps=2 format=3 uid="uid://cybertacticsmain"]
[ext_resource type="Script" path="res://scripts/main.gd" id="1"]
[node name="Main" type="Node2D"]
[node name="Camera2D" type="Camera2D" parent="."]
[node name="UI" type="CanvasLayer" parent="."]

The UI CanvasLayer has no children. In a running Godot session you can:

  • See the grid render (TacGrid/TileMap from grid_map.gd)
  • See squad + enemy units (Unit2D from unit.gd)
  • Have air/drop/smoke fire from abilities.gd if pre-targeted
    (no targeting UI exists either)
  • Watch the mission end when squad-wiped or all-hostiles-dead via the bare
    poll loop in main._process (lines 621–636)

What is not yet implemented:

  • Click a unit to select it (unit.gd has no click handler, no
    selection state, no UI element that knows what selected is)
  • Click a tile to queue a move/attack/interact order
  • Issue any of the 6 playbooks (no PlaybookLibrary.apply() callsite)
  • Change unit stance (no buttons, no keybinding)
  • Select a strike team for broadcast (F1/F2/F3 set selected_team but no
    consumer reads it — see #5)
  • See the pause/1×/2× topbar (the web build's topbar in
    web/index.html:9–19 has no Godot counterpart)
  • Save mid-mission (no saveMission/loadMission; user:// ConfigFile
    is read once at _ready and never written outside set_value calls
    for shards/roster/research, which is title-screen state only)
  • See the squad panel, order queue, support cooldowns, or log

The web build's full battle-UI contract is captured in issue #22 (filed
just before this); closing #1 here means porting that contract.

2. Map 4 (Supply Cache / repeatable shard raid) is not in the Godot campaign

web/js/maps.js has 4 missions. Map 3 is "Supply Cache" with
"skirmish": true (line 714). tools/research.js test 5 (line 49) asserts:

assert(MAPS.length === 4 && MAPS[3].skirmish === true, "4th mission is the repeatable shard raid")

This is the strategic endgame loop — the only way to keep playing after the
campaign chain is cleared. web/js/ui.js showStrategic() (line 534) tags
it SHARD RAID ↻ and exposes it on the sector map.

godot/scripts/maps_data.gd has only 3 maps (const MAPS: Array = [...]
ends at The Foundry). The strategic layer in the Godot port has no
repeatable node. Not yet ported — the data exists on the web side, this
is a transcription job plus a strategic-view binding.

3. Two research nodes are missing from the Godot port

web/js/data.js RESEARCH has 10 nodes. godot/scripts/main.gd RESEARCH
has 8 — the port omits the two capability unlocks:

ID Name Cost Effect Why it matters
ghostcell Ghost Cell Dispersal 3 (req: recon1) Unlocks SMOKE DROP support SUPPORT.smoke in data.js:64
aegis Aegis Doctrine 3 (req: medtech1) Unlocks PHALANX playbook PLAYBOOKS.phalanx in data.js:56

The downstream code is already in place:

  • abilities.gd:10 already wires &"smoke" as a support kind.
  • main.gd:292 already reads _research_own("ghostcell") to gate the use
    count.
  • playbook_library.gd:32 already implements &"phalanx".

What's missing is the two RESEARCH constant entries themselves — without
them, the player can't purchase the unlock via the title screen, and the
gated features stay locked at zero uses / never-called.

This is not yet ported — same data on the web side, transcription job.

4. Playbook UI is still missing in-mission

godot/scripts/playbook_library.gd is a full 1:1 port of the web build's
playbook table (breach, overwatch, pincer, hunt, fallback,
phalanx). The library is callable today:

PlaybookLibrary.apply(&"breach", squad_array, target_tile)

There is no callsite in main.gd because there are no playbook buttons in
the in-mission UI (see #1). The web build's analog is
web/js/game.js applyPlaybook() lines 718–767, invoked from the UI's
#playbooks button row.

This is not yet implemented — the function exists and is correct, but
the callsite (a button or hotkey + target resolver) needs to be added once
#1 lands.

5. Strike-team broadcast is not yet implemented

main.gd:230 declares var selected_team := "all". The F1/F2/F3 handler
(lines 644–650) updates it. Nothing in main.gd reads it.

The web build scopes orders via teamMembers(teamId) in
web/js/teams.js:32 and G.selTeam in web/js/game.js:641–649:
select team → RMB issues orders → enqueue for each member of the selected
team → applyPlaybook() enqueues the same formation move for every team
member.

main.gd:239 already has team_move(team_id, target) (formation-broadcast
move) — but it's not bound to a player input. F1/F2/F3 set selected_team
to a value nothing consumes.

This is not yet implemented — the broadcast function exists, the
selection variable exists, the binding between them does not.

6. README's "Verified" line is misleading

Lines 216–218 of README.md: "Verified on import with Godot 4.7.2 headless
(--import compiles all scripts clean; a 120-frame headless run of the main
scene raises no runtime errors)."

This is true. It is also the only runtime check. A 120-frame headless run of
the title screen (no mission started, _process returns early because
state == State.TITLE) will trivially complete. The verification does not
exercise the mission state machine at all. tools/godot_import_check.sh
should probably gate on a deeper smoke before claiming parity — out of scope
for this issue, but worth noting.

Acceptance

Fixing the five gaps above lands a Godot build that:

  • Lets you queue a move/attack/interact by clicking the map (Unit2D click
    handler + TacGrid.world_to_tile() inverse of world())
  • Issues a stance change from a panel button (4 stances, squad or
    selected-team scope)
  • Fires all 6 playbooks via PlaybookLibrary.apply() (scope to team or
    whole squad)
  • Selects ALPHA / BRAVO / ALL via F1/F2/F3, with team-broadcast order flow
  • Wires map 4 (Supply Cache) into the title screen and strategic view
  • Gates SMOKE DROP / PHALANX on ghostcell / aegis research and exposes
    both research nodes for purchase
  • Has a make godot-smoke target that headlessly spawns map 0 with a
    scripted plan and asserts the mission ends — parallel to
    node tools/smoke.js 0 but for the Godot runtime

Suggested order (no sub-issues unless triage approves them)

  1. Map 4 (smallest, fully testable in isolation; proves the
    MapsData.MAPS const can be re-extended without breaking the title
    screen).
  2. ghostcell + aegis research (one-line additions to the
    RESEARCH constant; exposes the gated capabilities).
  3. In-mission UI (the big one — likely a ui/canvas_layer.tscn with
    the 4-region layout from issue #22). Without this, none of the rest is
    reachable.
  4. Playbook callsites (one apply() call from the playbook button
    press; close-coupled to #3).
  5. Team broadcast for F1/F2/F3 (closes the "selected_team is set but
    unread" loop; needs the team-broadcast order flow).
  6. Godot headless smoke target (lands last; depends on the rest being
    wired).

Out of scope

  • Renaming TacGrid back to GridMap — AGENTS.md:48 rules this out.
  • Multiplayer / Mark/veilwalkers integration — AGENTS.md:50 rules it out.
  • Bundler for web/ — AGENTS.md:53 rules it out.
  • Visual fidelity pass on the Godot side (placeholder rectangles per
    README.md:211).

Refs

  • godot/scenes/main.tscn (entire file, 9 lines)
  • godot/scripts/main.gd team_move() line 239, _unhandled_input() lines
    639–652, _process() lines 621–636
  • godot/scripts/maps_data.gd (entire file, ends at The Foundry)
  • godot/scripts/playbook_library.gd (entire file, callable but uncalled)
  • godot/scripts/abilities.gd smoke support already wired at line 10,
    gated correctly in main.gd:292 but research node missing
  • Web side contracts: web/js/game.js teamMembers() via teams.js:32,
    web/js/ui.js showStrategic() line 517, web/js/data.js RESEARCH
    lines 117–127, web/js/maps.js map 3 lines 711–902
  • Closed issues that touched this surface: #1 (TileMap rendering — landed),
    #2 (export presets — landed), #4 (headless import gate — landed but
    surface-only)
## Context `README.md` lines 206–218 describe the Godot port as system-for-system parity with the web prototype: "TimeDirector = semi-realtime clock + command phases + snap-focus events, unit.gd = order queue + stances, playbook_library.gd, abilities.gd, grid_map.gd = AStarGrid2D + Bresenham LOS, scenery.gd". The verification line says "Verified on import with Godot 4.7.2 headless (`--import` compiles all scripts clean; a 120-frame headless run of the main scene raises no runtime errors)." That bar — compiles + 120 frames without runtime errors — is true. It is also the **only** verification. Open `godot/scenes/main.tscn` in the editor and `DEPLOY` from the title screen: the mission starts, units spawn on the grid, the hostiles patrol, abilities fire on timers, and either the squad gets wiped or all hostiles die. **Nothing else happens.** The README's claim of parity does not match what the port actually does. `AGENTS.md` line 9 calls `make ci` the contract; `make ci` runs `tools/godot_import_check.sh`, which only verifies scripts compile (parse + import). There is no Godot-side runtime test that exercises the title → mission → objective flow with assertions on player inputs. This issue enumerates the **unimplemented** surfaces in the Godot port. The parallel web build is the reference contract; closing a gap here means mirroring the web feature end-to-end (not just adding lines to a function). ## What's unimplemented ### 1. In-mission UI is empty `godot/scenes/main.tscn` (the entire scene — 9 lines): ``` [gd_scene load_steps=2 format=3 uid="uid://cybertacticsmain"] [ext_resource type="Script" path="res://scripts/main.gd" id="1"] [node name="Main" type="Node2D"] [node name="Camera2D" type="Camera2D" parent="."] [node name="UI" type="CanvasLayer" parent="."] ``` The `UI` CanvasLayer has no children. In a running Godot session you can: - See the grid render (TacGrid/TileMap from `grid_map.gd`) - See squad + enemy units (Unit2D from `unit.gd`) - Have air/drop/smoke fire from `abilities.gd` if pre-targeted (no targeting UI exists either) - Watch the mission end when squad-wiped or all-hostiles-dead via the bare poll loop in `main._process` (lines 621–636) What is **not yet implemented**: - Click a unit to select it (`unit.gd` has no click handler, no `selection` state, no UI element that knows what `selected` is) - Click a tile to queue a move/attack/interact order - Issue any of the 6 playbooks (no `PlaybookLibrary.apply()` callsite) - Change unit stance (no buttons, no keybinding) - Select a strike team for broadcast (F1/F2/F3 set `selected_team` but no consumer reads it — see #5) - See the pause/1×/2× topbar (the web build's topbar in `web/index.html:9–19` has no Godot counterpart) - Save mid-mission (no `saveMission`/`loadMission`; `user://` ConfigFile is read once at `_ready` and never written outside `set_value` calls for shards/roster/research, which is title-screen state only) - See the squad panel, order queue, support cooldowns, or log The web build's full battle-UI contract is captured in issue #22 (filed just before this); closing #1 here means porting that contract. ### 2. Map 4 (Supply Cache / repeatable shard raid) is not in the Godot campaign `web/js/maps.js` has 4 missions. Map 3 is `"Supply Cache"` with `"skirmish": true` (line 714). `tools/research.js` test 5 (line 49) asserts: ``` assert(MAPS.length === 4 && MAPS[3].skirmish === true, "4th mission is the repeatable shard raid") ``` This is the strategic endgame loop — the only way to keep playing after the campaign chain is cleared. `web/js/ui.js` `showStrategic()` (line 534) tags it `SHARD RAID ↻` and exposes it on the sector map. `godot/scripts/maps_data.gd` has only 3 maps (`const MAPS: Array = [...]` ends at The Foundry). The strategic layer in the Godot port has no repeatable node. **Not yet ported** — the data exists on the web side, this is a transcription job plus a strategic-view binding. ### 3. Two research nodes are missing from the Godot port `web/js/data.js` `RESEARCH` has 10 nodes. `godot/scripts/main.gd` `RESEARCH` has 8 — the port omits the two **capability** unlocks: | ID | Name | Cost | Effect | Why it matters | |---|---|---|---|---| | `ghostcell` | Ghost Cell Dispersal | 3 (req: recon1) | Unlocks SMOKE DROP support | `SUPPORT.smoke` in `data.js:64` | | `aegis` | Aegis Doctrine | 3 (req: medtech1) | Unlocks PHALANX playbook | `PLAYBOOKS.phalanx` in `data.js:56` | The downstream code is already in place: - `abilities.gd:10` already wires `&"smoke"` as a support kind. - `main.gd:292` already reads `_research_own("ghostcell")` to gate the use count. - `playbook_library.gd:32` already implements `&"phalanx"`. What's missing is the two `RESEARCH` constant entries themselves — without them, the player can't purchase the unlock via the title screen, and the gated features stay locked at zero uses / never-called. This is **not yet ported** — same data on the web side, transcription job. ### 4. Playbook UI is still missing in-mission `godot/scripts/playbook_library.gd` is a full 1:1 port of the web build's playbook table (`breach`, `overwatch`, `pincer`, `hunt`, `fallback`, `phalanx`). The library is callable today: ```gdscript PlaybookLibrary.apply(&"breach", squad_array, target_tile) ``` There is no callsite in `main.gd` because there are no playbook buttons in the in-mission UI (see #1). The web build's analog is `web/js/game.js applyPlaybook()` lines 718–767, invoked from the UI's `#playbooks` button row. This is **not yet implemented** — the function exists and is correct, but the callsite (a button or hotkey + target resolver) needs to be added once #1 lands. ### 5. Strike-team broadcast is not yet implemented `main.gd:230` declares `var selected_team := "all"`. The F1/F2/F3 handler (lines 644–650) updates it. Nothing in `main.gd` reads it. The web build scopes orders via `teamMembers(teamId)` in `web/js/teams.js:32` and `G.selTeam` in `web/js/game.js:641–649`: select team → RMB issues orders → enqueue for each member of the selected team → `applyPlaybook()` enqueues the same formation move for every team member. `main.gd:239` already has `team_move(team_id, target)` (formation-broadcast move) — but it's not bound to a player input. F1/F2/F3 set `selected_team` to a value nothing consumes. This is **not yet implemented** — the broadcast function exists, the selection variable exists, the binding between them does not. ### 6. README's "Verified" line is misleading Lines 216–218 of README.md: "Verified on import with Godot 4.7.2 headless (`--import` compiles all scripts clean; a 120-frame headless run of the main scene raises no runtime errors)." This is true. It is also the only runtime check. A 120-frame headless run of the title screen (no mission started, `_process` returns early because `state == State.TITLE`) will trivially complete. The verification does not exercise the mission state machine at all. `tools/godot_import_check.sh` should probably gate on a deeper smoke before claiming parity — out of scope for this issue, but worth noting. ## Acceptance Fixing the five gaps above lands a Godot build that: - Lets you queue a move/attack/interact by clicking the map (Unit2D click handler + `TacGrid.world_to_tile()` inverse of `world()`) - Issues a stance change from a panel button (4 stances, squad or selected-team scope) - Fires all 6 playbooks via `PlaybookLibrary.apply()` (scope to team or whole squad) - Selects ALPHA / BRAVO / ALL via F1/F2/F3, with team-broadcast order flow - Wires map 4 (Supply Cache) into the title screen and strategic view - Gates SMOKE DROP / PHALANX on `ghostcell` / `aegis` research and exposes both research nodes for purchase - Has a `make godot-smoke` target that headlessly spawns map 0 with a scripted plan and asserts the mission ends — parallel to `node tools/smoke.js 0` but for the Godot runtime ## Suggested order (no sub-issues unless triage approves them) 1. **Map 4** (smallest, fully testable in isolation; proves the `MapsData.MAPS` const can be re-extended without breaking the title screen). 2. **`ghostcell` + `aegis` research** (one-line additions to the `RESEARCH` constant; exposes the gated capabilities). 3. **In-mission UI** (the big one — likely a `ui/canvas_layer.tscn` with the 4-region layout from issue #22). Without this, none of the rest is reachable. 4. **Playbook callsites** (one `apply()` call from the playbook button press; close-coupled to #3). 5. **Team broadcast for F1/F2/F3** (closes the "selected_team is set but unread" loop; needs the team-broadcast order flow). 6. **Godot headless smoke target** (lands last; depends on the rest being wired). ## Out of scope - Renaming `TacGrid` back to `GridMap` — `AGENTS.md:48` rules this out. - Multiplayer / `Mark/veilwalkers` integration — `AGENTS.md:50` rules it out. - Bundler for `web/` — `AGENTS.md:53` rules it out. - Visual fidelity pass on the Godot side (placeholder rectangles per `README.md:211`). ## Refs - `godot/scenes/main.tscn` (entire file, 9 lines) - `godot/scripts/main.gd` `team_move()` line 239, `_unhandled_input()` lines 639–652, `_process()` lines 621–636 - `godot/scripts/maps_data.gd` (entire file, ends at The Foundry) - `godot/scripts/playbook_library.gd` (entire file, callable but uncalled) - `godot/scripts/abilities.gd` smoke support already wired at line 10, gated correctly in `main.gd:292` but research node missing - Web side contracts: `web/js/game.js` `teamMembers()` via `teams.js:32`, `web/js/ui.js` `showStrategic()` line 517, `web/js/data.js` `RESEARCH` lines 117–127, `web/js/maps.js` map 3 lines 711–902 - Closed issues that touched this surface: #1 (TileMap rendering — landed), #2 (export presets — landed), #4 (headless import gate — landed but surface-only) <!-- forgejo-id: Mark/cybertactics#23 -->
Mark changed title from Godot port is non-playable: no in-mission UI, missing map 4, missing ghostcell/aegis research, dead playbook library, write-only team selection to Godot port is non-playable: missing in-mission UI, map 4, ghostcell/aegis research, playbook callsites, and team-broadcast binding 2026-10-04 04:02:17 +00:00
Sign in to join this conversation.
No description provided.