Anleitung mit Hervorhebungen, Rückgängig, Kapitel 2, Herausforderungen, Fallen, Wiederverbinden, CI

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QPgKvy5W9WRNFHjkm5QsHx
This commit is contained in:
Team3
2026-09-16 13:27:41 +02:00
parent 536a523f50
commit 46c3fae233
12 changed files with 462 additions and 70 deletions

View File

@@ -26,6 +26,11 @@ var preview_timer := 0.0
var played_ids: Array = [] # vom Menschen gespielte Karten (Statistik)
var mulligan_selected: Array = []
var tutorial_step := 0
var challenge := {} # wöchentliche Herausforderung oder leer
var skip_ai := false # Gegnerzüge im Schnelldurchlauf
var undo_state: Game = null # Zustand vor der letzten rückgängig machbaren Aktion
var undo_log_len := 0
var undo_t := 0.0
# Ansichten (auch für Tests)
var tiles := {}
@@ -70,6 +75,22 @@ 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)
const TUTORIAL_STEPS := [
["Willkommen! Unten liegt deine Hand. Jede Karte kostet Energie (◆), du hast im ersten Zug 1. Spiele ein Wesen mit Kosten 1 per Klick.",
@@ -107,6 +128,12 @@ static func guided() -> BattleScreen:
return b
static func weekly(ch: Dictionary) -> BattleScreen:
var b := BattleScreen.new(int(ch.n_opp), -1)
b.challenge = ch
return b
func is_hotseat() -> bool:
return game != null and game.humans.size() > 1 and online_seat < 0
@@ -146,14 +173,24 @@ func _ready() -> void:
var opps := Campaign.opponents(0, r)
opps[0].perks = []
game = Game.new_battle_with(app.profile.deck_cards(), opps, r, true)
elif not challenge.is_empty():
game = Game.new_battle(app.profile.deck_cards(), n_opp, null, challenge.rules)
elif stage >= 0:
var r := Game.make_rng()
game = Game.new_battle_with(app.profile.deck_cards(), Campaign.opponents(stage, r), r)
game = Game.new_battle_with(app.profile.deck_cards(), Campaign.opponents(stage, r), r, false, own_rules())
else:
game = Game.new_battle(app.profile.deck_cards(), n_opp)
game = Game.new_battle(app.profile.deck_cards(), n_opp, null, own_rules())
_begin()
## Eigenes Merkmal des Spielers (nach Kapitel 1 wählbar).
func own_rules() -> Dictionary:
var p: Profile = app.profile
if p.perk != "" and p.perk_unlocked():
return {"human_perks": [p.perk]}
return {}
func _begin() -> void:
n_opp = game.players.size() - 1
if is_online():
@@ -207,6 +244,11 @@ func _build_static() -> void:
status_label = Style.label(self, "", 15, Style.MUTED, Rect2(349, 455, 400, 24))
msg_label = Style.label(self, "", 16, Style.ORANGE, Rect2(600, 455, 331, 24), "right", true)
b_end = Style.button(self, "Zug beenden", Rect2(1005, 527, 210, 48), end_turn, Color("285a3c"))
b_skip = Style.button(self, "Gegnerzüge überspringen", Rect2(1005, 527, 210, 48), func(): skip_ai = true)
b_skip.add_theme_font_size_override("font_size", 14)
b_skip.visible = false
b_undo = Style.button(self, "Rückgängig", Rect2(170, 553, 150, 40), undo)
b_undo.visible = false
wait_label = Style.label(self, "Warte auf den Host …", 22, Style.MUTED, Rect2(0, 300, 1280, 40), "center")
wait_label.visible = false
hero_layer = _layer()
@@ -214,6 +256,7 @@ func _build_static() -> void:
trap_layer = _layer()
hand_layer = _layer()
preview_holder = _layer()
highlight_layer = _layer()
zoom_layer = _layer()
fx_layer = _layer()
# Mulligan
@@ -380,6 +423,11 @@ func _input(event: InputEvent) -> void:
KEY_SPACE:
if my_turn() and not any_overlay():
end_turn()
elif b_skip.visible:
skip_ai = true
KEY_U:
if b_undo.visible:
undo()
KEY_ENTER, KEY_KP_ENTER:
if handover.visible:
take_over()
@@ -414,6 +462,12 @@ func _process(delta: float) -> void:
preview_timer -= delta
if preview_timer <= 0.0:
show_preview(null, false)
if undo_t > 0.0:
undo_t -= delta
if undo_t <= 0.0:
clear_undo()
if is_online() and net.is_host:
net.tick()
if game.winner != "":
if not ended:
ended = true
@@ -431,19 +485,24 @@ func _process(delta: float) -> void:
return
if is_client():
return
ai_timer -= delta
if skip_ai:
ai_timer = 0.0
else:
ai_timer -= delta
if ai_timer > 0:
return
var action := AI.choose_action(game, cur)
AI.apply_action(game, cur, action)
ai_timer = ai_delay * (0.6 if action.type == "end" else 1.0)
if game.current().is_human:
skip_ai = false
after_action()
func _finish() -> void:
var won := game.winner == "human"
if counts_for_profile():
reward = app.profile.record_battle(won, n_opp, app.profile.deck_cards(), stage, played_ids)
reward = app.profile.record_battle(won, n_opp, app.profile.deck_cards(), stage, played_ids, game.turn, challenge)
if tutorial:
app.profile.tutorial_done = true
app.save()
@@ -480,15 +539,52 @@ func play(card: Cards.Card, target) -> void:
if is_client():
net.send_action({"type": "play", "hand": perspective.hand.find(card), "target": Net.ref_dict(target) if target != null else null})
return
var before: Game = game.clone() if card.is_creature() and not is_online() else null
var log_len := game.log.size()
var err := game.play_card(perspective, card, target)
if err != "":
say(err)
say(tr(err))
return
# Rückgängig nur, wenn nichts weiter passiert ist (keine Effekte, Kombos, Fallen)
if before != null and game.events.size() == 1 and game.events[0].type == "play":
undo_state = before
undo_log_len = log_len
undo_t = 6.0
else:
clear_undo()
func clear_undo() -> void:
undo_state = null
undo_t = 0.0
b_undo.visible = false
## Nimmt das letzte Ausspielen eines Wesens zurück (nur solange nichts Verdecktes offenbart wurde).
func undo() -> void:
if undo_state == null:
return
var idx := game.players.find(perspective)
var restored := undo_state
restored.quiet = false
restored.rng = game.rng
restored.log = game.log.slice(0, undo_log_len)
restored.say("Rückgängig.")
game = restored
perspective = game.players[idx]
if is_online() and net.is_host:
net.game = game
clear_undo()
cancel()
build_hero_boxes()
after_action()
func end_turn() -> void:
if not my_turn():
return
cancel()
clear_undo()
zoom_player = null
if is_client():
net.send_action({"type": "end"})
@@ -552,7 +648,7 @@ func on_click(kind: String, obj) -> void:
if obj in g.valid_targets(perspective, card.target_type()):
play(card, obj)
else:
say("Abgebrochen")
say(tr("Abgebrochen"))
zoom_player = null
after_action()
return
@@ -560,12 +656,13 @@ func on_click(kind: String, obj) -> void:
var cr = selected
selected = null
if kind == "enemy" or kind == "hero":
clear_undo()
if is_client():
net.send_action({"type": "attack", "uid": cr.uid, "target": Net.ref_dict(obj)})
else:
var err := g.attack(cr, obj)
if err != "":
say(err)
say(tr(err))
zoom_player = null
after_action()
return
@@ -573,18 +670,18 @@ func on_click(kind: String, obj) -> void:
"hand":
var ok := g.can_play(perspective, obj)
if not ok[0]:
say(ok[1])
say(tr(ok[1]))
elif obj.target_type() != "" and not g.valid_targets(perspective, obj.target_type()).is_empty():
pending = obj
say("Wähle ein Ziel")
say(tr("Wähle ein Ziel"))
else:
play(obj, null)
"own":
if obj.can_attack():
selected = obj
say("Wähle ein Angriffsziel")
say(tr("Wähle ein Angriffsziel"))
else:
say("%s kann nicht angreifen" % obj.name)
say(tr("%s kann nicht angreifen") % obj.name)
after_action()
@@ -739,6 +836,51 @@ func tutorial_next() -> void:
refresh()
## Hebt hervor, was der aktuelle Schritt verlangt.
func _tutorial_highlights(mine: bool) -> void:
Style.clear(highlight_layer)
if not mine or any_overlay():
return
var g := game
var h := perspective
var rects: Array[Rect2] = []
match TUTORIAL_STEPS[tutorial_step][1]:
"played_creature":
for v in hand_views:
if v.card.is_creature() and g.can_play(h, v.card)[0]:
rects.append(Rect2(v.position, v.size))
"turn_passed":
rects.append(b_end.get_rect())
"attacked":
for v in own_views:
if v.creature.can_attack():
rects.append(Rect2(v.slot, v.size))
if not own_views.is_empty() and own_views.any(func(v): return v.creature.can_attack()):
for o in others():
if o.alive() and hero_panels.has(o):
rects.append(hero_panels[o].get_rect())
"resonance":
for v in 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 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 hand_views:
if v.card.kind == "Zauber" and g.can_play(h, v.card)[0]:
rects.append(Rect2(v.position, v.size))
for r in rects:
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)
func _tutorial_refresh() -> void:
while tutorial_step < TUTORIAL_STEPS.size() - 1 and tutorial_condition(TUTORIAL_STEPS[tutorial_step][1]):
tutorial_step += 1
@@ -876,8 +1018,12 @@ func refresh() -> void:
life_label.text = "♥ %d" % maxi(h.life, 0)
energy_label.text = "◆ %d/%d" % [h.energy, h.max_energy]
deck_label.text = "Deck %d" % h.deck.size()
status_label.text = "Zug %d · %s" % [g.turn, "Du bist dran" if mine else g.current().name + " ist dran …"]
status_label.text = (tr("Zug %d · Du bist dran") % g.turn) if mine else (tr("Zug %d · %s ist dran …") % [g.turn, g.current().name])
b_end.disabled = not mine
var ai_running: bool = g.winner == "" and not g.current().is_human and not is_client()
b_skip.visible = ai_running
b_end.visible = not ai_running
b_undo.visible = undo_state != null and mine
var lines: Array[String] = []
var start := maxi(0, g.log.size() - 60)
for i in range(start, g.log.size()):
@@ -891,6 +1037,7 @@ func refresh() -> void:
log_box.text = "\n".join(lines)
if tutorial:
_tutorial_refresh()
_tutorial_highlights(mine)
Style.clear(zoom_layer)
if zoom_player != null and zoom_player.alive():