493 lines
13 KiB
GDScript
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
|