Unveränderter Ausgangsstand inklusive Masterprompt und Konzeptbildern unter assets/. .godot/ und docs/screenshots/ bleiben laut .gitignore unversioniert; letztere sind unter ../backups gesichert. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
268 lines
10 KiB
GDScript
268 lines
10 KiB
GDScript
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}
|