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-04 22:40:18 +02:00
parent 5f9442497a
commit 0a612f91b3
16 changed files with 177 additions and 79 deletions

View File

@@ -20,19 +20,19 @@ Der Spielstand liegt unter `user://spielstand.json`
## Spielsysteme
- 77 Karten in `data/karten.json`, alle Wesen, jede mit genau einem der vier Elemente; Kombos sind die Namen verschmolzener Wesen
- Karten haben nur Element, Angriff und Leben; die Kosten sind zwei Drittel Angriff plus ein Drittel Leben, abgerundet, mindestens 1
- Mana: 5 im ersten Zug, jede Runde eins mehr ohne Grenze; was übrig bleibt, verfällt. Mana geht in Ziehen, Legen, Verbessern und Verschmelzen
- Kein Deckbau: beide ziehen vom gemeinsamen Stapel (jede Karte dreimal). Zu Beginn fünf Karten, Ziehen kostet 1 Mana und bringt eine; es kommt nur, was das Mana dieses Zugs hergibt, der Rest wandert unter den Stapel
- 81 Karten in `data/karten.json`, alle Wesen, jede mit genau einem der vier Elemente; Kombos sind die Namen verschmolzener Wesen
- Karten haben nur Element, Angriff und Leben; die Kosten sind zwei Drittel Angriff plus ein Drittel Leben, abgerundet; je Element gibt es ein 0/2-Wesen für 0 Mana
- Mana: 1 im ersten Zug, jede Runde eins mehr ohne Grenze; was übrig bleibt, verfällt. Mana geht in Ziehen, Legen, Verbessern und Verschmelzen
- Kein Deckbau: beide ziehen vom gemeinsamen Stapel (jede Karte dreimal). Zu Beginn fünf Karten, alle im ersten Zug spielbar; jeder Zug beginnt mit einer Karte vom Stapel (nicht bei voller Hand), weiteres Ziehen kostet 2 Mana; es kommt nur, was das Mana dieses Zugs hergibt, der Rest wandert unter den Stapel
- Eigenschaften: Spott (2, muss zuerst angegriffen werden), Flink (2, greift sofort an), Regeneration (3, heilt am Zugende ganz) und Doppelschlag (4) lassen sich für Mana auf jede Hand- oder Brettkarte kaufen (Rechtsklick oder Y). Gelegte Wesen greifen erst im nächsten Zug an, außer sie sind flink; verletzte Wesen heilen nicht von selbst
- Verdeckt legen (mittlere Maustaste auf die Handkarte oder im Kartenmenü): gleich teuer, das Wesen greift nicht an und zählt nicht für die Elementboni; aufdecken ist kostenlos, frühestens im nächsten Zug, danach greift es sofort an; ein Angriff deckt es auf
- Jede Karte gehört jedem von Anfang an. Gesammelt wird die Seltenheitsstufe, und die ist reine Optik wie bei Yu-Gi-Oh: Common matt, Rare silberner Name und Rahmen mit Glanzstreifen, Super Rare Holo-Bild, Ultra Rare goldener Name und Rahmen mit Holo-Bild, Ultimate Rare Relief, Holo auf Bild und Rahmen, Funkeln und schillernde Farben; auch auf dem Feld für alle sichtbar, Werte bleiben gleich. Verschmolzene Wesen tragen die höhere Stufe ihrer Eltern
- Booster (100 Münzen): fünf Karten mit gewürfelter Stufe (50/25/15/8/2 %, mindestens eine Rare), höhere werten die Karte auf, der Rest wird zu Staub (50/100/200/500/1000). Aufwerten kostet 100/200/500/1000 Staub, eine Stufe abgeben bringt den Zerlegewert zurück
- Elementboni: zwei eigene offene Wesen eines Elements geben allen eigenen Wesen dieses Elements seinen Effekt (Erde Spott, Luft Flink, Wasser Regeneration, Feuer Doppelschlag); drei offene Wesen eines Elements auf dem Tisch geben dem Element +1/+1 (fällt der Bonus weg, sinkt nur das Höchstleben). Verschmolzene Wesen zählen für alle ihre Elemente und bekommen alle Boni
- Elementvorteil beim Kampf (Wasser > Feuer > Luft > Erde > Wasser): der Angreifer macht 1 Schaden mehr im Vorteil und 1 weniger im Nachteil, der Konter bleibt gleich, gegen Helden nichts; ein Wesen mit mehreren Elementen kämpft mit dem, das gegen den Gegner am besten steht
- Elementvorteil beim Kampf (Wasser > Feuer > Luft > Erde > Wasser): wer im Vorteil ist, macht 1 Schaden mehr, wer im Nachteil ist, 1 weniger, beim Angriff wie beim Konter, gegen Helden nichts; ein Wesen mit mehreren Elementen kämpft mit dem, das gegen den Gegner am besten steht. Ist ein Angreifer gewählt, zeigen die Gegnerkarten ihren Konter gegen ihn (grün mehr, rot weniger); über einem Ziel zeigt der Angreifer seinen Angriff mit Vorteil, und wer stirbt, trägt die Marke „stirbt“
- Angriffe treffen erst Wesen (Wesen mit Spott zuerst); das Leben ist nur ohne Wesen angreifbar. Nichts schlägt durch
- Verschmelzen: eine Karte auf eine andere ziehen und eine Sekunde halten, auf dem Brett wie auf der Hand (Gamepad: Y, „Verschmelzen mit …“). Das neue Wesen hat je Wert den besseren plus ein Drittel des schwächeren (aufgerundet), Kosten passend dazu, Elemente und Eigenschaften beider; kostet 1 Mana, verdoppelt je weiterer Verschmelzung (1, 2, 4, 8); drei oder mehr Elemente ergeben eine Chimäre
- Duell mit ausgeloster Zugreihenfolge, wer nicht beginnt, bekommt einmalig 1 Mana; die Hand fasst 5 Karten, das Feld 5 Wesen
- Duell mit ausgeloster Zugreihenfolge, beide beginnen mit 1 Mana; die Hand fasst 5 Karten, das Feld 5 Wesen
- Schwierigkeit Leicht/Normal/Schwer: Gegner mit 25/30/35 Leben, einfache oder Vorausschau-KI, Münzen 0,75/1/1,5-fach
- KI mit Vorausschau in zwei Stufen und Zeitbudget (150 ms), sie rechnet im Hintergrund-Thread
- Kampagne: drei Kapitel mit je zehn Magiern, Bosse mit ein bis drei Merkmalen, eigenes Merkmal nach Kapitel 1

View File

@@ -15,8 +15,8 @@
- Gesammelt wird die Seltenheitsstufe einer Karte (Common, Rare, Super Rare, Ultra Rare, Ultimate Rare), und die ist reine Optik wie bei Yu-Gi-Oh: silberner oder goldener Name, Holo-Bild, Relief, Funkeln, Schillern; auch im Netzwerkspiel sichtbar; Fusionen tragen die höhere Stufe der Eltern
- Booster öffnen (Chancen 50/25/15/8/2 %, mindestens eine Rare): höhere Stufen werten Karten auf, der Rest wird zu Staub (50/100/200/500/1000)
- Staub wertet gezielt auf (100/200/500/1000), eine Stufe abgeben bringt den Zerlegewert zurück
- Kein Deckbau: alle ziehen vom gemeinsamen Stapel (jede Karte dreimal); zu Beginn fünf Karten; Ziehen kostet 1 Mana und bringt eine Karte; es kommt nur, was man sich in diesem Zug leisten kann
- Mana für Ziehen, Legen (Kosten = 2/3 Angriff + 1/3 Leben, abgerundet, mindestens 1), Verbessern und Verschmelzen; 5 im ersten Zug, dann je Runde eins mehr; der Rest verfällt; höchstens 5 Handkarten
- Kein Deckbau: alle ziehen vom gemeinsamen Stapel (jede Karte dreimal); zu Beginn fünf Karten, alle im ersten Zug spielbar; jeder Zug beginnt mit einer Karte vom Stapel (nicht bei voller Hand), weiteres Ziehen kostet 2 Mana; es kommt nur, was man sich in diesem Zug leisten kann
- Mana für Ziehen, Legen (Kosten = 2/3 Angriff + 1/3 Leben, abgerundet; 0/2 kostet 0), Verbessern und Verschmelzen; 1 im ersten Zug, dann je Runde eins mehr; der Rest verfällt; höchstens 5 Handkarten
- Karten haben nur Angriff und Leben; Eigenschaften kauft man per Rechtsklick für Mana dazu, auf Hand und Brett: Spott 2 (muss zuerst angegriffen werden), Flink 2 (greift sofort an), Regeneration 3 (heilt sich am Zugende ganz), Doppelschlag 4
- Gelegte Wesen greifen erst im nächsten Zug an, außer sie sind flink (auch im ersten Zug); verletzte Wesen heilen nicht von selbst
- Wesen lassen sich verdeckt legen (mittlere Maustaste oder Kartenmenü); aufdecken ist frei, frühestens im nächsten Zug, dann greift es sofort an; ein Angriff deckt auf
@@ -28,6 +28,6 @@
- Kombos sind die Namen verschmolzener Wesen (Feuer + Wasser wird Dampf, gleiche Elemente werden Inferno, Tiefsee, Berg, Orkan)
- Zwei eigene offene Wesen eines Elements: Sondereffekt für alle eigenen Wesen des Elements (Erde Spott, Luft Flink, Wasser Regeneration, Feuer Doppelschlag)
- Drei offene Wesen eines Elements auf dem Tisch: +1/+1 für das Element, kein Malus; die Karte zeigt vorher, was sie bekäme; verschmolzene Wesen zählen für alle ihre Elemente und bekommen alle Effekte und Boni
- Elementvorteil im Kreis: Wasser schlägt Feuer, Feuer schlägt Luft, Luft schlägt Erde, Erde schlägt Wasser (der Angreifer macht 1 Schaden mehr im Vorteil, 1 weniger im Nachteil, der Konter bleibt gleich; ein Wesen mit mehreren Elementen kämpft mit dem, das gegen den Gegner am besten steht)
- Elementvorteil im Kreis: Wasser schlägt Feuer, Feuer schlägt Luft, Luft schlägt Erde, Erde schlägt Wasser (wer im Vorteil ist, macht 1 Schaden mehr, wer im Nachteil ist, 1 weniger, beim Angriff wie beim Konter; ein Wesen mit mehreren Elementen kämpft mit dem, das gegen den Gegner am besten steht)
- Verschmelzen: eine Karte auf eine andere ziehen und eine Sekunde stillhalten, auf dem Brett oder auf der Hand (Gamepad: über das Kartenmenü); das neue erbt Elemente und Eigenschaften beider, je Wert den besseren plus ein Drittel des schwächeren (aufgerundet), Kosten folgen den Werten; Fusionskosten 1 Mana, verdoppelt je weiterer Verschmelzung
- Alle Karten sind Wesen: keine Zauber, keine Fallen; Effekte lösen beim Ausspielen aus

View File

@@ -769,6 +769,46 @@
],
"attack": 5,
"health": 4
},
{
"id": "f_funke",
"name": "Glutfunke",
"name_en": "Ember Spark",
"elements": [
"Feuer"
],
"attack": 0,
"health": 2
},
{
"id": "w_tropfen",
"name": "Tautropfen",
"name_en": "Dewdrop",
"elements": [
"Wasser"
],
"attack": 0,
"health": 2
},
{
"id": "e_kruemel",
"name": "Erdkrümel",
"name_en": "Soil Crumb",
"elements": [
"Erde"
],
"attack": 0,
"health": 2
},
{
"id": "l_hauch",
"name": "Windhauch",
"name_en": "Breeze",
"elements": [
"Luft"
],
"attack": 0,
"health": 2
}
]
}

View File

@@ -39,7 +39,7 @@ Stand: September 2026. Kein Enddesign, sondern die Richtung mit umgesetztem Entw
Nur, was gerade zählt, ist zu sehen. Von unten nach oben:
- **Hand** als bis zu fünf große Karten, links der Zieh-Stapel mit „◆ 1“ (Klick zieht), rechts
- **Hand** als bis zu fünf große Karten, links der Zieh-Stapel mit „◆ 2“ (Klick zieht), rechts
„Zug beenden“.
- **Verschmelzen** per Ziehen: eine Hand- oder Brettkarte auf eine passende ziehen, ein
goldener Ring füllt sich eine Sekunde lang, dann verschmelzen die beiden. Mit Gamepad über

View File

@@ -94,9 +94,9 @@ static func best_element(own: Array, other: Array) -> String:
return best
## Kosten einer Karte: zwei Drittel Angriff plus ein Drittel Leben, abgerundet, mindestens 1.
## Kosten einer Karte: zwei Drittel Angriff plus ein Drittel Leben, abgerundet (0/2 kostet nichts).
static func cost_of(attack: int, health: int) -> int:
return maxi(1, (2 * attack + health) / 3)
return (2 * attack + health) / 3
## Wert eines verschmolzenen Wesens: der bessere Wert plus ein Drittel des schwächeren,

View File

@@ -6,8 +6,8 @@ extends RefCounted
const MAX_BOARD := 5
const MAX_HAND := 5
const START_HAND := 5 # Startkarten, alle im ersten Zug bezahlbar
const START_MANA := 5 # Mana in Zug 1; jede Runde eins mehr, ohne Grenze; Rest verfällt
const DRAW_COST := 1 # Ziehen: eine bezahlbare Karte vom Stapel
const START_MANA := 1 # Mana in Zug 1; jede Runde eins mehr, ohne Grenze; Rest verfällt
const DRAW_COST := 2 # Ziehen: eine bezahlbare Karte vom Stapel
const START_LIFE := 30
const ELEMENT_PAIR := 2 # so viele eigene offene Wesen eines Elements: Sondereffekt des Elements
const ELEMENT_STACK := 3 # so viele offene Wesen eines Elements auf dem Tisch: +1/+1
@@ -131,7 +131,6 @@ class Player extends RefCounted:
var board: Array = []
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 looks := {} # Karten-ID -> Seltenheitsstufe, reine Optik (siehe Profile)
@@ -161,7 +160,6 @@ class Player extends RefCounted:
p.hand = hand.duplicate()
p.energy = energy
p.max_energy = max_energy
p.start_bonus = start_bonus
p.order = order
p.perks = perks.duplicate()
p.looks = looks
@@ -277,9 +275,7 @@ func start(fixed_order: bool = false) -> void:
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
for c in draw_cards(START_HAND, START_MANA + p.start_bonus):
for c in draw_cards(START_HAND, START_MANA):
p.hand.append(c.copy())
turn = 1
current_idx = 0
@@ -349,16 +345,19 @@ func begin_turn(p: Player) -> void:
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
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
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
say("--- Zug %d: %s ---" % [turn, p.name])
emit({"type": "turn", "player": players.find(p)})
# Jeder Zug beginnt mit einer Karte vom Stapel, die man sich in diesem Zug leisten kann.
if p.hand.size() < MAX_HAND:
p.hand.append(draw_cards(1, p.max_energy)[0].copy())
say("%s zieht eine Karte." % p.name)
emit({"type": "draw", "player": players.find(p)})
else:
say("%s hat die Hand voll und zieht nicht." % p.name)
## Zugende: Wesen mit Regeneration heilen ganz, dann ist der andere dran.
@@ -661,15 +660,15 @@ func attack(cr: Creature, target) -> String:
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; der Konter kennt keinen Elementvorteil
damage(cr, target.power())
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: +1 im Vorteil, -1 im Nachteil (die Verteidigung des
## Ziels), gegen Helden gibt es keinen.
## Elementvorteil beim Zuschlagen und beim Konter: +1 im Vorteil, -1 im Nachteil,
## gegen Helden gibt es keinen.
static func matchup_bonus(source: Creature, target) -> int:
if not (target is Creature):
return 0

View File

@@ -26,7 +26,7 @@ static func snapshot(g: Game, viewer: int) -> Dictionary:
"max_health": 0 if masked else c.max_health, "attacks_done": c.attacks_done,
"ready": c.ready, "hidden": c.hidden, "aura": c.aura})
ps.append({"name": p.name, "title": p.title, "is_human": p.is_human, "life": p.life, "max_life": p.max_life,
"energy": p.energy, "max_energy": p.max_energy, "start_bonus": p.start_bonus,
"energy": p.energy, "max_energy": p.max_energy,
"order": p.order, "perks": p.perks, "looks": p.looks, "turns": p.turns,
"hand": p.hand.map(card_ref) if mine else p.hand.size(), "board": board})
var evs := []
@@ -50,7 +50,6 @@ static func from_snapshot(d: Dictionary) -> Game:
p.max_life = int(pd.max_life)
p.energy = int(pd.energy)
p.max_energy = int(pd.max_energy)
p.start_bonus = int(pd.start_bonus)
p.order = int(pd.order)
p.perks = Array(pd.perks)
for cid in pd.get("looks", {}):

View File

@@ -16,6 +16,7 @@ var game: Game
var perspective: Game.Player # der Mensch, dessen Hand unten liegt
var fx: BattleFx
var selected = null # Game.Creature
var hover_target = null # fremdes Wesen unter der Maus: Vorschau des Angriffs mit dem gewählten Wesen
var fuse_first = null # Karte oder Wesen, für das im Menü „Verschmelzen mit …“ gewählt wurde
var hold_time := 1.0 # so lange still über dem Ziel halten, dann verschmelzen die beiden
var press_target = null # Karte oder Wesen unter der gedrückten linken Maustaste
@@ -928,6 +929,23 @@ func matchup_for(cr: Game.Creature) -> int:
return 0
## Vorschau für den Angriff auf das Wesen unter der Maus: der Angreifer zeigt seinen Angriff mit
## Elementvorteil, und wer dabei stirbt, bekommt die Marke. Verdeckte Ziele bleiben unbekannt.
func update_attack_preview() -> void:
var target = hover_target
if selected == null or target == null or target.hidden or not game.can_attack(selected, target)[0]:
target = null
for v in enemy_views:
v.doomed = target != null and v.creature == target and selected.power() + Game.matchup_bonus(selected, target) >= target.health
v.queue_redraw()
for v in own_views:
var mine: bool = v.creature == selected
v.attack_shift = Game.matchup_bonus(selected, target) if mine and target != null else 0
var counter: int = target.power() + Game.matchup_bonus(target, selected) if target != null and target.power() > 0 else 0
v.doomed = mine and target != null and counter >= selected.health
v.queue_redraw()
class Deck extends Control:
signal clicked
var enabled := true
@@ -1078,6 +1096,8 @@ func refresh() -> void:
tile_layer.add_child(tile)
var looks: Dictionary = p.looks
tile.hover.connect(func(card, entered):
hover_target = tile.creature if entered else null
update_attack_preview()
if tile.secret:
show_preview(null, false)
else:
@@ -1123,6 +1143,7 @@ func refresh() -> void:
var t: CreatureView = tiles[uid]
tiles.erase(uid)
fx.fade_out(t)
update_attack_preview()
deck.visible = not spectating()
deck.enabled = mine and g.can_draw(h)[0]
deck.count = g.deck.size()

View File

@@ -161,7 +161,7 @@ func _draw_full(rect: Rect2, frame: Color, seed: int) -> void:
fx, HORIZONTAL_ALIGNMENT_LEFT, box.size.x - 8, tsize, max_lines, Style.TEXT)
# Werte springen aus den unteren Ecken, dazwischen der Edelstein
Style.stat_circle(self, Vector2(r_stat - 2, size.y - r_stat + 2), r_stat, str(card.attack), Style.AMBER, f_stat)
Style.stat_circle(self, Vector2(size.x - r_stat + 2, size.y - r_stat + 2), r_stat, str(card.health), Style.CRIMSON, f_stat)
Style.stat_circle(self, Vector2(size.x - r_stat + 2, size.y - r_stat + 2), r_stat, str(card.health), Style.LIFE, f_stat)
if bonus > 0:
Style.text_outlined(self, "+%d/+%d" % [bonus, bonus], f_text, Style.GREEN, Vector2(r_stat * 2 + 4, size.y - r_stat + 2), "midleft", 3)
Style.rarity_gem(self, rarity, Vector2(size.x / 2, size.y - r_stat + 1), 8 if big else 6, seed)
@@ -188,7 +188,7 @@ func _draw_compact(rect: Rect2, frame: Color, seed: int) -> void:
Style.keyword_icon(self, k, Vector2(x + 6, size.y - 13), 6)
x += 18
Style.stat_circle(self, Vector2(11, size.y - 11), 10, str(card.attack), Style.AMBER, 12)
Style.stat_circle(self, Vector2(size.x - 11, size.y - 11), 10, str(card.health), Style.CRIMSON, 12)
Style.stat_circle(self, Vector2(size.x - 11, size.y - 11), 10, str(card.health), Style.LIFE, 12)
if bonus > 0:
Style.text_outlined(self, "+%d" % bonus, 10, Style.GREEN, Vector2(4, size.y - 29), "midleft", 3)
Style.rarity_sparkle(self, rect, rarity, seed)

View File

@@ -11,7 +11,9 @@ var width := 2
var flash := false # kurz rot: wurde gerade angegriffen
var slot: Vector2 # Zielposition im Raster
var busy := false # wird gerade animiert (kein Neupositionieren)
var matchup := 0 # Elementvorteil des gewählten Angreifers gegen dieses Wesen (+1/-1)
var matchup := 0 # Elementvorteil des gewählten Angreifers gegen dieses fremde Wesen (+1/-1); die Werte zeigen dann, was ihn erwartet
var attack_shift := 0 # eigener Angreifer: Elementvorteil gegen das Ziel unter der Maus
var doomed := false # stirbt bei dem Angriff, der gerade unter der Maus liegt
var secret := false # verdecktes fremdes Wesen: nur der Rücken ist zu sehen
var hold := 0.0 # Fortschritt des Verschmelzens, wenn eine Karte darüber gehalten wird (0..1)
@@ -66,14 +68,24 @@ func _draw() -> void:
bcol = Style.RED
Style.border(self, rect, bcol, 4 if flash else width, 6)
var fs := 11 if small else 13
var name_w: float = size.x - (30 if matchup != 0 else 6) # Platz für den Vorteilskreis oben rechts
var name_w: float = size.x - 6
var nf := Style.fit_size(creature.card.title(), true, fs, name_w)
draw_rect(Rect2(2, 2, size.x - 4, Style.fs(fs) + 8), Color(0.05, 0.05, 0.08, 0.7))
Style.text(self, Style.fit(creature.card.title(), true, Style.fs(nf), name_w), nf, Style.rarity_name_color(rar, seed), Vector2(3 + name_w / 2, 6), "midtop", true)
# Elementbonus als ein Abzeichen rechts unter dem Namen; die Symbole rücken dafür nach links
var badge := ""
var bw := 0.0
if creature.aura != 0:
badge = "+%d/+%d" % [creature.aura, creature.aura] if creature.aura > 0 else "−%d/−%d" % [-creature.aura, -creature.aura]
bw = Style.FONT_BOLD.get_string_size(badge, HORIZONTAL_ALIGNMENT_LEFT, -1, Style.fs(10)).x + 8
var by := Style.fs(fs) + 13
draw_rect(Rect2(size.x - bw - 4, by, bw, 16), Color(0.05, 0.05, 0.08, 0.75))
Style.text(self, badge, 10, Style.GREEN if creature.aura > 0 else Style.RED, Vector2(size.x - 4 - bw / 2, by + 8), "center", true)
# Eigenschaften als Symbole: gekaufte weiß, die vom Elementpaar in der Elementfarbe
var icon := 7.0 if small else 9.0
var kws := creature.keywords()
var x := size.x / 2 - (kws.size() * (icon * 2 + 6) - 6) / 2.0
var avail := size.x - (bw + 8 if badge != "" else 0.0)
var x := avail / 2 - (kws.size() * (icon * 2 + 6) - 6) / 2.0
var y := Style.fs(fs) + 14 + icon
for k in kws:
var col := Color("f2f2f6")
@@ -83,11 +95,14 @@ func _draw() -> void:
col = Style.lighter(Cards.ELEMENT_COLORS[e], 0.45)
Style.keyword_icon(self, k, Vector2(x + icon, y), icon, col)
x += icon * 2 + 6
_draw_stats(15 if small else 17, 10 if small else 12, 12.0 if small else 14.0)
_draw_stats(15 if small else 17, 12.0 if small else 14.0)
Style.rarity_gem(self, rar, Vector2(size.x / 2, size.y - 9), 5, seed)
if creature.hidden:
draw_rect(rect, Color(0.05, 0.05, 0.1, 0.55))
Style.text_outlined(self, tr("verdeckt"), 12, Style.TEXT, Vector2(size.x / 2, size.y / 2), "center", 3)
if doomed:
draw_rect(Rect2(0, size.y * 0.5 - 12, size.x, 24), Color(0.05, 0.02, 0.02, 0.8))
Style.text_outlined(self, tr("stirbt"), 14, Style.RED, Vector2(size.x / 2, size.y * 0.5), "center", 3)
Style.rarity_sparkle(self, rect, rar, seed)
_draw_hold()
@@ -110,21 +125,26 @@ func _draw_back(rect: Rect2) -> void:
draw_style_box(Style.box(Color("3c285a"), 6, bcol, 4 if flash else maxi(width, 2)), rect)
Style.border(self, rect.grow(-10), Color("5a3c82"), 2, 6)
Style.text(self, "?", 36, Style.GOLD, size / 2, "center", true)
if matchup != 0:
pass # unbekanntes Element: kein Vorteil anzeigen
## Angriff (grün, wenn der Elementbonus ihn hebt), Leben, Elementvorteil des Angreifers.
func _draw_stats(f: int, f_small: int, r: float) -> void:
var extra := creature.power() - creature.attack
var atk_col := Style.GREEN if extra > 0 else (Style.RED if extra < 0 else Style.AMBER)
Style.stat_circle(self, Vector2(r - 1, size.y - r + 1), r, str(creature.power()), atk_col, f)
if extra != 0:
Style.text_outlined(self, ("+%d" % extra) if extra > 0 else ("−%d" % -extra), f_small, Style.GREEN if extra > 0 else Style.RED, Vector2(r * 2 + 3, size.y - r + 1), "midleft", 3)
var hp_col := Style.CRIMSON if creature.health < creature.max_health else Color("b84a4a")
Style.stat_circle(self, Vector2(size.x - r + 1, size.y - r + 1), r, str(creature.health), hp_col, f)
## Angriff und Leben für den Angriff, der gerade vorbereitet wird. Fremdes Wesen bei gewähltem
## Angreifer: sein Konter gegen ihn (im Nachteil 1 weniger, wer 0 Angriff hat, kontert gar nicht),
## das Leben bleibt echt. Eigener Angreifer über einem Ziel: sein Angriff mit Elementvorteil.
func shown_stats() -> Array:
if matchup != 0:
var c := Vector2(size.x - 12, 12)
draw_circle(c, 11, Color("14141c"))
Style.text(self, ("+%d" % matchup) if matchup > 0 else ("−%d" % -matchup), f_small + 1,
Style.GREEN if matchup > 0 else Style.RED, c, "center", true)
var atk := 0 if creature.power() <= 0 else maxi(0, creature.power() - matchup)
return [atk, creature.health]
if attack_shift != 0:
return [maxi(0, creature.power() + attack_shift), creature.health]
return [creature.power(), creature.health]
## Angriff gelb, Leben blau; die Zahl wird grün, wenn sie für den aktuellen Angriff steigt, rot, wenn sie sinkt.
func _draw_stats(f: int, r: float) -> void:
var shown := shown_stats()
var atk: int = shown[0]
var hp: int = shown[1]
var atk_text := Style.GREEN if atk > creature.power() else (Style.RED if atk < creature.power() else Color.WHITE)
var hp_text := Style.GREEN if hp > creature.health else (Style.RED if hp < creature.health else Color.WHITE)
Style.stat_circle(self, Vector2(r - 1, size.y - r + 1), r, str(atk), Style.AMBER, f, atk_text)
Style.stat_circle(self, Vector2(size.x - r + 1, size.y - r + 1), r, str(hp), Style.LIFE, f, hp_text)

View File

@@ -68,9 +68,6 @@ func _draw() -> void:
draw_circle(c, 4.5, Color(0, 0, 0, 0.35))
draw_arc(c, 4.5, 0, TAU, 16, Color(col, 0.6), 1.0)
px += Game.ELEMENT_STACK * 13 + 2
if n > Game.ELEMENT_STACK:
Style.text(self, "+%d" % (n - Game.ELEMENT_STACK), 11, Style.MUTED, Vector2(px, cy), "midleft")
px += 18
if paired(e):
Style.keyword_icon(self, Cards.ELEMENT_KEYWORD[e], Vector2(px + 10, cy), 7, Style.lighter(col, 0.45))
if strong(e):

View File

@@ -19,7 +19,7 @@ const YELLOW := Color("ffe65a")
const ORANGE := Color("ff963c")
const PURPLE := Color("8c78dc")
const AMBER := Color("f0b429") # Angriff
const CRIMSON := Color("d64545") # Leben
const LIFE := Color("4a86d8") # Leben
const MANA := Color("3a78d2") # Kostenkristall
const PRIMARY := Color("285a3c") # Hauptaktion
const DANGER := Color("5a2828")
@@ -476,11 +476,11 @@ static func cost_crystal(ci: CanvasItem, c: Vector2, r: float, cost: int, font:
## Wertkreis (Angriff gelb, Leben rot) mit Zahl.
static func stat_circle(ci: CanvasItem, c: Vector2, r: float, value: String, color: Color, font: int) -> void:
static func stat_circle(ci: CanvasItem, c: Vector2, r: float, value: String, color: Color, font: int, text_col: Color = Color.WHITE) -> void:
ci.draw_circle(c, r + 2, Color("14141c"))
ci.draw_circle(c, r, color)
ci.draw_circle(c + Vector2(-r * 0.25, -r * 0.3), r * 0.45, Color(1, 1, 1, 0.16))
text_outlined(ci, value, font, Color.WHITE, c, "center", 3)
text_outlined(ci, value, font, text_col, c, "center", 3)
## Eigenschaft als Symbol auf dunklem Kreis: Spott Ausrufezeichen, Flink Doppelpfeil,

View File

@@ -90,14 +90,14 @@ func _initialize() -> void:
func test_kartenpool() -> void:
check(Cards.all().size() == 77, "77 Karten")
check(Cards.all().size() == 81, "81 Karten")
check(Cards.all().all(func(c): return c.elements.size() == 1 and c.elements[0] in Cards.ELEMENTS), "jede Karte hat genau ein Element")
for e in Cards.ELEMENTS:
check(Cards.all().any(func(c): return c.elements == [e]), "Element vorhanden: " + e)
for c in Cards.all():
check(c.health > 0 and c.cost == maxi(1, floori((2 * c.attack + c.health) / 3.0)), "Kosten = (2 · Angriff + Leben) / 3, abgerundet: " + c.id)
check(c.health > 0 and c.cost == floori((2 * c.attack + c.health) / 3.0), "Kosten = (2 · Angriff + Leben) / 3, abgerundet: " + c.id)
check(c.keywords.is_empty() and c.text() == "", "Karten kommen ohne Eigenschaften: " + c.id)
check(C("f_kobold").cost == 1 and C("f_wolf").cost == 2 and C("ff_phoenix").cost == 7 and C("e_wall").cost == 1 and C("f_drache").cost == 5, "Kostenbeispiele")
check(C("f_funke").cost == 0 and C("f_kobold").cost == 1 and C("f_wolf").cost == 2 and C("ff_phoenix").cost == 7 and C("e_wall").cost == 1 and C("f_drache").cost == 5, "Kostenbeispiele, 0/2 kostet nichts")
var a := C("f_wolf")
var b := C("f_wolf")
a.keywords.append("Spott")
@@ -111,7 +111,7 @@ func test_kombos() -> void:
check(Cards.combo_name(["Feuer", "Wasser", "Erde"]) == "Chimäre", "drei Elemente sind eine Chimäre")
## Mana: 5 im ersten Zug, jede Runde eins mehr, der Rest verfällt; Legen kostet die Kartenkosten.
## Mana: 1 im ersten Zug, jede Runde eins mehr, der Rest verfällt; Legen kostet die Kartenkosten.
func test_mana_und_kosten() -> void:
var s := setup(["f_wolf", "f_drache"])
var g: Game = s[0]
@@ -120,10 +120,10 @@ func test_mana_und_kosten() -> void:
h.max_energy = 0
h.turns = 0
g.begin_turn(h)
check(h.energy == Game.START_MANA and h.max_energy == Game.START_MANA, "erster Zug: 5 Mana")
check(h.energy == Game.START_MANA and h.max_energy == Game.START_MANA, "erster Zug: 1 Mana")
h.energy = 3 # Rest
g.begin_turn(h)
check(h.energy == Game.START_MANA + 1, "zweiter Zug: 6 Mana, der Rest ist verfallen")
check(h.energy == Game.START_MANA + 1, "zweiter Zug: 2 Mana, der Rest ist verfallen")
for i in 20:
g.begin_turn(h)
check(h.energy == Game.START_MANA + 21, "keine Obergrenze")
@@ -133,7 +133,8 @@ func test_mana_und_kosten() -> void:
check(Cards.fusion_value(4, 2) == 5 and Cards.fusion_value(8, 1) == 9 and Cards.fusion_value(2, 2) == 3 and Cards.fusion_value(0, 4) == 4 and Cards.fusion_value(8, 8) == 11, "Fusionswert: besserer Wert plus ein Drittel des schwächeren")
## Start: fünf bezahlbare Karten, keine Wahl. Ziehen: 1 Mana, eine bezahlbare Karte vom Stapel.
## Start: fünf im ersten Zug bezahlbare Karten, keine Wahl. Jeder Zug beginnt mit einer Karte;
## Ziehen: 2 Mana, eine weitere bezahlbare Karte vom Stapel.
func test_ziehen() -> void:
var r := RandomNumberGenerator.new()
r.seed = 1
@@ -141,14 +142,17 @@ func test_ziehen() -> void:
check(g.deck.size() == Cards.all().size() * DeckGen.COPIES - 2 * Game.START_HAND, "Stapel: jede Karte dreimal, zehn schon ausgegeben")
for p in g.players:
check(p.hand.size() == Game.START_HAND, "Start: fünf Karten auf der Hand")
check(p.hand.all(func(c): return c.cost <= Game.START_MANA + p.start_bonus), "nur Karten, die im ersten Zug spielbar sind")
check(p.hand.all(func(c): return c.cost <= Game.START_MANA), "nur Karten, die im ersten Zug spielbar sind")
check(p.hand.all(func(c): return c != Cards.by_id(c.id)), "eigene Exemplare")
var first: Game.Player = g.current()
check(g.draw(first) == "Deine Hand ist voll", "fünf Handkarten sind das Limit")
first.hand.pop_back()
check(g.draw(first) == "Nicht genug Mana", "im ersten Zug reicht das Mana nicht zum Ziehen")
first.energy = 3
first.max_energy = 3
var mana := first.energy
var deck_before := g.deck.size()
check(g.draw(first) == "" and first.energy == mana - 1 and first.hand.size() == Game.START_HAND, "gezogen: 1 Mana, eine Karte")
check(g.draw(first) == "" and first.energy == mana - Game.DRAW_COST and first.hand.size() == Game.START_HAND, "gezogen: 2 Mana, eine Karte")
check(first.hand.back().cost <= first.max_energy and g.deck.size() == deck_before - 1, "bezahlbar; zu teure liegen wieder unten im Stapel")
first.energy = 0
first.hand.pop_back()
@@ -165,10 +169,25 @@ func test_ziehen() -> void:
var seen_expensive := false
for i in 12:
first.hand = []
first.energy = 20
g.draw(first)
if first.hand[0].cost > 5:
seen_expensive = true
check(seen_expensive, "mit 20 Mana kommen auch teure Karten")
# Zugbeginn: eine Karte gratis, solange die Hand nicht voll ist
var g5 := Game.new_battle(r)
var p1: Game.Player = g5.current()
var p2: Game.Player = g5.enemy_of(p1)
g5.end_turn()
check(p2.hand.size() == Game.START_HAND, "volle Hand: zu Zugbeginn kommt nichts")
p2.hand.pop_back()
p2.hand.pop_back()
g5.end_turn()
p1.hand.pop_back()
g5.end_turn()
check(p2.hand.size() == Game.START_HAND - 1 and p2.hand.back().cost <= p2.max_energy, "Zugbeginn: eine bezahlbare Karte gezogen")
g5.end_turn()
check(p1.hand.size() == Game.START_HAND, "auch der Startspieler zieht zu Zugbeginn")
## Gelegte Wesen greifen erst im nächsten Zug an; kein Durchschlag, keine Heilung zu Zugbeginn.
@@ -219,12 +238,12 @@ func test_elementvorteil() -> void:
var wolf := put(o, "f_wolf") # Feuer 3/2
var life: int = o.life
g.attack(tiefen, wolf)
check(wolf.health <= 0 and o.life == life and tiefen.health == 6 - 3, "Wasser gegen Feuer: +1 beim Zuschlagen, der Konter bleibt gleich, nichts schlägt durch")
check(wolf.health <= 0 and o.life == life and tiefen.health == 6 - 2, "Wasser gegen Feuer: +1 beim Zuschlagen, der Konter macht 1 weniger, nichts schlägt durch")
var s3 := setup()
var w3 := put(s3[1], "f_wolf")
var t3 := put(s3[2], "w_tiefen")
s3[0].attack(w3, t3)
check(t3.health == 6 - 2 and w3.health <= 0, "Feuer gegen Wasser: 1 weniger Schaden, der Konter bleibt gleich")
check(t3.health == 6 - 2 and w3.health == 2 - 5, "Feuer gegen Wasser: 1 weniger Schaden, der Konter macht 1 mehr")
var s2 := setup()
var g2: Game = s2[0]
var geist := put(s2[1], "w_geist") # Wasser 2/2
@@ -451,7 +470,7 @@ func test_verschmelzen() -> void:
check(hc.name == "Dampf" and hc.attack == 5 and hc.health == 7 and hc.cost == 5 and hc.fusions == 1, "Dampf auf der Hand mit 5/7 und Kosten 5")
var lava := Cards.fused(C("f_asche"), C("f_kobold"), 4, 8)
check(Cards.fused(lava, C("f_kobold")).attack == 5 and Cards.fused(lava, C("f_kobold")).health == 9, "ein Kobold macht Lava 4/8 zu 5/9, nie schlechter")
check(g4.play_card(h4, hc) == "" and h4.energy == 4 and h4.board.back().name == "Dampf", "Ausspielen kostet die neuen Kosten")
check(g4.play_card(h4, hc) == "" and h4.energy == 9 - 5 and h4.board.back().name == "Dampf", "Ausspielen kostet die neuen Kosten")
check(g4.log.any(func(l): return l == "Flammenwolf und Tiefenwächter verschmelzen auf der Hand zu Dampf (5/7)."), "Verlauf")
@@ -475,18 +494,16 @@ func test_gleiche_regeln() -> void:
r.seed = 0
var g := Game.new_battle(r)
var first: Game.Player = g.players[0]
check(g.current() == first and first.max_energy == Game.START_MANA and first.energy == Game.START_MANA, "Startspieler: 5 Mana, kein Bonus")
check(g.current() == first and first.max_energy == Game.START_MANA and first.energy == Game.START_MANA, "Startspieler: 1 Mana, kein Bonus")
check(g.players.all(func(p): return p.hand.size() == Game.START_HAND), "jeder beginnt mit fünf Karten")
for p in g.players:
check(p.life == Game.START_LIFE, "gleiches Leben")
if p != first:
check(p.start_bonus == 1, "Nachzieher: Bonus offen")
g.end_turn()
var second: Game.Player = g.players[1]
check(g.current() == second and second.energy == Game.START_MANA + 1, "Nachzieher: 5 Mana plus Ausgleich")
check(g.current() == second and second.energy == Game.START_MANA, "Nachzieher: ebenfalls 1 Mana, kein Ausgleich")
for i in 2:
g.end_turn()
check(g.current() == second and second.max_energy == Game.START_MANA + 1 and second.energy == Game.START_MANA + 1, "Ausgleich nur einmal, Rest verfällt")
check(g.current() == second and second.max_energy == Game.START_MANA + 1 and second.energy == Game.START_MANA + 1, "zweiter Zug: 2 Mana")
func test_zugreihenfolge() -> void:
@@ -756,11 +773,11 @@ func test_merkmale() -> void:
var archi: Game.Player = g3.opponent
check(g3.players[0] == g3.human, "feste Reihenfolge: Mensch beginnt")
g3.end_turn()
check(g3.current() == archi and archi.energy == Game.START_MANA + 2 + 1, "Archimagier hat im ersten Zug 2 Mana mehr (plus Ausgleich)")
check(g3.current() == archi and archi.energy == Game.START_MANA + 2, "Archimagier hat im ersten Zug 2 Mana mehr")
var nyx: Game.Player = Campaign.opponent(5)
var g4 := Game.new_battle_with(nyx, r, true)
g4.end_turn()
check(nyx.max_energy == Game.START_MANA and nyx.energy == Game.START_MANA + 1 + 1, "Nyx bekommt 1 Mana mehr je Zug")
check(nyx.max_energy == Game.START_MANA and nyx.energy == Game.START_MANA + 1, "Nyx bekommt 1 Mana mehr je Zug")
func test_ki_zwei_schritte() -> void:

View File

@@ -331,7 +331,7 @@ func test_gegnerauswahl() -> void:
var opp: Game.Player = hard.game.opponent
check(opp.max_life == Profile.DIFFICULTIES.schwer.life and hard.smart_ai, "Schwer: Gegner mit %d Leben, Vorausschau-KI" % Profile.DIFFICULTIES.schwer.life)
check(hard.game.human.hand.size() == Game.START_HAND and hard.hand_views.size() == Game.START_HAND, "Start: fünf Karten auf der Hand")
check(hard.game.human.hand.all(func(c): return c.cost <= Game.START_MANA + 1), "alle fünf sind im ersten Zug spielbar")
check(hard.game.human.hand.all(func(c): return c.cost <= Game.START_MANA), "alle fünf sind im ersten Zug spielbar")
await shot("05_starthand")
check(hard.hero_panels[opp].position == BattleLayout.OPP_BOX.position, "der Gegner steht oben in der Mitte")
await shot("11_kampf")
@@ -507,7 +507,7 @@ func test_kampf_steuerung() -> void:
g.human.hand = [Cards.by_id("f_wolf").copy()]
scr.refresh()
check(scr.hand_views.size() == 1 and scr.deck.enabled, "eine Handkarte, Stapel bereit")
# Ziehen: 1 Mana weg, eine Karte landet auf der Hand
# Ziehen: 2 Mana weg, eine Karte landet auf der Hand
var mana: int = g.human.energy
await click(scr.deck)
check(g.human.energy == mana - Game.DRAW_COST and g.human.hand.size() == 2 and scr.hand_views.size() == 2, "gezogen: eine Karte auf der Hand")
@@ -636,7 +636,7 @@ func test_kampf_elemente_und_verschmelzen() -> void:
# Rechtsklick auf die Handkarte: Menü mit Verdeckt legen und Eigenschaften, ohne Verschmelzen
await hover(scr.hand_views[0])
await right_click_at(scr.hand_views[0].get_global_rect().get_center())
check(scr.menu_layer.visible and find_button("Verdeckt legen ◆ 1", scr.menu_layer) != null and find_button("Spott ◆ 2", scr.menu_layer) != null, "Rechtsklick öffnet das Kartenmenü")
check(scr.menu_layer.visible and find_button("Verdeckt legen ◆ %d" % g.human.hand[0].cost, scr.menu_layer) != null and find_button("Spott ◆ 2", scr.menu_layer) != null, "Rechtsklick öffnet das Kartenmenü")
check(find_button("Verschmelzen mit … ◆ 1", scr.menu_layer) == null, "kein Verschmelzen im Rechtsklick-Menü")
await shot("23_kartenmenue")
await right_click()
@@ -665,6 +665,10 @@ func test_kampf_elemente_und_verschmelzen() -> void:
# Angriffsziel: Elementvorteil, verdeckte ohne Anzeige, Held gesperrt
await click(scr.own_views[0])
check(scr.selected == wolf and scr.tiles[falke.uid].matchup == 1 and scr.tiles[secret.uid].matchup == 0, "Angreifer gewählt: +1 gegen Luft, nichts gegen Verdeckte")
check(scr.tiles[falke.uid].shown_stats() == [falke.power() - 1, falke.health], "der Falke zeigt seinen Konter gegen den Wolf (1 weniger) und sein echtes Leben")
await hover(scr.tiles[falke.uid])
check(scr.tiles[falke.uid].doomed and not scr.own_views[0].doomed and scr.own_views[0].shown_stats()[0] == wolf.power() + 1, "über dem Falken: er stirbt, der Wolf nicht, der Wolf schlägt mit +1 zu")
await shot("23c_angreifer_gewaehlt")
check(not g.can_attack(wolf, scr.opponent())[0], "Held nicht angreifbar, solange Wesen stehen")
await click(scr.tiles[secret.uid])
check(not secret.hidden and not (secret in scr.opponent().board), "Angriff deckt auf und tötet den Kobold")
@@ -686,7 +690,7 @@ func test_kampf_elemente_und_verschmelzen() -> void:
check(g.human.hand.size() == 2 and g.human.hand[0].name == "Inferno" and g.human.hand[0].attack == 3 and g.human.hand[0].health == 2, "zwei Kobolde auf der Hand zu Inferno 3/2")
# Gamepad-Menü (Y) bietet Verdeckt legen und Verschmelzen mit …
scr.open_menu(g.human.hand[0], Rect2(scr.hand_views[0].position, scr.hand_views[0].size), true)
check(find_button("Verdeckt legen ◆ 2", scr.menu_layer) != null, "Gamepad-Menü: Verdeckt legen")
check(find_button("Verdeckt legen ◆ %d" % g.human.hand[0].cost, scr.menu_layer) != null and g.human.hand[0].cost == 2, "Gamepad-Menü: Verdeckt legen, Inferno kostet 2")
await click(find_button("Verschmelzen mit … ◆ 2", scr.menu_layer))
check(scr.fuse_first == g.human.hand[0], "Partner wählen")
await click(scr.hand_views[1])

View File

@@ -154,3 +154,4 @@ Erst die Wesen mit Spott,Taunting creatures first
greift erst im nächsten Zug an,attacks next turn
%s durch das Element,%s from its element
Zwei verschiedene Karten wählen,Pick two different cards
stirbt,dies
1 keys en
154 greift erst im nächsten Zug an attacks next turn
155 %s durch das Element %s from its element
156 Zwei verschiedene Karten wählen Pick two different cards
157 stirbt dies

Binary file not shown.