Demolish mode and DeconstructionTask (#72) #122
No reviewers
Labels
No labels
assets
bug
duplicate
enhancement
help wanted
invalid
question
testing
ui
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
liamjd/UntitledColonyBuilder!122
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "72-demolish-mode-and-deconstruction-task"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #72
Gives
Buildings.remove()its caller. You mark a building in a new demolish mode. Depending on its status, the mark either withdraws the building on the spot or queues aDeconstructionTaskfor a pawn to go and do.What marking does, by status
marked_for_demolition) and queues aDeconstructionTaskat the entrance cell. When a pawn completes it,remove()runs andbuilding_removedis emittedClicking a marked building again unmarks it. That cancels the deconstruction task and restores the building's normal appearance.
Decisions
UNDER_CONSTRUCTION cancels immediately (asked for in the AC). Marking a half-built site cancels its construction task and removes it on the spot. The labour already spent on it is lost. There is nothing to recover yet, because materials arrive with #73. Until a building consumes materials, a part-built site holds only spent pawn-ticks, and a deconstruction task would spend more labour reclaiming nothing. Queueing one would also create a fourth building state, under construction and marked, which the task gate, the renderer and the work loop would each have to learn. Revisit when #73 lands. Delivered materials are worth reclaiming, and at that point a started site belongs on the BUILT branch. The tests for this branch are grouped under one heading so that change stays in one place.
The mark is a flag, not a
Building.Status. A marked building is still BUILT: it stands, it blocks navigation, and it counts against its type'sbuild_limit. Likeconstruction_blocked, setting the flag redraws the view and emits no signal.Demolition costs work.
BuildingType.deconstruct_work_ticks()derives it frombuild_days, with a floor, so that abuild_days = 0.0building is not demolished the moment a pawn arrives.A building carries at most one live task at a time. That is what lets the registry find a building's task by id without first deciding which kind it wanted.
Also in this PR
GameState.interaction_modereplaces the per-mode booleans:InteractionMode {NONE, PLACE, DEMOLISH, SELECT, TERRAIN}. With an enum, two modes can't both be on at once, and controllers read the value rather than caching their own copy.building_placement_canceledis gone, because it always fired alongside the mode change.BuildingPanelnow follows the mode instead of keeping its own copy, so ending demolish mode with a right-click no longer leaves the button lit.Buildings are picked by their own collider, not by the tile under them. Views had no collision shape, so a click raycast the terrain and asked which building stood there. Under the pitched camera, the visible body of a building is drawn offset from its tile, and the offset grows with height:
colony_rocketsensor_towerore_mineThis is why a building could be marked but almost never unmarked. Each view now gets a
StaticBody3Dsized from its mesh AABB, on physics layer 2 (namedbuildings).Buildings.pick(camera, screen_position)raycasts against that layer only.PlacementControllerstill picks the terrain cell on purpose, because that is the right answer for placement.place()now setsbuild_progress = 1.0when a building lands BUILT. Before this, a Colony Centre reported itself as 0% built.A Godot trap worth knowing
Pressing Demolish while a building card is selected makes ButtonGroup emit the outgoing button's
toggled(false)before the incomingtoggled(true), so the mode is briefly NONE in between. If the panel writes to its buttons at that moment, it cancels the press Godot is in the middle of performing. You end up with no button pressed and no mode set._applying_own_togglekeeps the panel's hands off the group during its own transitions, andtest_interaction_mode.gdpins both orderings.Verification
test_deconstruction_task.gd: the three status branches, unmarking, limits, the standalone registrytest_interaction_mode.gd: the mode property, plus the panel's arbitration, run against the real scenetest_building_picking.gd: every catalogue type, aimed at its own mesh centreMARKEDsection intest_building_view.gdtools/probe_demolish_click.gdputs the real cursor on a colony rocket's body and clicks twice. The building was marked, then unmarked. Screenshots taken before and after showed the orange tint with the shadow dropped, then the building back to its own colour with the shadow restored. The probe exists becausewarp_mouse()does nothing headless, so the unit tests can't exercise the real hover path.game.tscn, not committed): a pawn walked to the entrance, worked the 50 ticks, and the building was removed. Its cells were freed and the pawn went back to idle.Known gaps, not addressed here
DeconstructionTask._work()writes demolition progress intobuild_progress. If you unmark a building partway through, it is left BUILT with a partial figure. This is invisible, because BUILT takes no override, but the field is wrong.DeconstructionTask.on_cancel()ispass. Onlyunmark_for_demolitionclears the flag, and it is currently the only caller that cancels a deconstruction task. Any other cancel route, such as #85 or a debug panel, would leave a building marked with no task behind it.colony_rockettakes the BUILT branch like any other building. This is a design decision, not a bug, and should be settled separately.tools/probe_menu_clickthrough.gdnow arms placement through the mode. Its CONTROL step already failed headless before this branch (checked againste5093c5), so that probe gives no valid result headless for unrelated reasons.🤖 Generated with Claude Code
One enum, one property, one signal. Mutual exclusion comes from the type rather than from every controller remembering to clear the others' flags, and controllers read GameState.interaction_mode directly instead of caching a copy that can survive after the mode has ended elsewhere. - GameState: InteractionMode {NONE, PLACE, DEMOLISH, SELECT, TERRAIN} and the interaction_mode property. SELECT and TERRAIN have no controller yet; they are listed so adding one is a case rather than a new flag. - building_placement_canceled is gone. It and the mode change always fired together, and the panel now follows the mode instead. - BuildingPanel drives the mode from its buttons and follows it back, so a mode ended by right-click no longer leaves the button lit. - PlacementController and DemolishController both gate _unhandled_input on the mode. DemolishController is later in game.tscn and unhandled input is delivered bottom-up, so it took first refusal on every click: with stale hover state and no mode check, one click could both mark a building for demolition and swallow the placement it was meant to be. - DemolishController._clear_hover() actually clears. Empty, it let target_building hold the last building hovered indefinitely. - Its right-click cancel waits for the release and measures the travel, and does not mark the event handled - the right button also rotates the camera, which is why PlacementController does the same dance. The ButtonGroup trap this turned up, now pinned by tests: pressing Demolish over a selected card emits the outgoing button's toggled(false) BEFORE the incoming toggled(true), so the mode is briefly NONE between them. Writing to the buttons at that moment cancels the press Godot is performing, which left the panel with no button pressed and the mode at NONE. _applying_own_toggle keeps the panel's hands off the group while the group is mid-transition. tools/probe_menu_clickthrough.gd arms placement through the mode as well as the type, or it would report a blocked click it never actually attempted. Its CONTROL step already failed before this change - verified againste5093c5- so the probe remains void headless for unrelated reasons. Also carries Liam's uncommitted work in progress, swept in by this commit rather than landed separately: the DemolishController node in game.tscn, the demolish button in building_panel.tscn, and the edits to building.gd, buildings.gd, BuildingTypeCard.tscn and placement_rules.gd. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>Building views had no collision shape, so DemolishController raycast the terrain and asked which building stood on the cell that was hit. Under a pitched camera the visible body of a building is drawn offset from the tile it stands on, and the offset grows with height. Aiming at the middle of what the player sees landed here, measured against the shipped catalogue: colony_rocket (14 levels) -> 3 cells away, hit nothing sensor_tower ( 8 levels) -> 2 cells away, hit nothing ore_mine ( 3 levels) -> the right cell warehouse ( 2 levels) -> 1 cell away, saved by a 2-cell footprint So the clickable region was the footprint on the ground near the base, while the thing the player aims at is the body. Marking a tall building was luck; unmarking one - where you aim at the lit-up body you just created - was close to impossible. That is the "I can mark it but I can never cancel it" report, and it was never a timing problem. - Buildings._spawn_view() gives each view a StaticBody3D + BoxShape3D sized from the mesh AABB, on its own collision layer, carrying the building id. It is parented to the view, so it rotates with it and is freed with it. - Buildings.pick(camera, screen_position) raycasts masked to that layer and returns the Building, or null. The layer and the meta key stay private to the registry; #78's shared picker can call this and keep its own question about pawns. - DemolishController uses it and no longer needs the map at all. - project.godot names layer 2 "buildings". PlacementController deliberately keeps picking the terrain cell: for placing a building, "the cell under the cursor" is the right answer. Verified headed as well as by the suite - the unit tests compute a screen point, but warp_mouse() is a no-op headless so they cannot exercise the real hover path. tools/probe_demolish_click.gd puts the cursor on a 14-level colony rocket's body, clicks twice, and reports marked then unmarked, with a screenshot either side. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>