Render PLANNED, BLOCKED and UNDER_CONSTRUCTION buildings distinctly (#71) #121
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!121
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "71-render-planned-building-states"
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 #71.
A placed-but-unbuilt building now looks like a plan rather than a finished structure, and a
stalled one says so.
What it does
BuildingViewis a new class of pure static functions - no nodes, no scene, no map. It ownsthe whole decision table:
Building.Status.PLANNEDconstruction_blockedBuilding.Status.UNDER_CONSTRUCTIONBuilding.Status.BUILT(and RUINED, for now)Buildingsrefreshes the view from the record on every status, blocked or progress change,touching only
material_overrideandcast_shadow- never the mesh, whichBuildingMeshesshares between every building of a type.Blocking arrives from the task queue:
TaskManagergainedtask_blocked/task_unblocked,emitted from
next_for()only when a task's status actually crosses into or out of BLOCKED.Buildings.tasksis now a property whose setter makes the connection, so a registry builtstandalone in a test wires itself the same way
game.gddoes and there is no second place toforget it.
Design decisions
BLOCKED is deliberately not a
Building.Status. It is aTask.Status, and making it abuilding one would deadlock:
ConstructionTask._can_start()gates onstatus in [PLANNED, UNDER_CONSTRUCTION], so a building parked in a building-level BLOCKEDcould never start work again and therefore could never leave that state. Hence a separate
view-state enum derived from the record plus one transient flag.
construction_blockedis a cache, not truth, and is not part of the save format. It ischecked only for the two unbuilt states, so a stale
truecannot paint a finished buildingred; a reload recomputes it the moment a pawn next asks for work.
Opacity tracks progress rather than a flat scaffold colour - the ticket allowed either.
It costs
Building.build_progressand one write per worked tick. Exactly one unit testasserts the fade, so reverting to a flat colour is one deletion plus one line in
material_for().The reason a task is blocked is out of scope - that is #119, which names this PR's view
path as its hook. It widens the bool to a reason enum and will need to teach
state_for()that a task waiting on #73's material children must not read as blocked. Building the hook
as a bool first means #119 changes one function rather than introducing the path.
Two bugs found on the way
ConstructionTaskreported(build_days / work_remaining) * 100.0. Inverted, in thewrong unit (
build_daysis pawn-days,work_remainingis ticks), scaled to a percentagewhere an 0..1 fraction was wanted - and dividing by zero on the last tick of every
construction, since
work_remainingreaches exactly0.0there. Now1.0 - work_remaining / build_work_ticks(), which moves the zero case to the total, where itmeans "not pawn-raised at all" and a building needing no work is already built.
PLANNED's tint was white, which made BLOCKED unreadable. White lifts the red channel as
much as
Color.REDdoes, so the two states stopped being distinguishable by the exact channelmeant to separate them. It is now the placement ghost's pale blue, which also makes "not real
yet" read consistently between the ghost and the plan it becomes.
Verification
Unit tests: 599 passing, 0 failing. The one Risky/Pending is
test_what_a_cancelled_child_should_do_to_its_parent_is_undecided, which predates this branchand is waiting on #85.
Written spec-first, per the usual split: the tests landed red in
a5cd18dbefore anyimplementation existed.
test/unit/test_building_view.gd(new, 22 tests) - the decision table, including that astale blocked flag is ignored on a finished building, that each call returns its own
material, and that
apply()never writes to the shared mesh.test/unit/test_buildings.gd(+14) - the wiring: a status change reaches the mesh withoutrebuilding it, two buildings of one type are tinted independently, and a blocked task marks
and unmarks its building.
test/unit/test_construction_task.gd(+7) - progress reporting, including thedivide-by-zero above.
Visual check: headed,
tools/screenshot_building_states.gd. Draws five of one type in arow - PLANNED, BLOCKED, UNDER_CONSTRUCTION at 25% and 75%, BUILT - driving each through the
registry's own setters, so a correct picture is evidence the whole path works rather than
just that
BuildingViewcan produce a material.That check earned its keep: the alpha floor moved from 0.2 to 0.35 because a plan at 0.2
all but vanished against pale sand, while BLOCKED at the same alpha still read - red carries
against that background and pale blue does not. Not something the headless suite could ever
have reported. In the final image only BUILT casts a shadow, which is the other half of what
the shot is checking.
Caveats and things left undone
next_for()is theonly thing that re-evaluates a task, so with every pawn busy the view can lag arbitrarily.
This is pre-existing -
Task.statusand the debug panel have exactly the same lag - and isnot made worse here. Fixing it means a re-evaluation sweep on a timer; worth its own issue
if it ever bites.
look-and-feel pass.
tools/screenshot_building_states.gdrefers to every project class asNode/Objectthrough
load(), per the-scompile-order trap in CLAUDE.md -BuildingsreachesGameStateandBuildingTypereachesGameCalendar, so one type annotation would breakboth silently and produce a plausible wrong picture. Reasoning is in the file header.
tools/screenshot_buildings.gdnamesBuildingTypedirectly and so is likely broken in exactly that way since
build_work_ticks()landed in#88. Untouched, since it is unrelated to this change.
GameDate's docstring thatwas sitting in the working tree and got swept in by a
git add -A.🤖 Generated with Claude Code