Left-clicking a valid site places the building #58
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!58
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "54-building-placement-and-model"
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?
Left-clicking a valid site now commits the building: a solid mesh in the type's colour lands on the terrain, the cells it occupies are recorded so nothing else can be built on them, and placement mode ends.
This is the last acceptance criterion of the Building placement milestone's core flow, and it introduces the first piece of persistent colony state — so most of the work below is in the data model rather than the click handling.
Closes #54
What landed
Building(record) andBuildings(registry). The record is the truth and the mesh is one way of drawing it.Buildingstores onlytype,anchor,rotation_stepsandstatus; the cells it occupies are derived (offsets()→cells()) rather than stored, so the registry's_occupancydictionary is a cache of that derivation rather than a second copy that can disagree with it. The record holds no node references, which is what keeps it serialisable for save/load later.Buildings.place(),remove(),building_at(),is_occupied(),occupied_cells()andall().remove()has no caller yet — demolition UI is a separate issue — but it is in the API because it is the thing that proves occupancy is maintained in one place; #56 also depends on it for unblocking A* cells.OCCUPIEDplacement rule. Appended to the end ofReasonso existing ordinals do not shift, andoccupiedis an optionalDictionaryparameter soPlacementRulesstays static and dependency-free and every prior call site still compiles. Checked afterOUT_OF_BOUNDS, beforeWATER.BuildingSite— a static helper owning the site position and rotation maths, called by both the ghost and the placed mesh. This exists specifically so the two cannot drift: if each computed the site independently, the bug would surface as a building that does not land where the ghost promised.Input actions.
place_building(LMB) andcancel_building_placement(RMB), both listed in the keybindings panel.Verification
GUT: 131 tests, 131 passing, no orphans (up from 105 before this branch).
test/unit/test_buildings.gd(new, 19 tests) — record contents, distinct ids, occupancy under rotation, shape-not-bounding-box,building_at()identity across every cell of a multi-tile building, three overlap cases, four removal cases, record/view agreement, no-map operation, and bothbuilding_placedsignal cases.test/unit/test_building_site.gd(new, 10 tests) — tile centring, base-on-surface seating, seating on the highest cell of a straddling footprint, half- and quarter-turn behaviour, anchor translation, offsets-vs-absolute-cells, empty-footprint fallback, rotation sign.test/unit/test_placement_rules.gd(extended, 7 tests) — occupied cell rejected, single-cell overlap rejected, adjacent-but-clear accepted, bounding-box hole ignored, default argument unchanged, and both check-order pins (OUT_OF_BOUNDSbeforeOCCUPIED,OCCUPIEDbeforeWATER).Mutation-tested, because a green suite only proves the code does something:
place()reduced to an anchor-only overlap guardremove()erases the record but not the occupancyWATERchecked beforeOCCUPIEDThe first of those found a real gap: the original overlap tests all put the second building's anchor on an occupied cell, so they passed with or without the per-cell loop.
test_an_overlap_that_misses_the_anchor_is_still_rejectedwas added to cover the discriminating case and is the one that fails under that mutant.Rendering checked headed and by eye, since
gl_compatibilitydraws nothing headless. Four warehouses (five_thick_c, an asymmetric C) at all four rotations on flat ground, plus one of each catalogue type: each lands squarely on its own cells, sits flush on the surface, is opaque, casts a shadow, and carries its own colour. Confirmed numerically as well — the mesh centre matches the centre of the cells the record claims, for every rotation.Bugs found and fixed along the way
StandardMaterial3Dwas being reused asmaterial_overrideon every mesh; since materials are reference-typed resources, mutatingalbedo_colorrepainted every building already on the map. Fixed by removing the override entirely —BuildingMeshes.build()already bakesplaceholder_coloronto the mesh's own material, and letting that through is how a building gets its colour.duplicate()of the ghost, so it inherited the ghost's translucent override and itscast_shadow = OFF. Placed buildings now build a freshMeshInstance3D.position_for()was handed absolute cells instead of anchor-relative offsets, counting the anchor twice and landing a warehouse 5 world units from its footprint. It failed quietly rather than loudly becausecalculate_bounds()seeds its min/max at zero, which is only correct for an array containing the origin. Caught by the headed screenshot, not by the unit tests — every test at the time passed offsets. Two regression tests now pin it._hover_valid, so a building could be placed on a site the ghost showed as invalid. The three early returns in_physics_process()also did not clear the flag despite a comment saying they must; they now share_clear_hover().is_action_pressedto record the press point,is_action_releasedto measure the drag distance), and neither branch may callset_input_as_handled()—PlacementControllersits belowCameraRigin the tree and so sees the event first, and consuming it would break free-rotate.Buildings.place()only guarded the anchor cell, letting a rotated footprint silently steal cells from an existing building whosecells()still claimed them. Now guards every cell.Known and deliberate
OCCUPIEDrule's job; there is no physics fallback. This does not affect pawn navigation either — see #56, which covers making the A* graph aware of occupancy.reroll()/next_shape()leave placed buildings floating over a new map. Known debug-only wart, deliberately not addressed here.BUILT; construction progression,uniqueenforcement, demolition UI and save/load are all out of scope per the issue.level_at()is the sampled level, not the drawn one, so a placed mesh sinks on a shoreline exactly as the ghost does. That is #46's job.Not covered by the suite
Rendering. Colour, material, opacity and seating are verified by headed screenshot only, as
gl_compatibilityrenders nothing headless.Buildingsnode and core building placement logic 37e97c9f66BuildingSiteutility class. 8155063a54