Files
elements-autochess/scripts/core/PlayerState.gd
2026-09-16 13:21:45 +02:00

493 lines
13 KiB
GDScript

class_name PlayerState
extends RefCounted
## Zustand eines Spielers oder Bots: Gold, Leben, Bank, Board, Shop.
var pid: int = 0
var pname: String = "Spieler"
var is_bot: bool = false
var gold: int = Data.START_GOLD
var hp: int = Data.START_HP
var streak: int = 0 ## >0 Siegserie, <0 Niederlagenserie
var alive: bool = true
var last_won: bool = false
var placement: int = 0 ## Endplatzierung, 0 = noch im Spiel
var level: int = Data.START_LEVEL
var xp: int = 0
var shop_locked: bool = false ## eingefrorener Shop bleibt über die Runde
var last_element: int = -1 ## zuletzt gewählte Spezialisierung für Schnellkauf
var last_sale: Dictionary = {} ## letzter Verkauf, für Rückgängig
## Statistik für den Endbildschirm.
var stats := {"wins": 0, "losses": 0, "gold_earned": 0, "spent": 0, "max_star": 1}
var bench: Array = [] ## Avatar oder null
var board: Dictionary = {} ## Vector2i -> Avatar
var shop: Array = [] ## ShopEntry
## Elemente, die dieser Spieler aufgestellt hat. Nur die zeigt sein Shop.
var unlocked: Array[bool] = []
func _init(p_pid: int = 0, p_name: String = "Spieler", p_bot: bool = false) -> void:
pid = p_pid
pname = p_name
is_bot = p_bot
for i in Data.BENCH_SLOTS:
bench.append(null)
for i in Data.ELEMENT_COUNT:
unlocked.append(false)
func unlock(element: int) -> void:
if element >= 0:
unlocked[element] = true
## Anzahl der Elemente, die dieser Spieler aufgestellt hat.
func unlocked_count() -> int:
var n := 0
for e in Data.ELEMENT_COUNT:
if unlocked[e]:
n += 1
return n
## Elemente, die im eigenen Shop erscheinen können: selbst aufgestellt und
## im geteilten Vorrat noch verfügbar.
func shop_elements(pool: Pool) -> Array[int]:
var out: Array[int] = []
for e in Data.ELEMENT_COUNT:
if unlocked[e] and pool.element_left[e] > 0:
out.append(e)
return out
# --- Shop ------------------------------------------------------------------
func roll_shop(pool: Pool, rng: RandomNumberGenerator) -> void:
shop.clear()
var active := shop_elements(pool)
# Je mehr eigene Elemente aufgestellt sind, desto seltener kommt Neutral.
var neutral := Data.neutral_chance(unlocked_count())
for i in Data.SHOP_SLOTS:
var element := -1
if not active.is_empty() and rng.randf() >= neutral:
element = active[rng.randi_range(0, active.size() - 1)]
shop.append(ShopEntry.new(rng.randi_range(0, 2), element))
func reroll(pool: Pool, rng: RandomNumberGenerator) -> bool:
if gold < Data.REROLL_COST:
return false
gold -= Data.REROLL_COST
shop_locked = false
roll_shop(pool, rng)
return true
## Neue Runde: der Shop wird nur neu gefüllt, wenn er nicht eingefroren ist.
func new_round_shop(pool: Pool, rng: RandomNumberGenerator) -> void:
if shop_locked:
return
roll_shop(pool, rng)
func free_bench_slot() -> int:
for i in bench.size():
if bench[i] == null:
return i
return -1
## Kauft einen Avatar. Neutrale brauchen die beim Kauf gewählte Spezialisierung.
func buy(index: int, pool: Pool, chosen_element: int = -1) -> bool:
if index < 0 or index >= shop.size():
return false
var entry: ShopEntry = shop[index]
if entry.sold or gold < entry.price():
return false
var element := entry.element if entry.element >= 0 else chosen_element
if element < 0 or element >= Data.ELEMENT_COUNT:
return false
var slot := free_bench_slot()
if slot < 0:
return false
if not pool.take(element):
return false
gold -= entry.price()
stats["spent"] += entry.price()
bench[slot] = entry.to_avatar(element)
entry.sold = true
if entry.element < 0:
last_element = element
return true
func sell_bench(index: int, pool: Pool) -> bool:
if index < 0 or index >= bench.size() or bench[index] == null:
return false
var a: Avatar = bench[index]
pool.return_avatar(a)
var refund := maxi(1, a.invested)
gold += refund
bench[index] = null
last_sale = {"avatar": a, "refund": refund, "index": index}
return true
## Macht den letzten Bankverkauf rückgängig, solange Gold und Platz da sind.
func undo_sell(pool: Pool) -> bool:
if last_sale.is_empty():
return false
var a: Avatar = last_sale["avatar"]
var refund: int = last_sale["refund"]
if gold < refund:
return false
var idx: int = last_sale["index"]
if bench[idx] != null:
idx = free_bench_slot()
if idx < 0:
return false
# Bausteine wieder aus dem Vorrat nehmen; ist er leer, geht es nicht.
if not pool.take(a.elements[0]):
return false
if a.star >= 2:
for i in 2:
pool.take(a.elements[0])
if a.star >= 3:
for e in a.elements:
for i in 3:
pool.take(e)
gold -= refund
bench[idx] = a
last_sale = {}
return true
func sell_board(hex: Vector2i, pool: Pool) -> bool:
if not board.has(hex):
return false
var a: Avatar = board[hex]
pool.return_avatar(a)
gold += maxi(1, a.invested)
board.erase(hex)
return true
# --- Aufstellen ------------------------------------------------------------
func team_slots() -> int:
return level
func can_place() -> bool:
return board.size() < level
func gain_xp(amount: int) -> void:
xp += amount
while level < Data.MAX_LEVEL and xp >= Data.xp_needed(level):
xp -= Data.xp_needed(level)
level += 1
if level >= Data.MAX_LEVEL:
xp = 0
func can_buy_xp() -> bool:
return level < Data.MAX_LEVEL and gold >= Data.XP_BUY_COST
func buy_xp() -> bool:
if not can_buy_xp():
return false
gold -= Data.XP_BUY_COST
gain_xp(Data.XP_BUY_AMOUNT)
return true
## Setzt einen Avatar von der Bank aufs Board.
func place(bench_index: int, hex: Vector2i) -> bool:
if bench_index < 0 or bench_index >= bench.size() or bench[bench_index] == null:
return false
if board.has(hex) or not _valid_hex(hex):
return false
if not can_place():
return false
var a: Avatar = bench[bench_index]
board[hex] = a
bench[bench_index] = null
unlock(a.elements[0])
return true
func unplace(hex: Vector2i) -> bool:
if not board.has(hex):
return false
var slot := free_bench_slot()
if slot < 0:
return false
bench[slot] = board[hex]
board.erase(hex)
return true
func move_on_board(from_hex: Vector2i, to_hex: Vector2i) -> bool:
if not board.has(from_hex) or not _valid_hex(to_hex):
return false
var a: Avatar = board[from_hex]
if board.has(to_hex):
board[from_hex] = board[to_hex]
else:
board.erase(from_hex)
board[to_hex] = a
return true
## Verschiebt einen Avatar zwischen Bank und Board, in beide Richtungen.
## Ist das Ziel belegt, tauschen die beiden Avatare die Plätze.
## src und dst sind {"kind": "board"/"bench", "hex": Vector2i, "index": int}.
func move_slot(src: Dictionary, dst: Dictionary) -> bool:
if src.is_empty() or dst.is_empty() or src == dst:
return false
var sk: String = src["kind"]
var dk: String = dst["kind"]
if sk == "board" and dk == "board":
return move_on_board(src["hex"], dst["hex"])
if sk == "bench" and dk == "bench":
var i: int = src["index"]
var j: int = dst["index"]
if bench[i] == null:
return false
var tmp = bench[j]
bench[j] = bench[i]
bench[i] = tmp
return true
if sk == "board" and dk == "bench":
var hex: Vector2i = src["hex"]
var j2: int = dst["index"]
if not board.has(hex):
return false
var unit: Avatar = board[hex]
if bench[j2] == null:
board.erase(hex)
else:
board[hex] = bench[j2]
bench[j2] = unit
return true
# bench -> board
var i3: int = src["index"]
var hex3: Vector2i = dst["hex"]
if bench[i3] == null or not _valid_hex(hex3):
return false
if not board.has(hex3) and not can_place():
return false
var unit3: Avatar = bench[i3]
if board.has(hex3):
bench[i3] = board[hex3]
else:
bench[i3] = null
board[hex3] = unit3
unlock(unit3.elements[0])
return true
func _valid_hex(h: Vector2i) -> bool:
return h.x >= 0 and h.x < Data.BOARD_COLS and h.y >= 0 and h.y < Data.BOARD_ROWS
# --- Aufwerten -------------------------------------------------------------
## Wie viel Futter pro Element auf der Bank liegt.
func fodder_counts() -> Dictionary:
var counts := {}
for a in bench:
if a == null or a.is_elementless() or a.star != 1:
continue
var e: int = a.elements[0]
counts[e] = counts.get(e, 0) + 1
return counts
func can_upgrade(hex: Vector2i) -> bool:
if not board.has(hex):
return false
var a: Avatar = board[hex]
var cost := a.upgrade_cost()
if cost.is_empty():
return false
var have := fodder_counts()
for e in cost:
if have.get(e, 0) < cost[e]:
return false
return true
## Fehlende Futter-Avatare pro Element (für die Anzeige).
func upgrade_missing(hex: Vector2i) -> Dictionary:
if not board.has(hex):
return {}
var a: Avatar = board[hex]
var cost := a.upgrade_cost()
var have := fodder_counts()
var miss := {}
for e in cost:
var m: int = cost[e] - have.get(e, 0)
if m > 0:
miss[e] = m
return miss
func available_new_elements(hex: Vector2i) -> Array[int]:
var out: Array[int] = []
if not board.has(hex):
return out
var a: Avatar = board[hex]
for e in Data.ELEMENT_COUNT:
if not a.has_element(e):
out.append(e)
return out
func upgrade(hex: Vector2i, new_element: int, pool: Pool) -> bool:
if not can_upgrade(hex):
return false
var a: Avatar = board[hex]
if a.has_element(new_element) or new_element < 0 or new_element >= Data.ELEMENT_COUNT:
return false
var cost := a.upgrade_cost()
var spent := 0
for e in cost:
var need: int = cost[e]
for i in bench.size():
if need <= 0:
break
var f = bench[i]
if f != null and f.star == 1 and not f.is_elementless() and f.elements[0] == e:
spent += f.invested
bench[i] = null
need -= 1
a.invested += spent
a.star += 1
a.elements.append(new_element)
unlock(new_element)
stats["max_star"] = maxi(stats["max_star"], a.star)
return true
# --- Traits ----------------------------------------------------------------
## Zählt aufgestellte Einheiten pro Element.
func trait_counts() -> Dictionary:
var counts := {}
for hex in board:
var a: Avatar = board[hex]
for e in a.elements:
counts[e] = counts.get(e, 0) + 1
return counts
func active_combos() -> Array:
var counts := trait_counts()
var out := []
for c in Data.COMBO_TRAITS:
if counts.get(c["a"], 0) >= Data.COMBO_MIN and counts.get(c["b"], 0) >= Data.COMBO_MIN:
out.append(c)
return out
# --- Runde -----------------------------------------------------------------
func round_income() -> int:
var g := Data.BASE_INCOME
if last_won:
g += Data.WIN_GOLD
g += Data.streak_gold(streak)
g += Data.interest(gold)
return g
func register_result(won: bool) -> void:
last_won = won
if won:
streak = maxi(1, streak + 1)
stats["wins"] += 1
else:
streak = mini(-1, streak - 1)
stats["losses"] += 1
## Nur Leben abziehen. Das Ausscheiden setzt Game._check_eliminations,
## damit die Platzierung stimmt.
func take_damage(dmg: int) -> void:
hp = maxi(0, hp - dmg)
func board_units() -> Array:
var out := []
for hex in board:
out.append(board[hex])
return out
# --- Speichern -------------------------------------------------------------
static func _avatar_to_dict(a: Avatar) -> Dictionary:
return {"role": a.role, "elements": Array(a.elements), "star": a.star, "invested": a.invested}
static func _avatar_from_dict(d: Dictionary) -> Avatar:
var els: Array[int] = []
for e in d["elements"]:
els.append(int(e))
var a := Avatar.new(int(d["role"]), els, int(d["star"]))
a.invested = int(d["invested"])
return a
func to_dict() -> Dictionary:
var b := []
for a in bench:
b.append(null if a == null else _avatar_to_dict(a))
var brd := []
for hex in board:
brd.append({"x": hex.x, "y": hex.y, "a": _avatar_to_dict(board[hex])})
var sh := []
for e in shop:
sh.append({"role": e.role, "element": e.element, "sold": e.sold})
return {
"pid": pid, "pname": pname, "is_bot": is_bot, "gold": gold, "hp": hp, "streak": streak,
"alive": alive, "last_won": last_won, "placement": placement, "level": level, "xp": xp,
"shop_locked": shop_locked, "last_element": last_element, "unlocked": Array(unlocked),
"bench": b, "board": brd, "shop": sh, "stats": stats.duplicate(),
}
static func from_dict(d: Dictionary) -> PlayerState:
var p := PlayerState.new(int(d["pid"]), d["pname"], d["is_bot"])
p.gold = int(d["gold"])
p.hp = int(d["hp"])
p.streak = int(d["streak"])
p.alive = d["alive"]
p.last_won = d["last_won"]
p.placement = int(d["placement"])
p.level = int(d["level"])
p.xp = int(d["xp"])
p.shop_locked = d["shop_locked"]
p.last_element = int(d["last_element"])
for i in mini(p.unlocked.size(), d["unlocked"].size()):
p.unlocked[i] = d["unlocked"][i]
for i in mini(p.bench.size(), d["bench"].size()):
p.bench[i] = null if d["bench"][i] == null else _avatar_from_dict(d["bench"][i])
for entry in d["board"]:
p.board[Vector2i(int(entry["x"]), int(entry["y"]))] = _avatar_from_dict(entry["a"])
for entry in d["shop"]:
var se := ShopEntry.new(int(entry["role"]), int(entry["element"]))
se.sold = entry["sold"]
p.shop.append(se)
for k in d["stats"]:
p.stats[k] = int(d["stats"][k])
return p