585 lines
19 KiB
GDScript
585 lines
19 KiB
GDScript
extends Node
|
|
## Holds all runtime game state. Persisted on save/load.
|
|
## Supports multiple map layers (primary + transit layers like Ethereal Plane).
|
|
|
|
const PlayerScript: GDScript = preload("res://engine/src/entities/player.gd")
|
|
const GameMapScript: GDScript = preload("res://engine/src/map/game_map.gd")
|
|
const BuildingScript: GDScript = preload("res://engine/src/entities/building.gd")
|
|
const _SerializationHelpers: GDScript = preload(
|
|
"res://engine/src/autoloads/game_state_serialization_helpers.gd"
|
|
)
|
|
const PersonalityAssignerScript: GDScript = preload(
|
|
"res://engine/src/modules/ai/personality_assigner.gd"
|
|
)
|
|
|
|
const DEFAULT_SETTINGS: Dictionary = {
|
|
"map_size": "small",
|
|
"map_type": "continents",
|
|
"map_wrap": "sphere",
|
|
"difficulty": "normal",
|
|
"game_speed": "standard",
|
|
"num_players": 4,
|
|
"turn_limit": 150,
|
|
"mana_density": "normal",
|
|
"era_difficulty_correlation": true,
|
|
}
|
|
|
|
## Player colors for up to 12 players
|
|
const PLAYER_COLORS: Array[Color] = [
|
|
Color(0.2, 0.4, 1.0), # Blue
|
|
Color(0.9, 0.2, 0.2), # Red
|
|
Color(0.2, 0.8, 0.3), # Green
|
|
Color(0.9, 0.8, 0.1), # Yellow
|
|
Color(0.7, 0.3, 0.9), # Purple
|
|
Color(0.9, 0.5, 0.1), # Orange
|
|
Color(0.1, 0.8, 0.8), # Cyan
|
|
Color(0.8, 0.3, 0.5), # Pink
|
|
Color(0.5, 0.4, 0.3), # Brown
|
|
Color(0.6, 0.6, 0.6), # Gray
|
|
Color(0.4, 0.7, 0.4), # Olive
|
|
Color(0.3, 0.3, 0.6), # Navy
|
|
]
|
|
|
|
## Era definitions loaded from the game pack's eras.json via DataLoader.
|
|
## The engine defines no era names — all era content is game-pack-driven.
|
|
var era_data: Array = []
|
|
|
|
var current_theme: String = "fantasy"
|
|
var layers: Array = []
|
|
var players: Array = [] # Array of Player
|
|
var current_player_index: int = 0
|
|
var turn_number: int = 1
|
|
var era: int = 0
|
|
var game_settings: Dictionary = {}
|
|
var transit_nodes: Array = []
|
|
|
|
## Victory/defeat Replay button bridge to game_setup. Cleared after consume.
|
|
var replay_settings: Dictionary = {}
|
|
|
|
## Tracks wonders built: {wonder_id -> player_index}
|
|
var wonders_built: Dictionary = {}
|
|
|
|
## Per-player Ascension Ritual instances: {player_index -> AscensionRitual}
|
|
var ascension_rituals: Dictionary = {}
|
|
|
|
## Shared SpellSystem instance (singleton-style, managed here)
|
|
var spell_system: RefCounted = null
|
|
|
|
## Random seed used to generate this map. Stored so climate and other systems
|
|
## can derive per-turn deterministic seeds from it.
|
|
var map_seed: int = 0
|
|
|
|
## Central RNG for all GDScript gameplay randomness. Serialized so that loading
|
|
## a save reproduces the same random trajectory as the original run.
|
|
var game_rng: RandomNumberGenerator = RandomNumberGenerator.new()
|
|
|
|
## Difficulty modifier applied to AI production each turn.
|
|
## 1.0 = even, <1.0 = AI penalty, >1.0 = AI bonus.
|
|
var ai_difficulty_modifier: float = 1.0
|
|
|
|
## Difficulty modifier applied to AI research (science) each turn.
|
|
## Separate from production so Easy can penalise production more than research.
|
|
var ai_research_modifier: float = 1.0
|
|
|
|
## Per-yield difficulty multipliers (warcouncil p1-29 H4, 2026-04-27).
|
|
## "AI still has to acquire resources but gets more for the effort."
|
|
## All default 1.0 (Normal baseline).
|
|
var ai_gold_modifier: float = 1.0
|
|
var ai_culture_modifier: float = 1.0
|
|
var ai_luxury_modifier: float = 1.0
|
|
## Symmetric player handicap — Easy mode mirrors Hard's AI bonuses onto the
|
|
## human player ("you get the bonuses the AI would have on Hard"). All default 1.0.
|
|
var player_production_modifier: float = 1.0
|
|
var player_research_modifier: float = 1.0
|
|
var player_gold_modifier: float = 1.0
|
|
var player_culture_modifier: float = 1.0
|
|
var player_luxury_modifier: float = 1.0
|
|
## Linear yield growth per turn — added to the static multiplier per turn.
|
|
## Hard=0.003 → at T138 effective_mult ≈ static_mult + 0.414 (so 1.30 base → 1.71 at T138).
|
|
## Lets AI scale into mid/late game without overpowering early-game.
|
|
var ai_yield_per_turn_growth: float = 0.0
|
|
var player_yield_per_turn_growth: float = 0.0
|
|
|
|
## Gold added to every AI player at game start for the current difficulty tier.
|
|
var ai_starting_gold_bonus: int = 0
|
|
|
|
## Extra warrior-class units spawned per AI city at game start.
|
|
var ai_extra_starting_units: int = 0
|
|
|
|
## ID of the extra starting unit (e.g. "warrior").
|
|
var ai_extra_unit_id: String = "warrior"
|
|
|
|
## Per-player production multiplier override. Key=player_index (int), value=float.
|
|
## When non-empty, player-specific value takes precedence over ai_difficulty_modifier.
|
|
## Populated by auto_play.gd when AI_DIFFICULTY_P0/P1 env vars are set.
|
|
var ai_per_player_production_mult: Dictionary = {}
|
|
|
|
## Per-player research multiplier override. Same semantics as ai_per_player_production_mult.
|
|
var ai_per_player_research_mult: Dictionary = {}
|
|
|
|
## Diplomatic relations between players.
|
|
## Key: "min_idx_max_idx", value: "neutral" | "war" | "peace" | "alliance".
|
|
var diplomacy: Dictionary = {}
|
|
|
|
## Ley line anchor registry. Each entry: {position, strength, school, source, owner}
|
|
## position: Vector2i, strength: int 1-5, school: String ("" = neutral),
|
|
## source: String ("wellspring"|"mountain"|"wonder"|"terrain"), owner: int (-1 = world)
|
|
var ley_anchors: Array = []
|
|
|
|
## Dynamic ley resonance/disruption edges between wonder anchors.
|
|
## Rebuilt each climate turn by LeyNetwork.build_network().
|
|
## Array of LeyNetwork.LeyEdge objects — used by renderer for visualization.
|
|
var ley_edges: Array = []
|
|
|
|
## NPC buildings on the world map (lairs, villages, ruins). Array of Building.
|
|
var npc_buildings: Array = []
|
|
## Spatial index: "col,row" -> Array[Building] for quick tile lookups.
|
|
var _npc_buildings_by_tile: Dictionary = {}
|
|
|
|
|
|
func initialize_game(settings: Dictionary) -> void:
|
|
game_settings = DEFAULT_SETTINGS.duplicate()
|
|
for key: String in settings:
|
|
game_settings[key] = settings[key]
|
|
|
|
turn_number = 1
|
|
era = 0
|
|
var eras_raw: Dictionary = DataLoader.get_data("eras") as Dictionary
|
|
era_data = eras_raw.values() if eras_raw != null else []
|
|
era_data.sort_custom(
|
|
func(a: Dictionary, b: Dictionary) -> bool: return a.get("id", "") < b.get("id", "")
|
|
)
|
|
current_player_index = 0
|
|
players = []
|
|
layers = []
|
|
transit_nodes = []
|
|
ley_anchors = []
|
|
ley_edges = []
|
|
diplomacy = {}
|
|
npc_buildings = []
|
|
_npc_buildings_by_tile = {}
|
|
|
|
var settings_seed: int = int(game_settings.get("seed", 0))
|
|
if settings_seed != 0:
|
|
map_seed = settings_seed
|
|
game_rng = RandomNumberGenerator.new()
|
|
game_rng.seed = map_seed if map_seed != 0 else hash(Time.get_unix_time_from_system())
|
|
seed(game_rng.seed)
|
|
|
|
# Create primary map layer (index 0)
|
|
(
|
|
layers
|
|
. append(
|
|
{
|
|
"id": "primary",
|
|
"map": null,
|
|
"fog": null,
|
|
"units": [],
|
|
"settlements": [],
|
|
}
|
|
)
|
|
)
|
|
|
|
|
|
func get_current_player() -> RefCounted: # Returns Player
|
|
return get_player(current_player_index)
|
|
|
|
|
|
func get_player(index: int) -> RefCounted: # Returns Player
|
|
if index < 0 or index >= players.size():
|
|
push_warning("GameState: Invalid player index %d" % index)
|
|
return null
|
|
var p: Variant = players[index]
|
|
if p is PlayerScript:
|
|
return p
|
|
return null
|
|
|
|
|
|
func add_player(player: RefCounted) -> int: # Expects Player
|
|
player.index = players.size()
|
|
player.color = _player_color_for_index(player.index)
|
|
players.append(player)
|
|
return player.index
|
|
|
|
|
|
func _player_color_for_index(idx: int) -> Color:
|
|
## Route through ThemeAssets so the active palette variant (default or
|
|
## colorblind-safe) wins over the built-in PLAYER_COLORS fallback.
|
|
var tree: SceneTree = Engine.get_main_loop() as SceneTree
|
|
if tree != null and tree.root != null and tree.root.has_node("ThemeAssets"):
|
|
if ThemeAssets.has_palette_color(idx):
|
|
return ThemeAssets.get_player_color(idx)
|
|
if idx >= 0 and idx < PLAYER_COLORS.size():
|
|
return PLAYER_COLORS[idx]
|
|
return Color(0.6, 0.6, 0.6)
|
|
|
|
|
|
func create_player(
|
|
player_name: String,
|
|
race_id: String,
|
|
is_human: bool = true,
|
|
) -> RefCounted: # Returns Player
|
|
## Create and register a new Player with default state.
|
|
var player: RefCounted = PlayerScript.new()
|
|
player.player_name = player_name
|
|
player.race_id = race_id
|
|
player.is_human = is_human
|
|
var race_data: Dictionary = DataLoader.get_race(race_id)
|
|
var tier: String = str(race_data.get("growth_tier", ""))
|
|
if tier != "":
|
|
player.growth_tier = tier
|
|
add_player(player)
|
|
PersonalityAssignerScript.assign(player, game_rng)
|
|
return player
|
|
|
|
|
|
func get_era_name() -> String:
|
|
if era >= 0 and era < era_data.size():
|
|
return era_data[era].get("name", "unknown")
|
|
return "unknown"
|
|
|
|
|
|
func get_era_count() -> int:
|
|
return era_data.size()
|
|
|
|
|
|
func apply_ai_difficulty() -> void:
|
|
## Read game_settings["difficulty"] (id) and populate all ai_difficulty_* fields
|
|
## from difficulty.json ai_modifiers. Called after setup finishes.
|
|
var diff_id: String = str(game_settings.get("difficulty", "normal"))
|
|
var diff_data: Dictionary = DataLoader.get_data("difficulty") as Dictionary
|
|
if diff_data == null or diff_data.is_empty():
|
|
return
|
|
var entry: Dictionary = diff_data.get(diff_id, {}) as Dictionary
|
|
if entry.is_empty():
|
|
return
|
|
var mods: Dictionary = entry.get("ai_modifiers", {}) as Dictionary
|
|
if mods.is_empty():
|
|
return
|
|
ai_difficulty_modifier = float(mods.get("production_mult", 1.0))
|
|
ai_research_modifier = float(mods.get("research_mult", 1.0))
|
|
ai_gold_modifier = float(mods.get("gold_mult", 1.0))
|
|
ai_culture_modifier = float(mods.get("culture_mult", 1.0))
|
|
ai_luxury_modifier = float(mods.get("luxury_mult", 1.0))
|
|
ai_yield_per_turn_growth = float(mods.get("yield_per_turn_growth", 0.0))
|
|
ai_starting_gold_bonus = int(mods.get("starting_gold_bonus", 0))
|
|
ai_extra_starting_units = int(mods.get("extra_starting_units", 0))
|
|
ai_extra_unit_id = str(mods.get("extra_unit_id", "warrior"))
|
|
# Player handicap — symmetric inverse (Easy: player gets Hard-AI's bonuses).
|
|
var pmods: Dictionary = entry.get("player_modifiers", {}) as Dictionary
|
|
player_production_modifier = float(pmods.get("production_mult", 1.0))
|
|
player_research_modifier = float(pmods.get("research_mult", 1.0))
|
|
player_gold_modifier = float(pmods.get("gold_mult", 1.0))
|
|
player_culture_modifier = float(pmods.get("culture_mult", 1.0))
|
|
player_luxury_modifier = float(pmods.get("luxury_mult", 1.0))
|
|
player_yield_per_turn_growth = float(pmods.get("yield_per_turn_growth", 0.0))
|
|
print(
|
|
"GameState: difficulty=%s prod=%.2f research=%.2f gold_bonus=%d extra_units=%d"
|
|
% [
|
|
diff_id,
|
|
ai_difficulty_modifier,
|
|
ai_research_modifier,
|
|
ai_starting_gold_bonus,
|
|
ai_extra_starting_units
|
|
]
|
|
)
|
|
|
|
|
|
## Effective per-yield multiplier for a player on the current turn.
|
|
## Composes the static difficulty mult with the linear per-turn growth.
|
|
## yield_type: "production" | "research" | "gold" | "culture" | "luxury".
|
|
## Returns 1.0 for unknown yield types or null player.
|
|
##
|
|
## Per-player overrides (auto_play.gd batch testing) take precedence over the
|
|
## difficulty-derived value for production/research only — gold/culture/luxury
|
|
## use the global side (ai_X_modifier vs player_X_modifier).
|
|
func get_effective_yield_mult(player: RefCounted, yield_type: String) -> float:
|
|
if player == null:
|
|
return 1.0
|
|
var idx: int = -1
|
|
if player.get("index") != null:
|
|
idx = int(player.index)
|
|
var is_human: bool = bool(player.get("is_human") if player.get("is_human") != null else false)
|
|
# Per-player override path (production / research only — set by auto_play.gd
|
|
# AI_DIFFICULTY_P0/P1 batch testing).
|
|
if not is_human:
|
|
if yield_type == "production":
|
|
var per_p: float = float(ai_per_player_production_mult.get(idx, 0.0))
|
|
if per_p > 0.0:
|
|
return per_p + float(turn_number) * ai_yield_per_turn_growth
|
|
elif yield_type == "research":
|
|
var per_r: float = float(ai_per_player_research_mult.get(idx, 0.0))
|
|
if per_r > 0.0:
|
|
return per_r + float(turn_number) * ai_yield_per_turn_growth
|
|
# Side-resolution: AI gets ai_X_modifier, human gets player_X_modifier.
|
|
var base: float
|
|
var growth: float
|
|
if is_human:
|
|
base = _player_yield_mult_for(yield_type)
|
|
growth = player_yield_per_turn_growth
|
|
else:
|
|
base = _ai_yield_mult_for(yield_type)
|
|
growth = ai_yield_per_turn_growth
|
|
return base + float(turn_number) * growth
|
|
|
|
|
|
func _ai_yield_mult_for(yield_type: String) -> float:
|
|
match yield_type:
|
|
"production": return ai_difficulty_modifier
|
|
"research": return ai_research_modifier
|
|
"gold": return ai_gold_modifier
|
|
"culture": return ai_culture_modifier
|
|
"luxury": return ai_luxury_modifier
|
|
_: return 1.0
|
|
|
|
|
|
func _player_yield_mult_for(yield_type: String) -> float:
|
|
match yield_type:
|
|
"production": return player_production_modifier
|
|
"research": return player_research_modifier
|
|
"gold": return player_gold_modifier
|
|
"culture": return player_culture_modifier
|
|
"luxury": return player_luxury_modifier
|
|
_: return 1.0
|
|
|
|
|
|
func get_max_event_tier() -> int:
|
|
## Returns the max event tier allowed in the current era.
|
|
## When era_difficulty_correlation is disabled, returns 10 (uncapped).
|
|
if not game_settings.get("era_difficulty_correlation", true):
|
|
return 10
|
|
if era >= 0 and era < era_data.size():
|
|
return era_data[era].get("max_event_tier", 10)
|
|
return 10
|
|
|
|
|
|
func advance_era() -> void:
|
|
if era < era_data.size() - 1:
|
|
era += 1
|
|
EventBus.era_changed.emit(era, current_player_index)
|
|
|
|
|
|
func get_primary_layer() -> Dictionary:
|
|
if layers.is_empty():
|
|
return {}
|
|
return layers[0]
|
|
|
|
|
|
func get_layer(index: int) -> Dictionary:
|
|
if index < 0 or index >= layers.size():
|
|
push_warning("GameState: Invalid layer index %d" % index)
|
|
return {}
|
|
return layers[index]
|
|
|
|
|
|
func get_game_map() -> RefCounted: # Returns GameMap
|
|
## Convenience accessor for the primary layer's GameMap.
|
|
var primary: Dictionary = get_primary_layer()
|
|
if primary.is_empty():
|
|
return null
|
|
var map_ref: Variant = primary.get("map")
|
|
if map_ref is GameMapScript:
|
|
return map_ref
|
|
return null
|
|
|
|
|
|
## -- NPC building management --
|
|
|
|
|
|
func add_npc_building(building: RefCounted) -> void:
|
|
npc_buildings.append(building)
|
|
var key: String = "%d,%d" % [building.position.x, building.position.y]
|
|
if not _npc_buildings_by_tile.has(key):
|
|
_npc_buildings_by_tile[key] = []
|
|
_npc_buildings_by_tile[key].append(building)
|
|
|
|
|
|
func remove_npc_building(building: RefCounted) -> void:
|
|
npc_buildings.erase(building)
|
|
var key: String = "%d,%d" % [building.position.x, building.position.y]
|
|
if _npc_buildings_by_tile.has(key):
|
|
_npc_buildings_by_tile[key].erase(building)
|
|
|
|
|
|
func get_npc_buildings_at(pos: Vector2i) -> Array:
|
|
var key: String = "%d,%d" % [pos.x, pos.y]
|
|
return _npc_buildings_by_tile.get(key, [])
|
|
|
|
|
|
func get_npc_building_at(pos: Vector2i, type_filter: String = "") -> Variant:
|
|
## Returns the first NPC building at pos, optionally filtered by type_id. Null if none.
|
|
var buildings: Array = get_npc_buildings_at(pos)
|
|
for b: Variant in buildings:
|
|
if type_filter == "" or b.type_id == type_filter:
|
|
return b
|
|
return null
|
|
|
|
|
|
func get_all_npc_buildings_of_type(type_id: String) -> Array:
|
|
var result: Array = []
|
|
for b: Variant in npc_buildings:
|
|
if b.type_id == type_id:
|
|
result.append(b)
|
|
return result
|
|
|
|
|
|
func serialize() -> Dictionary:
|
|
var data: Dictionary = {
|
|
"current_theme": current_theme,
|
|
"turn_number": turn_number,
|
|
"era": era,
|
|
"current_player_index": current_player_index,
|
|
"game_settings": game_settings,
|
|
"wonders_built": wonders_built.duplicate(),
|
|
"rng_seed": game_rng.seed,
|
|
"rng_state": game_rng.state,
|
|
"map_seed": map_seed,
|
|
"players": [],
|
|
"layers": [],
|
|
"transit_nodes": transit_nodes,
|
|
"ley_anchors": _serialize_ley_anchors(),
|
|
"npc_buildings": _serialize_npc_buildings(),
|
|
}
|
|
|
|
# Serialize ascension rituals: {player_index_str -> ritual_dict}
|
|
var rituals_data: Dictionary = {}
|
|
for pi: Variant in ascension_rituals:
|
|
var ritual: Variant = ascension_rituals[pi]
|
|
if ritual != null and ritual.has_method("serialize"):
|
|
rituals_data[str(pi)] = ritual.serialize()
|
|
data["ascension_rituals"] = rituals_data
|
|
|
|
for player: Variant in players:
|
|
if player is PlayerScript:
|
|
data["players"].append(player.serialize())
|
|
|
|
for layer: Dictionary in layers:
|
|
data["layers"].append(_serialize_layer(layer))
|
|
|
|
return data
|
|
|
|
|
|
func deserialize(data: Dictionary) -> void:
|
|
current_theme = data.get("current_theme", "fantasy")
|
|
turn_number = data.get("turn_number", 1)
|
|
era = data.get("era", 0)
|
|
current_player_index = data.get("current_player_index", 0)
|
|
game_settings = data.get("game_settings", DEFAULT_SETTINGS.duplicate())
|
|
transit_nodes = data.get("transit_nodes", [])
|
|
wonders_built = data.get("wonders_built", {}).duplicate()
|
|
map_seed = data.get("map_seed", 0) as int
|
|
|
|
game_rng = RandomNumberGenerator.new()
|
|
var saved_rng_seed: int = data.get("rng_seed", 0) as int
|
|
var saved_rng_state: int = data.get("rng_state", 0) as int
|
|
if saved_rng_seed != 0:
|
|
game_rng.seed = saved_rng_seed
|
|
if saved_rng_state != 0:
|
|
game_rng.state = saved_rng_state
|
|
else:
|
|
game_rng.seed = map_seed if map_seed != 0 else hash(turn_number)
|
|
seed(game_rng.seed)
|
|
|
|
_deserialize_ley_anchors(data.get("ley_anchors", []))
|
|
_deserialize_npc_buildings(data.get("npc_buildings", []))
|
|
|
|
# Deserialize ascension rituals
|
|
var AscensionRitualScript: GDScript = preload(
|
|
"res://engine/src/modules/victory/ascension_ritual.gd"
|
|
)
|
|
ascension_rituals = {}
|
|
var rituals_raw: Variant = data.get("ascension_rituals", {})
|
|
if rituals_raw is Dictionary:
|
|
for key: Variant in rituals_raw:
|
|
var ritual_data: Variant = rituals_raw[key]
|
|
if ritual_data is Dictionary:
|
|
var ritual: RefCounted = AscensionRitualScript.new()
|
|
ritual.deserialize(ritual_data)
|
|
ascension_rituals[int(str(key))] = ritual
|
|
|
|
players = []
|
|
for player_data: Variant in data.get("players", []):
|
|
if player_data is Dictionary:
|
|
var player: RefCounted = PlayerScript.new()
|
|
player.deserialize(player_data)
|
|
players.append(player)
|
|
|
|
layers = []
|
|
for layer_data: Variant in data.get("layers", []):
|
|
if layer_data is Dictionary:
|
|
layers.append(_deserialize_layer(layer_data))
|
|
|
|
|
|
func _serialize_layer(layer: Dictionary) -> Dictionary:
|
|
return _SerializationHelpers.serialize_layer(layer)
|
|
|
|
|
|
func _deserialize_layer(layer_data: Dictionary) -> Dictionary:
|
|
return _SerializationHelpers.deserialize_layer(layer_data)
|
|
|
|
|
|
## ------------------------------------------------------------------
|
|
## Magic system helpers
|
|
## ------------------------------------------------------------------
|
|
|
|
|
|
func get_player_wonders(player_index: int) -> Array:
|
|
## Return list of wonder IDs owned by this player.
|
|
var result: Array = []
|
|
for wonder_id: String in wonders_built:
|
|
if wonders_built[wonder_id] == player_index:
|
|
result.append(wonder_id)
|
|
return result
|
|
|
|
|
|
func wonder_exists(wonder_id: String) -> bool:
|
|
return wonders_built.has(wonder_id)
|
|
|
|
|
|
func build_wonder(wonder_id: String, player_index: int) -> bool:
|
|
## First-to-build mechanic. Returns false if already built by anyone.
|
|
if wonders_built.has(wonder_id):
|
|
return false
|
|
wonders_built[wonder_id] = player_index
|
|
return true
|
|
|
|
|
|
func get_player_era(_player_index: int) -> int:
|
|
## Return the current era (global). Era is shared — not per-player.
|
|
return era + 1 # era 0-indexed internally; design spec uses 1-indexed
|
|
|
|
|
|
func _serialize_ley_anchors() -> Array:
|
|
return _SerializationHelpers.serialize_ley_anchors(ley_anchors)
|
|
|
|
|
|
func _deserialize_ley_anchors(raw: Array) -> void:
|
|
ley_anchors = _SerializationHelpers.deserialize_ley_anchors(raw)
|
|
|
|
|
|
func _serialize_npc_buildings() -> Array:
|
|
return _SerializationHelpers.serialize_npc_buildings(npc_buildings)
|
|
|
|
|
|
func _deserialize_npc_buildings(raw: Array) -> void:
|
|
npc_buildings = []
|
|
_npc_buildings_by_tile = {}
|
|
for entry: Variant in raw:
|
|
if entry is Dictionary:
|
|
var b: RefCounted = BuildingScript.from_dict(entry)
|
|
add_npc_building(b)
|
|
|
|
|
|
func rebuild_layer_references() -> void:
|
|
## After deserialization, rebuild the primary layer's unit list
|
|
## from all player unit arrays.
|
|
var primary: Dictionary = get_primary_layer()
|
|
if primary.is_empty():
|
|
return
|
|
|
|
var all_units: Array = []
|
|
for player: Variant in players:
|
|
if player is PlayerScript:
|
|
for unit: Variant in player.units:
|
|
all_units.append(unit)
|
|
primary["units"] = all_units
|