update
Some checks failed
Tests / headless (push) Has been cancelled
Tests / fenster-schnell (push) Has been cancelled
Tests / fenster-voll (push) Has been cancelled
Tests / balance (push) Has been cancelled

This commit is contained in:
Team3
2026-10-02 19:57:02 +02:00
parent f0a8f18de8
commit 24a03ed1ee
31 changed files with 1866 additions and 2620 deletions

View File

@@ -1,27 +1,30 @@
class_name Game
extends RefCounted
## Spielregeln: Spieler, Wesen, Züge, Angriffe, Effekte, Feldkombos, Resonanz.
## Spielregeln: Duell, Mana je Zug, Ziehen aus dem gemeinsamen Stapel, verdeckte Wesen,
## Vier-Elemente-Regel, Verschmelzen und Verbessern.
const MAX_BOARD := 5
const MAX_HAND := 8
const MAX_ENERGY := 10 # Einkommen je Zug, höher steigt es nicht
const SHOP_SIZE := 5 # so viele Karten liegen im Laden
const REROLL_COST := 1
# Jeder gegen jeden: alle Spieler haben dieselben Regeln und dasselbe Startleben.
const MAX_HAND := 5
const START_MANA := 5 # Mana in Zug 1; jede Runde eins mehr, ohne Grenze; Rest verfällt
const DRAW_COST := 1 # Ziehen: drei Karten sehen, eine behalten
const OFFER_SIZE := 3
const START_OFFER := 5 # zu Beginn fünf sehen, drei behalten
const START_PICKS := 3
const START_LIFE := 30
const ELEMENT_STACK := 4 # so viele offene Wesen eines Elements auf dem Tisch: +1/+1, das geschlagene Element -1/-1
# 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",
"ziehen_gratis": "Ziehen kostet kein Mana",
"wesen_angriff": "Wesen kommen mit +1 Angriff",
"wesen_leben": "Wesen kommen mit +1 Leben",
"extra_mana": "Bekommt jeden Zug 1 Mana mehr",
"leben_35": "Beginnt mit 5 Leben mehr",
"schild_wesen": "Wesen kommen mit Schild",
"alle_flink": "Alle Wesen sind flink",
"start_energie": "Beginnt mit 3 Mana",
"resonanz_doppelt": "Resonanz zählt doppelt",
"verbessern_billig": "Eigenschaften kosten 1 weniger",
"start_energie": "Hat im ersten Zug 2 Mana mehr",
"element_doppelt": "Elementbonus zählt doppelt",
}
## Für wiederholbare Tests: 0 = echter Zufall, sonst Startwert (zählt je Aufruf hoch).
@@ -38,21 +41,18 @@ static func make_rng() -> RandomNumberGenerator:
return r
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 field_bonus := 0 # +1, wenn das Feld auf dem eigenen Element steht
var health: int
var max_health: int
var shield: bool
var frozen := false
var ready: bool
var attacks_left: int
var hidden := false # verdeckt gelegt: greift nicht an, zählt nicht für die Elementregel
var aura := 0 # Vier-Elemente-Regel: +1 oder -1 auf den Angriff (Leben wird direkt angepasst)
func _init(c: Cards.Card, o, id: int) -> void:
card = c
@@ -63,43 +63,35 @@ class Creature extends RefCounted:
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
attacks_left = attacks_per_turn() # frisch gelegte Wesen greifen sofort an
func owner():
return owner_ref.get_ref()
## Angriffswert inklusive Resonanz, Feld und Boss-Merkmalen.
## Angriffswert inklusive Elementbonus und Boss-Merkmalen.
func power() -> int:
var v := attack + resonance + field_bonus
var v := attack + aura
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
return attacks_left > 0 and not hidden and power() > 0
func clone(new_owner) -> Creature:
var c := Creature.new(card, new_owner, uid)
c.attack = attack
c.resonance = resonance
c.field_bonus = field_bonus
c.health = health
c.max_health = max_health
c.shield = shield
c.frozen = frozen
c.ready = ready
c.attacks_left = attacks_left
c.hidden = hidden
c.aura = aura
return c
@@ -109,33 +101,33 @@ class Player extends RefCounted:
var is_human: bool
var life: int
var max_life: int
var shop: Array = [] # SHOP_SIZE Plätze, gekaufte sind null
var hand: Array = []
var offer: Array = [] # offene Wahl: angebotene Karten (Start: 5, Ziehen: 3)
var picks_left := 0 # wie viele davon noch zu nehmen sind
var hand: Array = [] # eigene Exemplare (Cards.Card.copy)
var board: Array = []
var energy := 0 # Mana; nicht ausgegebenes bleibt liegen
var max_energy := 0 # Einkommen je Zug
var energy := 0 # Mana in diesem Zug; der Rest verfällt
var max_energy := 0 # Mana zu Zugbeginn (START_MANA + eigene Züge)
var start_bonus := 0 # einmaliges Extra-Mana für den späteren Start
var order := 0 # Platz in der Zugreihenfolge
var perks: Array = [] # Boss-Merkmale (siehe PERKS)
var elements: Array = [] # Laden nur aus diesen Elementen (leer = alle)
var looks := {} # Karten-ID -> Seltenheitsstufe, reine Optik (siehe Profile)
var turns := 0 # begonnene eigene Züge
func _init(n: String, h: bool, l: int, t: String = "", els: Array = []) -> void:
func _init(n: String, h: bool, l: int, t: String = "") -> void:
name = n
title = t
is_human = h
life = l
max_life = l
elements = els.duplicate()
func alive() -> bool:
return life > 0
func clone() -> Player:
var p := Player.new(name, is_human, life, title, elements)
var p := Player.new(name, is_human, life, title)
p.max_life = max_life
p.shop = shop.duplicate()
p.offer = offer.duplicate()
p.picks_left = picks_left
p.hand = hand.duplicate()
p.energy = energy
p.max_energy = max_energy
@@ -155,6 +147,7 @@ var opponent: Player # der andere am Tisch
var players: Array = [] # in Zugreihenfolge
var winner_player: Player = null
var rng: RandomNumberGenerator
var deck: Array = [] # gemeinsamer Stapel, das Ende ist oben (nur Host und Einzelspieler)
var turn := 0
var current_idx := 0
var log: Array[String] = []
@@ -162,8 +155,6 @@ 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
var field_element := "" # gemeinsames Feld aller am Tisch, "" = neutral
var field_charge := {} # Element -> gespielte Karten seit dem letzten Wechsel
func _init(h: Player, o: Player, r: RandomNumberGenerator = null) -> void:
@@ -174,8 +165,6 @@ func _init(h: Player, o: Player, r: RandomNumberGenerator = null) -> void:
if r == null:
r = make_rng()
rng = r
for e in Cards.ELEMENTS:
field_charge[e] = 0
func current() -> Player:
@@ -198,7 +187,7 @@ func ref_of(target) -> Dictionary:
return {"kind": "player", "index": players.find(target)}
## Der Gegner von p, solange er lebt (als Liste, damit Effekte einheitlich bleiben).
## Der Gegner von p, solange er lebt (als Liste, für Schleifen über Ziele).
func enemies_of(p: Player) -> Array:
return players.filter(func(o): return o != p and o.alive())
@@ -215,7 +204,7 @@ func find_creature(uid: int) -> Creature:
return null
## Unabhängige Kopie des Spielzustands (für die KI-Vorausschau).
## Unabhängige Kopie des Spielzustands (für Rückgängig und die KI-Vorausschau).
func clone() -> Game:
var copies := []
for p in players:
@@ -223,27 +212,46 @@ func clone() -> Game:
var idx := players.find(human)
var g := Game.new(copies[idx], copies[1 - idx], RandomNumberGenerator.new())
g.players = copies # gleiche Zugreihenfolge wie das Original
g.deck = deck.duplicate()
g.turn = turn
g.current_idx = current_idx
g.winner = winner
g.next_uid = next_uid
g.field_element = field_element
g.field_charge = field_charge.duplicate()
g.quiet = true
return g
## Kopie aus Sicht von viewer: verdeckte gegnerische Wesen sind unbekannt (für die KI).
func masked_clone(viewer: Player) -> Game:
var g := clone()
var me: Player = g.players[players.find(viewer)]
for p in g.players:
if p == me:
continue
for c in p.board:
if c.hidden:
c.card = Cards.unknown()
c.name = c.card.name
c.attack = c.card.attack
c.health = c.card.health
c.max_health = c.card.health
c.shield = false
return g
# --- Zugablauf ---------------------------------------------------------------
func start(fixed_order: bool = false) -> void:
if not fixed_order:
shuffle(players, rng) # zufällige Zugreihenfolge
deck = DeckGen.deck(rng)
for i in players.size():
var p: Player = players[i]
p.order = i
# Wer nicht beginnt, bekommt im ersten Zug einmalig 1 Mana extra.
p.start_bonus = 0 if i == 0 else 1
roll_shop(p)
p.offer = draw_cards(START_OFFER, START_MANA + p.start_bonus)
p.picks_left = START_PICKS
turn = 1
current_idx = 0
say("Zugreihenfolge: " + ", ".join(players.map(func(p): return p.name)))
@@ -253,62 +261,102 @@ func start(fixed_order: bool = false) -> void:
begin_turn(current())
## Füllt den Laden neu: SHOP_SIZE Karten, höchstens so teuer wie das Einkommen plus eins.
func roll_shop(p: Player) -> void:
p.shop = DeckGen.shop_cards(rng, p.max_energy, p.elements)
## Nimmt n Karten vom Stapel, die höchstens max_cost kosten (was man sich in diesem Zug leisten
## kann); zu teure wandern unter den Stapel. Ist der Stapel leer, kommt ein frischer.
func draw_cards(n: int, max_cost: int) -> Array:
var out := []
var skipped := []
var looked := 0
while out.size() < n:
if deck.is_empty():
deck = DeckGen.deck(rng)
say("Der Stapel wird neu gemischt.")
var c: Cards.Card = deck.pop_back()
looked += 1
if c.cost <= max_cost or looked > deck.size() + skipped.size() + n:
out.append(c)
else:
skipped.append(c)
for c in skipped:
deck.insert(0, c)
return out
## Kauft eine Karte aus dem Laden auf die Hand. Gibt "" zurück oder eine Fehlermeldung.
func buy(p: Player, slot: int) -> String:
## Solange eine Wahl offen ist, geht nichts anderes.
func blocked(p: Player) -> String:
return "Erst wählen" if not p.offer.is_empty() else ""
## Ziehen: kostet DRAW_COST, zeigt OFFER_SIZE Karten, eine davon kommt auf die Hand (siehe pick).
func can_draw(p: Player) -> Array:
if winner != "" or p != current():
return "Nicht dein Zug"
if slot < 0 or slot >= p.shop.size() or p.shop[slot] == null:
return "Kein Angebot"
var card: Cards.Card = p.shop[slot]
if p.energy < card.cost:
return "Nicht genug Mana"
return [false, "Nicht dein Zug"]
if blocked(p) != "":
return [false, blocked(p)]
if p.hand.size() >= MAX_HAND:
return "Hand voll"
p.energy -= card.cost
p.shop[slot] = null
p.hand.append(card)
say("%s kauft %s." % [p.name, card.name])
emit({"type": "buy", "player": players.find(p), "card": card})
return [false, "Deine Hand ist voll"]
if p.energy < draw_cost(p):
return [false, "Nicht genug Mana"]
return [true, ""]
func draw_cost(p: Player) -> int:
return 0 if "ziehen_gratis" in p.perks else DRAW_COST
## Preis einer Eigenschaft für p.
func upgrade_cost(p: Player, keyword: String) -> int:
var c := int(Cards.KEYWORD_COST[keyword])
return maxi(1, c - 1) if "verbessern_billig" in p.perks else c
func draw(p: Player) -> String:
var ok := can_draw(p)
if not ok[0]:
return ok[1]
p.energy -= draw_cost(p)
p.offer = draw_cards(OFFER_SIZE, p.max_energy)
p.picks_left = 1
say("%s zieht." % p.name)
emit({"type": "draw", "player": players.find(p)})
return ""
## Würfelt den Laden neu. Gibt "" zurück oder eine Fehlermeldung.
func reroll(p: Player) -> String:
if winner != "" or p != current():
return "Nicht dein Zug"
if p.energy < REROLL_COST:
return "Nicht genug Mana"
p.energy -= REROLL_COST
roll_shop(p)
say("%s würfelt den Laden neu." % p.name)
emit({"type": "reroll", "player": players.find(p)})
## Nimmt eine der angebotenen Karten auf die Hand; jederzeit erlaubt, auch außerhalb des Zugs.
## Was liegen bleibt, wandert unter den Stapel.
func pick(p: Player, index: int) -> String:
if winner != "":
return "Vorbei"
if p.offer.is_empty():
return "Keine Wahl offen"
if index < 0 or index >= p.offer.size():
return "Keine solche Karte"
var card: Cards.Card = p.offer[index]
p.hand.append(card.copy())
p.offer.remove_at(index)
p.picks_left -= 1
if p.picks_left <= 0 or p.hand.size() >= MAX_HAND:
for rest in p.offer:
deck.insert(0, rest)
p.offer = []
p.picks_left = 0
say("%s nimmt eine Karte." % p.name)
emit({"type": "pick", "player": players.find(p)})
return ""
func begin_turn(p: Player) -> void:
p.max_energy = mini(MAX_ENERGY, p.max_energy + 1)
# Nicht ausgegebenes Mana bleibt liegen.
p.energy += p.max_energy + (1 if "extra_mana" in p.perks else 0)
p.max_energy = START_MANA + p.turns
p.energy = p.max_energy + (1 if "extra_mana" in p.perks else 0) # der Rest vom letzten Zug ist verfallen
if p.turns == 0 and "start_energie" in p.perks:
p.energy += 2 # einmalige Starthilfe, kein dauerhafter Vorsprung
p.energy += 2
if p.start_bonus > 0:
p.energy += p.start_bonus
say("%s erhält %d Mana extra für den späteren Start." % [p.name, p.start_bonus])
p.start_bonus = 0
roll_shop(p)
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()
c.health = c.max_health # Wesen erholen sich zu Zugbeginn ganz
c.attacks_left = c.attacks_per_turn()
p.turns += 1
say("--- Zug %d: %s ---" % [turn, p.name])
emit({"type": "turn", "player": players.find(p)})
@@ -317,6 +365,8 @@ func begin_turn(p: Player) -> void:
func end_turn() -> void:
if winner != "":
return
while not current().offer.is_empty(): # offene Wahl: die erste Karte, damit nichts verfällt
pick(current(), 0)
var n := players.size()
for i in range(1, n + 1):
var idx := (current_idx + i) % n
@@ -330,302 +380,296 @@ func end_turn() -> void:
# --- Karten spielen ----------------------------------------------------------
## Ausspielen kostet nichts mehr: bezahlt wird beim Kauf im Laden.
## Ausspielen kostet die Kartenkosten; verdeckt gelegt ist es gleich teuer.
func can_play(p: Player, card: Cards.Card) -> Array:
if winner != "" or p != current():
return [false, "Nicht dein Zug"]
if blocked(p) != "":
return [false, blocked(p)]
if not (card in p.hand):
return [false, "Nicht auf der Hand"]
if p.board.size() >= MAX_BOARD:
return [false, "Dein Feld ist voll"]
if p.energy < card.cost:
return [false, "Nicht genug Mana"]
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:
## Spielt eine Karte, offen oder verdeckt. Gibt "" zurück oder eine Fehlermeldung.
func play_card(p: Player, card: Cards.Card, face_down: bool = false) -> 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)
say("%s spielt %s." % [p.name, card.name])
var cr := _enter(p, card)
for fx in card.effects:
resolve(p, fx, target, cr, card.elements)
if cr in p.board:
trigger_field_combos(p, cr)
charge_field(card)
update_resonance()
cleanup()
check_end()
return ""
## Jede gespielte Karte lädt ihre Elemente. Wer FIELD_CHARGE erreicht, kippt das
## gemeinsame Feld auf sein Element; danach beginnen alle Zähler von vorn.
func charge_field(card: Cards.Card) -> void:
for e in card.elements:
field_charge[e] = int(field_charge.get(e, 0)) + 1
var best := ""
for e in Cards.ELEMENTS:
if e == field_element or int(field_charge.get(e, 0)) < Cards.FIELD_CHARGE:
continue
if best == "" or int(field_charge[e]) > int(field_charge[best]):
best = e
if best == "":
return
field_element = best
for e in Cards.ELEMENTS:
field_charge[e] = 0
say("Das Feld wird zu %s: %s." % [Cards.FIELD_NAMES[best], Cards.FIELD_TEXT[best]])
emit({"type": "field", "element": best})
update_resonance() # der Feldbonus gilt sofort
## 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
## Wesen betritt das Feld; Boss-Merkmale wirken beim Eintritt.
func _enter(p: Player, card: Cards.Card) -> Creature:
p.energy -= card.cost
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
cr.hidden = face_down
if "wesen_angriff" in p.perks:
cr.attack += 1
if "wesen_leben" in p.perks:
cr.health += 1
cr.max_health += 1
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
if face_down:
say("%s legt ein Wesen verdeckt." % p.name)
else:
say("%s spielt %s." % [p.name, card.name])
emit({"type": "play", "player": players.find(p), "uid": cr.uid, "card": card, "hidden": face_down})
apply_auras()
cleanup()
check_end()
return ""
## Prüft eine Verschmelzung: zwei eigene Wesen, genug Mana.
func can_fuse(p: Player, a: Creature, b: Creature) -> Array:
## Ein eigenes verdecktes Wesen aufdecken: kostenlos, im eigenen Zug.
func can_reveal(p: Player, cr: Creature) -> Array:
if winner != "" or p != current():
return [false, "Nicht dein Zug"]
if a == null or b == null or a == b:
return [false, "Zwei verschiedene Wesen wählen"]
if not (a in p.board) or not (b in p.board):
if blocked(p) != "":
return [false, blocked(p)]
if cr == null or not (cr in p.board):
return [false, "Nur eigene Wesen"]
if p.energy < Cards.fusion_cost(a.card, b.card):
if not cr.hidden:
return [false, "%s liegt schon offen" % cr.name]
return [true, ""]
func reveal(p: Player, cr: Creature) -> String:
var ok := can_reveal(p, cr)
if not ok[0]:
return ok[1]
_flip(cr)
cleanup()
check_end()
return ""
## Deckt ein Wesen auf; die Elementregel gilt sofort.
func _flip(cr: Creature) -> void:
cr.hidden = false
say("%s wird aufgedeckt." % cr.name)
emit({"type": "reveal", "uid": cr.uid, "card": cr.card})
apply_auras()
# --- Verbessern: Schlüsselwörter für Mana -----------------------------------
## target: eigenes Wesen auf dem Brett oder eigene Handkarte.
func can_upgrade(p: Player, target, keyword: String) -> Array:
if winner != "" or p != current():
return [false, "Nicht dein Zug"]
if blocked(p) != "":
return [false, blocked(p)]
if not Cards.KEYWORD_COST.has(keyword):
return [false, "Unbekannte Eigenschaft"]
var card: Cards.Card = target.card if target is Creature else target
if target is Creature:
if not (target in p.board):
return [false, "Nur eigene Wesen"]
elif not (target in p.hand):
return [false, "Nicht auf der Hand"]
if keyword in card.keywords:
return [false, "%s hat schon %s" % [card.name, keyword]]
if p.energy < upgrade_cost(p, keyword):
return [false, "Nicht genug Mana"]
return [true, ""]
## Verschmilzt zwei eigene Wesen zu einem. Es erbt Elemente und Schlüsselwörter beider
## Karten, seine Werte sind der abgerundete Durchschnitt plus 1. Kosten: Durchschnitt der
## Kartenkosten, aufgerundet. Eine Verschmelzung ist kein Ausspielen: sie löst keine
## Feldkombo aus und lädt das Feld nicht.
func upgrade(p: Player, target, keyword: String) -> String:
var ok := can_upgrade(p, target, keyword)
if not ok[0]:
return ok[1]
p.energy -= upgrade_cost(p, keyword)
var card: Cards.Card = (target.card if target is Creature else target).copy() # Kopien (KI) teilen sich Karten
card.keywords.append(keyword)
if target is Creature:
target.card = card
else:
p.hand[p.hand.find(target)] = card
if target is Creature:
match keyword:
"Schild":
target.shield = true
"Doppelschlag":
target.attacks_left = mini(target.attacks_per_turn(), target.attacks_left + 1)
say("%s bekommt %s." % [card.name, keyword])
emit({"type": "upgrade", "player": players.find(p), "keyword": keyword, "uid": target.uid if target is Creature else -1})
return ""
# --- Verschmelzen ------------------------------------------------------------
## Kosten: 1 Mana, verdoppelt je Verschmelzung, die schon in einem der beiden steckt.
static func fusion_cost(a: Creature, b: Creature) -> int:
return 1 << maxi(a.card.fusions, b.card.fusions)
func can_fuse(p: Player, a: Creature, b: Creature) -> Array:
if winner != "" or p != current():
return [false, "Nicht dein Zug"]
if blocked(p) != "":
return [false, blocked(p)]
if a == null or b == null or a == b:
return [false, "Zwei verschiedene Wesen wählen"]
if not (a in p.board) or not (b in p.board):
return [false, "Nur eigene Wesen"]
if p.energy < fusion_cost(a, b):
return [false, "Nicht genug Mana"]
return [true, ""]
## Verschmilzt zwei eigene Wesen zu einem offenen Wesen: Elemente und Schlüsselwörter beider,
## jeweils der bessere Wert (Angriff, Leben, Lebenshöchstwert) plus 1. Der Elementbonus der
## Eltern wird herausgerechnet, das neue Wesen bekommt seinen eigenen.
func fuse(p: Player, a: Creature, b: Creature) -> String:
var ok := can_fuse(p, a, b)
if not ok[0]:
return ok[1]
p.energy -= Cards.fusion_cost(a.card, b.card)
var card := Cards.fused(a.card, b.card, (a.attack + b.attack) / 2 + 1, (a.health + b.health) / 2 + 1)
p.energy -= fusion_cost(a, b)
var card := Cards.fused(a.card, b.card, maxi(a.attack, b.attack) + 1,
maxi(a.max_health - a.aura, b.max_health - b.aura) + 1)
var cr := Creature.new(card, p, next_uid)
next_uid += 1
cr.health = maxi(a.health - a.aura, b.health - b.aura) + 1
cr.shield = cr.shield or a.shield or b.shield
cr.frozen = a.frozen or b.frozen
cr.ready = a.ready and b.ready
cr.attacks_left = 0 if not cr.ready else mini(a.attacks_left, b.attacks_left)
cr.attacks_left = mini(a.attacks_left, b.attacks_left)
var at := p.board.find(a)
p.board.erase(a)
p.board.erase(b)
p.board.insert(mini(at, p.board.size()), cr)
say("%s und %s verschmelzen zu %s (%d/%d)." % [a.name, b.name, cr.name, cr.attack, cr.health])
emit({"type": "fuse", "player": players.find(p), "uid": cr.uid, "gone": [a.uid, b.uid]})
update_resonance()
apply_auras()
cleanup()
check_end()
return ""
## 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)]
# --- Vier-Elemente-Regel -----------------------------------------------------
## Vorschau fürs Ausspielen: Angriffsbonus aus Resonanz und Feld sowie die Feldkombo,
## die die Karte mit dem ersten eigenen Wesen aus anderem Element auslösen würde.
func forecast(p: Player, card: Cards.Card) -> Dictionary:
var partners := 0
var combo := ""
var partner: Creature = null
for o in p.board:
if o.card.elements.any(func(e): return e in card.elements):
partners += 1
if combo == "":
for e1 in card.elements:
for e2 in o.card.elements:
if e1 != e2 and combo == "":
combo = Cards.combo_name([e1, e2])
partner = o
var resonance := resonance_for(partners) * (2 if "resonanz_doppelt" in p.perks else 1)
var field := 1 if field_element != "" and field_element in card.elements else 0
return {"resonance": resonance, "field": field, "combo": combo, "partner": partner}
## Resonanz: abnehmender Angriffsbonus je Partner mit gleichem Element (siehe resonance_for).
func update_resonance() -> void:
## Offene Wesen je Element auf dem ganzen Tisch.
func element_counts() -> Dictionary:
var counts := {}
for e in Cards.ELEMENTS:
counts[e] = 0
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)
c.field_bonus = 1 if field_element != "" and field_element in c.card.elements else 0
if not c.hidden:
for e in c.card.elements:
counts[e] += 1
return counts
func resolve(p: Player, fx: Cards.Effect, target, source: Creature = null, elements: Array = []) -> 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":
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 + (Cards.matchup(elements, t.card.elements) if t is Creature else 0))
"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})
"mana":
p.energy += fx.amount
say("%s erhält %d Mana." % [p.name, fx.amount])
emit({"type": "mana", "player": players.find(p), "amount": 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"})
## Bonus aus der Elementregel für diese Elemente: +1, wenn ELEMENT_STACK offene Wesen des
## Elements stehen, -1, wenn das Element, das es schlägt, den Stapel voll hat.
## Mehrere Elemente nehmen den besten Wert.
static func aura_of(elements: Array, counts: Dictionary) -> int:
var best := 0
for i in elements.size():
var e: String = elements[i]
var a := 0
if int(counts[e]) >= ELEMENT_STACK:
a += 1
var predator := Cards.beaten_by(e)
if predator != "" and int(counts[predator]) >= ELEMENT_STACK:
a -= 1
best = a if i == 0 else maxi(best, a)
return best
## Bonus eines offenen Wesens aus der Elementregel; verdeckte zählen nicht.
func aura_for(c: Creature, counts: Dictionary) -> int:
if c.hidden:
return 0
var a := aura_of(c.card.elements, counts)
var o = c.owner()
if o != null and "element_doppelt" in o.perks:
a *= 2
return a
## Rechnet die Elementregel neu; Leben folgt dem Bonus direkt (+1/+1 bzw. -1/-1).
func apply_auras() -> void:
var counts := element_counts()
for p in players:
for c in p.board:
var a := aura_for(c, counts)
if a != c.aura:
var delta: int = a - c.aura
c.health += delta
c.max_health += delta
c.aura = a
## Vorschau fürs Ausspielen: der Elementbonus, den die Karte offen gelegt bekäme.
func forecast(p: Player, card: Cards.Card) -> int:
var counts := element_counts()
for e in card.elements:
counts[e] += 1
return aura_of(card.elements, counts) * (2 if "element_doppelt" in p.perks else 1)
# --- Kampf -------------------------------------------------------------------
## Leben lassen sich erst angreifen, wenn der Gegner kein Wesen mehr hat.
func can_attack(cr: Creature, target) -> Array:
if winner != "" or cr.owner() != current():
return [false, "Nicht dein Zug"]
if blocked(cr.owner()) != "":
return [false, blocked(cr.owner())]
if cr.hidden:
return [false, "%s liegt verdeckt" % cr.name]
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"]
if not (target is Creature) and not tgt_owner.board.is_empty():
return [false, "Erst die Wesen"]
return [true, ""]
## Greift an. Gibt "" zurück oder eine Fehlermeldung.
## Greift an. Ein verdecktes Ziel wird zuerst aufgedeckt. Schaden über das Leben eines Wesens
## hinaus schlägt auf seinen Besitzer durch (sonst wäre eine Mauer unverwundbar).
## 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
if target is Creature and target.hidden:
_flip(target)
if target.health <= 0: # der Elementbonus hat es schon erledigt
cleanup()
check_end()
return ""
say("%s greift %s an." % [cr.name, target.name])
emit({"type": "attack", "attacker": ref_of(cr), "target": ref_of(target)})
damage(target, cr.power() + matchup_bonus(cr, target))
if target is Creature and target.power() > 0: # wer nichts austeilt, kontert auch nicht
damage(cr, target.power() + matchup_bonus(target, cr))
var dealt := cr.power() + matchup_bonus(cr, target)
if target is Creature:
var had_shield: bool = target.shield
var hp_before: int = target.health
damage(target, dealt)
if not had_shield and dealt > hp_before:
var o = target.owner()
var excess := dealt - hp_before
o.life -= excess
say("%d Schaden schlagen durch auf %s." % [excess, o.name])
emit({"type": "damage", "target": ref_of(o), "amount": excess})
if target.power() > 0: # wer nichts austeilt, kontert auch nicht; der Konter kennt keinen Elementvorteil
damage(cr, target.power())
else:
damage(target, dealt)
cleanup()
check_end()
return ""
## Elementvorteil beim Zuschlagen: gegen Helden gibt es keinen.
## Elementvorteil beim Zuschlagen: +1 im Vorteil, -1 im Nachteil (die Verteidigung des
## Ziels), gegen Helden gibt es keinen.
static func matchup_bonus(source: Creature, target) -> int:
if not (target is Creature):
return 0
@@ -648,21 +692,24 @@ func damage(target, amount: int) -> void:
emit({"type": "damage", "target": ref_of(target), "amount": amount})
## Räumt Tote ab; danach gilt die Elementregel neu, was weitere Tote nach sich ziehen kann.
func cleanup() -> void:
var died := false
for p in players:
for c in p.board.duplicate():
# Wer besiegt ist, nimmt seine Wesen mit vom Tisch.
if c.health <= 0 or not p.alive():
p.board.erase(c)
if c.health > 0:
say("%s verschwindet mit %s." % [c.name, p.name])
else:
say("%s stirbt." % c.name)
emit({"type": "death", "target": ref_of(c)})
died = true
if died:
update_resonance()
for round in 8:
var died := false
for p in players:
for c in p.board.duplicate():
# Wer besiegt ist, nimmt seine Wesen mit vom Tisch.
if c.health <= 0 or not p.alive():
p.board.erase(c)
if c.health > 0:
say("%s verschwindet mit %s." % [c.name, p.name])
else:
say("%s stirbt." % c.name)
emit({"type": "death", "target": ref_of(c)})
died = true
if not died:
return
apply_auras()
## Ende: einer von beiden hat kein Leben mehr.