update
This commit is contained in:
391
scripts/game.gd
391
scripts/game.gd
@@ -4,26 +4,24 @@ extends RefCounted
|
||||
|
||||
const MAX_BOARD := 5
|
||||
const MAX_HAND := 8
|
||||
const MAX_ENERGY := 10
|
||||
const START_HAND := 4
|
||||
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 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",
|
||||
"wesen_leben": "Wesen kommen mit +1 Leben",
|
||||
"extra_mana": "Bekommt jeden Zug 1 Mana mehr",
|
||||
"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",
|
||||
"start_energie": "Beginnt mit 3 Mana",
|
||||
"resonanz_doppelt": "Resonanz zählt doppelt",
|
||||
}
|
||||
|
||||
@@ -40,9 +38,6 @@ static func make_rng() -> RandomNumberGenerator:
|
||||
r.randomize()
|
||||
return r
|
||||
|
||||
const DECK_SIZE := 20
|
||||
const MAX_COPIES := 3
|
||||
const MAX_DECK_ELEMENTS := 2
|
||||
|
||||
|
||||
class Creature extends RefCounted:
|
||||
@@ -52,6 +47,7 @@ class Creature extends RefCounted:
|
||||
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
|
||||
@@ -74,9 +70,9 @@ class Creature extends RefCounted:
|
||||
func owner():
|
||||
return owner_ref.get_ref()
|
||||
|
||||
## Angriffswert inklusive Resonanz und Boss-Merkmalen.
|
||||
## Angriffswert inklusive Resonanz, Feld und Boss-Merkmalen.
|
||||
func power() -> int:
|
||||
var v := attack + resonance
|
||||
var v := attack + resonance + field_bonus
|
||||
var o = owner()
|
||||
if o != null:
|
||||
if "feuer_angriff" in o.perks and "Feuer" in card.elements:
|
||||
@@ -98,6 +94,7 @@ class Creature extends RefCounted:
|
||||
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
|
||||
@@ -113,43 +110,41 @@ class Player extends RefCounted:
|
||||
var is_human: bool
|
||||
var life: int
|
||||
var max_life: int
|
||||
var deck: Array = []
|
||||
var shop: Array = [] # SHOP_SIZE Plätze, gekaufte sind null
|
||||
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 energy := 0 # Mana; nicht ausgegebenes bleibt liegen
|
||||
var max_energy := 0 # Einkommen je Zug
|
||||
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
|
||||
var mulligan_done := false
|
||||
|
||||
func _init(n: String, d: Array, h: bool, l: int, t: String = "") -> void:
|
||||
func _init(n: String, h: bool, l: int, t: String = "", els: Array = []) -> void:
|
||||
name = n
|
||||
title = t
|
||||
is_human = h
|
||||
life = l
|
||||
max_life = l
|
||||
deck = d.duplicate()
|
||||
elements = els.duplicate()
|
||||
|
||||
func alive() -> bool:
|
||||
return life > 0
|
||||
|
||||
func clone() -> Player:
|
||||
var p := Player.new(name, deck, is_human, life, title)
|
||||
var p := Player.new(name, is_human, life, title, elements)
|
||||
p.max_life = max_life
|
||||
p.shop = shop.duplicate()
|
||||
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.looks = looks
|
||||
p.turns = turns
|
||||
p.mulligan_done = mulligan_done
|
||||
for c in board:
|
||||
p.board.append(c.clone(p))
|
||||
return p
|
||||
@@ -168,6 +163,8 @@ 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, opps: Array, r: RandomNumberGenerator = null) -> void:
|
||||
@@ -178,6 +175,8 @@ func _init(h: Player, opps: Array, r: RandomNumberGenerator = null) -> void:
|
||||
if r == null:
|
||||
r = make_rng()
|
||||
rng = r
|
||||
for e in Cards.ELEMENTS:
|
||||
field_charge[e] = 0
|
||||
|
||||
|
||||
func current() -> Player:
|
||||
@@ -225,20 +224,12 @@ func clone() -> Game:
|
||||
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 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:
|
||||
@@ -247,13 +238,9 @@ func start(fixed_order: bool = false) -> void:
|
||||
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))
|
||||
# Wer nicht beginnt, bekommt im ersten Zug einmalig 1 Mana extra.
|
||||
p.start_bonus = 0 if i == 0 else 1
|
||||
roll_shop(p)
|
||||
turn = 1
|
||||
current_idx = 0
|
||||
say("Zugreihenfolge: " + ", ".join(players.map(func(p): return p.name)))
|
||||
@@ -263,28 +250,54 @@ func start(fixed_order: bool = false) -> void:
|
||||
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()
|
||||
## 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)
|
||||
|
||||
|
||||
## Kauft eine Karte aus dem Laden auf die Hand. Gibt "" zurück oder eine Fehlermeldung.
|
||||
func buy(p: Player, slot: int) -> String:
|
||||
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"
|
||||
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 ""
|
||||
|
||||
|
||||
## 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)})
|
||||
return ""
|
||||
|
||||
|
||||
func begin_turn(p: Player) -> void:
|
||||
p.max_energy = mini(MAX_ENERGY, p.max_energy + 1)
|
||||
p.energy = p.max_energy
|
||||
# Nicht ausgegebenes Mana bleibt liegen.
|
||||
p.energy += p.max_energy + (1 if "extra_mana" in p.perks else 0)
|
||||
if p.turns == 0 and "start_energie" in p.perks:
|
||||
p.energy += 2 # einmalige Starthilfe, kein dauerhafter Vorsprung
|
||||
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])
|
||||
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:
|
||||
@@ -296,28 +309,6 @@ func begin_turn(p: Player) -> void:
|
||||
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:
|
||||
@@ -336,18 +327,14 @@ func end_turn() -> void:
|
||||
|
||||
# --- Karten spielen ----------------------------------------------------------
|
||||
|
||||
## Ausspielen kostet nichts mehr: bezahlt wird beim Kauf im Laden.
|
||||
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:
|
||||
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 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, ""]
|
||||
|
||||
|
||||
@@ -379,37 +366,40 @@ func play_card(p: Player, card: Cards.Card, target = null) -> String:
|
||||
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
|
||||
var cr := _enter(p, card)
|
||||
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:
|
||||
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:
|
||||
@@ -427,26 +417,6 @@ func trigger_field_combos(p: Player, cr: Creature) -> void:
|
||||
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)
|
||||
@@ -454,6 +424,9 @@ func _enter(p: Player, card: Cards.Card) -> Creature:
|
||||
if "waechter_leben" in p.perks and cr.taunt():
|
||||
cr.health += 2
|
||||
cr.max_health += 2
|
||||
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:
|
||||
@@ -464,14 +437,45 @@ func _enter(p: Player, card: Cards.Card) -> Creature:
|
||||
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])
|
||||
## Prüft eine Verschmelzung: zwei eigene Wesen, genug Mana.
|
||||
func can_fuse(p: Player, a: Creature, b: 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):
|
||||
return [false, "Nur eigene Wesen"]
|
||||
if p.energy < Cards.fusion_cost(a.card, b.card):
|
||||
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 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)
|
||||
var cr := Creature.new(card, p, next_uid)
|
||||
next_uid += 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)
|
||||
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()
|
||||
cleanup()
|
||||
check_end()
|
||||
return ""
|
||||
|
||||
|
||||
## Resonanz je Anzahl Partner mit gleichem Element: +1, +2, +3, +3.
|
||||
@@ -488,9 +492,10 @@ func update_resonance() -> void:
|
||||
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
|
||||
|
||||
|
||||
func resolve(p: Player, fx: Cards.Effect, target, source: Creature = null, bonus: int = 0) -> void:
|
||||
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:
|
||||
@@ -508,7 +513,7 @@ func resolve(p: Player, fx: Cards.Effect, target, source: Creature = null, bonus
|
||||
targets = p.board.duplicate()
|
||||
elif fx.target == "self_hero":
|
||||
targets = [p]
|
||||
elif fx.target == "self_creature" or fx.target == "trigger_source":
|
||||
elif fx.target == "self_creature":
|
||||
targets = [source] if source != null else []
|
||||
elif fx.target == "strongest_enemy_creature":
|
||||
var best: Creature = null
|
||||
@@ -527,13 +532,15 @@ func resolve(p: Player, fx: Cards.Effect, target, source: Creature = null, bonus
|
||||
for t in targets:
|
||||
match fx.kind:
|
||||
"damage":
|
||||
damage(t, fx.amount + bonus)
|
||||
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})
|
||||
"draw":
|
||||
draw(p, 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
|
||||
@@ -558,8 +565,6 @@ func resolve(p: Player, fx: Cards.Effect, target, source: Creature = null, bonus
|
||||
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 -------------------------------------------------------------------
|
||||
@@ -589,19 +594,21 @@ func attack(cr: Creature, target) -> String:
|
||||
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())
|
||||
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))
|
||||
cleanup()
|
||||
check_end()
|
||||
return ""
|
||||
|
||||
|
||||
## Elementvorteil beim Zuschlagen: gegen Helden gibt es keinen.
|
||||
static func matchup_bonus(source: Creature, target) -> int:
|
||||
if not (target is Creature):
|
||||
return 0
|
||||
return Cards.matchup(source.card.elements, target.card.elements)
|
||||
|
||||
|
||||
func damage(target, amount: int) -> void:
|
||||
if amount <= 0:
|
||||
return
|
||||
@@ -622,9 +629,13 @@ func cleanup() -> void:
|
||||
var died := false
|
||||
for p in players:
|
||||
for c in p.board.duplicate():
|
||||
if c.health <= 0:
|
||||
# Wer besiegt ist, nimmt seine Wesen mit vom Tisch.
|
||||
if c.health <= 0 or not p.alive():
|
||||
p.board.erase(c)
|
||||
say("%s stirbt." % c.name)
|
||||
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:
|
||||
@@ -657,7 +668,7 @@ static func from_snapshot(d: Dictionary) -> Game:
|
||||
return NetState.from_snapshot(d)
|
||||
|
||||
|
||||
# --- Deckregeln und Gegner ---------------------------------------------------
|
||||
# --- Gegner und Kampfstart ---------------------------------------------------
|
||||
|
||||
static func shuffle(arr: Array, r: RandomNumberGenerator) -> void:
|
||||
for i in range(arr.size() - 1, 0, -1):
|
||||
@@ -667,81 +678,27 @@ static func shuffle(arr: Array, r: RandomNumberGenerator) -> void:
|
||||
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:
|
||||
## rules: {"human_perks": [...], "opponent_life": int, "looks": {...}}
|
||||
static func new_battle(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 life: int = int(rules.get("opponent_life", START_LIFE))
|
||||
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)
|
||||
opps.append(DeckGen.random_opponent(i + 1, life))
|
||||
return new_battle_with(opps, r, false, rules)
|
||||
|
||||
|
||||
static func new_battle_with(deck: Array, opps: Array, r: RandomNumberGenerator = null, fixed_order: bool = false, rules: Dictionary = {}) -> Game:
|
||||
static func new_battle_with(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)
|
||||
var human := Player.new("Spieler", true, START_LIFE)
|
||||
human.perks = Array(rules.get("human_perks", []))
|
||||
human.looks = rules.get("looks", {})
|
||||
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:
|
||||
if "leben_35" in p.perks and p.life == START_LIFE:
|
||||
p.life = 35
|
||||
p.max_life = 35
|
||||
g.start(fixed_order)
|
||||
|
||||
Reference in New Issue
Block a user