fix(@projects/@magic-civilization): 🐛 fix axial-to-offset conversion for grid visibility

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-06-08 17:51:48 -07:00
parent d83ded529e
commit a804ceb430
2 changed files with 57 additions and 4 deletions

View file

@ -329,10 +329,18 @@ static func _refresh_learned_player_snapshot(gd_state: RefCounted) -> void:
for u: RefCounted in p.units:
if u == null or not u.is_alive():
continue
# CRITICAL (p1-29k root-cause fix): GDScript stores unit `position`
# in AXIAL (q, r), but the Rust grid + `compute_vision` +
# Move/Attack legal-action enumeration all operate in ODD-Q OFFSET
# (col, row) space (`neighbours_offset` parity-by-column +
# `wrap_coord` rectangular bounds). Passing raw axial put units
# off-grid → no tile under them → visible_tiles=0, move/attack=0.
# Convert to offset so units sit on the same grid the vision walk uses.
var u_off: Vector2i = HexUtilsScript.axial_to_offset(u.position)
unit_dicts.append({
"id": pi * ID_STRIDE + ui,
"col": int(u.position.x),
"row": int(u.position.y),
"col": u_off.x,
"row": u_off.y,
"hp": int(u.hp),
"max_hp": int(u.max_hp),
"attack": int(u.attack),
@ -349,9 +357,13 @@ static func _refresh_learned_player_snapshot(gd_state: RefCounted) -> void:
for c: RefCounted in p.cities:
if c == null:
continue
# Same axial→offset conversion as units — city centres feed
# city-sourced vision (`compute_player_visible_set`) and the
# capital position, both in offset space.
var c_off: Vector2i = HexUtilsScript.axial_to_offset(c.position)
city_dicts.append({
"col": int(c.position.x),
"row": int(c.position.y),
"col": c_off.x,
"row": c_off.y,
"population": maxi(1, int(c.population)),
"food_yield": int(c.get("food_yield")) if c.get("food_yield") != null else 2,
"prod_yield": int(c.get("prod_yield")) if c.get("prod_yield") != null else 2,

View file

@ -4883,6 +4883,47 @@ impl GdGameState {
d.set("attack_bits", attack_bits as i64);
d.set("queue_production_bits", queue_production_bits as i64);
d.set("trivial_bits", trivial_bits as i64);
// Grid introspection (hypothesis 1) — prove the grid is populated and
// that the player's first unit sits ON a real tile with a biome
// label. `tile_under_unit_biome` empty ⇒ the unit is off-grid
// (coordinate-space mismatch) — the exact failure mode behind
// visible_tiles=0.
match self.inner.grid.as_ref() {
Some(g) => {
d.set("grid_width", g.width as i64);
d.set("grid_height", g.height as i64);
d.set("grid_tiles", g.tiles.len() as i64);
let labelled = g
.tiles
.iter()
.filter(|t| !t.biome_label_id.is_empty())
.count();
d.set("grid_tiles_with_biome", labelled as i64);
if let Some(sample) = g.tiles.iter().find(|t| !t.biome_label_id.is_empty()) {
d.set(
"grid_sample_biome",
GString::from(sample.biome_label_id.as_str()),
);
}
// First own unit: report its (col,row) and the biome of the
// tile it occupies (empty string = no tile under it).
if let Some(ps) = self.inner.players.iter().find(|p| p.player_index == player) {
if let Some(u0) = ps.units.first() {
d.set("unit0_col", u0.col as i64);
d.set("unit0_row", u0.row as i64);
let biome_under = g
.tile(u0.col, u0.row)
.map(|t| t.biome_label_id.clone())
.unwrap_or_default();
d.set("tile_under_unit_biome", GString::from(biome_under.as_str()));
}
}
}
None => {
d.set("grid_tiles", 0i64);
}
}
d
}