664 lines
23 KiB
GDScript
664 lines
23 KiB
GDScript
class_name Game
|
||
extends RefCounted
|
||
## Spielregeln: Duell, Mana je Zug, Ziehen aus dem gemeinsamen Stapel, verdeckte Wesen,
|
||
## Elementboni und Verschmelzen.
|
||
|
||
const MAX_BOARD := 5
|
||
const START_HAND := 5 # Startkarten: die obersten des eigenen Stapels
|
||
const START_MANA := 1 # Mana in Zug 1; jede Runde eins mehr, ohne Grenze; Rest verfällt
|
||
const DECK_MIN := 30 # Karten je Stapel: mindestens so viele (Gegner genau so viele: alle Karten eines Elements)
|
||
const DECK_MAX := 60 # und höchstens so viele
|
||
const START_LIFE := 30
|
||
const ELEMENT_PAIR := 2 # so viele eigene offene Wesen eines Elements: Sondereffekt des Elements
|
||
const DISCARD_GAIN := 1 # Abwerfen einer Handkarte bringt so viel Mana
|
||
|
||
# Merkmale der Kampagnen-Bosse (Player.perks).
|
||
const PERKS := {
|
||
"feuer_angriff": "Feuerwesen haben +1 Angriff",
|
||
"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",
|
||
"alle_flink": "Wesen greifen sofort an",
|
||
"start_energie": "Hat im ersten Zug 2 Mana mehr",
|
||
"kein_nachteil": "Wesen haben keinen Elementnachteil",
|
||
}
|
||
|
||
## 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
|
||
|
||
|
||
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 health: int
|
||
var max_health: int
|
||
var attacks_done := 0 # Angriffe in diesem Zug
|
||
var ready := false # lag schon zu Beginn des eigenen Zugs: darf angreifen und aufgedeckt werden
|
||
var hidden := false # verdeckt gelegt: greift nicht an, zählt nicht fürs Elementpaar
|
||
|
||
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
|
||
|
||
func owner():
|
||
return owner_ref.get_ref()
|
||
|
||
## Eigenschaft: als Sondereffekt des Elementpaars der eigenen Seite, aus der Karte
|
||
## (verschmolzene erben) oder als Boss-Merkmal. Verdeckte Wesen haben keine.
|
||
func has(kw: String) -> bool:
|
||
if hidden:
|
||
return false
|
||
if kw in card.keywords:
|
||
return true
|
||
var o = owner()
|
||
if o == null:
|
||
return false
|
||
if kw == "Flink" and "alle_flink" in o.perks:
|
||
return true
|
||
for e in card.elements:
|
||
if Cards.ELEMENT_KEYWORD.get(e, "") == kw and o.element_count(e) >= Game.ELEMENT_PAIR:
|
||
return true
|
||
return false
|
||
|
||
## Alle wirksamen Eigenschaften in fester Reihenfolge.
|
||
func keywords() -> Array[String]:
|
||
var out: Array[String] = []
|
||
for k in Cards.KEYWORDS:
|
||
if has(k):
|
||
out.append(k)
|
||
return out
|
||
|
||
## Angriffswert inklusive Boss-Merkmalen.
|
||
func power() -> int:
|
||
var v := attack
|
||
var o = owner()
|
||
if o != null:
|
||
if "feuer_angriff" in o.perks and "Feuer" in card.elements:
|
||
v += 1
|
||
return v
|
||
|
||
func attacks_per_turn() -> int:
|
||
return 2 if has("Doppelschlag") else 1
|
||
|
||
func attacks_left() -> int:
|
||
return maxi(0, attacks_per_turn() - attacks_done)
|
||
|
||
## Angreifen darf, wer offen liegt, noch Angriffe hat und seit einer Runde liegt (oder flink ist).
|
||
## Darf dieses Wesen angreifen? Ob es Schaden macht, hängt vom Ziel ab (Elementvorteil, Game.can_attack).
|
||
func can_attack() -> bool:
|
||
return not hidden and attacks_left() > 0 and (ready or has("Flink"))
|
||
|
||
func clone(new_owner) -> Creature:
|
||
var c := Creature.new(card, new_owner, uid)
|
||
c.attack = attack
|
||
c.health = health
|
||
c.max_health = max_health
|
||
c.attacks_done = attacks_done
|
||
c.ready = ready
|
||
c.hidden = hidden
|
||
return c
|
||
|
||
|
||
class Player extends RefCounted:
|
||
var name: String
|
||
var title: String
|
||
var is_human: bool
|
||
var life: int
|
||
var max_life: int
|
||
var hand: Array = [] # eigene Exemplare (Cards.Card, in der eigenen Seltenheitsstufe)
|
||
var board: Array = []
|
||
var deck: Array = [] # eigener Stapel: Karten-IDs, das Ende ist oben
|
||
var energy := 0 # Mana in diesem Zug; der Rest verfällt
|
||
var max_energy := 0 # Mana zu Zugbeginn (START_MANA + eigene Züge)
|
||
var order := 0 # Platz in der Zugreihenfolge
|
||
var perks: Array = [] # Boss-Merkmale (siehe PERKS)
|
||
var looks := {} # Karten-ID -> Seltenheitsstufe, reine Optik (siehe Profile)
|
||
var turns := 0 # begonnene eigene Züge
|
||
var cards_played := 0 # ausgespielte Karten (Münzen nach dem Kampf)
|
||
var draws := 0 # in diesem Zug gezogene Karten; die n-te kostet n-1 Mana
|
||
var fatigue := 0 # Züge ohne Karten im Stapel: so viel Leben kostet der nächste
|
||
var bonus_attack := 0 # Skill Stark: so viel mehr Angriff auf jeder gezogenen Karte
|
||
var bonus_health := 0 # Skill Zäh: so viel mehr Leben
|
||
var cheap_from := 99 # Skill Sparsam: Karten ab diesen Kosten sind 1 Mana billiger
|
||
|
||
func _init(n: String, h: bool, l: int, t: String = "") -> void:
|
||
name = n
|
||
title = t
|
||
is_human = h
|
||
life = l
|
||
max_life = l
|
||
|
||
func alive() -> bool:
|
||
return life > 0
|
||
|
||
## Die oberste Karte des eigenen Stapels als Exemplar in der eigenen Stufe und mit den Skillboni
|
||
## (Stark, Zäh, Sparsam); null, wenn er leer ist.
|
||
func draw_from_deck() -> Cards.Card:
|
||
if deck.is_empty():
|
||
return null
|
||
var id: String = deck.pop_back()
|
||
return boost(Cards.by_id(id).leveled(int(looks.get(id, 0))))
|
||
|
||
## Legt die Skillboni auf ein Exemplar: Stark und Zäh getrennt von den Grundwerten (Fusionen
|
||
## rechnen ohne sie), Sparsam macht teure Karten 1 Mana billiger.
|
||
func boost(c: Cards.Card) -> Cards.Card:
|
||
c.boost_attack = bonus_attack
|
||
c.boost_health = bonus_health
|
||
c.attack += bonus_attack
|
||
c.health += bonus_health
|
||
if c.cost >= cheap_from:
|
||
c.cost -= 1
|
||
return c
|
||
|
||
## Offene eigene Wesen mit diesem Element.
|
||
func element_count(e: String) -> int:
|
||
var n := 0
|
||
for c in board:
|
||
if not c.hidden and e in c.card.elements:
|
||
n += 1
|
||
return n
|
||
|
||
func clone() -> Player:
|
||
var p := Player.new(name, is_human, life, title)
|
||
p.max_life = max_life
|
||
p.hand = hand.duplicate()
|
||
p.energy = energy
|
||
p.max_energy = max_energy
|
||
p.order = order
|
||
p.perks = perks.duplicate()
|
||
p.looks = looks
|
||
p.turns = turns
|
||
p.cards_played = cards_played
|
||
p.deck = deck.duplicate()
|
||
p.draws = draws
|
||
p.fatigue = fatigue
|
||
p.bonus_attack = bonus_attack
|
||
p.bonus_health = bonus_health
|
||
p.cheap_from = cheap_from
|
||
for c in board:
|
||
p.board.append(c.clone(p))
|
||
return p
|
||
|
||
|
||
var human: Player # der menschliche Spieler
|
||
var opponent: Player # der andere am Tisch
|
||
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, o: Player, r: RandomNumberGenerator = null) -> void:
|
||
human = h
|
||
opponent = o
|
||
players = [h, o]
|
||
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)}
|
||
|
||
|
||
## 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())
|
||
|
||
|
||
func enemy_of(p: Player) -> Player:
|
||
return opponent if p == human else human
|
||
|
||
|
||
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 Rückgängig und 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[1 - idx], 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 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
|
||
return g
|
||
|
||
|
||
# --- Zugablauf ---------------------------------------------------------------
|
||
|
||
## Jeder spielt mit seinem eigenen Stapel (leer: ein zufälliger); die Starthand sind die
|
||
## obersten fünf Karten daraus, wie gemischt.
|
||
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 p.deck.is_empty():
|
||
p.deck = DeckGen.random_deck(rng)
|
||
shuffle(p.deck, rng)
|
||
for k in START_HAND:
|
||
var c := p.draw_from_deck()
|
||
if c != null:
|
||
p.hand.append(c)
|
||
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())
|
||
|
||
|
||
## Ziehen: eine Karte vom eigenen Stapel. Die erste im Zug ist frei, jede weitere kostet eins
|
||
## mehr (0, 1, 2 …).
|
||
func can_draw(p: Player) -> Array:
|
||
if winner != "" or p != current():
|
||
return [false, "Nicht dein Zug"]
|
||
if p.deck.is_empty():
|
||
return [false, "Dein Stapel ist leer"]
|
||
if p.energy < draw_cost(p):
|
||
return [false, "Nicht genug Mana"]
|
||
return [true, ""]
|
||
|
||
|
||
func draw_cost(p: Player) -> int:
|
||
return maxi(0, p.draws - (1 if "ziehen_gratis" in p.perks else 0))
|
||
|
||
|
||
func draw(p: Player) -> String:
|
||
var ok := can_draw(p)
|
||
if not ok[0]:
|
||
return ok[1]
|
||
p.energy -= draw_cost(p)
|
||
p.draws += 1
|
||
p.hand.append(p.draw_from_deck())
|
||
say("%s zieht." % p.name)
|
||
emit({"type": "draw", "player": players.find(p)})
|
||
return ""
|
||
|
||
|
||
func begin_turn(p: Player) -> void:
|
||
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
|
||
for c in p.board:
|
||
c.ready = true # lag eine Runde: greift jetzt an, Verdecktes darf aufgedeckt werden
|
||
c.attacks_done = 0
|
||
p.turns += 1
|
||
p.draws = 0 # das erste Ziehen im Zug ist wieder frei
|
||
say("--- Zug %d: %s ---" % [turn, p.name])
|
||
emit({"type": "turn", "player": players.find(p)})
|
||
if p.deck.is_empty(): # Erschöpfung: ohne Stapel kostet jeder Zug mehr Leben, damit es ein Ende gibt
|
||
p.fatigue += 1
|
||
p.life -= p.fatigue
|
||
say("%s hat keine Karten mehr und verliert %d Leben." % [p.name, p.fatigue])
|
||
emit({"type": "damage", "target": ref_of(p), "amount": p.fatigue})
|
||
check_end()
|
||
|
||
|
||
## Zugende: Wesen mit Regeneration heilen ganz, dann ist der andere dran.
|
||
func end_turn() -> void:
|
||
if winner != "":
|
||
return
|
||
for c in current().board:
|
||
if c.has("Regeneration") and c.health < c.max_health:
|
||
c.health = c.max_health
|
||
say("%s regeneriert sich." % c.name)
|
||
emit({"type": "heal", "uid": c.uid})
|
||
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 ----------------------------------------------------------
|
||
|
||
## 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 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, ""]
|
||
|
||
|
||
## Spielt eine Karte, offen oder verdeckt. Das Wesen greift erst im nächsten Zug an (außer flink).
|
||
## 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]
|
||
p.hand.erase(card)
|
||
p.energy -= card.cost
|
||
var cr := Creature.new(card, p, next_uid)
|
||
next_uid += 1
|
||
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
|
||
p.board.append(cr)
|
||
p.cards_played += 1
|
||
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})
|
||
cleanup()
|
||
check_end()
|
||
return ""
|
||
|
||
|
||
## Abwerfen: eine Handkarte loswerden und dafür DISCARD_GAIN Mana bekommen.
|
||
func can_discard(p: Player, card: Cards.Card) -> Array:
|
||
if winner != "" or p != current():
|
||
return [false, "Nicht dein Zug"]
|
||
if not (card in p.hand):
|
||
return [false, "Nicht auf der Hand"]
|
||
return [true, ""]
|
||
|
||
|
||
func discard(p: Player, card: Cards.Card) -> String:
|
||
var ok := can_discard(p, card)
|
||
if not ok[0]:
|
||
return ok[1]
|
||
p.hand.erase(card)
|
||
p.energy += DISCARD_GAIN
|
||
say("%s wirft %s ab." % [p.name, card.name])
|
||
emit({"type": "discard", "player": players.find(p)})
|
||
return ""
|
||
|
||
|
||
## Ein eigenes verdecktes Wesen aufdecken: kostenlos, im eigenen Zug, frühestens einen Zug
|
||
## nach dem Legen. Aufgedeckt greift es sofort an.
|
||
func can_reveal(p: Player, cr: Creature) -> Array:
|
||
if winner != "" or p != current():
|
||
return [false, "Nicht dein Zug"]
|
||
if cr == null or not (cr in p.board):
|
||
return [false, "Nur eigene Wesen"]
|
||
if not cr.hidden:
|
||
return [false, "%s liegt schon offen" % cr.name]
|
||
if not cr.ready:
|
||
return [false, "%s kann erst im nächsten Zug aufgedeckt werden" % 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; das Elementpaar 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})
|
||
|
||
|
||
# --- Verschmelzen ------------------------------------------------------------
|
||
|
||
static func card_of(x) -> Cards.Card:
|
||
return x.card if x is Creature else x
|
||
|
||
|
||
## Seltenheitsstufe einer Karte von p: bei verschmolzenen die geerbte, sonst die der Sammlung.
|
||
static func look_of(p: Player, c: Cards.Card) -> int:
|
||
return c.look if c.look >= 0 else int(p.looks.get(c.id, 0))
|
||
|
||
|
||
## a und b: zwei verschiedene eigene Karten, auf der Hand oder als Wesen auf dem Brett, auch
|
||
## gemischt. Verschmelzen kostet kein Mana.
|
||
func can_fuse(p: Player, a, b) -> Array:
|
||
if winner != "" or p != current():
|
||
return [false, "Nicht dein Zug"]
|
||
if a == null or b == null or a == b:
|
||
return [false, "Zwei verschiedene Karten wählen"]
|
||
for x in [a, b]:
|
||
if not ((x in p.board) if x is Creature else (x in p.hand)):
|
||
return [false, "Nur eigene Karten"]
|
||
return [true, ""]
|
||
|
||
|
||
## Die Karte, die aus a und b entsteht (siehe Cards.fused), in der höheren Stufe der Eltern.
|
||
static func fusion_result(p: Player, a, b) -> Cards.Card:
|
||
return p.boost(Cards.fused(card_of(a), card_of(b), maxi(look_of(p, card_of(a)), look_of(p, card_of(b)))))
|
||
|
||
|
||
## Verschmilzt zwei eigene Karten zu einer neuen Handkarte: Wesen verlassen das Brett, die neue
|
||
## Karte ist unverletzt und wird wie jede andere ausgespielt. Sie landet am Platz der vorderen
|
||
## Handkarte, sonst hinten.
|
||
func fuse(p: Player, a, b) -> String:
|
||
var ok := can_fuse(p, a, b)
|
||
if not ok[0]:
|
||
return ok[1]
|
||
var card := fusion_result(p, a, b)
|
||
var at := p.hand.size()
|
||
var gone := []
|
||
for x in [a, b]:
|
||
if x is Creature:
|
||
p.board.erase(x)
|
||
gone.append(x.uid)
|
||
else:
|
||
at = mini(at, p.hand.find(x))
|
||
p.hand.erase(x)
|
||
p.hand.insert(mini(at, p.hand.size()), card)
|
||
say("%s und %s verschmelzen zu %s (%d/%d) auf der Hand." % [a.name, b.name, card.name, card.attack, card.health])
|
||
emit({"type": "fuse", "player": players.find(p), "uid": -1, "gone": gone})
|
||
return ""
|
||
|
||
|
||
# --- Kampf -------------------------------------------------------------------
|
||
|
||
## Leben lassen sich erst angreifen, wenn der Gegner kein Wesen mehr hat; Wesen mit Spott
|
||
## müssen vor den anderen angegriffen werden.
|
||
func can_attack(cr: Creature, target) -> Array:
|
||
if winner != "" or cr.owner() != current():
|
||
return [false, "Nicht dein Zug"]
|
||
if cr.hidden:
|
||
return [false, "%s liegt verdeckt" % cr.name]
|
||
if not cr.ready and not cr.has("Flink") and cr.attacks_left() > 0:
|
||
return [false, "%s greift erst im nächsten Zug an" % 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"]
|
||
if cr.power() + matchup_bonus(cr, target) <= 0: # 0 Angriff macht auch im Vorteil nichts
|
||
return [false, "%s würde keinen Schaden machen" % cr.name]
|
||
if not (target is Creature) and not tgt_owner.board.is_empty():
|
||
return [false, "Erst die Wesen"]
|
||
if target is Creature and not target.has("Spott") and tgt_owner.board.any(func(c): return c.has("Spott")):
|
||
return [false, "Erst die Wesen mit Spott"]
|
||
return [true, ""]
|
||
|
||
|
||
## Greift an. Ein verdecktes Ziel wird zuerst aufgedeckt. 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_done += 1
|
||
if target is Creature and target.hidden:
|
||
_flip(target)
|
||
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: # Konter mit Elementvorteil; wer so nichts austeilt, kontert nicht (damage ignoriert 0)
|
||
damage(cr, target.power() + matchup_bonus(target, cr))
|
||
cleanup()
|
||
check_end()
|
||
return ""
|
||
|
||
|
||
## Elementvorteil beim Zuschlagen und beim Konter, als Verschiebung des Schadens: im Vorteil
|
||
## doppelt (+ Angriff), im Nachteil halb, aufgerundet (− Angriff / 2). Gegen Helden gibt es keinen;
|
||
## mit dem Merkmal kein_nachteil entfällt der Nachteil.
|
||
static func matchup_bonus(source: Creature, target) -> int:
|
||
if not (target is Creature):
|
||
return 0
|
||
var m := Cards.matchup(source.card.elements, target.card.elements)
|
||
if m > 0:
|
||
return source.power()
|
||
if m < 0:
|
||
var o = source.owner()
|
||
return 0 if o != null and "kein_nachteil" in o.perks else -(source.power() / 2)
|
||
return 0
|
||
|
||
|
||
func damage(target, amount: int) -> void:
|
||
if amount <= 0:
|
||
return
|
||
if target is Creature:
|
||
target.health -= amount
|
||
else:
|
||
target.life -= amount
|
||
say("%s erleidet %d Schaden." % [target.name, amount])
|
||
emit({"type": "damage", "target": ref_of(target), "amount": amount})
|
||
|
||
|
||
## Räumt Tote ab; wer besiegt ist, nimmt seine Wesen mit vom Tisch.
|
||
func cleanup() -> void:
|
||
for p in players:
|
||
for c in p.board.duplicate():
|
||
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)})
|
||
|
||
|
||
## Ende: einer von beiden hat kein Leben mehr.
|
||
func check_end() -> void:
|
||
if winner != "":
|
||
return
|
||
var alive := players.filter(func(p): return p.alive())
|
||
if alive.size() != 1:
|
||
return
|
||
winner_player = alive[0]
|
||
winner = "human" if winner_player.is_human else "opponents"
|
||
say("Der Gegner ist besiegt!" if winner == "human" else "Du wurdest besiegt.")
|
||
|
||
|
||
# --- Gegner und Kampfstart ---------------------------------------------------
|
||
|
||
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
|
||
|
||
|
||
## Duell gegen einen zufälligen Gegner (Tests, Simulation). rules: {"human_perks": [...], "looks": {...}, "deck": [ids]}
|
||
static func new_battle(r: RandomNumberGenerator = null, rules: Dictionary = {}) -> Game:
|
||
if r == null:
|
||
r = make_rng()
|
||
return new_battle_with(DeckGen.random_opponent(), r, false, rules)
|
||
|
||
|
||
static func new_battle_with(opp: Player, r: RandomNumberGenerator = null, fixed_order: bool = false, rules: Dictionary = {}) -> Game:
|
||
if r == null:
|
||
r = make_rng()
|
||
var human := Player.new("Spieler", true, START_LIFE)
|
||
human.perks = Array(rules.get("human_perks", []))
|
||
human.looks = rules.get("looks", {})
|
||
human.deck = rules.get("deck", []).duplicate()
|
||
human.bonus_attack = int(rules.get("bonus_attack", 0))
|
||
human.bonus_health = int(rules.get("bonus_health", 0))
|
||
human.cheap_from = int(rules.get("cheap_from", 99))
|
||
var g := Game.new(human, opp, r)
|
||
for p in g.players:
|
||
if "leben_35" in p.perks:
|
||
p.life += 5
|
||
p.max_life += 5
|
||
g.start(fixed_order)
|
||
return g
|