← Devblog

Road Construction Moves to MuJS

Green map-order road preview dragged across the city between two temples

Road building is one of the most-used tools in the game — you drag it across the map constantly, and it has to do real work on every frame: find a path, decide which junction sprite each tile should show, and tell you whether what you're about to place is valid. All of that logic used to live in C++ inside building_road. It now lives in scripts, in src/scripts/building/road.js, where it can be hot-reloaded and read like the gameplay rules it actually encodes.

What Changed

How the Drag Preview Works Now

While you drag a road, construction_update fires each frame. It restores the map from the undo buffer, recomputes routing distances for the road from the drag's start tile, and asks the routed-building helper for the path to the end tile:

__game_undo_restore_map(0)
city.planner.preview_path = null

var start = { x: ev.start_x, y: ev.start_y }
var end   = { x: ev.end_x,   y: ev.end_y }

if (!__map_routing_calculate_distances_for_building(ROUTED_BUILDING_ROAD, start)) return

var preview = routed_building.preview_path(ROUTED_BUILDING_ROAD, start, end)
if (!preview.ok) return

var items = __place_routed_building(start, end, ROUTED_BUILDING_ROAD)
city.planner.preview_path = preview.tiles

The path tiles are placed on the map temporarily — and undone again next frame — so the ghost can read each tile's real sprite via __map_image_at. That is what makes a half-drawn road show correct corners and intersections instead of a row of disconnected straight pieces.

Two Ways to Draw the Ghost

A new option, gameui_road_preview_in_map_order, lets the dragged preview render inside the city's isometric pass instead of always floating on top via the planner overlay. Valid tiles are marked with the green TERRAIN_PLANER_FUTURE mask and sort naturally with the terrain; blocked tiles still fall back to the red overlay, and plain hover-without-drag keeps the old overlay behaviour. The result reads more like part of the map and less like a HUD layer.

Fixes Along the Way

Moving this much logic surfaced a few rough edges, now fixed:

Verification

To keep this from regressing, there's a new integral test — tests/12_road_segment_placement.js — that builds a road across the map through the real build_planner (routing preview, then construction start / update / finalize) and asserts that every tile along the previewed path ends up flagged TERRAIN_ROAD.

Why This Matters

Road sprite selection and placement validity are gameplay rules, not engine internals — keeping them in script means they can be read, tweaked, and hot-reloaded without a recompile, the same way building windows and overlays already are. It also pushes more of the construction planner across the C++/JS boundary, which is the longer-term direction for the whole build system.

← Painting Terrain at Runtime: The Debug Brush