feat(@projects/@magic-civilization): improve land tile handling for demo

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-05-10 23:09:43 -07:00
parent 18072e1479
commit 981e856200

View file

@ -88,8 +88,14 @@ func _ready() -> void:
_p1 = GameState.players[0] as PlayerScript
_p2 = GameState.players[1] as PlayerScript
_p1_pos = _p1.units[0].position
_p2_pos = _p2.units[0].position
# Honour FORBIDDEN_BIOMES — relocate any founder that the harness dropped
# onto ocean/coast/mountains to the nearest land tile so cities never
# get founded on water in the demo (matches the production game's
# `mc-mapgen::spawn_box` gate). See the user-reported issue.
_p1_pos = _find_land_tile_near(_p1.units[0].position, 12)
_p2_pos = _find_land_tile_near(_p2.units[0].position, 12)
_p1.units[0].position = _p1_pos
_p2.units[0].position = _p2_pos
_setup_renderers()
_setup_camera_fit()
@ -131,8 +137,18 @@ func _ready() -> void:
_c1.set_population(7)
await _capture("07_capital_pop7", "focus", _p1_pos, 6.0)
# Add placed buildings around the capital
var ring1: Array[Vector2i] = _hex_ring(_p1_pos, 1)
# Add placed buildings around the capital. Filter the ring to land
# tiles only so the building icons never overlap ocean/coast hexes —
# the visual demo should respect the same constraints the production
# city-tile picker enforces (mc-city::tile_picker rejects water tiles).
var ring1: Array[Vector2i] = _filter_land(_hex_ring(_p1_pos, 1))
if ring1.size() < 4:
# Top up from ring 2 if ring 1 had ocean/coast hexes pruned out.
var ring2_land: Array[Vector2i] = _filter_land(_hex_ring(_p1_pos, 2))
for r2: Vector2i in ring2_land:
if ring1.size() >= 4:
break
ring1.append(r2)
_c1.placed_buildings = {
"dwarf_granary": ring1[0] if ring1.size() > 0 else _p1_pos,
"dwarf_forge": ring1[1] if ring1.size() > 1 else _p1_pos,
@ -164,9 +180,12 @@ func _ready() -> void:
await _capture("12_two_culture_zones_wide", "fit")
# ── ACT IV: Military buildup ───────────────────────────────────────────
# Filter the ring to land tiles so warriors never spawn on ocean —
# matches `pathfinder.gd::_is_passable` rejecting `water` in the
# unit's terrain flags for land units.
var spawned: int = 0
for n_pos: Vector2i in ring1:
if not _game_map.tiles.has(n_pos):
if not _is_land_tile(n_pos):
continue
var u: UnitScript = UnitScript.new("dwarf_warrior", 0, n_pos)
u.id = "p1_army_%d" % spawned
@ -176,24 +195,22 @@ func _ready() -> void:
break
await _capture("13_p1_army_3warriors", "focus", _p1_pos, 7.0)
# Add a mixed army: archer + scout
var ring2: Array[Vector2i] = _hex_ring(_p1_pos, 2)
if ring2.size() > 1 and _game_map.tiles.has(ring2[0]):
var arch: UnitScript = UnitScript.new("dwarf_archer", 0, ring2[0])
# Add a mixed army: archer + scout — same land-only filter.
var ring2_land: Array[Vector2i] = _filter_land(_hex_ring(_p1_pos, 2))
if ring2_land.size() > 0:
var arch: UnitScript = UnitScript.new("dwarf_archer", 0, ring2_land[0])
arch.id = "p1_archer"
_p1.units.append(arch)
if ring2.size() > 2 and _game_map.tiles.has(ring2[1]):
var sct: UnitScript = UnitScript.new("dwarf_scout", 0, ring2[1])
if ring2_land.size() > 1:
var sct: UnitScript = UnitScript.new("dwarf_scout", 0, ring2_land[1])
sct.id = "p1_scout"
_p1.units.append(sct)
await _capture("14_mixed_army", "focus", _p1_pos, 9.0)
# P2 puts up a defensive force too
for j: int in range(2):
var n2: Vector2i = _hex_ring(_p2_pos, 1)[j] if _hex_ring(_p2_pos, 1).size() > j else _p2_pos
if not _game_map.tiles.has(n2):
continue
var u2: UnitScript = UnitScript.new("dwarf_warrior", 1, n2)
# P2 puts up a defensive force too — land-only.
var p2_ring1_land: Array[Vector2i] = _filter_land(_hex_ring(_p2_pos, 1))
for j: int in range(min(2, p2_ring1_land.size())):
var u2: UnitScript = UnitScript.new("dwarf_warrior", 1, p2_ring1_land[j])
u2.id = "p2_army_%d" % j
_p2.units.append(u2)
await _capture("15_p2_defenders", "focus", _p2_pos, 7.0)
@ -518,3 +535,54 @@ func _hex_ring(centre: Vector2i, radius: int) -> Array[Vector2i]:
result.append(current)
current = current + dirs[side]
return result
## True when `pos` is a land tile a city or land unit can occupy. Mirrors
## `mc_mapgen::spawn_box::FORBIDDEN_BIOMES` (ocean / coast / mountains)
## and the GDScript `pathfinder.gd::_is_passable` water/impassable gates.
## Defensive: missing tile, missing biome string, or non-land biome all
## return false so demo placements never land in the sea.
func _is_land_tile(pos: Vector2i) -> bool:
if not _game_map.tiles.has(pos):
return false
var tile: Resource = _game_map.tiles[pos]
if tile == null:
return false
var biome: String = ""
if "biome_id" in tile:
biome = String(tile.biome_id)
elif "biome_label_id" in tile:
biome = String(tile.biome_label_id)
if biome.is_empty():
return false
# Forbidden biomes for cities + land-unit footprints. Mountains are
# walkable in some game modes but never a valid capital site, so we
# treat them as forbidden for the demo's placement helpers.
const FORBIDDEN: Array[String] = [
"ocean", "deep_ocean", "coast", "inland_sea", "lake",
"mountains", "volcano", "ice",
]
return biome not in FORBIDDEN
## Pick the first land tile in concentric rings expanding outward from
## `centre`, scanning up to `max_radius`. Returns `centre` when nothing
## land-eligible is found so the demo still proceeds (the projector will
## render the city dot regardless).
func _find_land_tile_near(centre: Vector2i, max_radius: int) -> Vector2i:
if _is_land_tile(centre):
return centre
for r: int in range(1, max_radius + 1):
for candidate: Vector2i in _hex_ring(centre, r):
if _is_land_tile(candidate):
return candidate
return centre
## Filter `positions` down to land-only tiles. Order-preserving.
func _filter_land(positions: Array[Vector2i]) -> Array[Vector2i]:
var out: Array[Vector2i] = []
for p: Vector2i in positions:
if _is_land_tile(p):
out.append(p)
return out