When placing a building, highlight the tiles under the cursor and show placement validity #37
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#37
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?
When placing a building, highlight the tiles under the cursor and show whether the site is valid: green if the ground is flat enough to build on, red if it is not.
This issue owns the tile highlight and the placement validity rule. The ghost mesh and placement mode are #39 (done); rotating the footprint is #44; committing a building to the map is not yet filed.
Prerequisites
PlacementControlleralready computes the hovered anchor cell and iteratesBuildingType.footprint.cells()every physics frame. This issue reuses that anchor rather than raycasting again.Decisions
Flatness is measured within the footprint, not by
TerrainGrid.is_sloped()is_sloped(cell)asks a different question: it compares a cell against its eight neighbours and returns true if any differs by more thanmax_step_levels. That is a pathfinding query — "is there a cliff next to this tile" — and it is wrong here twice over:The rule this issue wants is the level spread over the occupied cells:
Expose
max_level_spread: intas an@exportonPlacementControllerso it can be tuned by eye, default0.On "test how likely a threshold of 0 is": for a 1x1 footprint the spread is always 0, so the check is degenerate and every dry tile is green. It only starts biting at 2x2 and above.
sensor_towerandcolony_centreare 1x1 today, so testing this needs a multi-tile footprint — use the 3x3 or the L-shape fromentities/buildings/resources/footprints/.Water and off-map are invalid regardless of flatness
Flatness alone is not validity. A footprint is invalid if any occupied cell is:
map.in_bounds(cell)is false, ormap.is_walkable(cell)is false.Both must be checked before the spread, because
level_at()on an out-of-bounds cell silently returns_datum_indexrather than erroring, which would quietly compute a spread against a fictional tile.Gotchas found in the current code
These are the three ways a naive implementation goes wrong here. All three are real, in the code as it stands today.
1.
world_to_cell()clamps, so it can never report off-mapThe anchor is always in bounds, but
anchor + offsetfor the other footprint cells is not. Bounds rejection has to be an explicitin_bounds()test per cell — you cannot detect it by checking the anchor.2. Water is drawn at the datum, not at its own level
_paint_cells()places submerged tiles at_datum_indexso the sea reads as one surface, butlevel_at()still returns the seabed level. Sosurface_y(level_at(cell))on a water tile gives a Y below the visible water, and a highlight quad placed there disappears under the sea.height_at()already does this correction. Either reuse it, or reject water cells before you need their height — the latter is simpler, since a water cell makes the whole site invalid anyway.3. The highlight is not planar
The ground is terraced, so on a mixed-level site each cell's top face is at a different Y. Draw each quad at its own
surface_y(level_at(cell)), not at one shared height for the site. The outline then steps with the terrain, which is correct and reads well.Implementation notes
Rendering: one
ImmediateMesh, not a node per tileAdd a single
MeshInstance3Dchild ofPlacementController(sibling of the ghost) holding anImmediateMesh, rebuilt when the highlighted set changes. This avoids pooling and freeing nodes as the cursor moves.Everything is drawn into one
PRIMITIVE_TRIANGLESsurface:Use quads for the border, not
PRIMITIVE_LINES. Line width is not adjustable undergl_compatibility— it is always one pixel, so an outline drawn as lines is invisible when zoomed out and never thickens as you zoom in. A quad ~0.06 m wide gives a border with real world-space thickness.A cell's edge in direction
dis on the perimeter iffcell + dis not in the footprint set. That rule handles L-shapes and holes for free; do not assume the footprint is a rectangle just becausebounds()returns one.One material, colour carried on the vertices. An earlier draft of this issue called for a material per surface and swapping
albedo_colorbetween the valid and invalid tints. Use vertex colours instead:The highlight has two independent colour axes — fill vs. border (different alpha) and valid vs. invalid (different hue) — which is four materials handled by
albedo_color, or one whenImmediateMesh.surface_set_color()carries it. It also makes per-cell colouring free, which is what a later "show me which tile is the bad one" needs.Three notes on it:
vertex_color_use_as_albedomust betrueor the colours are silently ignored and everything draws flat white.albedo_colormultiplies with the vertex colour, so leave it white.surface_set_color()is state applied to the next vertex added — call it beforesurface_add_vertex(). Reversed, every colour lands one vertex late, which reads as a bleed between cells rather than an obvious error.Build the material and the
ImmediateMeshonce in_ready(); never allocate either per frame.Vertices are in the mesh instance's space, not the map's
surface_y()andcell.x * tile_sizeare map-local, so align the two once in_ready():Then emit raw map-local coordinates and no
to_global()is needed anywhere. It also means the highlight follows the Map if it ever moves off the origin instead of silently desyncing.Cell
cxspans[cx * tile_size, (cx + 1) * tile_size).GridMap.cell_center_xistrue, which sounds like a half-cell offset but is not — verified in 4.7.2:map_to_local(Vector3i(0,0,0))returns(1.0, …)attile_size2, i.e. the centre of a cell spanning 0..2, which agrees with bothcell_to_world()'s+0.5andworld_to_cell()'sfloor().Offset the quads ~0.02 m above
surface_yto avoid z-fighting with the tile top.Rebuild only when the anchor or the footprint changes
_physics_processruns 60 times a second and the cursor usually sits over the same tile across many frames. Cache the last anchor (and the selected type) and return early when neither has changed. Rebuilding anImmediateMeshper frame for a 3x3 is affordable, but the early-out is one line and removes the question.Tint the ghost to match
The ghost's
material_overrideis already built in code inPlacementController._ready(), so itsalbedo_colorcan move between the valid and invalid tints alongside the highlight. It must stay amaterial_overrideand never be written back onto the mesh —BuildingMeshes.build()bakes colour intoPrimitiveMesh.material, and #41 will start sharing those meshes between calls.Separate the rule from the drawing so it can be tested
gl_compatibilityrenders nothing headless, so none of the drawing is testable — the highlight has to be verified by eye with a headed screenshot. That makes it worth pushing the decision out of the draw code entirely:Pure apart from the queries it makes on
map, and a test double can supply those: aTestTerrain extends TerrainGridoverridingin_bounds(),level_at()andis_walkable()from a hand-authored array gives full control without generating noise or touching the renderer.Returning a
Reasonrather than aboolcosts nothing now and means a future "why can't I build here?" tooltip does not have to re-derive it.Tests (
test/unit/test_placement_rules.gd)VALIDmax_level_spread = 0→TOO_STEEP; the same site withmax_level_spread = 1→VALIDWATER, and it wins over a spread that would also failOUT_OF_BOUNDSVALID(guards against evaluatingbounds()instead ofcells())VALIDon dry land, whatever the surrounding terrainAcceptance criteria
PlacementRulesis covered by the tests above and the suite is green headless.tools/screenshot_buildings.gdis the working template for the wait-N-frames-then-save pattern.When placing a building, highlight the titles under the cursor from the building footprintto When placing a building, highlight the tiles under the cursor and show placement validity