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-09-19 13:37:12 +02:00
parent 89db101137
commit 8944fdd017
21 changed files with 62 additions and 562 deletions

View File

@@ -117,6 +117,19 @@ func confirm() -> void:
index = 0
## Pulsierender Rahmen als Zeiger.
class Highlight extends Control:
var t := 0.0
func _process(delta: float) -> void:
t += delta
queue_redraw()
func _draw() -> void:
var a := 0.55 + 0.45 * sin(t * 6.0)
Style.border(self, Rect2(Vector2.ZERO, size), Color(Style.GOLD, a), 4, 8)
func draw_cursor() -> void:
Style.clear(screen.cursor_layer)
if not active or screen.game == null or not screen.my_turn():
@@ -127,7 +140,7 @@ func draw_cursor() -> void:
index = clampi(index, 0, items.size() - 1)
var item = items[index]
var r: Rect2 = Rect2(item.slot, item.size) if item is CreatureView else Rect2(item.position, item.size)
var hl := BattleScreen.Highlight.new()
var hl := Highlight.new()
hl.position = r.position - Vector2(6, 6)
hl.size = r.size + Vector2(12, 12)
hl.mouse_filter = Control.MOUSE_FILTER_IGNORE

View File

@@ -1,12 +1,11 @@
class_name BattleScreen
extends Control
## Kampf: Zustand, Eingabe und Abgleich der Ansicht. Layout in BattleLayout, Animationen in BattleFx.
## Modi: Einzelspieler, Kampagne, Anleitung, Netzwerk (Host, Mitspieler oder Zuschauer).
## Modi: Einzelspieler, Kampagne, Netzwerk (Host, Mitspieler oder Zuschauer).
var app
var n_opp: int
var stage := -1 # Kampagnenstufe oder -1
var tutorial := false
var net_play := false # Netzwerkspiel (auch als Zuschauer)
var online_seat := -1 # Netzwerk: eigener Platz, Zuschauer -1
var net: Net
@@ -26,11 +25,6 @@ var ended := false
var zoom_player = null
var preview_timer := 0.0
var played_ids: Array = [] # vom Menschen gespielte Karten (Statistik)
var guide: BattleTutorial
var tutorial_part := 1
var tutorial_step: int:
get:
return guide.step if guide != null else 0
var pad: BattleGamepad
var gp_zone: String:
get:
@@ -96,26 +90,9 @@ var mulligan_selected: Array:
get:
return mulligan.selected
var cursor_layer: Control
var tutorial_panel: Panel
var tutorial_text: RichTextLabel
var b_tut_next: Button
var wait_label: Label
var b_skip: Button
var b_undo: Button
var highlight_layer: Control
## Pulsierender Rahmen für die Anleitung.
class Highlight extends Control:
var t := 0.0
func _process(delta: float) -> void:
t += delta
queue_redraw()
func _draw() -> void:
var a := 0.55 + 0.45 * sin(t * 6.0)
Style.border(self, Rect2(Vector2.ZERO, size), Color(Style.GOLD, a), 4, 8)
func _init(n: int = 2, campaign_stage: int = -1) -> void:
@@ -130,13 +107,6 @@ static func online(seat: int) -> BattleScreen:
return b
static func guided(part: int = 1) -> BattleScreen:
var b := BattleScreen.new(1, -1)
b.tutorial = true
b.tutorial_part = part
return b
## Unterbrochenen Kampf aus der Datei fortsetzen (siehe save_battle).
static func resume(data: Dictionary) -> BattleScreen:
var s := BattleScreen.new(int(data.get("n_opp", 1)), int(data.get("stage", -1)))
@@ -183,13 +153,6 @@ func _ready() -> void:
return # der erste Zustand kommt vom Host
elif saved_game != null:
game = saved_game
elif tutorial:
guide = BattleTutorial.new(self, tutorial_part)
var r := Game.make_rng()
var opps := Campaign.opponents(guide.stage_index(), r, Game.deck_rating(app.profile.deck_cards()))
opps[0].perks = []
game = Game.new_battle_with(app.profile.deck_cards(), opps, r, true)
guide.inject_cards(game)
elif stage >= 0:
var r := Game.make_rng()
game = Game.new_battle_with(app.profile.deck_cards(), Campaign.opponents(stage, r, app.profile.target_rating()), r, false, own_rules())
@@ -235,22 +198,6 @@ func _build_static() -> void:
log_box.add_theme_font_size_override("normal_font_size", 13)
log_box.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(log_box)
if tutorial:
tutorial_panel = Style.panel(self, Rect2(10, 205, 320, 150), Style.PANEL, 8, Style.GOLD, 2)
tutorial_panel.mouse_filter = Control.MOUSE_FILTER_STOP
Style.label(self, "Anleitung", 14, Style.GOLD, Rect2(20, 209, 200, 20), "left", true)
tutorial_text = RichTextLabel.new()
tutorial_text.position = Vector2(20, 230)
tutorial_text.size = Vector2(300, 90)
tutorial_text.bbcode_enabled = false
tutorial_text.scroll_active = false
tutorial_text.add_theme_font_size_override("normal_font_size", 13)
tutorial_text.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(tutorial_text)
b_tut_next = Style.button(self, "Weiter", Rect2(230, 318, 90, 30), tutorial_next)
b_tut_next.add_theme_font_size_override("font_size", 14)
log_box.position = Vector2(18, 365)
log_box.size = Vector2(304, 173)
b_giveup = Style.button(self, "Aufgeben", Rect2(10, 553, 150, 40), func(): confirm.visible = true)
Style.panel(self, Rect2(950, 205, 320, 380))
preview_hint = Style.label(self, "Karte überfahren für Details", 14, Style.MUTED, Rect2(950, 345, 320, 24), "center")
@@ -275,7 +222,6 @@ func _build_static() -> void:
trap_layer = _layer()
hand_layer = _layer()
preview_holder = _layer()
highlight_layer = _layer()
zoom_layer = _layer()
fx_layer = _layer()
cursor_layer = _layer()
@@ -313,7 +259,7 @@ func _overlay() -> Control:
## Einzelspieler-Kämpfe (auch Kampagne) lassen sich unterbrechen und fortsetzen.
func resumable() -> bool:
return counts_for_profile() and not tutorial
return counts_for_profile()
func save_battle() -> void:
@@ -336,12 +282,6 @@ func leave() -> void:
net.leave()
if resumable() and game != null and game.winner == "":
app.clear_battle_save() # aufgegeben
if tutorial:
if tutorial_part == 1:
app.profile.tutorial_done = true
else:
app.profile.tutorial2_done = true
app.save()
app.show_screen(MenuScreen.new())
@@ -466,8 +406,6 @@ func _input(event: InputEvent) -> void:
do_mulligan([])
elif overlay.visible:
leave()
elif tutorial and b_tut_next.visible:
tutorial_next()
_:
if event.keycode >= KEY_1 and event.keycode <= KEY_9 and my_turn() and not any_overlay():
var idx: int = event.keycode - KEY_1
@@ -555,13 +493,8 @@ func _exit_tree() -> void:
func _finish() -> void:
var won := game.winner == "human"
if counts_for_profile():
var factor: float = float(app.profile.difficulty().reward) if stage < 0 and not tutorial else 1.0
var factor: float = float(app.profile.difficulty().reward) if stage < 0 else 1.0
reward = app.profile.record_battle(won, n_opp, app.profile.deck_cards(), stage, played_ids, game.turn, factor, game.human.life)
if tutorial:
if tutorial_part == 1:
app.profile.tutorial_done = true
else:
app.profile.tutorial2_done = true
app.save()
app.sfx.play("win" if won else "lose")
app.clear_battle_save()
@@ -846,30 +779,6 @@ func _note_events(evs: Array) -> void:
app.profile.daily_event("combo")
# --- Anleitung ------------------------------------------------------------------------------
func tutorial_next() -> void:
if guide != null and guide.next():
app.sfx.play("click")
refresh()
func _tutorial_refresh() -> void:
guide.advance()
tutorial_text.text = guide.text()
b_tut_next.visible = not guide.finished()
func _tutorial_highlights(mine: bool) -> void:
Style.clear(highlight_layer)
for r in guide.highlight_rects(mine):
var hl := Highlight.new()
hl.position = r.position - Vector2(4, 4)
hl.size = r.size + Vector2(8, 8)
hl.mouse_filter = Control.MOUSE_FILTER_IGNORE
highlight_layer.add_child(hl)
# --- Ansicht angleichen --------------------------------------------------------------------
func refresh() -> void:
@@ -1022,9 +931,6 @@ func refresh() -> void:
else:
lines.append(line)
log_box.text = "\n".join(lines)
if tutorial:
_tutorial_refresh()
_tutorial_highlights(mine)
pad.draw_cursor()
Style.clear(zoom_layer)

View File

@@ -1,149 +0,0 @@
class_name BattleTutorial
extends RefCounted
## Geführte Partien: Schritte, Bedingungen und Hervorhebungen (ausgelagert aus dem Kampfbildschirm).
const STEPS1 := [
["Willkommen! Unten liegt deine Hand. Jede Karte kostet Energie (◆), du hast im ersten Zug 1. Spiele ein Wesen mit Kosten 1 per Klick.",
"played_creature"],
["Wesen greifen erst ab dem nächsten Zug an. Beende deinen Zug mit dem Knopf rechts oder der Leertaste.", "turn_passed"],
["Grüne Rahmen zeigen angriffsbereite Wesen. Klicke ein Wesen und dann Fenn oder eines seiner Wesen. Wächter müssen zuerst fallen.", "attacked"],
["Resonanz: Wesen mit gleichem Element geben sich gegenseitig Angriff. Spiele ein zweites Feuer- oder Erde-Wesen.", "resonance"],
["Feldkombo: Ein Wesen neben einem eigenen Wesen mit anderem Element löst einen Effekt aus. Lava macht 1 Schaden an das stärkste gegnerische Wesen.", "combo"],
["Zauber wirken sofort. Funke oder Feuerball treffen auch Fenn direkt. Ziele wählst du per Klick, Rechtsklick bricht ab.", "spell"],
["Fenn verliert bei 0 Leben. Tausche im ersten Zug einer Partie schwache Starthände (Mulligan). Viel Erfolg!", ""],
]
const STEPS2 := [
["Mulligan: Im ersten Zug darfst du einmal Handkarten zurücklegen und neu ziehen. Entscheide in der Maske, was du behältst.", "mulligan"],
["Fallen liegen verdeckt und lösen im gegnerischen Zug aus. Lege die Frostfalle, sobald du 2 Energie hast.", "trap_set"],
["Beende den Zug und warte. Greift Mara dich oder ein Wesen an, verpufft der Angriff und ihr Wesen friert ein.", "trap_fired"],
["Feldkombo Dampf: Ein Wasser-Wesen neben einem Feuer-Wesen friert das stärkste gegnerische Wesen ein. Spiele beide nacheinander.", "combo_dampf"],
["Rückgängig (U) nimmt ein gerade gespieltes Wesen zurück, solange nichts ausgelöst wurde. Viel Erfolg gegen Mara!", ""],
]
var screen # BattleScreen
var part := 1
var step := 0
func _init(s, p: int = 1) -> void:
screen = s
part = p
func steps() -> Array:
return STEPS1 if part == 1 else STEPS2
func stage_index() -> int:
return 0 if part == 1 else 1
## Teil 1: ein Wesen für 1 Energie auf der Hand, damit der erste Schritt immer möglich ist.
## Teil 2: Lehrkarten auf die eigene Hand; Mara bekommt Wesen, damit sie sicher in die Falle läuft.
func inject_cards(g: Game) -> void:
if part == 1 and not g.human.hand.any(func(c): return c.is_creature() and c.cost == 1):
g.human.hand.append(Cards.by_id("f_kobold"))
if part == 2:
g.human.hand.append(Cards.by_id("w_falle"))
g.human.hand.append(Cards.by_id("w_reiter"))
for i in 2:
g.opponents[0].hand.append(Cards.by_id("w_reiter"))
func condition(kind: String) -> bool:
var g: Game = screen.game
var h: Game.Player = g.human
match kind:
"played_creature":
return g.log.any(func(l): return l.begins_with("Du spielt")) and h.board.size() > 0
"turn_passed":
return h.turns >= 2
"attacked":
return g.log.any(func(l): return l.begins_with(h.board[0].name + " greift") if not h.board.is_empty() else false) \
or g.log.any(func(l): return l.ends_with("greift Fenn an."))
"resonance":
return h.board.any(func(c): return c.resonance > 0)
"combo":
return g.log.any(func(l): return l.begins_with("Feldkombo"))
"spell":
return g.log.any(func(l): return l == "Du spielt Funke." or l == "Du spielt Feuerball." or l == "Du spielt Verhärten.")
"mulligan":
return h.mulligan_done
"trap_set":
return g.log.any(func(l): return l == "Du legt eine Falle.")
"trap_fired":
return g.log.any(func(l): return l.begins_with("Falle von Du"))
"combo_dampf":
return g.log.any(func(l): return l.begins_with("Feldkombo Dampf"))
return false
func advance() -> void:
while step < steps().size() - 1 and condition(steps()[step][1]):
step += 1
func next() -> bool:
if step < steps().size() - 1:
step += 1
return true
return false
func text() -> String:
return "%d/%d %s" % [step + 1, steps().size(), screen.tr(steps()[step][0])]
func finished() -> bool:
return step >= steps().size() - 1
## Rechtecke, die der aktuelle Schritt hervorhebt.
func highlight_rects(mine: bool) -> Array[Rect2]:
var rects: Array[Rect2] = []
if not mine or screen.any_overlay():
return rects
var g: Game = screen.game
var h: Game.Player = screen.perspective
match steps()[step][1]:
"played_creature":
for v in screen.hand_views:
if v.card.is_creature() and g.can_play(h, v.card)[0]:
rects.append(Rect2(v.position, v.size))
"turn_passed", "trap_fired":
rects.append(screen.b_end.get_rect())
"attacked":
for v in screen.own_views:
if v.creature.can_attack():
rects.append(Rect2(v.slot, v.size))
if screen.own_views.any(func(v): return v.creature.can_attack()):
for o in screen.others():
if o.alive() and screen.hero_panels.has(o):
rects.append(screen.hero_panels[o].get_rect())
"resonance":
for v in screen.hand_views:
var c: Cards.Card = v.card
if c.is_creature() and g.can_play(h, c)[0] and h.board.any(func(b): return b.card.elements.any(func(e): return e in c.elements)):
rects.append(Rect2(v.position, v.size))
"combo":
for v in screen.hand_views:
var c: Cards.Card = v.card
if c.is_creature() and g.can_play(h, c)[0] and h.board.any(func(b): return b.card.elements.any(func(e): return not (e in c.elements))):
rects.append(Rect2(v.position, v.size))
"spell":
for v in screen.hand_views:
if v.card.kind == "Zauber" and g.can_play(h, v.card)[0]:
rects.append(Rect2(v.position, v.size))
"trap_set":
for v in screen.hand_views:
if v.card.is_trap() and g.can_play(h, v.card)[0]:
rects.append(Rect2(v.position, v.size))
"combo_dampf":
var has_fire: bool = h.board.any(func(b): return "Feuer" in b.card.elements)
for v in screen.hand_views:
var c: Cards.Card = v.card
if not c.is_creature() or not g.can_play(h, c)[0]:
continue
if (has_fire and "Wasser" in c.elements) or (not has_fire and "Feuer" in c.elements):
rects.append(Rect2(v.position, v.size))
return rects

View File

@@ -1 +0,0 @@
uid://cg78afyod2873

View File

@@ -1,6 +1,7 @@
class_name WorkshopScreen
class_name CollectionScreen
extends Control
## Werkstatt: Karten zerlegen (Staub) und herstellen.
## Sammlung: alle Karten mit Besitzstand; ausgewählte Karten lassen sich
## zerlegen (Staub) oder herstellen.
var app
var filter := ""
@@ -18,7 +19,7 @@ var b_extras: Button
func _ready() -> void:
Style.label(self, "Werkstatt", 30, Style.GOLD, Rect2(20, 8, 400, 40), "left", true)
Style.label(self, "Sammlung", 30, Style.GOLD, Rect2(20, 8, 400, 40), "left", true)
Style.label(self, "Karte anklicken, dann zerlegen oder herstellen. Überschuss: alles über %d Kopien." % Game.MAX_COPIES,
14, Style.MUTED, Rect2(240, 18, 600, 24))
Style.button(self, "Alle", Rect2(20, 55, 60, 34), set_filter.bind(""))

View File

@@ -0,0 +1 @@
uid://dsqeag3p0ehbq

View File

@@ -290,7 +290,7 @@ func add(card: Cards.Card) -> void:
var owned: int = app.profile.collection.get(card.id, 0)
var have := deck.count(card.id)
if owned == 0:
say("%s fehlt in deiner Sammlung (Werkstatt)" % card.name, false)
say("%s fehlt in deiner Sammlung" % card.title(), false)
elif deck.size() >= Game.DECK_SIZE:
say("Das Deck ist voll (%d)" % Game.DECK_SIZE, false)
elif have >= mini(owned, Game.MAX_COPIES):

View File

@@ -5,7 +5,6 @@ var app
var b_single: Button
var b_campaign: Button
var b_online: Button
var b_tutorial: Button
var b_resume: Button
var coins_label: Label
var coll_label: Label
@@ -33,21 +32,14 @@ func _ready() -> void:
center.add_child(grid)
col.add_child(center)
var sz := Vector2(270, 44)
var p0: Profile = app.profile
var part2: bool = p0.tutorial_done and not p0.tutorial2_done
if FileAccess.file_exists(app.battle_save_path()):
b_resume = Style.cbutton(grid, "Kampf fortsetzen", sz, _resume, Color("5a4628"))
b_tutorial = Style.cbutton(grid, "Anleitung 2: Fallen" if part2 else "Anleitung", sz,
func(): app.show_screen(BattleScreen.guided(2 if part2 else 1)),
Color("5a4628") if not (p0.tutorial_done and p0.tutorial2_done) else null)
b_single = Style.cbutton(grid, "Einzelspieler", sz, func(): app.show_screen(OpponentSelectScreen.new()), Color("285a3c"))
b_campaign = Style.cbutton(grid, "Kampagne", sz, func(): app.show_screen(CampaignScreen.new()), Color("285a3c"))
b_online = Style.cbutton(grid, "Netzwerkspiel", sz, func(): app.show_screen(LobbyScreen.new()), Color("285a3c"))
Style.cbutton(grid, "Booster öffnen", sz, func(): app.show_screen(BoosterScreen.new()))
Style.cbutton(grid, "Deck bauen", sz, func(): app.show_screen(DeckScreen.new()))
Style.cbutton(grid, "Werkstatt", sz, func(): app.show_screen(WorkshopScreen.new()))
Style.cbutton(grid, "Statistik", sz, func(): app.show_screen(StatsScreen.new()))
Style.cbutton(grid, "Regeln", sz, func(): app.show_screen(RulesScreen.new()))
Style.cbutton(grid, "Sammlung", sz, func(): app.show_screen(CollectionScreen.new()))
Style.cbutton(grid, "Einstellungen", sz, func(): app.show_screen(SettingsScreen.new()))
Style.cbutton(grid, "Beenden", sz, func(): get_tree().quit())
Style.spacer(col, 10)
@@ -73,7 +65,6 @@ func refresh() -> void:
var ok: Array = p.deck_valid()
b_single.disabled = not ok[0]
b_campaign.disabled = not ok[0]
b_tutorial.disabled = not ok[0]
b_online.disabled = not ok[0]
var total := 0
for cid in p.collection:
@@ -89,12 +80,6 @@ func refresh() -> void:
else:
daily_label.text = tr("Tagesaufgabe: %s (%d/%d) · +%d Münzen") % [task.text, p.daily.progress, task.goal, task.reward]
warn_label.text = "" if ok[0] else tr("Aktives Deck nicht spielbar: ") + tr(ok[1])
if ok[0] and not p.tutorial_done:
warn_label.text = "Neu hier? Die Anleitung erklärt Energie, Angriffe, Resonanz und Feldkombos."
warn_label.add_theme_color_override("font_color", Style.GOLD)
elif ok[0] and not p.tutorial2_done:
warn_label.text = "Anleitung 2 zeigt Mulligan, Fallen und die Feldkombo Dampf."
warn_label.add_theme_color_override("font_color", Style.GOLD)
var notes := p.take_notices()
notice_label.text = " · ".join(notes)
if not notes.is_empty():

View File

@@ -1,25 +0,0 @@
class_name RulesScreen
extends Control
var app
func _ready() -> void:
Style.label(self, "Regeln", 36, Style.GOLD, Rect2(0, 18, 1280, 50), "center", true)
var scroll := ScrollContainer.new()
scroll.position = Vector2(60, 78)
scroll.size = Vector2(1160, 640)
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
add_child(scroll)
var text := Label.new()
text.text = Style.rules_text()
text.autowrap_mode = TextServer.AUTOWRAP_WORD
text.custom_minimum_size = Vector2(1140, 0)
text.add_theme_font_size_override("font_size", 15)
scroll.add_child(text)
Style.button(self, "Zurück", Rect2(560, 735, 160, 44), func(): app.show_screen(MenuScreen.new()))
func _input(event: InputEvent) -> void:
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
app.show_screen(MenuScreen.new())

View File

@@ -1 +0,0 @@
uid://bkemrytc2pjem

View File

@@ -1,46 +0,0 @@
class_name StatsScreen
extends Control
## Statistik: Kämpfe, Tagesaufgabe, Bestenliste, beste Karten und CSV-Export.
var app
var export_label: Label
func _ready() -> void:
var p: Profile = app.profile
var col := Style.column(self, 900, 12, 4)
Style.clabel(col, "Statistik", 32, Style.GOLD, "center", true)
var st: Dictionary = p.stats
Style.clabel(col, "Kämpfe %d · Siege %d · Booster %d · Kampagne %d/%d" % [
st.games, st.wins, st.boosters, p.campaign_stage, Campaign.total()], 16, Style.MUTED)
var task := p.daily_task()
var daily_text: String = ("Tagesaufgabe erledigt: %s ✓" % task.text) if p.daily.done else \
"Tagesaufgabe: %s (%d/%d) · +%d Münzen" % [task.text, p.daily.progress, task.goal, task.reward]
Style.clabel(col, daily_text, 16, Style.BLUE)
Style.clabel(col, "Serie: %d (beste %d) · Schnellster Sieg: %s" % [p.records.streak, p.records.best_streak,
("%d Züge" % p.records.fastest_win) if p.records.fastest_win > 0 else "–"], 15, Style.MUTED)
Style.spacer(col, 4)
var top := p.top_cards(5)
if not top.is_empty():
Style.spacer(col, 4)
var parts: Array[String] = []
for row in top:
parts.append("%s %d/%d" % [row.card.name, row.wins, row.games])
Style.clabel(col, "Beste Karten (Siege/Partien): " + " · ".join(parts), 14, Style.MUTED)
export_label = Style.clabel(col, "", 13, Style.GREEN)
Style.spacer(col, 2)
var br := Style.row(col, 16)
br.alignment = BoxContainer.ALIGNMENT_CENTER
Style.cbutton(br, "Statistik exportieren", Vector2(220, 40), export_stats)
Style.cbutton(br, "Zurück", Vector2(160, 40), func(): app.show_screen(MenuScreen.new()))
func export_stats() -> void:
var path := "user://statistik.csv"
var err: String = app.profile.export_stats(path)
export_label.text = ("Gespeichert: " + ProjectSettings.globalize_path(path)) if err == "" else err
func _input(event: InputEvent) -> void:
if event.is_action_pressed("ui_cancel"):
app.show_screen(MenuScreen.new())

View File

@@ -1 +0,0 @@
uid://t0h7fpmpeuq4

View File

@@ -267,48 +267,6 @@ static func fill_elements(ci: CanvasItem, rect: Rect2, elements: Array, radius:
ci.draw_style_box(right, Rect2(rect.position + Vector2(half, 0), Vector2(rect.size.x - half, rect.size.y)))
static func rules_text() -> String:
var lines := [
"Ziel: Besiege alle anderen. Du verlierst, wenn dein Leben auf 0 fällt.",
"Du spielst gegen 1 bis 5 Magier, jeder gegen jeden, alle nach denselben Regeln:",
"%d Leben, pro Zug 1 Energie mehr (höchstens 10) und 1 Karte. Die Zugreihenfolge wird ausgelost." % Game.START_LIFE,
"Wer nicht beginnt, startet mit einer Karte mehr und hat im ersten Zug 1 Energie extra.",
"Fallen liegen verdeckt (höchstens 2) und lösen aus, wenn dich jemand angreift oder ein Wesen ausspielt.",
"",
"Karten kosten Energie. Wesen kommen aufs Feld (max. 5) und greifen ab dem nächsten Zug an.",
"Zauber wirken sofort. Ein Wesen greift ein gegnerisches Wesen oder direkt den Gegner an;",
"beide Seiten verursachen ihren Angriffswert als Schaden.",
"",
"Elemente: Feuer (Schaden), Wasser (Kontrolle), Erde (Zähigkeit), Luft (Tempo).",
"Kombos verbinden zwei Elemente, auch das gleiche doppelt (Feuer+Feuer = Inferno).",
"Kombokarten sind seltener und stärker, Dreifach-Kombos sind mythisch. Ein Deck darf höchstens 2 Elemente enthalten.",
Cards.RESONANCE_TEXT,
"Feldkombo: Spielst du ein Wesen neben ein eigenes Wesen mit anderem Element, löst eine Kombo aus (eine je Ausspielen):",
" Dampf friert das stärkste gegnerische Wesen, Lava macht 1 Schaden an das stärkste gegnerische Wesen, Blitz 2 Schaden",
" an den schwächsten Gegner, Schlamm heilt dich um 3, Sturm zieht 1 Karte, Sand gibt dem neuen Wesen +1/+1.",
"",
"Schlüsselwörter:",
]
for k in Cards.KEYWORDS:
lines.append(" %s: %s" % [k, Cards.KEYWORD_HELP[k]])
lines.append("")
lines.append("Deck: genau %d Karten, höchstens %dx dieselbe Karte." % [Game.DECK_SIZE, Game.MAX_COPIES])
lines.append("Leeres Deck: jede weitere gezogene Karte verursacht steigenden Schaden.")
lines.append("Booster: 5 Karten für 100 Münzen, mindestens eine seltene. Siege bringen Münzen.")
lines.append("Elementbooster: 5 Karten eines Elements (und seiner Kombos) für %d Münzen." % Profile.ELEMENT_BOOSTER_COST)
lines.append("Deck-Code: im Deckbau unter „Code“ kopieren und mit anderen teilen.")
lines.append("Ein unterbrochener Einzelspieler-Kampf lässt sich im Menü fortsetzen.")
lines.append("")
lines.append("Werkstatt: Karten zerlegen bringt Staub, aus Staub entstehen neue Karten. Fünf Deck-Slots.")
lines.append("Netzwerkspiel: Fällt der Host aus, übernimmt ein Mitspieler; Zuschauer sehen die Partie ohne Hände.")
lines.append("Mulligan: Im ersten eigenen Zug darfst du einmal Handkarten zurücklegen und nachziehen.")
lines.append("Deckstärke: Seltenheit, Schlüsselwörter und Effekte ergeben je Karte einen Wert. Gegnerdecks werden auf deine")
lines.append(" Stärke gebaut (Leicht 0,9-fach, Normal 1-fach, Schwer 1,2-fach mit mehr Münzen). Die Kampagne liegt nie darunter.")
lines.append("Kampagnen-Bosse haben ein Merkmal (★), zum Beispiel stärkere Feuerwesen. Netzwerkspiel im lokalen Netz.")
lines.append("Steuerung: Linksklick spielt Karten und wählt Ziele, Rechtsklick oder Esc bricht ab, Leertaste beendet den Zug, 1 bis 9 spielen Handkarten.")
return "\n".join(lines)
# --- Kartensymbole (prozedural, je Element und Kartenart) --------------------------
static func glyph(ci: CanvasItem, card, center: Vector2, size: float) -> void:

View File

@@ -1 +0,0 @@
uid://cf42h6bsh8lvg