Show a ghost of the selected building type when placing building #39
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#39
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, show a ghost of the building mesh under the mouse cursor. The ghost should be semi-transparent and follow the cursor, snapped to the tile grid.
Right-clicking cancels the placement, clears the ghost and unsets the button in the building panel.
This issue owns the ghost and the placement mode state. The meshes it displays, the
BuildingFootprintresource and theBuildingTypefields are #36 — a prerequisite. Validity feedback (green/red tinting, slope checks) is #37; actually committing a building to the map is not yet filed.Prerequisites
BuildingType.mesh,BuildingType.footprint,BuildingType.height_levels, theBuildingFootprintresource, and the three meshes.Current gaps this has to close
BuildingPanelbuilds its cards into aButtonGroupwithallow_unpress = true, but nothing is connected to the press signal — selection currently goes nowhere.Decisions
Taken 2026-08-24.
Placement state lives on a
PlacementControllernode, notGameStateenum Mode { SELECT, PLACE_BUILDING }and the selectedBuildingTypebelong to a newPlacementControllernode ingame.tscn, not on theGameStateautoload as originally sketched in #36.Mouse mode is a per-scene concern. On the autoload it would survive a return to the main menu and be mutable from anywhere; on a scene node it dies with the scene and has one owner.
Right-click: cancel on tap, rotate on drag
CameraRig._unhandled_input()already bindsMOUSE_BUTTON_RIGHTto free-rotate drag. Rather than surrendering rotation while placing — which is exactly when you most want to spin the view to check a site — distinguish click from drag:event.positionon right-pressDo not call
set_input_as_handled()on that release.CameraRigclears_rotating_with_mouseon the same event; swallowing it strands the camera in permanent rotate mode.Implementation notes
Node placement
PlacementControllergoes ingame.tscnafterCameraRigin the tree —_unhandled_inputpropagates last-child-first, so it sees events before the camera does. It exports aNodePathtoMapand toBuildingPanel, creates one childMeshInstance3Din_ready()and shows/hides it. Create the ghost once; never re-instantiate per frame.Wiring the selection
Connect each card's
toggled(toggled_on)signal inBuildingPanel._ready()and re-emit a panel-levelsignal building_type_selected(type: BuildingType), passingnullon untoggle.ButtonGroup.pressedonly fires on press — withallow_unpress = trueit gives no clean deselection event, which is whytoggledis the right hook.The controller connects to the panel, not the reverse; the panel stays ignorant of placement. For cancel, the panel also needs a
clear_selection()method (button_group.get_pressed_button().button_pressed = false).Snap maths
Footprints are anchor-relative (see #36), so the tile under the cursor is the anchor and there is no parity maths, no derived centre and no
cell_to_world()special-casing:Clamp so every occupied cell stays in bounds — reject the position outright rather than clamping the anchor, or an L-shape slides oddly along the map edge.
For the ghost mesh position, centre on the footprint's bounding box:
Sanity checks: the 3x3 rocket lands on the anchor tile's centre; the 2x2 mine lands on the corner between the anchor and its +X/+Z neighbour (a consequence of where
@was authored, now visible in the file); an L-shape centres on its bounding box rather than its centre of mass, which is why #36 says to model non-rectangular meshes to fill the bounding box.Note
TerrainGrid.cell_to_world()hardcodes+0.5and is a 1x1 helper — it is right for finding a tile centre, wrong for positioning a multi-tile ghost.Ground height and sitting the mesh on it
Take the maximum
surface_y(level_at(cell))across the occupied cells. The minimum sinks the ghost into a hillside; the max floats it, which is the honest read until #37 lands validity.Then sit the mesh on that ground:
Godot's mesh primitives are centred on their origin and have no offset property of their own (
PlaneMesh.center_offsetis the sole exception), so the meshes from #36 arrive centred. Theget_aabb()subtraction lifts any mesh onto the ground without the caller knowing its height, and still works if a mesh later becomes a hand-builtArrayMeshwith its origin already at the base.Per-frame update, in
_physics_processUpdate every frame rather than on
InputEventMouseMotion— the camera can pan under a stationary cursor, and a motion-driven ghost would lag behind the terrain.The
gui_get_hovered_control()guard matters because the raycast will happily hit terrain underneath the building panel. Placement clicks are already safe —_unhandled_inputnever sees an event a Control consumed.Use a physics raycast, not
CameraRig._ground_point(). That helper intersects a plane at the rig's Y, which is correct for zoom-to-cursor and wrong here — on a hill the ghost lands several tiles off.CameraRig.camera()exposes the camera. The GridMap's colliders are already a singletile_sizecube at the top of each column rather than the full column —TerrainGrid._build_mesh_library()notes that a full-depth collider makes raycasts pick the cliff behind the tile you clicked. That was written for this feature.Ghost material
Build it in code in
_ready()and assign asmaterial_override:Shadows off — a solid shadow under a translucent ghost reads as a bug.
Build the material rather than
preload()ing a.tres.preload()/load()return the shared cached resource, so #37's green/red tinting would otherwise mutate the material for every ghost for the whole session.Testing
The bounding-box centring and the bounds clamp are pure maths — extract them as standalone functions so they are reachable without instantiating the scene, and cover them in GUT:
BuildingFootprint's own parsing androtated()tests belong with #36.The ghost itself is not testable —
gl_compatibilityrenders nothing headless. Verify it headed, by screenshot.