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:
parent
d87998f93f
commit
629fd05c67
22 changed files with 1411 additions and 1098 deletions
2094
pnpm-lock.yaml
generated
2094
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
237
scripts/dev-setup/osx.sh
Executable file
237
scripts/dev-setup/osx.sh
Executable 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 ""
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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" "$@"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
54
src/game/engine/scenes/tests/headless_screenshot.gd
Normal file
54
src/game/engine/scenes/tests/headless_screenshot.gd
Normal 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
|
||||
|
|
@ -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", "")
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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", "")
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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", "")
|
||||
|
|
|
|||
|
|
@ -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", "")
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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::*;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue