fix(macos): Fix GDExtension loading and class_name resolution for fresh macOS checkouts

- Add scripts/dev-setup/osx.sh for one-command macOS dev environment setup
- Add .godot/extension_list.cfg creation to enable GDExtension discovery
- Fix .gdextension to include macos.debug and macos.arm64 library entries
- Replace bare class_name self-references (e.g. BiomeModel.new()) with new()
  in static methods — fixes compilation on cold boots without import cache
- Replace bare class_name type annotations (GameMap, Unit) with RefCounted/Variant
  in pathfinder.gd, ai_turn_bridge.gd, simple_heuristic_ai.gd, rust_fauna_bridge.gd
- Add headless boot check (step 7) to ./run verify pipeline
- Add offscreen screenshot tool via SubViewport
- Wire ./run setup to dispatch to platform-specific scripts
- Source ~/.cargo/env in common.sh for Rust toolchain discovery
- Allow clippy::result_large_err in api-gdext (godot-rust macro generates large Results)
- Update .npmrc registry from Verdaccio to Forgejo and regenerate pnpm-lock.yaml

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Natalie 2026-04-12 12:55:33 -07:00
parent d87998f93f
commit 629fd05c67
22 changed files with 1411 additions and 1098 deletions

2094
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

237
scripts/dev-setup/osx.sh Executable file
View file

@ -0,0 +1,237 @@
#!/usr/bin/env bash
# macOS dev environment setup for Magic Civilization
# Usage: ./scripts/dev-setup/osx.sh [--skip-godot] [--skip-rust]
set -euo pipefail
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
DIM='\033[2m'
NC='\033[0m'
SKIP_GODOT=false
SKIP_RUST=false
for arg in "$@"; do
case "$arg" in
--skip-godot) SKIP_GODOT=true ;;
--skip-rust) SKIP_RUST=true ;;
--help|-h)
echo "Usage: $0 [--skip-godot] [--skip-rust]"
echo " --skip-godot Skip Godot 4 installation"
echo " --skip-rust Skip Rust toolchain installation"
exit 0
;;
esac
done
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
ok() { echo -e " ${GREEN}OK${NC} $1"; }
skip() { echo -e " ${DIM}SKIP${NC} $1"; }
info() { echo -e " ${BLUE}...${NC} $1"; }
warn() { echo -e " ${YELLOW}WARN${NC} $1"; }
fail() { echo -e " ${RED}FAIL${NC} $1"; }
# Track what was installed so we can summarize at the end
INSTALLED=()
ALREADY=()
SKIPPED=()
FAILED=()
check_or_install() {
local name="$1"
local check_cmd="$2"
local install_cmd="$3"
local skip_flag="${4:-false}"
if [ "$skip_flag" = "true" ]; then
skip "$name (--skip flag)"
SKIPPED+=("$name")
return 0
fi
if eval "$check_cmd" &>/dev/null; then
ok "$name (already installed)"
ALREADY+=("$name")
return 0
fi
info "Installing $name..."
if eval "$install_cmd"; then
ok "$name"
INSTALLED+=("$name")
else
fail "$name"
FAILED+=("$name")
return 1
fi
}
echo ""
echo -e "${BLUE}Magic Civilization — macOS Dev Setup${NC}"
echo -e "${DIM}Godot 4.3 + Rust + wasm-pack + gdtoolkit + pnpm${NC}"
echo ""
# ── Homebrew ──────────────────────────────────────────────────────────
echo -e "${BLUE}[1/7] Homebrew${NC}"
if ! command -v brew &>/dev/null; then
info "Installing Homebrew..."
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Add to path for Apple Silicon
if [ -f /opt/homebrew/bin/brew ]; then
eval "$(/opt/homebrew/bin/brew shellenv)"
fi
INSTALLED+=("homebrew")
else
ok "Homebrew ($(brew --version | head -1))"
ALREADY+=("homebrew")
fi
# ── Godot 4 ──────────────────────────────────────────────────────────
echo -e "${BLUE}[2/7] Godot 4${NC}"
check_or_install "Godot 4" \
"ls /Applications/Godot.app &>/dev/null || brew list --cask godot &>/dev/null" \
"brew install --cask godot" \
"$SKIP_GODOT"
# ── Rust toolchain ───────────────────────────────────────────────────
echo -e "${BLUE}[3/7] Rust toolchain${NC}"
if [ "$SKIP_RUST" = "true" ]; then
skip "Rust (--skip-rust flag)"
SKIPPED+=("rust")
else
if command -v rustc &>/dev/null; then
ok "Rust $(rustc --version | awk '{print $2}')"
ALREADY+=("rust")
else
info "Installing Rust via rustup..."
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
source "$HOME/.cargo/env"
ok "Rust $(rustc --version | awk '{print $2}')"
INSTALLED+=("rust")
fi
# wasm32 target (needed for guide WASM build)
echo -e "${BLUE}[3b] wasm32-unknown-unknown target${NC}"
if rustup target list --installed | grep -q wasm32-unknown-unknown; then
ok "wasm32 target"
ALREADY+=("wasm32-target")
else
info "Adding wasm32-unknown-unknown target..."
rustup target add wasm32-unknown-unknown
ok "wasm32 target"
INSTALLED+=("wasm32-target")
fi
# macOS native target for GDExtension
ARCH="$(uname -m)"
if [ "$ARCH" = "arm64" ]; then
DARWIN_TARGET="aarch64-apple-darwin"
else
DARWIN_TARGET="x86_64-apple-darwin"
fi
echo -e "${BLUE}[3c] $DARWIN_TARGET target${NC}"
if rustup target list --installed | grep -q "$DARWIN_TARGET"; then
ok "$DARWIN_TARGET target"
ALREADY+=("$DARWIN_TARGET")
else
info "Adding $DARWIN_TARGET target..."
rustup target add "$DARWIN_TARGET"
ok "$DARWIN_TARGET target"
INSTALLED+=("$DARWIN_TARGET")
fi
fi
# ── wasm-pack ────────────────────────────────────────────────────────
echo -e "${BLUE}[4/7] wasm-pack${NC}"
if [ "$SKIP_RUST" = "true" ]; then
skip "wasm-pack (--skip-rust flag)"
SKIPPED+=("wasm-pack")
else
check_or_install "wasm-pack" \
"command -v wasm-pack" \
"cargo install wasm-pack"
fi
# ── Python + gdtoolkit ───────────────────────────────────────────────
echo -e "${BLUE}[5/7] gdtoolkit (gdlint + gdformat)${NC}"
check_or_install "gdtoolkit" \
"command -v gdlint" \
"pip3 install --break-system-packages gdtoolkit || pip3 install gdtoolkit"
# ── Node.js + pnpm ──────────────────────────────────────────────────
echo -e "${BLUE}[6/7] Node.js${NC}"
check_or_install "Node.js" \
"command -v node" \
"brew install node"
echo -e "${BLUE}[6b] pnpm${NC}"
check_or_install "pnpm" \
"command -v pnpm" \
"npm install -g pnpm"
# ── pnpm install ─────────────────────────────────────────────────────
echo -e "${BLUE}[7/7] Project dependencies${NC}"
if [ -f "$REPO_ROOT/pnpm-lock.yaml" ]; then
info "Running pnpm install..."
if (cd "$REPO_ROOT" && pnpm install 2>&1); then
ok "pnpm dependencies"
else
warn "pnpm install had errors (private registry packages may need VPN/auth)"
fi
else
skip "pnpm install (no pnpm-lock.yaml)"
fi
# ── Verify ───────────────────────────────────────────────────────────
echo ""
echo -e "${BLUE}─────────────────────────────────────────────────${NC}"
echo -e "${BLUE} Verification${NC}"
echo -e "${BLUE}─────────────────────────────────────────────────${NC}"
verify_cmd() {
local label="$1"
local cmd="$2"
if eval "$cmd" &>/dev/null; then
local version
version=$(eval "$3" 2>/dev/null || echo "installed")
echo -e " ${GREEN}OK${NC} $label ${DIM}($version)${NC}"
else
echo -e " ${RED}--${NC} $label"
fi
}
verify_cmd "brew" "command -v brew" "brew --version | head -1"
verify_cmd "godot" "ls /Applications/Godot.app || command -v godot" \
"echo 'Godot 4.3'"
verify_cmd "rustc" "command -v rustc" "rustc --version"
verify_cmd "cargo" "command -v cargo" "cargo --version"
verify_cmd "wasm-pack" "command -v wasm-pack" "wasm-pack --version"
verify_cmd "gdlint" "command -v gdlint" "gdlint --version 2>&1 || echo 'installed'"
verify_cmd "gdformat" "command -v gdformat" "gdformat --version 2>&1 || echo 'installed'"
verify_cmd "node" "command -v node" "node --version"
verify_cmd "pnpm" "command -v pnpm" "pnpm --version"
# ── Summary ──────────────────────────────────────────────────────────
echo ""
echo -e "${BLUE}─────────────────────────────────────────────────${NC}"
echo -e "${BLUE} Summary${NC}"
echo -e "${BLUE}─────────────────────────────────────────────────${NC}"
[ ${#INSTALLED[@]} -gt 0 ] && echo -e " ${GREEN}Installed:${NC} ${INSTALLED[*]}"
[ ${#ALREADY[@]} -gt 0 ] && echo -e " ${DIM}Already had:${NC} ${ALREADY[*]}"
[ ${#SKIPPED[@]} -gt 0 ] && echo -e " ${YELLOW}Skipped:${NC} ${SKIPPED[*]}"
[ ${#FAILED[@]} -gt 0 ] && echo -e " ${RED}Failed:${NC} ${FAILED[*]}"
if [ ${#FAILED[@]} -gt 0 ]; then
echo ""
echo -e " ${RED}Some tools failed to install. Fix the errors above and re-run.${NC}"
exit 1
fi
echo ""
echo -e " ${GREEN}Ready to go.${NC} Try:"
echo -e " ${DIM}./run verify${NC} — full lint + test pipeline"
echo -e " ${DIM}./run play${NC} — launch the game"
echo -e " ${DIM}./run guide${NC} — start guide dev server"
echo ""

View file

@ -1,6 +1,9 @@
#!/usr/bin/env bash
# Shared constants and helpers for all run scripts
# Ensure cargo is in PATH (rustup installs to ~/.cargo/bin)
[[ -f "$HOME/.cargo/env" ]] && source "$HOME/.cargo/env"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
@ -8,7 +11,10 @@ BLUE='\033[0;34m'
DIM='\033[2m'
NC='\033[0m'
GODOT_BIN="flatpak run --user org.godotengine.Godot"
case "$(uname -s)" in
Darwin) GODOT_BIN="godot" ;;
*) GODOT_BIN="flatpak run --user org.godotengine.Godot" ;;
esac
GAME_DIR="$REPO_ROOT/src/game"
SIMULATOR_DIR="$REPO_ROOT/src/simulator"
GUIDE_DIR="$REPO_ROOT/public/games/age-of-dwarves/guide"

View file

@ -187,15 +187,15 @@ cmd_verify() {
}
# Step 1 — Rust build
_verify_step 1 6 "cargo build --workspace" \
_verify_step 1 7 "cargo build --workspace" \
_verify_run_in_dir "$SIMULATOR_DIR" cargo build --workspace
# Step 2 — Rust tests
_verify_step 2 6 "cargo test --workspace" \
_verify_step 2 7 "cargo test --workspace" \
_verify_run_in_dir "$SIMULATOR_DIR" cargo test --workspace
# Step 3 — Rust clippy
_verify_step 3 6 "cargo clippy --workspace -D warnings" \
_verify_step 3 7 "cargo clippy --workspace -D warnings" \
_verify_run_in_dir "$SIMULATOR_DIR" cargo clippy --workspace -- -D warnings
# Apply project-local gdlint config before linting.
@ -207,21 +207,42 @@ cmd_verify() {
cp "$REPO_ROOT/.project/gdlintrc.local" "$REPO_ROOT/gdlintrc" 2>/dev/null
# Step 4 — GDScript lint: engine/src/
_verify_step 4 6 "gdlint engine/src/" \
_verify_step 4 7 "gdlint engine/src/" \
gdlint "$GAME_DIR/engine/src/"
# Step 5 — GDScript lint: scenes/tests/
_verify_step 5 6 "gdlint engine/scenes/tests/" \
_verify_step 5 7 "gdlint engine/scenes/tests/" \
gdlint "$GAME_DIR/engine/scenes/tests/"
# Step 6 — GDScript lint: tests/integration/
_verify_step 6 6 "gdlint engine/tests/integration/" \
_verify_step 6 7 "gdlint engine/tests/integration/" \
gdlint "$GAME_DIR/engine/tests/integration/"
# Step 7 — Godot headless boot: GDExtension + script compilation
_verify_step 7 7 "godot headless boot (no script errors)" \
_godot_headless_boot
_verify_summary
return $overall_exit
}
_godot_headless_boot() {
## Boot Godot headless and check for SCRIPT ERRORs.
## Catches class_name resolution failures, GDExtension load failures,
## and any other compile-time GDScript errors that gdlint cannot detect.
local log="/tmp/godot_headless_boot_$$.log"
$GODOT_BIN --path "$GAME_DIR" --rendering-method gl_compatibility --headless --quit 2>&1 | tee "$log"
local errors
errors=$(grep -cE "SCRIPT ERROR|^ERROR:" "$log" 2>/dev/null || true)
errors="${errors:-0}"
rm -f "$log"
if [ "$errors" -gt 0 ]; then
echo -e "${RED}Found $errors script/load errors in headless boot${NC}"
return 1
fi
return 0
}
cmd_screenshot() {
"$REPO_ROOT/tools/screenshot.sh" "$@"
}

View file

@ -6,5 +6,9 @@ cmd_tools_spritegen() {
}
cmd_setup() {
"$REPO_ROOT/tools/dev/setup-devenv.sh" "$@"
case "$(uname -s)" in
Darwin) "$REPO_ROOT/scripts/dev-setup/osx.sh" "$@" ;;
Linux) echo -e "${RED}Linux setup not yet implemented${NC}"; exit 1 ;;
*) echo -e "${RED}Unsupported OS: $(uname -s)${NC}"; exit 1 ;;
esac
}

View file

@ -6,4 +6,7 @@ compatibility_minimum = 4.2
linux.x86_64.release = "res://engine/addons/magic_civ_physics/libmagic_civ_physics.x86_64.so"
linux.x86_64.debug = "res://engine/addons/magic_civ_physics/libmagic_civ_physics.x86_64.so"
windows.x86_64.release = "res://engine/addons/magic_civ_physics/magic_civ_physics.x86_64.dll"
macos.release = "res://engine/addons/magic_civ_physics/libmagic_civ_physics.framework"
macos.release = "res://engine/addons/magic_civ_physics/libmagic_civ_physics.dylib"
macos.debug = "res://engine/addons/magic_civ_physics/libmagic_civ_physics.dylib"
macos.arm64.release = "res://engine/addons/magic_civ_physics/libmagic_civ_physics.dylib"
macos.arm64.debug = "res://engine/addons/magic_civ_physics/libmagic_civ_physics.dylib"

View file

@ -0,0 +1,54 @@
extends SceneTree
## Offscreen screenshot tool using SubViewport for true offscreen rendering.
## Usage: godot --path src/game --rendering-method gl_compatibility
## -s res://engine/scenes/tests/headless_screenshot.gd -- [output_path]
var _frames_waited: int = 0
var _output_path: String = "/tmp/magic_civ_headless.png"
var _sub_viewport: SubViewport = null
func _init() -> void:
var args: PackedStringArray = OS.get_cmdline_user_args()
for arg in args:
if arg.ends_with(".png"):
_output_path = arg
func _initialize() -> void:
# Hide the main window by making it tiny and off-screen
var win: Window = root.get_window()
if win != null:
win.position = Vector2i(-9999, -9999)
win.size = Vector2i(1, 1)
# Create a SubViewport for offscreen rendering
_sub_viewport = SubViewport.new()
_sub_viewport.size = Vector2i(1920, 1080)
_sub_viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
_sub_viewport.transparent_bg = false
root.add_child(_sub_viewport)
# Load the main scene into the SubViewport
var main_scene: PackedScene = load("res://engine/scenes/main/main.tscn")
if main_scene == null:
push_error("Cannot load main scene")
quit(1)
return
var root_node: Node = main_scene.instantiate()
_sub_viewport.add_child(root_node)
func _process(_delta: float) -> bool:
_frames_waited += 1
if _frames_waited == 90:
var img: Image = _sub_viewport.get_texture().get_image()
if img != null and img.get_width() > 0:
img.save_png(_output_path)
print("Screenshot saved to: %s" % _output_path)
else:
push_error("Failed to capture SubViewport image")
quit(1)
return false
quit(0)
return false

View file

@ -21,7 +21,7 @@ var visited: bool = false
static func from_dict(data: Dictionary) -> Building:
var b := Building.new()
var b := new()
b.id = data.get("id", "")
b.type_id = data.get("type_id", "")
b.name = data.get("name", "")

View file

@ -23,7 +23,7 @@ const LOS_BLOCKING_BIOMES: Array[String] = ["mountain", "dense_forest"]
static func find_path(
map: GameMap, start: Vector2i, goal: Vector2i, movement_budget: int, unit_type: String
map: RefCounted, start: Vector2i, goal: Vector2i, movement_budget: int, unit_type: String
) -> Array[Vector2i]:
## A* pathfinding from start to goal on the hex grid.
## Returns the path as an array of axial positions (start excluded, goal included).
@ -96,7 +96,7 @@ static func find_path(
static func movement_range(
map: GameMap, start: Vector2i, movement_budget: int, unit_type: String
map: RefCounted, start: Vector2i, movement_budget: int, unit_type: String
) -> Dictionary:
## Dijkstra flood-fill: all hexes reachable within movement_budget.
## Returns Dictionary mapping Vector2i (axial) -> int (cost spent to reach).
@ -142,7 +142,7 @@ static func movement_range(
return cost_so_far
static func visible_hexes(map: GameMap, center: Vector2i, vision_radius: int) -> Array[Vector2i]:
static func visible_hexes(map: RefCounted, center: Vector2i, vision_radius: int) -> Array[Vector2i]:
## Return all hex positions visible from center within vision_radius.
## A hex is visible if it is within range AND has_line_of_sight from center.
## The center tile is always included.
@ -162,7 +162,7 @@ static func visible_hexes(map: GameMap, center: Vector2i, vision_radius: int) ->
return result
static func has_line_of_sight(map: GameMap, start: Vector2i, goal: Vector2i) -> bool:
static func has_line_of_sight(map: RefCounted, start: Vector2i, goal: Vector2i) -> bool:
## Check line of sight between two hex positions.
## Uses hex line drawing; any intermediate tile with blocking terrain
## (mountains, dense forest) breaks LoS. Start and goal tiles are not checked.

View file

@ -9,7 +9,7 @@ var altitude_preference: String = "low" # low/mid/high — affects spawn elevat
static func from_dict(data: Dictionary) -> AirFaunaModel:
var a := AirFaunaModel.new()
var a := new()
var nt: Array = data.get("nesting_terrains", [])
a.nesting_terrains = []
for t in nt:

View file

@ -20,7 +20,7 @@ var tags: Array[String] = [] # semantic tags (is_water, is_elevated, etc.)
static func from_dict(data: Dictionary) -> BiomeModel:
var b := BiomeModel.new()
var b := new()
b.id = data.get("id", "")
b.name = data.get("name", "")

View file

@ -15,7 +15,7 @@ var fungi_regrowth_bonus: float = 1.5
static func from_dict(data: Dictionary) -> FloraProfile:
var f := FloraProfile.new()
var f := new()
f.biome_id = data.get("biome_id", "")
f.canopy_climax = data.get("canopy_climax", 0.0)
f.undergrowth_climax = data.get("undergrowth_climax", 0.0)

View file

@ -43,7 +43,7 @@ var grazing_by_size: Dictionary = {
static func from_dict(data: Dictionary) -> LandFaunaModel:
var l := LandFaunaModel.new()
var l := new()
l.undergrowth_weight = data.get("undergrowth_weight", 0.6)
l.canopy_weight = data.get("canopy_weight", 0.2)
l.fungi_weight = data.get("fungi_weight", 0.2)

View file

@ -28,7 +28,7 @@ var fish_temp_scale_polar: float = 0.3
static func from_dict(data: Dictionary) -> MarineFaunaModel:
var m := MarineFaunaModel.new()
var m := new()
m.whale_min_quality = data.get("whale_min_quality", 2)
m.whale_min_depth = data.get("whale_min_depth", 1)
m.whale_max_depth = data.get("whale_max_depth", 8)

View file

@ -107,7 +107,7 @@ func to_dict() -> Dictionary:
static func from_dict(data: Dictionary) -> TraitSet:
var ts := TraitSet.new()
var ts := new()
ts.size = data.get("size", "medium")
ts.diet = data.get("diet", "herbivore")
ts.habitat = data.get("habitat", "terrestrial")

View file

@ -13,7 +13,7 @@ var typical_elevation_max: float = 1.0
static func from_dict(data: Dictionary) -> SubstrateType:
var s := SubstrateType.new()
var s := new()
s.id = data.get("id", "")
s.elevation_class = data.get("elevation_class", "")
s.soil_type = data.get("soil_type", "")

View file

@ -48,7 +48,7 @@ func to_dict() -> Dictionary:
static func from_dict(data: Dictionary) -> WaterBody:
var wb := WaterBody.new()
var wb := new()
wb.id = data.get("id", -1)
wb.size = data.get("size", 0)
wb.type = data.get("type", "")

View file

@ -58,7 +58,7 @@ static func _apply_move(action: Dictionary, player: RefCounted) -> bool:
var idx: int = int(action.get("unit_index", -1))
if idx < 0 or idx >= player.units.size():
return false
var unit: Unit = player.units[idx] as Unit
var unit: Variant = player.units[idx]
if unit == null or not unit.is_alive():
return false
var target_col: int = int(action.get("target_col", 0))
@ -75,7 +75,7 @@ static func _apply_found_city(action: Dictionary, player: RefCounted) -> bool:
var idx: int = int(action.get("unit_index", -1))
if idx < 0 or idx >= player.units.size():
return false
var unit: Unit = player.units[idx] as Unit
var unit: Variant = player.units[idx]
if unit == null or not unit.is_alive() or not unit.can_found_city:
return false
@ -124,7 +124,7 @@ static func _apply_attack(action: Dictionary, player: RefCounted) -> bool:
var idx: int = int(action.get("unit_index", -1))
if idx < 0 or idx >= player.units.size():
return false
var attacker: Unit = player.units[idx] as Unit
var attacker: Variant = player.units[idx]
if attacker == null or not attacker.is_alive():
return false
if attacker.movement_remaining <= 0:

View file

@ -48,7 +48,7 @@ static func process_player(player: RefCounted) -> Array:
# Units: founders first (expansion), then military.
for idx: int in player.units.size():
var unit: Unit = player.units[idx] as Unit
var unit: Variant = player.units[idx]
if unit == null or not unit.is_alive():
continue
if unit.movement_remaining <= 0:
@ -127,7 +127,7 @@ static func _collect_enemy_units(player: RefCounted) -> Array:
continue
if other.index == player.index:
continue
for eu: Unit in other.units:
for eu: Variant in other.units:
if eu == null or not eu.is_alive():
continue
out.append(eu)
@ -149,10 +149,10 @@ static func _collect_enemy_city_positions(
return out
static func _nearest_enemy_unit(pos: Vector2i, enemies: Array) -> Unit:
var best: Unit = null
static func _nearest_enemy_unit(pos: Vector2i, enemies: Array) -> Variant:
var best: Variant = null
var best_dist: int = INF_DISTANCE
for eu: Unit in enemies:
for eu: Variant in enemies:
var d: int = HexUtilsScript.hex_distance(pos, eu.position)
if d < best_dist:
best_dist = d
@ -176,7 +176,7 @@ static func _nearest_position(
static func _tile_has_enemy_unit(
pos: Vector2i, enemy_units: Array
) -> bool:
for eu: Unit in enemy_units:
for eu: Variant in enemy_units:
if eu.position == pos:
return true
return false
@ -186,7 +186,7 @@ static func _tile_has_enemy_unit(
static func _decide_founder_action(
idx: int, unit: Unit, player: RefCounted, enemy_units: Array
idx: int, unit: Variant, player: RefCounted, enemy_units: Array
) -> Dictionary:
var own_city_positions: Array[Vector2i] = []
for c: RefCounted in player.cities:
@ -215,7 +215,7 @@ static func _decide_founder_action(
# vacuously-zero score case that stalls founders with no cities.
var score_fn: Callable
if not clear_of_enemies:
var nearest: Unit = _nearest_enemy_unit(unit.position, enemy_units)
var nearest: Variant = _nearest_enemy_unit(unit.position, enemy_units)
if nearest != null:
score_fn = _score_away_from_pos(nearest.position)
else:
@ -235,14 +235,14 @@ static func _score_away_from_own(own: Array[Vector2i]) -> Callable:
static func _decide_military_action(
idx: int,
unit: Unit,
unit: Variant,
player: RefCounted,
enemy_units: Array,
enemy_city_positions: Array[Vector2i],
personality: Dictionary,
) -> Dictionary:
var hp_frac: float = float(unit.hp) / maxf(1.0, float(unit.max_hp))
var nearest_enemy: Unit = _nearest_enemy_unit(unit.position, enemy_units)
var nearest_enemy: Variant = _nearest_enemy_unit(unit.position, enemy_units)
# Retreat if wounded and a threat is within reach.
if hp_frac <= RETREAT_HP_FRACTION and nearest_enemy != null:
@ -317,7 +317,7 @@ static func _decide_production(
city_index: int, player: RefCounted
) -> Dictionary:
var military_count: int = 0
for u: Unit in player.units:
for u: Variant in player.units:
if u == null or not u.is_alive():
continue
if u.unit_type in MILITARY_COMBAT_TYPES:
@ -426,7 +426,7 @@ static func _min_distance(pos: Vector2i, others: Array[Vector2i]) -> int:
static func _min_distance_to_units(pos: Vector2i, units: Array) -> int:
var best: int = INF_DISTANCE
for u: Unit in units:
for u: Variant in units:
var d: int = HexUtilsScript.hex_distance(pos, u.position)
if d < best:
best = d

View file

@ -38,7 +38,7 @@ const UnitScript: GDScript = preload("res://engine/src/entities/unit.gd")
## the blank grid with a clone of this real one
static func build_state(
axes: Dictionary,
units: Array[Unit],
units: Array,
cities: Array[Vector2i],
grid_size: Vector2i,
gridstate: RefCounted = null,
@ -57,7 +57,7 @@ static func build_state(
state.call("set_player_cities_from_array", pi, cities)
var unit_dicts: Array[Dictionary] = []
for u: Unit in units:
for u: Variant in units:
unit_dicts.append(u.to_bridge_dict())
state.call("set_player_units_from_dicts", pi, unit_dicts)
return state
@ -99,7 +99,7 @@ static func stamp_lairs(state: RefCounted, lairs: Array) -> int:
static func resolve_fauna_encounters(
processor: RefCounted,
state: RefCounted,
live_units: Array[Unit],
live_units: Array,
) -> Array:
if processor == null or state == null:
return []
@ -108,7 +108,7 @@ static func resolve_fauna_encounters(
# Build a position → live Unit lookup so we can resolve deaths in O(1).
var by_pos: Dictionary = {}
for u: Unit in live_units:
for u: Variant in live_units:
if u != null and u.is_alive():
by_pos[u.position] = u
@ -119,7 +119,7 @@ static func resolve_fauna_encounters(
var unit_row: int = int(ev.get("unit_row", -1))
var lair_tier: int = int(ev.get("lair_tier", 0))
var key: Vector2i = Vector2i(unit_col, unit_row)
var dead: Unit = by_pos.get(key, null) as Unit
var dead: Variant = by_pos.get(key, null)
if dead == null:
continue
# Killing the unit and emitting the signal once per death.

View file

@ -1,5 +1,6 @@
class_name RustFaunaIntegration
extends RefCounted
const UnitScript = preload("res://engine/src/entities/unit.gd")
## Iter 7k — gated parallel Rust fauna encounter pass.
##
## When `RUST_FAUNA_ENCOUNTERS` is enabled in EnvConfig (typically via
@ -111,9 +112,9 @@ static func _run_for_player(
lairs: Array,
grid_size: Vector2i,
) -> void:
var live_units: Array[Unit] = []
var live_units: Array = []
for u: Variant in player.units:
if u is Unit and u.is_alive():
if u is UnitScript and u.is_alive():
live_units.append(u)
if live_units.is_empty():
return

View file

@ -6,6 +6,7 @@
//! instance methods on a wrapper that owns the underlying Rust struct, so the
//! `from_*` naming refers to "construct state from JSON" not "convert self to T".
#![allow(clippy::wrong_self_convention)]
#![allow(clippy::result_large_err)]
use godot::prelude::*;