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