Demolish mode and DeconstructionTask #72
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#72
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
Part of milestone #5. Depends on #70.
Buildings.remove()has no caller. Give it one via a demolish mode, driven by a task so a pawn has to go and do it.AC:
DeconstructionTaskat its entrance cell; completion callsBuildings.remove()and emitsbuilding_removedSpec written — tests are on
72-demolish-mode-and-deconstruction-tasktest/unit/test_deconstruction_task.gd(new) and aMARKEDsection intest/unit/test_building_view.gd. Both currently fail to parse, onDeconstructionTaskandBuildingView.State.MARKED— the same shape #70 and #71started in.
Decision 1 — UNDER_CONSTRUCTION cancels immediately
The AC left this open. Specced as the same branch as PLANNED: marking a
half-built site cancels its construction task, removes the record on the spot,
and the labour already spent on it is lost.
The argument is that there is nothing yet to recover. Materials are #73; until a
building consumes them a part-built site holds nothing but spent pawn-ticks, and
no deconstruction task can hand those back — it would just spend more labour
reclaiming nothing. It would also create a fourth building state (under
construction and marked) that
ConstructionTask._can_start(),BuildingViewand the work loop would each have to learn.
Worth revisiting when #73 lands: delivered materials are worth reclaiming, and
at that point a started site belongs on the BUILT branch. The tests for it are
grouped under one heading so that change is one section, not a scatter.
Decision 2 — marking is a flag, not a
Building.StatusBuilding.marked_for_demolition: bool, alongsideconstruction_blocked. A markedbuilding is still BUILT: it stands, it blocks navigation, it counts against its
type's
build_limit. A fifthStatuswould also be reachable from PLANNED,which Decision 1 says cannot exist.
Like
construction_blockedit redraws the view and emits no signal —building_status_changedis about the record's own lifecycle, and there isnothing here yet worth committing to an event shape for.
Decision 3 — demolition costs work, derived from
build_daysBuildingType.deconstruct_work_ticks(), mirroringbuild_work_ticks(): aratio of the build effort with a floor of one tick. The floor is what stops a
build_days = 0.0building (the Colony Centre) being demolishable the instant apawn arrives. The tests assert the relations — non-zero, and less than the
build effort — not the ratio, so it stays tunable.
Decision 4 — invariant: a building carries at most one live task
It is what lets the registry find a building's task by scanning for its id
without first deciding which kind it wanted, and it is why the PLANNED and
UNDER_CONSTRUCTION branches cancel before doing anything else. Pinned by
test_a_building_never_carries_two_tasks_at_once.Registry API the tests are written against
mark_for_demolitionbranches on status: BUILT/RUINED flags the record andenqueues a
DeconstructionTaskatbuilding.entrance; PLANNED andUNDER_CONSTRUCTION cancel the construction task, which already removes the record
through
ConstructionTask.on_cancel().unmark_for_demolitioncancels the deconstruction task;DeconstructionTask.on_cancel()clears the flag viaset_marked_for_demolition,which does not touch tasks — that is what stops the two recursing into each
other. Same trap
test_cancelling_does_not_recurse_through_removalcovers onelevel along.
UI: the ButtonGroup already enforces the arbitration
Rather than a new controller fighting
PlacementControllerfor the click: add aDemolish toggle to
BuildingPanel's existingbutton_group. Selecting itunpresses any building card (and vice versa) for free, and the card's
toggled(false)already emitsbuilding_type_selected(null), which cancelsplacement. A
GameState.demolish_modebool with a signal, mirroringdebug_mode, carries it to whatever handles the click.One trap:
BuildingPanel._cancel_placement()usesset_pressed_no_signal(false),which will unpress the demolish toggle without clearing
GameState.demolish_mode.Right-click already routes through
PlacementController.cancel_placement(), sothat path has to clear the flag explicitly.
Also in scope, small
place()setsbuild_progress = 1.0on thebuild_days == 0.0branch. Itcurrently leaves it at 0.0, so the Colony Centre claims to be 0% built for its
whole life — nothing read the field until #72 came to render a BUILT building
from it. Covered by
test_a_building_that_lands_built_reads_as_fully_built;drop it if you would rather keep it out of this branch.
Revised AC
building marks it for removal, with visual feedback that it is marked
DeconstructionTaskat the entrance cell; completioncalls
Buildings.remove()and emitsbuilding_removeddeconstruction task
building's normal appearance
build_limitTaskManagerat allcurrently red
Clicking a building picked the wrong thing, and it wasn't a timing bug
Reported as "I can mark a building but I can almost never cancel it". The cause
is measurable and had nothing to do with pawns or task timing.
Building views had no collider, so
DemolishControllerraycast the terrainand asked
building_at()which building stood on the cell that was hit. Under apitched 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, against the shipped catalogue:
colony_rocketsensor_towerore_minewarehouseSo 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 body you just lit up orange — was close to
impossible.
Fix
Buildings._spawn_view()gives each view aStaticBody3D+BoxShape3Dsizedfrom the mesh AABB, on its own collision layer (2, now named
buildings),carrying the building id as metadata. Parented to the view, so it rotates with
it and is freed with it.
Buildings.pick(camera, screen_position) -> Buildingraycasts masked to thatlayer. The layer and the meta key stay private to the registry.
DemolishControllercalls it and no longer needsmapat all.PlacementControllerdeliberately keeps picking the terrain cell — forplacing, "the cell under the cursor" is the correct answer.
This also fixes clicking a building whose roof overhangs a neighbour, and it is
the same mechanism #78 wants for pawn picking, so that issue can call
pick()for buildings and add its own question for pawns rather than inventing a second
scheme.
Verification
test/unit/test_building_picking.gd— 6 tests, including one that loops everytype in the catalogue and aims at its own mesh centre, so a tall building added
later cannot quietly reintroduce the offset.
The unit tests compute a screen point directly; they cannot exercise the real
hover path because
warp_mouse()is a no-op headless. So this was also checkedheaded:
tools/probe_demolish_click.gdputs the cursor on a 14-level colonyrocket's body, clicks twice, and reports marked then unmarked, saving a
screenshot either side. The marked shot shows the orange tint with the shadow
dropped; the unmarked shot shows it back to its own colour with the shadow
restored.
Also on this branch
GameState.interaction_modereplaced the per-mode booleans (InteractionMode{NONE, PLACE, DEMOLISH, SELECT, TERRAIN}), and
building_placement_canceledisgone — it and the mode change always fired together.
BuildingPanelnow followsthe mode rather than keeping its own copy, so a mode ended by right-click no
longer leaves the button lit.
One Godot trap worth recording, pinned by
test/unit/test_interaction_mode.gd:pressing Demolish while a card is selected emits the outgoing button's
toggled(false)before the incomingtoggled(true), so the mode is brieflyNONE between them. Writing to the buttons at that moment cancels the press
ButtonGroup is performing, and you end up with no button pressed and no mode set.