extends Node ## All player and AI orders use this transaction boundary. signal changed(team: int) const TEAM_COUNT: int = 2 const GRID_SIZE: Vector2i = Vector2i(6, 8) const MAX_WORKERS: int = 8 const MAX_SENDS: int = 8 const SPARK_CAP: int = 400 var teams: Array = [] var current_wave: int = 0 var build_open: bool = false var _histories: Array = [[], []] var _next_ids: Array[int] = [1, 1] var _opened_waves: Dictionary = {} var _frozen_batches: Dictionary = {} var _settlements: Dictionary = {} func _ready() -> void: reset_match() func reset_match() -> void: teams.clear() for team: int in TEAM_COUNT: var empty_queue: Array[String] = [] teams.append({"faction": GameState.factions[team], "gold": 220, "sparks": 0, "income": 20, "workers": 1, "pending_workers": 0, "roster": [], "send_queue": empty_queue}) _histories = [[], []] _next_ids = [1, 1] _opened_waves.clear() _frozen_batches.clear() _settlements.clear() current_wave = 0 build_open = false for team: int in TEAM_COUNT: changed.emit(team) func get_team(team: int) -> Dictionary: if not _valid_team(team): return {} return teams[team].duplicate(true) func open_build(wave: int) -> void: # Repeated phase callbacks never produce more resources or unlock a frozen wave. if wave < 1 or wave > DataCatalog.wave_count() or _opened_waves.has(wave): return if wave != current_wave + 1 or (current_wave > 0 and not _frozen_batches.has(current_wave)): return current_wave = wave build_open = true _opened_waves[wave] = true _histories = [[], []] for team: int in TEAM_COUNT: var state: Dictionary = teams[team] state["workers"] += int(state["pending_workers"]) state["pending_workers"] = 0 state["sparks"] = mini(SPARK_CAP, int(state["sparks"]) + 10 * int(state["workers"])) changed.emit(team) func freeze(wave: int) -> Array: if _frozen_batches.has(wave): return _frozen_batches[wave].duplicate(true) if not build_open or wave != current_wave: return [[], []] # Close commands before either side is committed or notified. build_open = false var batches: Array = [[], []] for team: int in TEAM_COUNT: var state: Dictionary = teams[team] batches[team] = state["send_queue"].duplicate() state["sparks"] -= reserved_sparks(team) for id: String in state["send_queue"]: state["income"] += int(DataCatalog.SENDS[id]["income_gain"]) state["send_queue"].clear() _frozen_batches[wave] = batches.duplicate(true) _histories = [[], []] for team: int in TEAM_COUNT: changed.emit(team) return batches func settle(team: int, wave: int, bounty: int) -> Dictionary: if not _valid_team(team) or not _frozen_batches.has(wave): return {} var key: String = "%d:%d" % [team, wave] if _settlements.has(key): return _settlements[key].duplicate(true) # An old wave cannot be first settled after a newer wave has started. if current_wave != wave or build_open: return {} var state: Dictionary = teams[team] var base_gold: int = 55 + 3 * (wave - 1) var earned_bounty: int = maxi(0, bounty) var total: int = base_gold + int(state["income"]) + earned_bounty state["gold"] += total var report: Dictionary = {"team": team, "wave": wave, "base_gold": base_gold, "income": state["income"], "bounty": earned_bounty, "total_gold": total, "total": total, "gold": state["gold"]} _settlements[key] = report.duplicate(true) changed.emit(team) return report func reserved_sparks(team: int) -> int: if not _valid_team(team): return 0 var reserved: int = 0 for id: String in teams[team]["send_queue"]: reserved += int(DataCatalog.SENDS[id]["cost"]) return reserved func worker_cost(team: int) -> int: if not _valid_team(team): return 0 var owned: int = int(teams[team]["workers"]) + int(teams[team]["pending_workers"]) return 70 + 20 * (owned - 1) func send_budget(wave: int) -> int: return 60 + 15 * (wave - 1) if wave >= 1 and wave <= DataCatalog.wave_count() else 0 func apply_command(team: int, command: Dictionary) -> Dictionary: if not _valid_team(team): return _result(false, "Ungültiges Team.") if not build_open: return _result(false, "Befehle sind nur in der Bauphase möglich.") var kind: String = str(command.get("type", "")) if kind == "undo": if _histories[team].is_empty(): return _result(false, "Keine Aktion zum Rückgängigmachen.") teams[team] = _histories[team].pop_back() changed.emit(team) return _result(true, "Letzte Aktion zurückgenommen.") var before: Dictionary = teams[team].duplicate(true) var result: Dictionary = _execute(team, kind, command) if result["ok"]: _histories[team].append(before) changed.emit(team) return result func _execute(team: int, kind: String, command: Dictionary) -> Dictionary: var state: Dictionary = teams[team] var roster: Array = state["roster"] match kind: "build": var type_id: String = str(command.get("type_id", "")) if not DataCatalog.faction_ids(str(state["faction"])).has(type_id): return _result(false, "Nur Basiseinheiten können gekauft werden.") if not _valid_cell(command.get("cell")): return _result(false, "Wähle ein Feld im 6 × 8 Baufeld.") var cell: Vector2i = command["cell"] if _occupied(roster, cell): return _result(false, "Dieses Feld ist bereits belegt.") var definition: Dictionary = DataCatalog.get_unit(type_id) var cost: int = int(definition["cost"]) if int(state["gold"]) < cost: return _missing_gold(cost - int(state["gold"])) state["gold"] -= cost # IDs remain monotonic across undo; team namespaces prevent collisions. var uid: int = team * 1000000 + _next_ids[team] _next_ids[team] += 1 roster.append({"uid": uid, "type_id": type_id, "cell": cell, "invested": cost, "target_policy": 0}) return _result(true, "%s gebaut." % definition["display_name"]) "upgrade", "sell", "move", "target": if typeof(command.get("uid")) != TYPE_INT: return _result(false, "Keine gültige Einheit ausgewählt.") var index: int = _unit_index(roster, int(command["uid"])) if index < 0: return _result(false, "Die ausgewählte Einheit gehört nicht zu diesem Team.") var entry: Dictionary = roster[index] if kind == "upgrade": var definition: Dictionary = DataCatalog.get_unit(entry["type_id"]) var upgrade_id: String = definition["upgrade_id"] if upgrade_id.is_empty(): return _result(false, "Diese Einheit ist bereits vollständig aufgewertet.") var upgrade: Dictionary = DataCatalog.get_unit(upgrade_id) var cost: int = int(upgrade["cost"]) if int(state["gold"]) < cost: return _missing_gold(cost - int(state["gold"])) state["gold"] -= cost entry["type_id"] = upgrade_id entry["invested"] += cost return _result(true, "Zu %s aufgewertet." % upgrade["display_name"]) if kind == "sell": var refund: int = floori(float(entry["invested"]) * 0.75) state["gold"] += refund roster.remove_at(index) return _result(true, "Einheit für %d Gold verkauft." % refund) if kind == "move": if not _valid_cell(command.get("cell")): return _result(false, "Wähle ein Feld im 6 × 8 Baufeld.") var cell: Vector2i = command["cell"] if _occupied(roster, cell): return _result(false, "Dieses Feld ist bereits belegt.") entry["cell"] = cell return _result(true, "Einheit versetzt.") if typeof(command.get("policy")) != TYPE_INT: return _result(false, "Ungültige Zielwahl.") var policy: int = int(command["policy"]) if policy < 0 or policy > 2: return _result(false, "Ungültige Zielwahl.") if policy == int(entry["target_policy"]): return _result(false, "Diese Zielwahl ist bereits aktiv.") entry["target_policy"] = policy return _result(true, "Zielwahl geändert.") "worker": if current_wave >= DataCatalog.wave_count(): return _result(false, "In der letzten Welle produzieren neue Arbeiter keine Funken mehr.") if int(state["workers"]) + int(state["pending_workers"]) >= MAX_WORKERS: return _result(false, "Das Maximum von 8 Arbeitern ist erreicht.") var cost: int = worker_cost(team) if int(state["gold"]) < cost: return _missing_gold(cost - int(state["gold"])) state["gold"] -= cost state["pending_workers"] += 1 return _result(true, "Funkenleser bestellt; aktiv ab der nächsten Bauphase.") "queue_send": if current_wave < 3: return _result(false, "Söldner sind ab Welle 3 verfügbar.") var type_id: String = str(command.get("type_id", "")) if not DataCatalog.SENDS.has(type_id): return _result(false, "Unbekannter Söldner.") if state["send_queue"].size() >= MAX_SENDS: return _result(false, "Höchstens 8 Söldner pro Welle.") var cost: int = int(DataCatalog.SENDS[type_id]["cost"]) var reserved: int = reserved_sparks(team) if reserved + cost > send_budget(current_wave): return _result(false, "Das Funkenbudget dieser Welle reicht nicht aus.") if reserved + cost > int(state["sparks"]): return _result(false, "Es fehlen %d freie Funken." % (reserved + cost - int(state["sparks"]))) state["send_queue"].append(type_id) return _result(true, "%s vorgemerkt." % DataCatalog.SENDS[type_id]["display_name"]) "cancel_send": if typeof(command.get("index")) != TYPE_INT: return _result(false, "Ungültiger Söldnerauftrag.") var index: int = int(command["index"]) if index < 0 or index >= state["send_queue"].size(): return _result(false, "Ungültiger Söldnerauftrag.") state["send_queue"].remove_at(index) return _result(true, "Söldnerauftrag entfernt; Funken wieder frei.") return _result(false, "Unbekannter Befehl.") func _valid_team(team: int) -> bool: return team >= 0 and team < TEAM_COUNT and teams.size() == TEAM_COUNT func _valid_cell(value: Variant) -> bool: if typeof(value) != TYPE_VECTOR2I: return false var cell: Vector2i = value return cell.x >= 0 and cell.x < GRID_SIZE.x and cell.y >= 0 and cell.y < GRID_SIZE.y func _occupied(roster: Array, cell: Vector2i) -> bool: for entry: Dictionary in roster: if entry["cell"] == cell: return true return false func _unit_index(roster: Array, uid: int) -> int: for index: int in roster.size(): if int(roster[index]["uid"]) == uid: return index return -1 func _missing_gold(missing: int) -> Dictionary: return _result(false, "Es fehlen %d Gold." % missing) func _result(ok: bool, message: String) -> Dictionary: return {"ok": ok, "message": message}