- Einzelspieler: Schwierigkeit, darunter nur die zwei Deckstärken, Gegnerzahl als Schieberegler, Starten und Zurück; kein Würfeln, die Gegnerdecks entstehen beim Start - Kampagne: nur noch Name, Elementpunkte und Knopf, mit Innenabstand in den Zeilen - Hotseat (Lokal zu mehreren) samt Übergabe-Vorhang, Auswahlbildschirm und Tests entfernt - Netzwerktests laufen nur mit DINGER_NETZ=1, weil die Bot-Prozesse Minuten brauchen Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPgKvy5W9WRNFHjkm5QsHx
749 lines
25 KiB
GDScript
749 lines
25 KiB
GDScript
class_name Game
|
|
extends RefCounted
|
|
## Spielregeln: Spieler, Wesen, Züge, Angriffe, Effekte, Feldkombos, Resonanz.
|
|
|
|
const MAX_BOARD := 5
|
|
const MAX_HAND := 8
|
|
const MAX_ENERGY := 10
|
|
const START_HAND := 4
|
|
# Jeder gegen jeden: alle Spieler haben dieselben Regeln und dasselbe Startleben.
|
|
const START_LIFE := 30
|
|
const MAX_OPPONENTS := 5
|
|
|
|
const RESONANCE_MAX := 4
|
|
const MAX_TRAPS := 2
|
|
|
|
# Merkmale der Kampagnen-Bosse (Player.perks).
|
|
const PERKS := {
|
|
"feuer_angriff": "Feuerwesen haben +1 Angriff",
|
|
"flink_angriff": "Flinke Wesen haben +1 Angriff",
|
|
"waechter_leben": "Wächter kommen mit +2 Leben",
|
|
"extra_karten": "Zieht 2 Karten pro Zug",
|
|
"leben_35": "Beginnt mit 35 Leben",
|
|
"zauber_schaden": "Schadenszauber machen +1 Schaden",
|
|
"schild_wesen": "Wesen kommen mit Schild",
|
|
"alle_flink": "Alle Wesen sind flink",
|
|
"start_energie": "Beginnt mit 3 Energie",
|
|
"resonanz_doppelt": "Resonanz zählt doppelt",
|
|
}
|
|
|
|
## Für wiederholbare Tests: 0 = echter Zufall, sonst Startwert (zählt je Aufruf hoch).
|
|
static var test_seed := 0
|
|
|
|
|
|
static func make_rng() -> RandomNumberGenerator:
|
|
var r := RandomNumberGenerator.new()
|
|
if test_seed != 0:
|
|
r.seed = test_seed
|
|
test_seed += 1
|
|
else:
|
|
r.randomize()
|
|
return r
|
|
|
|
const DECK_SIZE := 20
|
|
const MAX_COPIES := 3
|
|
const MAX_DECK_ELEMENTS := 2
|
|
|
|
|
|
class Creature extends RefCounted:
|
|
var card: Cards.Card
|
|
var owner_ref: WeakRef # schwache Referenz: kein Zyklus Spieler <-> Wesen
|
|
var name: String
|
|
var uid: int
|
|
var attack: int
|
|
var resonance := 0 # +1, wenn ein anderes eigenes Wesen ein Element teilt
|
|
var health: int
|
|
var max_health: int
|
|
var shield: bool
|
|
var frozen := false
|
|
var ready: bool
|
|
var attacks_left: int
|
|
|
|
func _init(c: Cards.Card, o, id: int) -> void:
|
|
card = c
|
|
owner_ref = weakref(o)
|
|
name = c.name
|
|
uid = id
|
|
attack = c.attack
|
|
health = c.health
|
|
max_health = c.health
|
|
shield = "Schild" in c.keywords
|
|
ready = "Flink" in c.keywords
|
|
attacks_left = attacks_per_turn() if ready else 0
|
|
|
|
func owner():
|
|
return owner_ref.get_ref()
|
|
|
|
## Angriffswert inklusive Resonanz und Boss-Merkmalen.
|
|
func power() -> int:
|
|
var v := attack + resonance
|
|
var o = owner()
|
|
if o != null:
|
|
if "feuer_angriff" in o.perks and "Feuer" in card.elements:
|
|
v += 1
|
|
if "flink_angriff" in o.perks and "Flink" in card.keywords:
|
|
v += 1
|
|
return v
|
|
|
|
func attacks_per_turn() -> int:
|
|
return 2 if "Doppelschlag" in card.keywords else 1
|
|
|
|
func taunt() -> bool:
|
|
return "Wächter" in card.keywords
|
|
|
|
func can_attack() -> bool:
|
|
return ready and attacks_left > 0 and not frozen and power() > 0
|
|
|
|
func clone(new_owner) -> Creature:
|
|
var c := Creature.new(card, new_owner, uid)
|
|
c.attack = attack
|
|
c.resonance = resonance
|
|
c.health = health
|
|
c.max_health = max_health
|
|
c.shield = shield
|
|
c.frozen = frozen
|
|
c.ready = ready
|
|
c.attacks_left = attacks_left
|
|
return c
|
|
|
|
|
|
class Player extends RefCounted:
|
|
var name: String
|
|
var title: String
|
|
var is_human: bool
|
|
var life: int
|
|
var max_life: int
|
|
var deck: Array = []
|
|
var hand: Array = []
|
|
var board: Array = []
|
|
var energy := 0
|
|
var max_energy := 0
|
|
var fatigue := 0
|
|
var start_bonus := 0 # einmalige Extra-Energie im ersten Zug (Ausgleich fürs Nachziehen)
|
|
var traps: Array = [] # verdeckt liegende Fallenkarten
|
|
var order := 0 # Platz in der Zugreihenfolge
|
|
var perks: Array = [] # Boss-Merkmale (siehe PERKS)
|
|
var turns := 0 # begonnene eigene Züge
|
|
var mulligan_done := false
|
|
|
|
func _init(n: String, d: Array, h: bool, l: int, t: String = "") -> void:
|
|
name = n
|
|
title = t
|
|
is_human = h
|
|
life = l
|
|
max_life = l
|
|
deck = d.duplicate()
|
|
|
|
func alive() -> bool:
|
|
return life > 0
|
|
|
|
func clone() -> Player:
|
|
var p := Player.new(name, deck, is_human, life, title)
|
|
p.max_life = max_life
|
|
p.hand = hand.duplicate()
|
|
p.energy = energy
|
|
p.max_energy = max_energy
|
|
p.fatigue = fatigue
|
|
p.start_bonus = start_bonus
|
|
p.traps = traps.duplicate()
|
|
p.order = order
|
|
p.perks = perks.duplicate()
|
|
p.turns = turns
|
|
p.mulligan_done = mulligan_done
|
|
for c in board:
|
|
p.board.append(c.clone(p))
|
|
return p
|
|
|
|
|
|
var human: Player # der (erste) menschliche Spieler
|
|
var humans: Array = [] # alle menschlichen Spieler (im Netz mehrere)
|
|
var opponents: Array = [] # alle außer dem ersten Menschen
|
|
var players: Array = [] # in Zugreihenfolge
|
|
var winner_player: Player = null
|
|
var rng: RandomNumberGenerator
|
|
var turn := 0
|
|
var current_idx := 0
|
|
var log: Array[String] = []
|
|
var events: Array = [] # strukturierte Ereignisse für Animationen
|
|
var winner := "" # "", "human" oder "opponents"
|
|
var quiet := false # in KI-Kopien: kein Verlauf, keine Ereignisse
|
|
var next_uid := 1
|
|
|
|
|
|
func _init(h: Player, opps: Array, r: RandomNumberGenerator = null) -> void:
|
|
human = h
|
|
opponents = opps
|
|
players = [h] + opps
|
|
humans = players.filter(func(p): return p.is_human)
|
|
if r == null:
|
|
r = make_rng()
|
|
rng = r
|
|
|
|
|
|
func current() -> Player:
|
|
return players[current_idx]
|
|
|
|
|
|
func say(t: String) -> void:
|
|
if not quiet:
|
|
log.append(t)
|
|
|
|
|
|
func emit(ev: Dictionary) -> void:
|
|
if not quiet:
|
|
events.append(ev)
|
|
|
|
|
|
func ref_of(target) -> Dictionary:
|
|
if target is Creature:
|
|
return {"kind": "creature", "uid": target.uid}
|
|
return {"kind": "player", "index": players.find(target)}
|
|
|
|
|
|
## Jeder gegen jeden: alle anderen lebenden Spieler sind Gegner.
|
|
func enemies_of(p: Player) -> Array:
|
|
return players.filter(func(o): return o != p and o.alive())
|
|
|
|
|
|
func find_creature(uid: int) -> Creature:
|
|
for p in players:
|
|
for c in p.board:
|
|
if c.uid == uid:
|
|
return c
|
|
return null
|
|
|
|
|
|
## Unabhängige Kopie des Spielzustands (für die KI-Vorausschau).
|
|
func clone() -> Game:
|
|
var copies := []
|
|
for p in players:
|
|
copies.append(p.clone())
|
|
var idx := players.find(human)
|
|
var g := Game.new(copies[idx], copies.slice(0, idx) + copies.slice(idx + 1), RandomNumberGenerator.new())
|
|
g.players = copies # gleiche Zugreihenfolge wie das Original
|
|
g.turn = turn
|
|
g.current_idx = current_idx
|
|
g.winner = winner
|
|
g.next_uid = next_uid
|
|
g.quiet = true
|
|
return g
|
|
|
|
|
|
## Kopie aus Sicht von p: fremde Fallen sind unbekannt und fehlen (die KI schummelt nicht).
|
|
func clone_for(p: Player) -> Game:
|
|
var g := clone()
|
|
var p2: Player = g.players[players.find(p)]
|
|
for q in g.players:
|
|
if q != p2:
|
|
q.traps = []
|
|
return g
|
|
|
|
|
|
# --- Zugablauf ---------------------------------------------------------------
|
|
|
|
func start(fixed_order: bool = false) -> void:
|
|
if not fixed_order:
|
|
shuffle(players, rng) # zufällige Zugreihenfolge
|
|
for i in players.size():
|
|
var p: Player = players[i]
|
|
p.order = i
|
|
if "start_energie" in p.perks:
|
|
p.max_energy = 2
|
|
shuffle(p.deck, rng)
|
|
# Wer nicht beginnt, startet mit einer Karte mehr und hat im ersten Zug
|
|
# einmalig 1 Energie extra (Ausgleich für den Startvorteil).
|
|
draw(p, START_HAND + (0 if i == 0 else 1))
|
|
p.start_bonus = 0 if i == 0 else 1
|
|
turn = 1
|
|
current_idx = 0
|
|
say("Zugreihenfolge: " + ", ".join(players.map(func(p): return p.name)))
|
|
for p in players:
|
|
for perk in p.perks:
|
|
say("%s: %s." % [p.name, PERKS[perk]])
|
|
begin_turn(current())
|
|
|
|
|
|
func draw(p: Player, n: int = 1) -> void:
|
|
for i in n:
|
|
if p.deck.is_empty():
|
|
p.fatigue += 1
|
|
say("%s hat keine Karten mehr und erleidet %d Schaden." % [p.name, p.fatigue])
|
|
damage(p, p.fatigue)
|
|
continue
|
|
var card = p.deck.pop_back()
|
|
if p.hand.size() >= MAX_HAND:
|
|
say("%s: Hand voll, %s verbrennt." % [p.name, card.name])
|
|
else:
|
|
p.hand.append(card)
|
|
check_end()
|
|
|
|
|
|
func begin_turn(p: Player) -> void:
|
|
p.max_energy = mini(MAX_ENERGY, p.max_energy + 1)
|
|
p.energy = p.max_energy
|
|
if p.start_bonus > 0:
|
|
p.energy += p.start_bonus
|
|
say("%s erhält %d Energie extra für den späteren Start." % [p.name, p.start_bonus])
|
|
p.start_bonus = 0
|
|
for c in p.board:
|
|
c.ready = true
|
|
if c.frozen:
|
|
c.frozen = false
|
|
c.attacks_left = 0
|
|
say("%s taut auf und setzt aus." % c.name)
|
|
else:
|
|
c.attacks_left = c.attacks_per_turn()
|
|
p.turns += 1
|
|
say("--- Zug %d: %s ---" % [turn, p.name])
|
|
emit({"type": "turn", "player": players.find(p)})
|
|
draw(p, 2 if "extra_karten" in p.perks else 1)
|
|
|
|
|
|
## Tauscht einmalig im ersten eigenen Zug Karten der Hand: zurück ins Deck, mischen, nachziehen.
|
|
func mulligan(p: Player, cards: Array) -> String:
|
|
if p.mulligan_done:
|
|
return "Schon getauscht"
|
|
if p != current() or p.turns != 1:
|
|
return "Nur im ersten eigenen Zug"
|
|
p.mulligan_done = true
|
|
var n := 0
|
|
for c in cards:
|
|
if c in p.hand:
|
|
p.hand.erase(c)
|
|
p.deck.append(c)
|
|
n += 1
|
|
if n > 0:
|
|
shuffle(p.deck, rng)
|
|
draw(p, n)
|
|
say(("%s tauscht 1 Karte." % p.name if n == 1 else "%s tauscht %d Karten." % [p.name, n]) if n > 0 else "%s behält die Hand." % p.name)
|
|
emit({"type": "mulligan", "player": players.find(p), "count": n})
|
|
return ""
|
|
|
|
|
|
func end_turn() -> void:
|
|
if winner != "":
|
|
return
|
|
var n := players.size()
|
|
for i in range(1, n + 1):
|
|
var idx := (current_idx + i) % n
|
|
if players[idx].alive():
|
|
if idx <= current_idx:
|
|
turn += 1
|
|
current_idx = idx
|
|
break
|
|
begin_turn(current())
|
|
|
|
|
|
# --- Karten spielen ----------------------------------------------------------
|
|
|
|
func can_play(p: Player, card: Cards.Card) -> Array:
|
|
if winner != "" or p != current():
|
|
return [false, "Nicht dein Zug"]
|
|
if p.energy < card.cost:
|
|
return [false, "Nicht genug Energie"]
|
|
if card.is_creature() and p.board.size() >= MAX_BOARD:
|
|
return [false, "Dein Feld ist voll"]
|
|
if card.is_trap() and p.traps.size() >= MAX_TRAPS:
|
|
return [false, "Höchstens %d Fallen" % MAX_TRAPS]
|
|
var tt := card.target_type()
|
|
if tt != "" and not card.is_creature() and valid_targets(p, tt).is_empty():
|
|
return [false, "Kein gültiges Ziel"]
|
|
return [true, ""]
|
|
|
|
|
|
func valid_targets(p: Player, target_type: String) -> Array:
|
|
var enemies := enemies_of(p)
|
|
var creatures := []
|
|
for e in enemies:
|
|
creatures.append_array(e.board)
|
|
match target_type:
|
|
"enemy_creature":
|
|
return creatures
|
|
"enemy_any":
|
|
return creatures + enemies
|
|
"friendly_creature":
|
|
return p.board.duplicate()
|
|
return []
|
|
|
|
|
|
## Spielt eine Karte. Gibt "" zurück oder eine Fehlermeldung.
|
|
func play_card(p: Player, card: Cards.Card, target = null) -> String:
|
|
var ok := can_play(p, card)
|
|
if not ok[0]:
|
|
return ok[1]
|
|
var tt := card.target_type()
|
|
if tt != "":
|
|
var valid := valid_targets(p, tt)
|
|
if not valid.is_empty() and not (target in valid):
|
|
return "Ungültiges Ziel"
|
|
if valid.is_empty():
|
|
target = null
|
|
p.hand.erase(card)
|
|
p.energy -= card.cost
|
|
if card.is_trap():
|
|
p.traps.append(card)
|
|
say("%s legt eine Falle." % p.name)
|
|
emit({"type": "trap_set", "player": players.find(p)})
|
|
check_end()
|
|
return ""
|
|
say("%s spielt %s." % [p.name, card.name])
|
|
var cr: Creature = null
|
|
var bonus := 0
|
|
if card.is_creature():
|
|
cr = _enter(p, card)
|
|
else:
|
|
emit({"type": "spell", "player": players.find(p), "card": card})
|
|
if "zauber_schaden" in p.perks:
|
|
bonus = 1
|
|
for fx in card.effects:
|
|
resolve(p, fx, target, cr, bonus)
|
|
if cr != null and cr in p.board:
|
|
for e in enemies_of(p):
|
|
if not (cr in p.board):
|
|
break
|
|
trigger_trap(e, "creature_played", cr)
|
|
if cr != null and cr in p.board:
|
|
trigger_field_combos(p, cr)
|
|
update_resonance()
|
|
cleanup()
|
|
check_end()
|
|
return ""
|
|
|
|
|
|
## Feldkombo: das neue Wesen bildet mit dem ersten eigenen Wesen aus anderem
|
|
## Element eine Kombo; pro Ausspielen löst höchstens eine Kombo aus.
|
|
func trigger_field_combos(p: Player, cr: Creature) -> void:
|
|
for other in p.board:
|
|
if other == cr:
|
|
continue
|
|
for e1 in cr.card.elements:
|
|
for e2 in other.card.elements:
|
|
if e1 == e2:
|
|
continue
|
|
var name := Cards.combo_name([e1, e2])
|
|
say("Feldkombo %s: %s." % [name, Cards.FIELD_COMBO_TEXT[name]])
|
|
emit({"type": "combo", "name": name, "player": players.find(p), "uid": cr.uid})
|
|
resolve(p, Cards.field_combo_effect(name), null, cr)
|
|
return
|
|
|
|
|
|
## Löst die erste passende Falle von owner aus. Gibt true zurück, wenn der
|
|
## auslösende Angriff abgebrochen wird.
|
|
func trigger_trap(owner: Player, trigger: String, source: Creature) -> bool:
|
|
for trap in owner.traps:
|
|
if trap.trigger != trigger:
|
|
continue
|
|
owner.traps.erase(trap)
|
|
say("Falle von %s: %s!" % [owner.name, trap.name])
|
|
emit({"type": "trap", "player": players.find(owner), "card": trap, "source": ref_of(source)})
|
|
var cancel := false
|
|
for fx in trap.effects:
|
|
if fx.kind == "cancel":
|
|
cancel = true
|
|
else:
|
|
resolve(owner, fx, null, source)
|
|
cleanup()
|
|
return cancel
|
|
return false
|
|
|
|
|
|
## Wesen betritt das Feld; Boss-Merkmale wirken beim Eintritt.
|
|
func _enter(p: Player, card: Cards.Card) -> Creature:
|
|
var cr := Creature.new(card, p, next_uid)
|
|
next_uid += 1
|
|
if "waechter_leben" in p.perks and cr.taunt():
|
|
cr.health += 2
|
|
cr.max_health += 2
|
|
if "schild_wesen" in p.perks:
|
|
cr.shield = true
|
|
if "alle_flink" in p.perks:
|
|
cr.ready = true
|
|
cr.attacks_left = cr.attacks_per_turn()
|
|
p.board.append(cr)
|
|
emit({"type": "play", "player": players.find(p), "uid": cr.uid, "card": card})
|
|
return cr
|
|
|
|
|
|
## Setzt ein Wesen aufs Feld (Fallen, Effekte).
|
|
func summon(p: Player, card: Cards.Card) -> void:
|
|
if p.board.size() >= MAX_BOARD:
|
|
say("%s: kein Platz für %s." % [p.name, card.name])
|
|
return
|
|
_enter(p, card)
|
|
say("%s beschwört %s." % [p.name, card.name])
|
|
update_resonance()
|
|
|
|
|
|
## Resonanz je Anzahl Partner mit gleichem Element: +1, +2, +3, +3.
|
|
static func resonance_for(partners: int) -> int:
|
|
return DeckGen.RESONANCE_STEPS[mini(partners, DeckGen.RESONANCE_STEPS.size() - 1)]
|
|
|
|
|
|
## Resonanz: abnehmender Angriffsbonus je Partner mit gleichem Element (siehe resonance_for).
|
|
func update_resonance() -> void:
|
|
for p in players:
|
|
for c in p.board:
|
|
var bonus := 0
|
|
for o in p.board:
|
|
if o != c and o.card.elements.any(func(e): return e in c.card.elements):
|
|
bonus += 1
|
|
c.resonance = resonance_for(bonus) * (2 if "resonanz_doppelt" in p.perks else 1)
|
|
|
|
|
|
func resolve(p: Player, fx: Cards.Effect, target, source: Creature = null, bonus: int = 0) -> void:
|
|
var enemies := enemies_of(p)
|
|
var enemy_creatures := []
|
|
for e in enemies:
|
|
enemy_creatures.append_array(e.board)
|
|
var targets := []
|
|
if fx.target in Cards.CHOSEN_TARGETS:
|
|
targets = [target] if target != null else []
|
|
elif fx.target == "all_enemy_creatures":
|
|
targets = enemy_creatures
|
|
elif fx.target == "all_enemy_heroes":
|
|
targets = enemies.duplicate()
|
|
elif fx.target == "all_enemies":
|
|
targets = enemy_creatures + enemies
|
|
elif fx.target == "all_friendly_creatures":
|
|
targets = p.board.duplicate()
|
|
elif fx.target == "self_hero":
|
|
targets = [p]
|
|
elif fx.target == "self_creature" or fx.target == "trigger_source":
|
|
targets = [source] if source != null else []
|
|
elif fx.target == "strongest_enemy_creature":
|
|
var best: Creature = null
|
|
for c in enemy_creatures:
|
|
if best == null or c.power() > best.power():
|
|
best = c
|
|
targets = [best] if best != null else []
|
|
elif fx.target == "weakest_enemy_hero":
|
|
var weakest: Player = null
|
|
for e in enemies:
|
|
if weakest == null or e.life < weakest.life:
|
|
weakest = e
|
|
targets = [weakest] if weakest != null else []
|
|
else:
|
|
targets = [null]
|
|
for t in targets:
|
|
match fx.kind:
|
|
"damage":
|
|
damage(t, fx.amount + bonus)
|
|
"heal":
|
|
t.life = mini(t.max_life, t.life + fx.amount)
|
|
say("%s heilt sich um %d." % [t.name, fx.amount])
|
|
emit({"type": "heal", "target": ref_of(t), "amount": fx.amount})
|
|
"draw":
|
|
draw(p, fx.amount)
|
|
"freeze":
|
|
t.frozen = true
|
|
t.attacks_left = 0
|
|
say("%s ist eingefroren." % t.name)
|
|
emit({"type": "freeze", "target": ref_of(t)})
|
|
"bounce":
|
|
var o = t.owner()
|
|
o.board.erase(t)
|
|
emit({"type": "bounce", "target": ref_of(t)})
|
|
if o.hand.size() < MAX_HAND:
|
|
o.hand.append(t.card)
|
|
say("%s kehrt auf die Hand zurück." % t.name)
|
|
else:
|
|
say("%s wird weggeweht (Hand voll)." % t.name)
|
|
"buff":
|
|
t.attack += fx.amount
|
|
t.health += fx.amount2
|
|
t.max_health += fx.amount2
|
|
say("%s erhält +%d/+%d." % [t.name, fx.amount, fx.amount2])
|
|
emit({"type": "buff", "target": ref_of(t), "text": "+%d/+%d" % [fx.amount, fx.amount2]})
|
|
"shield":
|
|
t.shield = true
|
|
say("%s erhält Schild." % t.name)
|
|
emit({"type": "buff", "target": ref_of(t), "text": "Schild"})
|
|
"summon":
|
|
summon(p, Cards.by_id(fx.card_id))
|
|
|
|
|
|
# --- Kampf -------------------------------------------------------------------
|
|
|
|
func can_attack(cr: Creature, target) -> Array:
|
|
if winner != "" or cr.owner() != current():
|
|
return [false, "Nicht dein Zug"]
|
|
if not cr.can_attack():
|
|
return [false, "%s kann nicht angreifen" % cr.name]
|
|
var tgt_owner = target.owner() if target is Creature else target
|
|
if not (tgt_owner in enemies_of(cr.owner())):
|
|
return [false, "Kein gültiges Ziel"]
|
|
var has_taunt := false
|
|
for c in tgt_owner.board:
|
|
if c.taunt():
|
|
has_taunt = true
|
|
if has_taunt and not (target is Creature and target.taunt()):
|
|
return [false, "Ein Wächter muss zuerst angegriffen werden"]
|
|
return [true, ""]
|
|
|
|
|
|
## Greift an. Gibt "" zurück oder eine Fehlermeldung.
|
|
func attack(cr: Creature, target) -> String:
|
|
var ok := can_attack(cr, target)
|
|
if not ok[0]:
|
|
return ok[1]
|
|
cr.attacks_left -= 1
|
|
say("%s greift %s an." % [cr.name, target.name])
|
|
emit({"type": "attack", "attacker": ref_of(cr), "target": ref_of(target)})
|
|
var tgt_owner: Player = target.owner() if target is Creature else target
|
|
var cancelled := trigger_trap(tgt_owner, "attacked", cr)
|
|
if cancelled or not (cr in cr.owner().board) or (target is Creature and not (target in tgt_owner.board)):
|
|
check_end()
|
|
return ""
|
|
damage(target, cr.power())
|
|
if target is Creature:
|
|
damage(cr, target.power())
|
|
cleanup()
|
|
check_end()
|
|
return ""
|
|
|
|
|
|
func damage(target, amount: int) -> void:
|
|
if amount <= 0:
|
|
return
|
|
if target is Creature:
|
|
if target.shield:
|
|
target.shield = false
|
|
say("Schild von %s blockt den Schaden." % target.name)
|
|
emit({"type": "blocked", "target": ref_of(target)})
|
|
return
|
|
target.health -= amount
|
|
else:
|
|
target.life -= amount
|
|
say("%s erleidet %d Schaden." % [target.name, amount])
|
|
emit({"type": "damage", "target": ref_of(target), "amount": amount})
|
|
|
|
|
|
func cleanup() -> void:
|
|
var died := false
|
|
for p in players:
|
|
for c in p.board.duplicate():
|
|
if c.health <= 0:
|
|
p.board.erase(c)
|
|
say("%s stirbt." % c.name)
|
|
emit({"type": "death", "target": ref_of(c)})
|
|
died = true
|
|
if died:
|
|
update_resonance()
|
|
|
|
|
|
## Ende: nur noch ein Spieler übrig, oder kein Mensch mehr am Tisch.
|
|
func check_end() -> void:
|
|
if winner != "":
|
|
return
|
|
var alive := players.filter(func(p): return p.alive())
|
|
var humans_alive := alive.filter(func(p): return p.is_human)
|
|
if humans_alive.is_empty():
|
|
winner = "opponents"
|
|
winner_player = alive[0] if alive.size() == 1 else null
|
|
say("Du wurdest besiegt." if humans.size() == 1 else "Alle Spieler sind besiegt, die KI gewinnt.")
|
|
elif alive.size() == 1:
|
|
winner = "human"
|
|
winner_player = alive[0]
|
|
say("Alle Gegner sind besiegt!" if humans.size() == 1 else "%s gewinnt!" % winner_player.name)
|
|
|
|
|
|
# --- Zustand für das Netzwerk (siehe NetState) ------------------------------------
|
|
|
|
func snapshot(viewer: int) -> Dictionary:
|
|
return NetState.snapshot(self, viewer)
|
|
|
|
|
|
static func from_snapshot(d: Dictionary) -> Game:
|
|
return NetState.from_snapshot(d)
|
|
|
|
|
|
# --- Deckregeln und Gegner ---------------------------------------------------
|
|
|
|
static func shuffle(arr: Array, r: RandomNumberGenerator) -> void:
|
|
for i in range(arr.size() - 1, 0, -1):
|
|
var j := r.randi_range(0, i)
|
|
var tmp = arr[i]
|
|
arr[i] = arr[j]
|
|
arr[j] = tmp
|
|
|
|
|
|
static func deck_elements(cards: Array) -> Array:
|
|
var out := []
|
|
for c in cards:
|
|
for e in c.elements:
|
|
if not (e in out):
|
|
out.append(e)
|
|
return out
|
|
|
|
|
|
## Prüft ein Deck. owned: Dictionary id -> Anzahl (null = keine Besitzprüfung).
|
|
static func validate_deck(cards: Array, owned = null) -> Array:
|
|
if cards.size() != DECK_SIZE:
|
|
return [false, "Deck braucht genau %d Karten (%d)" % [DECK_SIZE, cards.size()]]
|
|
var counts := {}
|
|
for c in cards:
|
|
counts[c.id] = counts.get(c.id, 0) + 1
|
|
for cid in counts:
|
|
if counts[cid] > MAX_COPIES:
|
|
return [false, "Höchstens %dx dieselbe Karte" % MAX_COPIES]
|
|
if owned != null and owned.get(cid, 0) < counts[cid]:
|
|
return [false, "Karte nicht oft genug in der Sammlung"]
|
|
if deck_elements(cards).size() > MAX_DECK_ELEMENTS:
|
|
return [false, "Höchstens %d Elemente im Deck" % MAX_DECK_ELEMENTS]
|
|
return [true, ""]
|
|
|
|
|
|
## rules: {"human_perks": [...], "everyone_perk": String, "start_life": int, "target_rating": float, "tier": String}
|
|
# --- Deckstärke und Gegner (siehe DeckGen) --------------------------------------
|
|
|
|
static func deck_rating(cards: Array, stats: Dictionary = {}) -> float:
|
|
return DeckGen.deck_rating(cards, stats)
|
|
|
|
|
|
static func build_opponent_deck(r: RandomNumberGenerator, elements: Array, bias: float = 1.0) -> Array:
|
|
return DeckGen.build_opponent_deck(r, elements, bias)
|
|
|
|
|
|
static func build_opponent_deck_rated(r: RandomNumberGenerator, elements: Array, target: float, tolerance: float = 0.12) -> Array:
|
|
return DeckGen.build_opponent_deck_rated(r, elements, target, tolerance)
|
|
|
|
|
|
static func random_opponent(r: RandomNumberGenerator, index: int, life: int = START_LIFE, target: float = 0.0, tier: String = "") -> Player:
|
|
return DeckGen.random_opponent(r, index, life, target, tier)
|
|
|
|
|
|
static func archetypes(tier: String = "") -> Array:
|
|
return DeckGen.archetypes(tier)
|
|
|
|
|
|
static func new_battle(deck: Array, n_opp: int, r: RandomNumberGenerator = null, rules: Dictionary = {}) -> Game:
|
|
assert(n_opp >= 1 and n_opp <= MAX_OPPONENTS, "1 bis 5 Gegner")
|
|
if r == null:
|
|
r = make_rng()
|
|
var target: float = float(rules.get("target_rating", 0.0))
|
|
var tier: String = str(rules.get("tier", ""))
|
|
var opps := []
|
|
for i in n_opp:
|
|
opps.append(random_opponent(r, i + 1, START_LIFE, target, tier))
|
|
return new_battle_with(deck, opps, r, false, rules)
|
|
|
|
|
|
static func new_battle_with(deck: Array, opps: Array, r: RandomNumberGenerator = null, fixed_order: bool = false, rules: Dictionary = {}) -> Game:
|
|
if r == null:
|
|
r = make_rng()
|
|
var human := Player.new("Du", deck, true, START_LIFE)
|
|
human.perks = Array(rules.get("human_perks", []))
|
|
var g := Game.new(human, opps, r)
|
|
var everyone: String = rules.get("everyone_perk", "")
|
|
for p in g.players:
|
|
if everyone != "" and not (everyone in p.perks):
|
|
p.perks.append(everyone)
|
|
if rules.has("start_life"):
|
|
p.life = int(rules.start_life)
|
|
p.max_life = p.life
|
|
elif "leben_35" in p.perks and p.life == START_LIFE:
|
|
p.life = 35
|
|
p.max_life = 35
|
|
g.start(fixed_order)
|
|
return g
|