saumwacht-godot/scripts/autoload/economy_manager.gd
Martin Stickelbroeck c2b447610c Baseline: Saumwacht 0.15.0 (Phase 20A) vor dem Umbau zu Champions of Luneraria
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>
2026-09-27 20:00:36 +02:00

268 lines
10 KiB
GDScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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}