Dinger: Kartenspiel in Godot 4.7
Elemente, Kombos, Fallen, KI mit Vorausschau, Kampagne, Hotseat und Netzwerk, Werkstatt, Erfolge, Tagesaufgaben, Tests und Exporte. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPgKvy5W9WRNFHjkm5QsHx
This commit is contained in:
45
scripts/ui/achievements_screen.gd
Normal file
45
scripts/ui/achievements_screen.gd
Normal file
@@ -0,0 +1,45 @@
|
||||
class_name AchievementsScreen
|
||||
extends Control
|
||||
|
||||
var app
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
var p: Profile = app.profile
|
||||
var col := Style.column(self, 900, 20, 5)
|
||||
Style.clabel(col, "Erfolge", 36, 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.STAGES.size()], 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.spacer(col, 8)
|
||||
for a in Profile.ACHIEVEMENTS:
|
||||
var done: bool = a.id in p.achievements
|
||||
var panel := PanelContainer.new()
|
||||
panel.add_theme_stylebox_override("panel", Style.box(Style.PANEL if done else Style.darker(Style.PANEL, 0.8), 6))
|
||||
col.add_child(panel)
|
||||
var r := Style.row(panel, 12)
|
||||
r.custom_minimum_size = Vector2(0, 42)
|
||||
Style.clabel(r, "✓" if done else "·", 22, Style.GREEN if done else Style.MUTED, "center", true, 40)
|
||||
Style.clabel(r, a.name, 18, Style.TEXT if done else Style.MUTED, "left", true, 250)
|
||||
Style.clabel(r, a.text, 15, Style.MUTED, "left", false, 380)
|
||||
Style.clabel(r, "+%d" % a.reward, 16, Style.GOLD, "right", true, 90)
|
||||
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)
|
||||
Style.spacer(col, 6)
|
||||
var br := Style.row(col)
|
||||
br.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
Style.cbutton(br, "Zurück", Vector2(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())
|
||||
1
scripts/ui/achievements_screen.gd.uid
Normal file
1
scripts/ui/achievements_screen.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cw4rtcukefdl8
|
||||
159
scripts/ui/battle_fx.gd
Normal file
159
scripts/ui/battle_fx.gd
Normal file
@@ -0,0 +1,159 @@
|
||||
class_name BattleFx
|
||||
extends RefCounted
|
||||
## Animationen und Töne des Kampfes aus den Spielereignissen.
|
||||
|
||||
var screen # BattleScreen
|
||||
var enabled := true
|
||||
|
||||
|
||||
func _init(s) -> void:
|
||||
screen = s
|
||||
|
||||
|
||||
func sfx(kind: String) -> void:
|
||||
screen.app.sfx.play(kind)
|
||||
|
||||
|
||||
## Spielt die Ereignisse einer Aktion ab. centers: vorher gemerkte Positionen (für gestorbene Wesen).
|
||||
func play_events(evs: Array, centers: Dictionary) -> void:
|
||||
var g: Game = screen.game
|
||||
var delay := 0.0
|
||||
for ev in evs:
|
||||
match ev.type:
|
||||
"attack":
|
||||
sfx("attack")
|
||||
var atk: CreatureView = screen.tiles.get(ev.attacker.uid)
|
||||
lunge(atk, center_of(ev.target, centers))
|
||||
flash_target(ev.target)
|
||||
delay = 0.12
|
||||
"damage":
|
||||
sfx("damage")
|
||||
popup("-%d" % ev.amount, Style.RED, center_of(ev.target, centers), delay)
|
||||
"heal":
|
||||
sfx("heal")
|
||||
popup("+%d" % ev.amount, Style.GREEN, center_of(ev.target, centers), delay)
|
||||
"buff":
|
||||
popup(ev.text, Style.GREEN, center_of(ev.target, centers), delay)
|
||||
"blocked":
|
||||
popup("Schild!", Style.BLUE, center_of(ev.target, centers), delay)
|
||||
"freeze":
|
||||
popup("Eis", Color("aaddff"), center_of(ev.target, centers), delay)
|
||||
"combo":
|
||||
sfx("combo")
|
||||
var where: Vector2 = screen.target_center({"kind": "creature", "uid": ev.uid})
|
||||
if where.x < 0:
|
||||
where = screen.target_center({"kind": "player", "index": ev.player})
|
||||
popup("Feldkombo %s!" % ev.name, Style.GOLD, where + Vector2(0, -30), 0.1)
|
||||
"play":
|
||||
sfx("play")
|
||||
"spell":
|
||||
sfx("play")
|
||||
var caster: Game.Player = g.players[ev.player]
|
||||
popup(ev.card.name, Style.GOLD, screen.target_center({"kind": "player", "index": ev.player}), 0.0)
|
||||
if caster != screen.perspective:
|
||||
screen.show_preview(ev.card, true)
|
||||
screen.preview_timer = 1.6
|
||||
"trap_set":
|
||||
sfx("click")
|
||||
popup("Falle gelegt", Style.MUTED, screen.target_center({"kind": "player", "index": ev.player}), 0.0)
|
||||
"trap":
|
||||
sfx("combo")
|
||||
popup("Falle: %s!" % ev.card.name, Style.ORANGE, screen.target_center({"kind": "player", "index": ev.player}) + Vector2(0, -20), 0.0)
|
||||
screen.show_preview(ev.card, true)
|
||||
screen.preview_timer = 1.8
|
||||
"mulligan":
|
||||
if ev.count > 0:
|
||||
popup("tauscht %d Karten" % ev.count, Style.MUTED, screen.target_center({"kind": "player", "index": ev.player}), 0.0)
|
||||
|
||||
|
||||
func center_of(ref: Dictionary, centers: Dictionary) -> Vector2:
|
||||
var c: Vector2 = centers.get(screen.ref_key(ref), Vector2(-1, -1))
|
||||
if c.x < 0:
|
||||
c = screen.target_center(ref)
|
||||
return c
|
||||
|
||||
|
||||
func popup(text: String, color: Color, at: Vector2, delay: float) -> void:
|
||||
if not enabled or at.x < 0:
|
||||
return
|
||||
var l := Label.new()
|
||||
l.text = text
|
||||
l.add_theme_font_size_override("font_size", 22)
|
||||
l.add_theme_font_override("font", Style.FONT_BOLD)
|
||||
l.add_theme_color_override("font_color", color)
|
||||
l.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.9))
|
||||
l.add_theme_constant_override("outline_size", 6)
|
||||
l.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
screen.fx_layer.add_child(l)
|
||||
l.reset_size()
|
||||
l.position = at - l.size / 2
|
||||
l.modulate.a = 0.0
|
||||
var tw: Tween = screen.create_tween()
|
||||
tw.tween_interval(delay)
|
||||
tw.tween_property(l, "modulate:a", 1.0, 0.08)
|
||||
tw.tween_property(l, "position:y", l.position.y - 44, 0.8).set_ease(Tween.EASE_OUT)
|
||||
tw.parallel().tween_property(l, "modulate:a", 0.0, 0.5).set_delay(0.35)
|
||||
tw.tween_callback(l.queue_free)
|
||||
|
||||
|
||||
func lunge(tile: CreatureView, target: Vector2) -> void:
|
||||
if tile == null or not enabled or target.x < 0:
|
||||
return
|
||||
tile.busy = true
|
||||
var start := tile.slot
|
||||
var dest := start + (target - (start + tile.size / 2)) * 0.35
|
||||
var tw := tile.create_tween()
|
||||
tw.tween_property(tile, "position", dest, 0.12).set_ease(Tween.EASE_OUT)
|
||||
tw.tween_property(tile, "position", start, 0.15).set_ease(Tween.EASE_IN)
|
||||
tw.tween_callback(func():
|
||||
tile.busy = false
|
||||
tile.position = tile.slot)
|
||||
|
||||
|
||||
func flash_target(ref: Dictionary) -> void:
|
||||
if not enabled:
|
||||
return
|
||||
if ref.kind == "creature":
|
||||
var tile: CreatureView = screen.tiles.get(ref.uid)
|
||||
if tile == null:
|
||||
return
|
||||
tile.flash = true
|
||||
tile.queue_redraw()
|
||||
var tw := tile.create_tween()
|
||||
tw.tween_interval(0.35)
|
||||
tw.tween_callback(func():
|
||||
tile.flash = false
|
||||
tile.queue_redraw())
|
||||
else:
|
||||
screen.flash_until[int(ref.index)] = Time.get_ticks_msec() + 350
|
||||
var tw: Tween = screen.create_tween()
|
||||
tw.tween_interval(0.37)
|
||||
tw.tween_callback(screen.refresh)
|
||||
|
||||
|
||||
func pop_in(tile: CreatureView) -> void:
|
||||
if not enabled:
|
||||
return
|
||||
tile.scale = Vector2(0.2, 0.2)
|
||||
tile.modulate.a = 0.0
|
||||
var tw := tile.create_tween().set_parallel(true)
|
||||
tw.tween_property(tile, "scale", Vector2.ONE, 0.25).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
|
||||
tw.tween_property(tile, "modulate:a", 1.0, 0.15)
|
||||
|
||||
|
||||
func fade_out(tile: CreatureView) -> void:
|
||||
tile.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
if not enabled:
|
||||
tile.queue_free()
|
||||
return
|
||||
var tw := tile.create_tween().set_parallel(true)
|
||||
tw.tween_property(tile, "modulate:a", 0.0, 0.3)
|
||||
tw.tween_property(tile, "scale", Vector2(0.6, 0.6), 0.3)
|
||||
tw.chain().tween_callback(tile.queue_free)
|
||||
|
||||
|
||||
func move_to(tile: CreatureView, pos: Vector2) -> void:
|
||||
if enabled:
|
||||
screen.create_tween().tween_property(tile, "position", pos, 0.2)
|
||||
else:
|
||||
tile.position = pos
|
||||
1
scripts/ui/battle_fx.gd.uid
Normal file
1
scripts/ui/battle_fx.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://defclm4nv6rkn
|
||||
40
scripts/ui/battle_layout.gd
Normal file
40
scripts/ui/battle_layout.gd
Normal file
@@ -0,0 +1,40 @@
|
||||
class_name BattleLayout
|
||||
## Feste Bildschirmaufteilung des Kampfes (1280x800).
|
||||
|
||||
const SIZES := {1: [110, 110, 10], 2: [96, 110, 8], 3: [78, 100, 4], 4: [54, 72, 4], 5: [44, 62, 3]}
|
||||
const HAND_W := 130
|
||||
const HAND_H := 185
|
||||
const HAND_GAP := 8
|
||||
const BAR := Rect2(335, 415, 610, 68)
|
||||
const OWN_X := (1280 - 5 * 110 - 4 * 10) / 2
|
||||
|
||||
|
||||
static func compact(n_others: int) -> bool:
|
||||
return n_others >= 4
|
||||
|
||||
|
||||
static func hero_box_rect(n_others: int, i: int) -> Rect2:
|
||||
var col_w := 1280 / n_others
|
||||
var margin := 6 if compact(n_others) else 10
|
||||
return Rect2(i * col_w + margin, 10, col_w - 2 * margin, 64)
|
||||
|
||||
|
||||
static func own_slot(j: int) -> Rect2:
|
||||
return Rect2(OWN_X + j * 120, 250, 110, 140)
|
||||
|
||||
|
||||
static func other_slot(n_others: int, i: int, j: int) -> Rect2:
|
||||
var col_w := 1280 / n_others
|
||||
var sz: Array = SIZES[n_others]
|
||||
var bx: int = i * col_w + (col_w - 5 * sz[0] - 4 * sz[2]) / 2
|
||||
return Rect2(bx + j * (sz[0] + sz[2]), 84, sz[0], sz[1])
|
||||
|
||||
|
||||
static func hand_rect(count: int, i: int) -> Rect2:
|
||||
var total := count * HAND_W + maxi(0, count - 1) * HAND_GAP
|
||||
var x := (1280 - total) / 2
|
||||
return Rect2(x + i * (HAND_W + HAND_GAP), 800 - HAND_H - 8, HAND_W, HAND_H)
|
||||
|
||||
|
||||
static func trap_rect(i: int) -> Rect2:
|
||||
return Rect2(400 + i * 78, 490, 70, 98)
|
||||
1
scripts/ui/battle_layout.gd.uid
Normal file
1
scripts/ui/battle_layout.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://5sjeymvcgfl7
|
||||
945
scripts/ui/battle_screen.gd
Normal file
945
scripts/ui/battle_screen.gd
Normal file
@@ -0,0 +1,945 @@
|
||||
class_name BattleScreen
|
||||
extends Control
|
||||
## Kampf: Zustand, Eingabe und Abgleich der Ansicht. Layout in BattleLayout, Animationen in BattleFx.
|
||||
## Modi: Einzelspieler, Kampagne, Anleitung, Hotseat (Perspektivwechsel), Netzwerk (Host oder Mitspieler).
|
||||
|
||||
var app
|
||||
var n_opp: int
|
||||
var stage := -1 # Kampagnenstufe oder -1
|
||||
var tutorial := false
|
||||
var hotseat_decks: Array = []
|
||||
var hotseat_ai := 0
|
||||
var online_seat := -1 # Netzwerk: eigener Platz, sonst -1
|
||||
var net: Net
|
||||
var game: Game
|
||||
var perspective: Game.Player # der Mensch, dessen Hand unten liegt
|
||||
var fx: BattleFx
|
||||
var selected = null # Game.Creature
|
||||
var pending = null # Cards.Card, wartet auf Ziel
|
||||
var msg_t := 0.0
|
||||
var ai_delay := 0.75
|
||||
var ai_timer := 0.0
|
||||
var reward := -1
|
||||
var ended := false
|
||||
var zoom_player = null
|
||||
var preview_timer := 0.0
|
||||
var played_ids: Array = [] # vom Menschen gespielte Karten (Statistik)
|
||||
var mulligan_selected: Array = []
|
||||
var tutorial_step := 0
|
||||
|
||||
# Ansichten (auch für Tests)
|
||||
var tiles := {}
|
||||
var hand_views: Array = []
|
||||
var own_views: Array = []
|
||||
var enemy_views: Array = []
|
||||
var trap_views: Array = []
|
||||
var hero_panels := {} # Player -> Panel
|
||||
var hero_labels := {}
|
||||
var flash_until := {} # Spielerindex -> Zeitpunkt (ms)
|
||||
|
||||
var hero_layer: Control
|
||||
var tile_layer: Control
|
||||
var hand_layer: Control
|
||||
var trap_layer: Control
|
||||
var zoom_layer: Control
|
||||
var fx_layer: Control
|
||||
var log_box: RichTextLabel
|
||||
var preview_holder: Control
|
||||
var preview_hint: Label
|
||||
var bar_panel: Panel
|
||||
var name_label: Label
|
||||
var life_label: Label
|
||||
var energy_label: Label
|
||||
var deck_label: Label
|
||||
var status_label: Label
|
||||
var msg_label: Label
|
||||
var b_end: Button
|
||||
var b_giveup: Button
|
||||
var overlay: Control
|
||||
var result_label: Label
|
||||
var reward_label: Label
|
||||
var notice_label: Label
|
||||
var confirm: Control
|
||||
var handover: Control
|
||||
var handover_label: Label
|
||||
var mulligan_box: Control
|
||||
var mulligan_layer: Control
|
||||
var b_keep: Button
|
||||
var b_swap: Button
|
||||
var tutorial_panel: Panel
|
||||
var tutorial_text: RichTextLabel
|
||||
var b_tut_next: Button
|
||||
var wait_label: Label
|
||||
|
||||
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.",
|
||||
"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 +1 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 alle gegnerischen 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!", ""],
|
||||
]
|
||||
|
||||
|
||||
func _init(n: int = 2, campaign_stage: int = -1) -> void:
|
||||
n_opp = n
|
||||
stage = campaign_stage
|
||||
|
||||
|
||||
static func hotseat(decks: Array, n_ai: int) -> BattleScreen:
|
||||
var b := BattleScreen.new(0, -1)
|
||||
b.hotseat_decks = decks
|
||||
b.hotseat_ai = n_ai
|
||||
return b
|
||||
|
||||
|
||||
static func online(seat: int) -> BattleScreen:
|
||||
var b := BattleScreen.new(0, -1)
|
||||
b.online_seat = seat
|
||||
return b
|
||||
|
||||
|
||||
static func guided() -> BattleScreen:
|
||||
var b := BattleScreen.new(1, -1)
|
||||
b.tutorial = true
|
||||
return b
|
||||
|
||||
|
||||
func is_hotseat() -> bool:
|
||||
return game != null and game.humans.size() > 1 and online_seat < 0
|
||||
|
||||
|
||||
func is_online() -> bool:
|
||||
return online_seat >= 0
|
||||
|
||||
|
||||
func is_client() -> bool:
|
||||
return is_online() and not net.is_host
|
||||
|
||||
|
||||
func counts_for_profile() -> bool:
|
||||
return not is_hotseat() and not is_online()
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
var s: Dictionary = app.profile.settings
|
||||
fx = BattleFx.new(self)
|
||||
fx.enabled = s.animations
|
||||
ai_delay = 0.75 / float(s.ai_speed)
|
||||
_build_static()
|
||||
if is_online():
|
||||
net = app.net
|
||||
net.ended.connect(_on_net_ended)
|
||||
if net.is_host:
|
||||
game = net.game
|
||||
net.action_applied.connect(after_action)
|
||||
else:
|
||||
net.snapshot_received.connect(_on_snapshot)
|
||||
wait_label.visible = true
|
||||
return # der erste Zustand kommt vom Host
|
||||
elif not hotseat_decks.is_empty():
|
||||
game = Game.new_hotseat(hotseat_decks, hotseat_ai)
|
||||
elif tutorial:
|
||||
var r := Game.make_rng()
|
||||
var opps := Campaign.opponents(0, r)
|
||||
opps[0].perks = []
|
||||
game = Game.new_battle_with(app.profile.deck_cards(), opps, r, true)
|
||||
elif stage >= 0:
|
||||
var r := Game.make_rng()
|
||||
game = Game.new_battle_with(app.profile.deck_cards(), Campaign.opponents(stage, r), r)
|
||||
else:
|
||||
game = Game.new_battle(app.profile.deck_cards(), n_opp)
|
||||
_begin()
|
||||
|
||||
|
||||
func _begin() -> void:
|
||||
n_opp = game.players.size() - 1
|
||||
if is_online():
|
||||
perspective = game.players[online_seat]
|
||||
else:
|
||||
game.events.clear()
|
||||
perspective = game.current() if game.current().is_human else game.humans[0]
|
||||
build_hero_boxes()
|
||||
if is_online() and net.is_host:
|
||||
net.broadcast()
|
||||
game.events.clear()
|
||||
refresh()
|
||||
|
||||
|
||||
func _build_static() -> void:
|
||||
Style.panel(self, Rect2(10, 205, 320, 340))
|
||||
Style.label(self, "Verlauf", 14, Style.MUTED, Rect2(20, 209, 200, 20))
|
||||
log_box = RichTextLabel.new()
|
||||
log_box.position = Vector2(18, 230)
|
||||
log_box.size = Vector2(304, 308)
|
||||
log_box.bbcode_enabled = true
|
||||
log_box.scroll_following = true
|
||||
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")
|
||||
Style.panel(self, Rect2(BattleLayout.OWN_X - 10, 240, 5 * 110 + 4 * 10 + 20, 160), Style.darker(Style.PANEL, 0.8), 10)
|
||||
bar_panel = Style.panel(self, BattleLayout.BAR)
|
||||
name_label = Style.label(self, "Du", 20, Style.TEXT, Rect2(349, 421, 120, 30), "left", true)
|
||||
life_label = Style.label(self, "", 24, Style.RED, Rect2(470, 418, 100, 34), "left", true)
|
||||
energy_label = Style.label(self, "", 24, Style.BLUE, Rect2(570, 418, 150, 34), "left", true)
|
||||
deck_label = Style.label(self, "", 16, Style.MUTED, Rect2(780, 424, 150, 24), "right")
|
||||
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"))
|
||||
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()
|
||||
tile_layer = _layer()
|
||||
trap_layer = _layer()
|
||||
hand_layer = _layer()
|
||||
preview_holder = _layer()
|
||||
zoom_layer = _layer()
|
||||
fx_layer = _layer()
|
||||
# Mulligan
|
||||
mulligan_box = _overlay()
|
||||
Style.panel(mulligan_box, Rect2(140, 250, 1000, 320), Style.PANEL, 10, Style.GOLD, 2)
|
||||
Style.label(mulligan_box, "Hand tauschen?", 26, Style.TEXT, Rect2(140, 262, 1000, 40), "center", true)
|
||||
Style.label(mulligan_box, "Klicke Karten an die du zurücklegen willst. Einmal pro Partie.", 15, Style.MUTED, Rect2(140, 300, 1000, 24), "center")
|
||||
mulligan_layer = Control.new()
|
||||
mulligan_layer.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
mulligan_box.add_child(mulligan_layer)
|
||||
b_keep = Style.button(mulligan_box, "Behalten", Rect2(430, 515, 200, 44), func(): do_mulligan(false), Color("285a3c"))
|
||||
b_swap = Style.button(mulligan_box, "Tauschen", Rect2(650, 515, 200, 44), func(): do_mulligan(true))
|
||||
# Spielende
|
||||
overlay = _overlay()
|
||||
result_label = Style.label(overlay, "", 56, Style.GREEN, Rect2(0, 250, 1280, 80), "center", true)
|
||||
reward_label = Style.label(overlay, "", 26, Style.GOLD, Rect2(0, 335, 1280, 40), "center")
|
||||
notice_label = Style.label(overlay, "", 18, Style.GREEN, Rect2(0, 385, 1280, 80), "center")
|
||||
Style.button(overlay, "Zurück zum Menü", Rect2(530, 480, 220, 50), leave, Color("285a3c"))
|
||||
# Aufgeben bestätigen
|
||||
confirm = _overlay()
|
||||
Style.panel(confirm, Rect2(390, 300, 500, 180), Style.PANEL, 10, Style.MUTED, 2)
|
||||
Style.label(confirm, "Wirklich aufgeben?", 26, Style.TEXT, Rect2(390, 320, 500, 40), "center", true)
|
||||
Style.label(confirm, "Der Kampf endet ohne Münzen.", 16, Style.MUTED, Rect2(390, 365, 500, 24), "center")
|
||||
Style.button(confirm, "Ja, aufgeben", Rect2(420, 410, 200, 46), leave, Color("5a2828"))
|
||||
Style.button(confirm, "Weiterspielen", Rect2(660, 410, 200, 46), func(): confirm.visible = false)
|
||||
# Übergabe zwischen Menschen (Hotseat)
|
||||
handover = _overlay()
|
||||
Style.panel(handover, Rect2(340, 280, 600, 220), Style.PANEL, 10, Style.GOLD, 2)
|
||||
handover_label = Style.label(handover, "", 30, Style.TEXT, Rect2(340, 310, 600, 44), "center", true)
|
||||
Style.label(handover, "Maus weitergeben, dann weiter. Die Hand des Vorgängers ist verdeckt.", 16, Style.MUTED, Rect2(340, 362, 600, 24), "center")
|
||||
Style.button(handover, "Weiter", Rect2(540, 420, 200, 50), take_over, Color("285a3c"))
|
||||
|
||||
|
||||
func _layer() -> Control:
|
||||
var c := Control.new()
|
||||
c.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(c)
|
||||
return c
|
||||
|
||||
|
||||
func _overlay() -> Control:
|
||||
var o := Control.new()
|
||||
o.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
o.visible = false
|
||||
var dark := ColorRect.new()
|
||||
dark.color = Color(0, 0, 0, 0.67)
|
||||
dark.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
dark.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
o.add_child(dark)
|
||||
add_child(o)
|
||||
return o
|
||||
|
||||
|
||||
func leave() -> void:
|
||||
if is_online():
|
||||
net.leave()
|
||||
if tutorial and not app.profile.tutorial_done:
|
||||
app.profile.tutorial_done = true
|
||||
app.save()
|
||||
app.show_screen(MenuScreen.new())
|
||||
|
||||
|
||||
# --- Layout-Helfer ------------------------------------------------------------------
|
||||
|
||||
func others() -> Array:
|
||||
var out := []
|
||||
var n := game.players.size()
|
||||
var start := game.players.find(perspective)
|
||||
for i in range(1, n):
|
||||
out.append(game.players[(start + i) % n])
|
||||
return out
|
||||
|
||||
|
||||
func compact() -> bool:
|
||||
return BattleLayout.compact(others().size())
|
||||
|
||||
|
||||
func slot_rect(p: Game.Player, j: int) -> Rect2:
|
||||
if p == perspective:
|
||||
return BattleLayout.own_slot(j)
|
||||
var os := others()
|
||||
return BattleLayout.other_slot(os.size(), os.find(p), j)
|
||||
|
||||
|
||||
func ref_key(ref: Dictionary) -> String:
|
||||
return "c%d" % ref.uid if ref.kind == "creature" else "p%d" % ref.index
|
||||
|
||||
|
||||
func target_center(ref: Dictionary) -> Vector2:
|
||||
if ref.kind == "creature":
|
||||
if tiles.has(ref.uid):
|
||||
var t: CreatureView = tiles[ref.uid]
|
||||
return t.slot + t.size / 2
|
||||
return Vector2(-1, -1)
|
||||
var p: Game.Player = game.players[ref.index]
|
||||
if p == perspective:
|
||||
return BattleLayout.BAR.position + BattleLayout.BAR.size / 2
|
||||
if hero_panels.has(p):
|
||||
var panel: Panel = hero_panels[p]
|
||||
return panel.position + panel.size / 2
|
||||
return Vector2(-1, -1)
|
||||
|
||||
|
||||
func build_hero_boxes() -> void:
|
||||
Style.clear(hero_layer)
|
||||
hero_panels = {}
|
||||
hero_labels = {}
|
||||
var os := others()
|
||||
var c := compact()
|
||||
for i in os.size():
|
||||
var o: Game.Player = os[i]
|
||||
var box := BattleLayout.hero_box_rect(os.size(), i)
|
||||
var panel := Style.panel(hero_layer, box)
|
||||
panel.gui_input.connect(func(ev):
|
||||
if ev is InputEventMouseButton and ev.pressed and ev.button_index == MOUSE_BUTTON_LEFT:
|
||||
on_click("hero", o))
|
||||
if not o.perks.is_empty():
|
||||
panel.tooltip_text = "Merkmal: " + ", ".join(o.perks.map(func(k): return Game.PERKS[k]))
|
||||
hero_panels[o] = panel
|
||||
var labels := {}
|
||||
var title: String = ("%s · %s" % [o.name, o.title]) if o.title != "" else o.name
|
||||
if not o.perks.is_empty():
|
||||
title += " ★"
|
||||
labels["name"] = Style.label(hero_layer, title, 14 if c else 18, Style.TEXT,
|
||||
Rect2(box.position.x + 10, box.position.y + 6, box.size.x - (44 if c else 20), 26), "left", true)
|
||||
labels["life"] = Style.label(hero_layer, "", 16 if c else 20, Style.RED,
|
||||
Rect2(box.position.x + 10, box.position.y + (35 if c else 32), 120, 28), "left", true)
|
||||
labels["stats"] = Style.label(hero_layer, "", 12 if c else 14, Style.MUTED,
|
||||
Rect2(box.position.x + 90, box.position.y + 38, box.size.x - 100, 20), "right")
|
||||
hero_labels[o] = labels
|
||||
if c:
|
||||
var b := Style.button(hero_layer, "Lupe", Rect2(box.end.x - 44, box.position.y + 5, 38, 22), func():
|
||||
zoom_player = o
|
||||
refresh())
|
||||
b.add_theme_font_size_override("font_size", 11)
|
||||
|
||||
|
||||
# --- Ablauf und Eingabe ------------------------------------------------------------------
|
||||
|
||||
func my_turn() -> bool:
|
||||
return game != null and game.winner == "" and game.current() == perspective
|
||||
|
||||
|
||||
func any_overlay() -> bool:
|
||||
return confirm.visible or handover.visible or mulligan_box.visible or overlay.visible
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if game == null:
|
||||
return
|
||||
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_RIGHT:
|
||||
cancel_all()
|
||||
return
|
||||
if not (event is InputEventKey and event.pressed and not event.echo):
|
||||
return
|
||||
match event.keycode:
|
||||
KEY_ESCAPE:
|
||||
if confirm.visible:
|
||||
confirm.visible = false
|
||||
elif selected != null or pending != null or zoom_player != null:
|
||||
cancel_all()
|
||||
elif not any_overlay():
|
||||
confirm.visible = true
|
||||
KEY_SPACE:
|
||||
if my_turn() and not any_overlay():
|
||||
end_turn()
|
||||
KEY_ENTER, KEY_KP_ENTER:
|
||||
if handover.visible:
|
||||
take_over()
|
||||
elif mulligan_box.visible:
|
||||
do_mulligan(false)
|
||||
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
|
||||
if idx < perspective.hand.size():
|
||||
on_click("hand", perspective.hand[idx])
|
||||
|
||||
|
||||
func cancel_all() -> void:
|
||||
if confirm.visible:
|
||||
confirm.visible = false
|
||||
cancel()
|
||||
zoom_player = null
|
||||
refresh()
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if game == null:
|
||||
return
|
||||
msg_t = maxf(0.0, msg_t - delta)
|
||||
if msg_t <= 0.0 and msg_label.text != "":
|
||||
msg_label.text = ""
|
||||
if preview_timer > 0.0:
|
||||
preview_timer -= delta
|
||||
if preview_timer <= 0.0:
|
||||
show_preview(null, false)
|
||||
if game.winner != "":
|
||||
if not ended:
|
||||
ended = true
|
||||
_finish()
|
||||
return
|
||||
var cur := game.current()
|
||||
if cur.is_human:
|
||||
if cur != perspective and is_hotseat() and not handover.visible:
|
||||
handover_label.text = "%s ist dran" % cur.name
|
||||
handover.visible = true
|
||||
hand_layer.visible = false
|
||||
trap_layer.visible = false
|
||||
elif cur == perspective and cur.turns == 1 and not cur.mulligan_done and not mulligan_box.visible and not handover.visible:
|
||||
show_mulligan()
|
||||
return
|
||||
if is_client():
|
||||
return
|
||||
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)
|
||||
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)
|
||||
if tutorial:
|
||||
app.profile.tutorial_done = true
|
||||
app.save()
|
||||
app.sfx.play("win" if won else "lose")
|
||||
else:
|
||||
var wp := game.winner_player
|
||||
var mine: bool = wp != null and wp == perspective
|
||||
app.sfx.play("win" if mine else "lose")
|
||||
refresh()
|
||||
|
||||
|
||||
func take_over() -> void:
|
||||
handover.visible = false
|
||||
hand_layer.visible = true
|
||||
trap_layer.visible = true
|
||||
perspective = game.current()
|
||||
cancel()
|
||||
zoom_player = null
|
||||
build_hero_boxes()
|
||||
refresh()
|
||||
|
||||
|
||||
func say(m: String) -> void:
|
||||
msg_t = 2.5
|
||||
msg_label.text = m
|
||||
|
||||
|
||||
func cancel() -> void:
|
||||
selected = null
|
||||
pending = null
|
||||
|
||||
|
||||
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 err := game.play_card(perspective, card, target)
|
||||
if err != "":
|
||||
say(err)
|
||||
|
||||
|
||||
func end_turn() -> void:
|
||||
if not my_turn():
|
||||
return
|
||||
cancel()
|
||||
zoom_player = null
|
||||
if is_client():
|
||||
net.send_action({"type": "end"})
|
||||
return
|
||||
game.end_turn()
|
||||
ai_timer = ai_delay
|
||||
after_action()
|
||||
|
||||
|
||||
func show_mulligan() -> void:
|
||||
mulligan_selected = []
|
||||
mulligan_box.visible = true
|
||||
Style.clear(mulligan_layer)
|
||||
var hand: Array = perspective.hand
|
||||
for i in hand.size():
|
||||
var r := BattleLayout.hand_rect(hand.size(), i)
|
||||
var v := CardView.new(hand[i], r.size)
|
||||
v.position = Vector2(r.position.x, 322)
|
||||
var idx := i
|
||||
v.clicked.connect(func(_view): toggle_mulligan(idx))
|
||||
mulligan_layer.add_child(v)
|
||||
_mulligan_borders()
|
||||
|
||||
|
||||
func toggle_mulligan(idx: int) -> void:
|
||||
if idx in mulligan_selected:
|
||||
mulligan_selected.erase(idx)
|
||||
else:
|
||||
mulligan_selected.append(idx)
|
||||
_mulligan_borders()
|
||||
|
||||
|
||||
func _mulligan_borders() -> void:
|
||||
var views := mulligan_layer.get_children()
|
||||
for i in views.size():
|
||||
views[i].border = Style.YELLOW if i in mulligan_selected else null
|
||||
views[i].queue_redraw()
|
||||
b_swap.text = "Tauschen (%d)" % mulligan_selected.size() if not mulligan_selected.is_empty() else "Tauschen"
|
||||
|
||||
|
||||
func do_mulligan(swap: bool) -> void:
|
||||
mulligan_box.visible = false
|
||||
var idxs: Array = mulligan_selected.duplicate() if swap else []
|
||||
if is_client():
|
||||
net.send_action({"type": "mulligan", "hand": idxs})
|
||||
return
|
||||
var cards := []
|
||||
for i in idxs:
|
||||
cards.append(perspective.hand[i])
|
||||
game.mulligan(perspective, cards)
|
||||
after_action()
|
||||
|
||||
|
||||
func on_click(kind: String, obj) -> void:
|
||||
var g := game
|
||||
if not my_turn() or any_overlay():
|
||||
return
|
||||
if pending != null:
|
||||
var card = pending
|
||||
pending = null
|
||||
if obj in g.valid_targets(perspective, card.target_type()):
|
||||
play(card, obj)
|
||||
else:
|
||||
say("Abgebrochen")
|
||||
zoom_player = null
|
||||
after_action()
|
||||
return
|
||||
if selected != null:
|
||||
var cr = selected
|
||||
selected = null
|
||||
if kind == "enemy" or kind == "hero":
|
||||
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)
|
||||
zoom_player = null
|
||||
after_action()
|
||||
return
|
||||
match kind:
|
||||
"hand":
|
||||
var ok := g.can_play(perspective, obj)
|
||||
if not ok[0]:
|
||||
say(ok[1])
|
||||
elif obj.target_type() != "" and not g.valid_targets(perspective, obj.target_type()).is_empty():
|
||||
pending = obj
|
||||
say("Wähle ein Ziel")
|
||||
else:
|
||||
play(obj, null)
|
||||
"own":
|
||||
if obj.can_attack():
|
||||
selected = obj
|
||||
say("Wähle ein Angriffsziel")
|
||||
else:
|
||||
say("%s kann nicht angreifen" % obj.name)
|
||||
after_action()
|
||||
|
||||
|
||||
func on_tile_click(cr: Game.Creature) -> void:
|
||||
var o = cr.owner()
|
||||
if o == null:
|
||||
return
|
||||
if o == perspective:
|
||||
on_click("own", cr)
|
||||
elif o.alive():
|
||||
on_click("enemy", cr)
|
||||
|
||||
|
||||
func show_preview(card, entered: bool) -> void:
|
||||
Style.clear(preview_holder)
|
||||
if entered and card != null:
|
||||
var v := CardView.new(card, Vector2(210, 294))
|
||||
v.position = Vector2(1005, 215)
|
||||
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
preview_holder.add_child(v)
|
||||
preview_hint.visible = not (entered and card != null)
|
||||
|
||||
|
||||
## Vorschau einer Handkarte mit Hinweisen auf Resonanz und Feldkombos mit dem eigenen Feld.
|
||||
func show_hand_preview(card: Cards.Card, entered: bool) -> void:
|
||||
Style.clear(preview_holder)
|
||||
preview_hint.visible = not entered
|
||||
if not entered:
|
||||
return
|
||||
var hints: Array[String] = combo_hints(card)
|
||||
var v := CardView.new(card, Vector2(190, 266) if not hints.is_empty() else Vector2(210, 294))
|
||||
v.position = Vector2(1015, 212) if not hints.is_empty() else Vector2(1005, 215)
|
||||
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
preview_holder.add_child(v)
|
||||
if not hints.is_empty():
|
||||
var box := RichTextLabel.new()
|
||||
box.position = Vector2(958, 480)
|
||||
box.size = Vector2(304, 46)
|
||||
box.bbcode_enabled = true
|
||||
box.scroll_active = false
|
||||
box.fit_content = false
|
||||
box.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
box.add_theme_font_size_override("normal_font_size", 11)
|
||||
box.add_theme_color_override("default_color", Style.GOLD)
|
||||
box.text = "[center]" + "\n".join(hints.slice(0, 2)) + "[/center]"
|
||||
preview_holder.add_child(box)
|
||||
|
||||
|
||||
func combo_hints(card: Cards.Card) -> Array[String]:
|
||||
var out: Array[String] = []
|
||||
if not card.is_creature():
|
||||
return out
|
||||
var partners: Array[String] = []
|
||||
var combos := {}
|
||||
for other in perspective.board:
|
||||
if other.card.elements.any(func(e): return e in card.elements):
|
||||
partners.append(other.name)
|
||||
for e1 in card.elements:
|
||||
for e2 in other.card.elements:
|
||||
if e1 != e2:
|
||||
var name := Cards.combo_name([e1, e2])
|
||||
if not combos.has(name):
|
||||
combos[name] = other.name
|
||||
if not partners.is_empty():
|
||||
out.append("Resonanz +%d mit %s" % [mini(partners.size(), Game.RESONANCE_MAX), ", ".join(partners)])
|
||||
for name in combos:
|
||||
out.append("Feldkombo %s mit %s: %s" % [name, combos[name], Cards.FIELD_COMBO_TEXT[name]])
|
||||
return out
|
||||
|
||||
|
||||
# --- Netzwerk -------------------------------------------------------------------------------
|
||||
|
||||
func _on_snapshot(snap: Dictionary) -> void:
|
||||
var first := game == null
|
||||
game = Game.from_snapshot(snap)
|
||||
wait_label.visible = false
|
||||
if first:
|
||||
_begin()
|
||||
return
|
||||
perspective = game.players[online_seat]
|
||||
build_hero_boxes()
|
||||
after_action()
|
||||
|
||||
|
||||
func _on_net_ended(reason: String) -> void:
|
||||
if game == null or game.winner == "":
|
||||
say(reason)
|
||||
await get_tree().create_timer(1.5).timeout
|
||||
if is_inside_tree():
|
||||
app.show_screen(MenuScreen.new())
|
||||
|
||||
|
||||
# --- Ereignisse ----------------------------------------------------------------------------
|
||||
|
||||
## Nach jeder Aktion: Ereignisse sichern, Ansicht angleichen, Animationen abspielen.
|
||||
func after_action() -> void:
|
||||
if is_online() and net.is_host:
|
||||
net.broadcast()
|
||||
var evs := game.events.duplicate()
|
||||
game.events.clear()
|
||||
if counts_for_profile():
|
||||
_note_events(evs)
|
||||
var centers := {}
|
||||
for ev in evs:
|
||||
for key in ["target", "attacker", "source"]:
|
||||
if ev.has(key):
|
||||
centers[ref_key(ev[key])] = target_center(ev[key])
|
||||
refresh()
|
||||
fx.play_events(evs, centers)
|
||||
|
||||
|
||||
func _note_events(evs: Array) -> void:
|
||||
var hi := game.players.find(game.human)
|
||||
for ev in evs:
|
||||
if ev.get("player", -1) != hi:
|
||||
continue
|
||||
match ev.type:
|
||||
"play", "spell":
|
||||
played_ids.append(ev.card.id)
|
||||
if ev.type == "play":
|
||||
app.profile.daily_event("creature_played")
|
||||
"trap_set":
|
||||
app.profile.daily_event("trap_set")
|
||||
"combo":
|
||||
app.profile.daily_event("combo")
|
||||
|
||||
|
||||
# --- Anleitung ------------------------------------------------------------------------------
|
||||
|
||||
func tutorial_condition(kind: String) -> bool:
|
||||
var h := game.human
|
||||
match kind:
|
||||
"played_creature":
|
||||
return game.log.any(func(l): return l.begins_with("Du spielt")) and h.board.size() > 0
|
||||
"turn_passed":
|
||||
return h.turns >= 2
|
||||
"attacked":
|
||||
return game.log.any(func(l): return l.ends_with("greift Fenn an.") or (" greift " in l and l.begins_with(h.board[0].name) if not h.board.is_empty() else false))
|
||||
"resonance":
|
||||
return h.board.any(func(c): return c.resonance > 0)
|
||||
"combo":
|
||||
return game.log.any(func(l): return l.begins_with("Feldkombo"))
|
||||
"spell":
|
||||
return game.log.any(func(l): return l == "Du spielt Funke." or l == "Du spielt Feuerball." or l == "Du spielt Verhärten.")
|
||||
return false
|
||||
|
||||
|
||||
func tutorial_next() -> void:
|
||||
if tutorial_step < TUTORIAL_STEPS.size() - 1:
|
||||
tutorial_step += 1
|
||||
app.sfx.play("click")
|
||||
refresh()
|
||||
|
||||
|
||||
func _tutorial_refresh() -> void:
|
||||
while tutorial_step < TUTORIAL_STEPS.size() - 1 and tutorial_condition(TUTORIAL_STEPS[tutorial_step][1]):
|
||||
tutorial_step += 1
|
||||
tutorial_text.text = "%d/%d %s" % [tutorial_step + 1, TUTORIAL_STEPS.size(), TUTORIAL_STEPS[tutorial_step][0]]
|
||||
b_tut_next.visible = tutorial_step < TUTORIAL_STEPS.size() - 1
|
||||
|
||||
|
||||
# --- Ansicht angleichen --------------------------------------------------------------------
|
||||
|
||||
func refresh() -> void:
|
||||
if game == null:
|
||||
return
|
||||
var g := game
|
||||
var h := perspective
|
||||
var now := Time.get_ticks_msec()
|
||||
var mine := my_turn()
|
||||
var valid: Array = g.valid_targets(h, pending.target_type()) if pending != null else []
|
||||
hand_views = []
|
||||
own_views = []
|
||||
enemy_views = []
|
||||
trap_views = []
|
||||
|
||||
for o in others():
|
||||
var panel: Panel = hero_panels[o]
|
||||
var bcol = null
|
||||
var bw := 0
|
||||
if g.current() == o:
|
||||
bcol = Style.BLUE
|
||||
bw = 3
|
||||
if o in valid or (selected != null and o.alive() and g.can_attack(selected, o)[0]):
|
||||
bcol = Style.ORANGE
|
||||
bw = 3
|
||||
if int(flash_until.get(g.players.find(o), 0)) > now:
|
||||
bcol = Style.RED
|
||||
bw = 4
|
||||
panel.add_theme_stylebox_override("panel", Style.box(Style.PANEL if o.alive() else Style.darker(Style.PANEL, 0.6), 8, bcol, bw))
|
||||
panel.mouse_filter = Control.MOUSE_FILTER_STOP if o.alive() else Control.MOUSE_FILTER_IGNORE
|
||||
var labels: Dictionary = hero_labels[o]
|
||||
labels.name.add_theme_color_override("font_color", Style.TEXT if o.alive() else Style.MUTED)
|
||||
if o.alive():
|
||||
labels.life.text = "♥ %d" % maxi(o.life, 0)
|
||||
var traps_txt := (" F %d" if compact() else " Fallen %d") % o.traps.size() if not o.traps.is_empty() else ""
|
||||
labels.stats.text = ("H %d D %d%s ◆ %d" if compact() else "Hand %d Deck %d%s ◆ %d") % [o.hand.size(), o.deck.size(), traps_txt, o.max_energy]
|
||||
else:
|
||||
labels.life.text = "besiegt"
|
||||
labels.life.add_theme_color_override("font_color", Style.MUTED)
|
||||
labels.stats.text = ""
|
||||
|
||||
var seen := {}
|
||||
for p in g.players:
|
||||
for j in p.board.size():
|
||||
var cr: Game.Creature = p.board[j]
|
||||
var rect := slot_rect(p, j)
|
||||
seen[cr.uid] = true
|
||||
var tile: CreatureView
|
||||
if tiles.has(cr.uid):
|
||||
tile = tiles[cr.uid]
|
||||
tile.creature = cr
|
||||
if tile.size != rect.size:
|
||||
tile.size = rect.size
|
||||
tile.custom_minimum_size = rect.size
|
||||
tile.pivot_offset = rect.size / 2
|
||||
else:
|
||||
tile = CreatureView.new(cr, rect.size)
|
||||
tile.position = rect.position
|
||||
tile.slot = rect.position
|
||||
tiles[cr.uid] = tile
|
||||
tile_layer.add_child(tile)
|
||||
tile.hover.connect(show_preview)
|
||||
var uid := cr.uid
|
||||
tile.clicked.connect(func(_v):
|
||||
var c := game.find_creature(uid)
|
||||
if c != null:
|
||||
on_tile_click(c))
|
||||
fx.pop_in(tile)
|
||||
tile.slot = rect.position
|
||||
if not tile.busy and tile.position != rect.position:
|
||||
fx.move_to(tile, rect.position)
|
||||
var b = null
|
||||
var w := 2
|
||||
if cr == selected:
|
||||
b = Style.YELLOW
|
||||
w = 4
|
||||
elif cr in valid or (selected != null and g.can_attack(selected, cr)[0]):
|
||||
b = Style.ORANGE
|
||||
w = 3
|
||||
elif p == h and mine and cr.can_attack():
|
||||
b = Style.GREEN
|
||||
w = 3
|
||||
tile.border = b
|
||||
tile.width = w
|
||||
tile.queue_redraw()
|
||||
if p == h:
|
||||
own_views.append(tile)
|
||||
else:
|
||||
enemy_views.append(tile)
|
||||
for uid in tiles.keys():
|
||||
if not seen.has(uid):
|
||||
var t: CreatureView = tiles[uid]
|
||||
tiles.erase(uid)
|
||||
fx.fade_out(t)
|
||||
|
||||
Style.clear(trap_layer)
|
||||
if not h.traps.is_empty():
|
||||
Style.label(trap_layer, "Fallen", 13, Style.MUTED, Rect2(335, 492, 60, 20))
|
||||
for i in h.traps.size():
|
||||
var tv := CardView.new(h.traps[i], BattleLayout.trap_rect(i).size)
|
||||
tv.position = BattleLayout.trap_rect(i).position
|
||||
tv.hover.connect(show_preview)
|
||||
trap_layer.add_child(tv)
|
||||
trap_views.append(tv)
|
||||
|
||||
Style.clear(hand_layer)
|
||||
for i in h.hand.size():
|
||||
var card: Cards.Card = h.hand[i]
|
||||
var playable: bool = mine and g.can_play(h, card)[0]
|
||||
var r := BattleLayout.hand_rect(h.hand.size(), i)
|
||||
var v := CardView.new(card, r.size)
|
||||
v.position = r.position
|
||||
if card == pending:
|
||||
v.border = Style.YELLOW
|
||||
elif playable:
|
||||
v.border = Style.GREEN
|
||||
v.dim = not playable
|
||||
v.hover.connect(show_hand_preview)
|
||||
v.clicked.connect(func(_view): on_click("hand", card))
|
||||
hand_layer.add_child(v)
|
||||
hand_views.append(v)
|
||||
|
||||
var bar_border = Style.BLUE if mine else null
|
||||
if int(flash_until.get(g.players.find(h), 0)) > now:
|
||||
bar_border = Style.RED
|
||||
bar_panel.add_theme_stylebox_override("panel", Style.box(Style.PANEL, 8, bar_border, 3 if bar_border != null else 0))
|
||||
name_label.text = h.name
|
||||
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 …"]
|
||||
b_end.disabled = not mine
|
||||
var lines: Array[String] = []
|
||||
var start := maxi(0, g.log.size() - 60)
|
||||
for i in range(start, g.log.size()):
|
||||
var line := g.log[i]
|
||||
if line.begins_with("---"):
|
||||
lines.append("[color=#6eaaf0]%s[/color]" % line)
|
||||
elif line.begins_with("Feldkombo") or line.begins_with("Falle von"):
|
||||
lines.append("[color=#f5c850]%s[/color]" % line)
|
||||
else:
|
||||
lines.append(line)
|
||||
log_box.text = "\n".join(lines)
|
||||
if tutorial:
|
||||
_tutorial_refresh()
|
||||
|
||||
Style.clear(zoom_layer)
|
||||
if zoom_player != null and zoom_player.alive():
|
||||
build_zoom(valid)
|
||||
|
||||
if g.winner != "":
|
||||
var wp := g.winner_player
|
||||
if counts_for_profile():
|
||||
var won := g.winner == "human"
|
||||
result_label.text = "Sieg!" if won else "Niederlage"
|
||||
result_label.add_theme_color_override("font_color", Style.GREEN if won else Style.RED)
|
||||
reward_label.text = "+%d Münzen" % maxi(reward, 0)
|
||||
var notes: Array[String] = app.profile.take_notices()
|
||||
if not notes.is_empty():
|
||||
notice_label.text = "\n".join(notes)
|
||||
else:
|
||||
var mine_win: bool = wp != null and wp == perspective
|
||||
result_label.text = ("%s gewinnt!" % wp.name) if wp != null and wp.is_human else "Die KI gewinnt"
|
||||
result_label.add_theme_color_override("font_color", Style.GREEN if mine_win else Style.RED)
|
||||
reward_label.text = "Gemeinsame Partie, keine Münzen"
|
||||
overlay.visible = true
|
||||
confirm.visible = false
|
||||
handover.visible = false
|
||||
mulligan_box.visible = false
|
||||
|
||||
|
||||
func build_zoom(valid: Array) -> void:
|
||||
var o: Game.Player = zoom_player
|
||||
var g := game
|
||||
var box := Rect2(190, 190, 900, 300)
|
||||
var back := Style.panel(zoom_layer, box, Style.PANEL, 10, Style.GOLD, 2)
|
||||
back.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
Style.label(zoom_layer, "%s · %s ♥ %d Hand %d Deck %d Fallen %d ◆ %d" % [o.name, o.title, o.life, o.hand.size(), o.deck.size(), o.traps.size(), o.max_energy],
|
||||
18, Style.TEXT, Rect2(box.position.x + 20, box.position.y + 12, 800, 30), "left", true)
|
||||
for j in o.board.size():
|
||||
var cr: Game.Creature = o.board[j]
|
||||
var b = null
|
||||
if cr in valid or (selected != null and g.can_attack(selected, cr)[0]):
|
||||
b = Style.ORANGE
|
||||
var v := CreatureView.new(cr, Vector2(110, 140), b, 3 if b != null else 2)
|
||||
v.position = Vector2(box.position.x + 20 + j * 122, box.position.y + 56)
|
||||
v.hover.connect(show_preview)
|
||||
v.clicked.connect(func(_x): on_tile_click(cr))
|
||||
zoom_layer.add_child(v)
|
||||
if o.board.is_empty():
|
||||
Style.label(zoom_layer, "Keine Wesen auf dem Feld", 16, Style.MUTED, Rect2(box.position.x + 20, box.position.y + 110, 600, 30))
|
||||
var hero_ok: bool = (o in valid) or (selected != null and g.can_attack(selected, o)[0])
|
||||
var bh := Style.button(zoom_layer, "Held als Ziel", Rect2(box.position.x + 20, box.end.y - 60, 180, 44), func(): on_click("hero", o), Color("5a3c28"))
|
||||
bh.disabled = not hero_ok
|
||||
Style.button(zoom_layer, "Schließen", Rect2(box.end.x - 180, box.end.y - 60, 160, 44), func():
|
||||
zoom_player = null
|
||||
refresh())
|
||||
1
scripts/ui/battle_screen.gd.uid
Normal file
1
scripts/ui/battle_screen.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bmf82cuhs7h1a
|
||||
122
scripts/ui/booster_screen.gd
Normal file
122
scripts/ui/booster_screen.gd
Normal file
@@ -0,0 +1,122 @@
|
||||
class_name BoosterScreen
|
||||
extends Control
|
||||
|
||||
const CARD_W := 200
|
||||
const CARD_H := 280
|
||||
|
||||
var app
|
||||
var cards: Array = []
|
||||
var revealed := {}
|
||||
var new_ids := {}
|
||||
var coins_label: Label
|
||||
var msg_label: Label
|
||||
var b_buy: Button
|
||||
var b_all: Button
|
||||
var slots: Control
|
||||
|
||||
|
||||
class CardBack extends Control:
|
||||
signal clicked
|
||||
|
||||
func _init(sz: Vector2) -> void:
|
||||
size = sz
|
||||
custom_minimum_size = sz
|
||||
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
|
||||
func _gui_input(event: InputEvent) -> void:
|
||||
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||
accept_event()
|
||||
clicked.emit()
|
||||
|
||||
func _draw() -> void:
|
||||
var rect := Rect2(Vector2.ZERO, size)
|
||||
draw_style_box(Style.box(Color("3c285a"), 8, Style.GOLD, 3), rect)
|
||||
Style.border(self, rect.grow(-15), Color("5a3c82"), 2, 6)
|
||||
Style.text(self, "?", 48, Style.GOLD, size / 2, "center", true)
|
||||
|
||||
|
||||
func slot_rect(i: int) -> Rect2:
|
||||
var gap := 20
|
||||
var x0 := (1280 - 5 * CARD_W - 4 * gap) / 2
|
||||
return Rect2(x0 + i * (CARD_W + gap), 230, CARD_W, CARD_H)
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
Style.label(self, "Booster", 36, Style.GOLD, Rect2(0, 20, 1280, 50), "center", true)
|
||||
coins_label = Style.label(self, "", 22, Style.GOLD, Rect2(0, 78, 1280, 30), "center")
|
||||
Style.label(self, "5 Karten pro Booster, mindestens eine seltene. Klicke auf eine Karte zum Aufdecken.",
|
||||
16, Style.MUTED, Rect2(0, 115, 1280, 24), "center")
|
||||
slots = Control.new()
|
||||
slots.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(slots)
|
||||
msg_label = Style.label(self, "", 18, Style.RED, Rect2(0, 610, 1280, 30), "center")
|
||||
b_buy = Style.button(self, "Booster kaufen (%d Münzen)" % Profile.BOOSTER_COST, Rect2(360, 720, 260, 48), buy, Color("5a4628"))
|
||||
b_all = Style.button(self, "Alle aufdecken", Rect2(640, 720, 140, 48), reveal_all)
|
||||
Style.button(self, "Zurück", Rect2(800, 720, 120, 48), func(): app.show_screen(MenuScreen.new()))
|
||||
refresh()
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
|
||||
app.show_screen(MenuScreen.new())
|
||||
|
||||
|
||||
func buy() -> void:
|
||||
var p: Profile = app.profile
|
||||
var before: Dictionary = p.collection.duplicate()
|
||||
var got: Array = p.open_booster()
|
||||
if got.is_empty():
|
||||
msg_label.text = "Nicht genug Münzen"
|
||||
msg_label.add_theme_color_override("font_color", Style.RED)
|
||||
return
|
||||
app.save()
|
||||
app.sfx.play("play")
|
||||
cards = got
|
||||
revealed = {}
|
||||
new_ids = {}
|
||||
for c in cards:
|
||||
if before.get(c.id, 0) == 0:
|
||||
new_ids[c.id] = true
|
||||
var notes: Array[String] = p.take_notices()
|
||||
msg_label.text = " · ".join(notes)
|
||||
msg_label.add_theme_color_override("font_color", Style.GREEN)
|
||||
refresh()
|
||||
|
||||
|
||||
func reveal(i: int) -> void:
|
||||
revealed[i] = true
|
||||
app.sfx.play("click")
|
||||
refresh()
|
||||
|
||||
|
||||
func reveal_all() -> void:
|
||||
for i in 5:
|
||||
revealed[i] = true
|
||||
refresh()
|
||||
|
||||
|
||||
func refresh() -> void:
|
||||
var p: Profile = app.profile
|
||||
coins_label.text = "Münzen: %d" % p.coins
|
||||
b_buy.disabled = p.coins < Profile.BOOSTER_COST
|
||||
b_all.disabled = cards.is_empty() or revealed.size() >= 5
|
||||
Style.clear(slots)
|
||||
for i in 5:
|
||||
var r := slot_rect(i)
|
||||
if cards.is_empty():
|
||||
Style.panel(slots, r, Style.PANEL, 8, Style.PANEL_LIGHT, 2)
|
||||
elif revealed.has(i):
|
||||
var c: Cards.Card = cards[i]
|
||||
var v := CardView.new(c, r.size)
|
||||
v.position = r.position
|
||||
slots.add_child(v)
|
||||
Style.label(slots, c.rarity, 16, Cards.RARITY_COLORS[c.rarity], Rect2(r.position.x, r.end.y + 8, r.size.x, 24), "center", true)
|
||||
if new_ids.has(c.id):
|
||||
Style.label(slots, "Neu!", 16, Style.GREEN, Rect2(r.position.x, r.end.y + 32, r.size.x, 24), "center", true)
|
||||
else:
|
||||
Style.label(slots, "jetzt %dx" % p.collection[c.id], 14, Style.MUTED, Rect2(r.position.x, r.end.y + 32, r.size.x, 24), "center")
|
||||
else:
|
||||
var back := CardBack.new(r.size)
|
||||
back.position = r.position
|
||||
back.clicked.connect(reveal.bind(i))
|
||||
slots.add_child(back)
|
||||
1
scripts/ui/booster_screen.gd.uid
Normal file
1
scripts/ui/booster_screen.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://d3irimwei0s4d
|
||||
59
scripts/ui/campaign_screen.gd
Normal file
59
scripts/ui/campaign_screen.gd
Normal file
@@ -0,0 +1,59 @@
|
||||
class_name CampaignScreen
|
||||
extends Control
|
||||
|
||||
var app
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
var p: Profile = app.profile
|
||||
var col := Style.column(self, 1000, 24, 6)
|
||||
Style.clabel(col, "Kampagne", 36, Style.GOLD, "center", true)
|
||||
Style.clabel(col, "Zehn Magier warten. Jede Stufe bringt mehr Gegner an den Tisch, alle nach denselben Regeln.", 16, Style.MUTED)
|
||||
Style.spacer(col, 6)
|
||||
for i in Campaign.STAGES.size():
|
||||
var st: Dictionary = Campaign.STAGES[i]
|
||||
var done := i < p.campaign_stage
|
||||
var next := i == p.campaign_stage
|
||||
var panel := PanelContainer.new()
|
||||
panel.add_theme_stylebox_override("panel", Style.box(Style.PANEL if not done else Style.darker(Style.PANEL, 0.8), 6,
|
||||
Style.GOLD if next else null, 2 if next else 0))
|
||||
col.add_child(panel)
|
||||
var r := Style.row(panel, 12)
|
||||
r.custom_minimum_size = Vector2(0, 50)
|
||||
Style.clabel(r, str(i + 1), 20, Style.GOLD if next else Style.MUTED, "center", true, 40)
|
||||
Style.clabel(r, "%s · %s" % [st.name, st.title], 17, Style.TEXT if not done else Style.MUTED, "left", true, 300)
|
||||
var dots := Control.new()
|
||||
dots.custom_minimum_size = Vector2(60, 0)
|
||||
dots.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
var elems: Array = st.elements
|
||||
dots.draw.connect(func():
|
||||
for k in elems.size():
|
||||
dots.draw_circle(Vector2(10 + k * 18, dots.size.y / 2), 7, Cards.ELEMENT_COLORS[elems[k]]))
|
||||
r.add_child(dots)
|
||||
var table: String = "Duell" if st.extra == 0 else "%d am Tisch" % (st.extra + 2)
|
||||
Style.clabel(r, "%s · %d Münzen\n★ %s" % [table, st.reward, Game.PERKS[st.perk]], 13, Style.MUTED, "left", false, 300)
|
||||
if done:
|
||||
Style.clabel(r, "abgeschlossen", 14, Style.GREEN, "right", false, 110)
|
||||
Style.cbutton(r, "Wiederholen", Vector2(140, 38), _start.bind(i))
|
||||
elif next:
|
||||
Style.clabel(r, "", 14, Style.GREEN, "right", false, 110)
|
||||
Style.cbutton(r, "Kämpfen", Vector2(140, 38), _start.bind(i), Color("285a3c"))
|
||||
else:
|
||||
Style.clabel(r, "", 14, Style.GREEN, "right", false, 110)
|
||||
var b := Style.cbutton(r, "Gesperrt", Vector2(140, 38), func(): pass)
|
||||
b.disabled = true
|
||||
if p.campaign_stage >= Campaign.STAGES.size():
|
||||
Style.clabel(col, "Kampagne abgeschlossen. Der Archimagier ist besiegt!", 18, Style.GREEN, "center", true)
|
||||
Style.spacer(col, 4)
|
||||
var br := Style.row(col)
|
||||
br.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
Style.cbutton(br, "Zurück", Vector2(160, 44), func(): app.show_screen(MenuScreen.new()))
|
||||
|
||||
|
||||
func _start(stage: int) -> void:
|
||||
app.show_screen(BattleScreen.new(0, stage))
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
|
||||
app.show_screen(MenuScreen.new())
|
||||
1
scripts/ui/campaign_screen.gd.uid
Normal file
1
scripts/ui/campaign_screen.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bk50mk881o2hr
|
||||
50
scripts/ui/card_grid.gd
Normal file
50
scripts/ui/card_grid.gd
Normal file
@@ -0,0 +1,50 @@
|
||||
class_name CardGrid
|
||||
extends ScrollContainer
|
||||
## Scrollbares Kartenraster mit Beschriftung unter jeder Karte.
|
||||
|
||||
signal card_clicked(card: Cards.Card)
|
||||
signal card_hover(card: Cards.Card, entered: bool)
|
||||
|
||||
const COLS := 7
|
||||
const CW := 100
|
||||
const CH := 140
|
||||
const GAP := 12
|
||||
|
||||
var grid: GridContainer
|
||||
|
||||
|
||||
func _init(rect: Rect2) -> void:
|
||||
position = rect.position
|
||||
size = rect.size
|
||||
horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
grid = GridContainer.new()
|
||||
grid.columns = COLS
|
||||
grid.add_theme_constant_override("h_separation", GAP)
|
||||
grid.add_theme_constant_override("v_separation", GAP)
|
||||
add_child(grid)
|
||||
|
||||
|
||||
## cards: Karten; label_fn(card) -> String; dim_fn(card) -> bool
|
||||
func fill(cards: Array, label_fn: Callable, dim_fn: Callable) -> void:
|
||||
Style.clear(grid)
|
||||
for c in cards:
|
||||
var cell := Control.new()
|
||||
cell.custom_minimum_size = Vector2(CW, CH + 16)
|
||||
cell.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
var v := CardView.new(c, Vector2(CW, CH))
|
||||
v.dim = dim_fn.call(c)
|
||||
v.clicked.connect(func(_view): card_clicked.emit(c))
|
||||
v.hover.connect(func(card, entered): card_hover.emit(card, entered))
|
||||
cell.add_child(v)
|
||||
var text: String = label_fn.call(c)
|
||||
Style.label(cell, text, 12, Style.MUTED if v.dim else Style.TEXT, Rect2(0, CH + 1, CW, 16), "center")
|
||||
grid.add_child(cell)
|
||||
|
||||
|
||||
func views() -> Array:
|
||||
var out := []
|
||||
for cell in grid.get_children():
|
||||
for v in cell.get_children():
|
||||
if v is CardView:
|
||||
out.append(v)
|
||||
return out
|
||||
1
scripts/ui/card_grid.gd.uid
Normal file
1
scripts/ui/card_grid.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bil3i64hn7rfd
|
||||
65
scripts/ui/card_view.gd
Normal file
65
scripts/ui/card_view.gd
Normal file
@@ -0,0 +1,65 @@
|
||||
class_name CardView
|
||||
extends Control
|
||||
## Zeichnet eine Karte (Hand, Sammlung, Booster, Vorschau).
|
||||
|
||||
signal clicked(view: CardView)
|
||||
signal hover(card: Cards.Card, entered: bool)
|
||||
|
||||
var card: Cards.Card
|
||||
var border = null # Color oder null (dann Seltenheitsfarbe)
|
||||
var dim := false
|
||||
|
||||
|
||||
func _init(c: Cards.Card, sz: Vector2) -> void:
|
||||
card = c
|
||||
custom_minimum_size = sz
|
||||
size = sz
|
||||
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
mouse_entered.connect(func(): hover.emit(card, true))
|
||||
mouse_exited.connect(func(): hover.emit(card, false))
|
||||
|
||||
|
||||
func _gui_input(event: InputEvent) -> void:
|
||||
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||
accept_event()
|
||||
clicked.emit(self)
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
var rect := Rect2(Vector2.ZERO, size)
|
||||
var big := size.x >= 180
|
||||
var mid := size.x >= 115
|
||||
var f_name := 18 if big else (14 if mid else 12)
|
||||
var f_text := 14 if big else (11 if mid else 10)
|
||||
var f_stat := 24 if big else (18 if mid else 15)
|
||||
Style.fill_elements(self, rect, card.elements, 8)
|
||||
var bcol: Color = border if border != null else Cards.RARITY_COLORS[card.rarity]
|
||||
Style.border(self, rect, bcol, 3 if big else 2, 8)
|
||||
# Kosten
|
||||
var r := 16 if big else 12
|
||||
var cc := Vector2(r + 4, r + 4)
|
||||
draw_circle(cc, r, Color("14141c"))
|
||||
Style.text(self, str(card.cost), f_stat, Style.GOLD, cc, "center", true)
|
||||
# Name und Kombo
|
||||
var nm := Style.fit(card.name, true, f_name, size.x - 2 * r - 12)
|
||||
Style.text(self, nm, f_name, Style.TEXT, Vector2(size.x / 2 + r / 2.0, 4), "midtop", true)
|
||||
if mid:
|
||||
var sub: String = (card.combo() + " · " + card.kind) if card.is_combo() else card.kind
|
||||
Style.text(self, sub, f_text, Color("e6e6e6"), Vector2(size.x / 2, 2 * r + 4), "midtop")
|
||||
# Bild
|
||||
var band_top: float = 2 * r + 4 + (f_text + 6 if mid else 0)
|
||||
var band_bottom: float = size.y * 0.42 - 4
|
||||
if band_bottom - band_top > 14:
|
||||
var gsize: float = minf(band_bottom - band_top, size.x * 0.42)
|
||||
Style.glyph(self, card, Vector2(size.x / 2, (band_top + band_bottom) / 2), gsize)
|
||||
# Textfeld
|
||||
var box := Rect2(6, size.y * 0.42, size.x - 12, size.y * 0.42)
|
||||
draw_rect(box, Color(0.06, 0.06, 0.09, 0.67))
|
||||
draw_multiline_string(Style.FONT, Vector2(box.position.x + 4, box.position.y + 4 + Style.FONT.get_ascent(f_text)),
|
||||
card.text(), HORIZONTAL_ALIGNMENT_LEFT, box.size.x - 8, f_text, 6 if big else 4, Style.TEXT)
|
||||
# Werte
|
||||
if card.is_creature():
|
||||
Style.text(self, str(card.attack), f_stat, Style.YELLOW, Vector2(10, size.y - 6), "bottomleft", true)
|
||||
Style.text(self, str(card.health), f_stat, Style.RED, Vector2(size.x - 10, size.y - 6), "bottomright", true)
|
||||
if dim:
|
||||
draw_rect(rect, Color(0, 0, 0, 0.47))
|
||||
1
scripts/ui/card_view.gd.uid
Normal file
1
scripts/ui/card_view.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://oom648upeavd
|
||||
92
scripts/ui/creature_view.gd
Normal file
92
scripts/ui/creature_view.gd
Normal file
@@ -0,0 +1,92 @@
|
||||
class_name CreatureView
|
||||
extends Control
|
||||
## Zeichnet ein Wesen auf dem Feld. Bleibt über Zustandsänderungen bestehen (für Animationen).
|
||||
|
||||
signal clicked(view: CreatureView)
|
||||
signal hover(card: Cards.Card, entered: bool)
|
||||
|
||||
var creature: Game.Creature
|
||||
var border = null
|
||||
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)
|
||||
|
||||
|
||||
func _init(cr: Game.Creature, sz: Vector2, b = null, w: int = 2) -> void:
|
||||
creature = cr
|
||||
border = b
|
||||
width = w
|
||||
custom_minimum_size = sz
|
||||
size = sz
|
||||
pivot_offset = sz / 2
|
||||
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
mouse_entered.connect(func(): hover.emit(creature.card, true))
|
||||
mouse_exited.connect(func(): hover.emit(creature.card, false))
|
||||
|
||||
|
||||
func _gui_input(event: InputEvent) -> void:
|
||||
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||
accept_event()
|
||||
clicked.emit(self)
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
var rect := Rect2(Vector2.ZERO, size)
|
||||
var small := size.x < 100
|
||||
var mini := size.x < 60
|
||||
Style.fill_elements(self, rect, creature.card.elements, 6)
|
||||
var bcol: Color = border if border != null else Cards.RARITY_COLORS[creature.card.rarity]
|
||||
if flash:
|
||||
bcol = Style.RED
|
||||
Style.border(self, rect, bcol, 4 if flash else width, 6)
|
||||
if mini:
|
||||
_draw_mini()
|
||||
return
|
||||
var fs := 11 if small else 13
|
||||
var nm := Style.fit(creature.name, true, fs, size.x - 6)
|
||||
Style.text(self, nm, fs, Style.TEXT, Vector2(size.x / 2, 4), "midtop", true)
|
||||
var badges: Array[String] = []
|
||||
if creature.taunt():
|
||||
badges.append("Wächter")
|
||||
if creature.shield:
|
||||
badges.append("Schild")
|
||||
if creature.attacks_per_turn() == 2:
|
||||
badges.append("x2")
|
||||
if creature.frozen:
|
||||
badges.append("Eis")
|
||||
if creature.resonance > 0:
|
||||
badges.append("Resonanz")
|
||||
var y := 18.0 if small else 22.0
|
||||
for b in badges:
|
||||
Style.text(self, b, 10 if small else 12, Color("ebebf5"), Vector2(size.x / 2, y), "midtop")
|
||||
y += 12 if small else 15
|
||||
var f := 16 if small else 20
|
||||
var atk_col := Style.GREEN if creature.resonance > 0 else Style.YELLOW
|
||||
Style.text(self, str(creature.power()), f, atk_col, Vector2(6, size.y - 4), "bottomleft", true)
|
||||
var hp_col := Style.RED if creature.health < creature.max_health else Color("ffc8c8")
|
||||
Style.text(self, str(creature.health), f, hp_col, Vector2(size.x - 6, size.y - 4), "bottomright", true)
|
||||
if creature.frozen:
|
||||
draw_rect(rect, Color(0.55, 0.78, 1.0, 0.35))
|
||||
|
||||
|
||||
## Sehr kleine Kacheln (4 oder 5 Gegner): nur Kürzel und Werte, Details per Vorschau oder Lupe.
|
||||
func _draw_mini() -> void:
|
||||
var letters: Array[String] = []
|
||||
if creature.taunt():
|
||||
letters.append("W")
|
||||
if creature.shield:
|
||||
letters.append("S")
|
||||
if creature.attacks_per_turn() == 2:
|
||||
letters.append("2")
|
||||
if creature.frozen:
|
||||
letters.append("E")
|
||||
if creature.resonance > 0:
|
||||
letters.append("R")
|
||||
Style.text(self, " ".join(letters), 10, Color("ebebf5"), Vector2(size.x / 2, 3), "midtop", true)
|
||||
var atk_col := Style.GREEN if creature.resonance > 0 else Style.YELLOW
|
||||
Style.text(self, str(creature.power()), 13, atk_col, Vector2(4, size.y - 2), "bottomleft", true)
|
||||
var hp_col := Style.RED if creature.health < creature.max_health else Color("ffc8c8")
|
||||
Style.text(self, str(creature.health), 13, hp_col, Vector2(size.x - 4, size.y - 2), "bottomright", true)
|
||||
if creature.frozen:
|
||||
draw_rect(Rect2(Vector2.ZERO, size), Color(0.55, 0.78, 1.0, 0.35))
|
||||
1
scripts/ui/creature_view.gd.uid
Normal file
1
scripts/ui/creature_view.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bcnj0sdt2jbjb
|
||||
351
scripts/ui/deck_screen.gd
Normal file
351
scripts/ui/deck_screen.gd
Normal file
@@ -0,0 +1,351 @@
|
||||
class_name DeckScreen
|
||||
extends Control
|
||||
## Deckbau mit fünf Slots, Suche, Sortierung und fehlenden Karten.
|
||||
|
||||
const SORTS := ["Kosten", "Name", "Element", "Seltenheit"]
|
||||
|
||||
var app
|
||||
var slot := 0
|
||||
var deck: Array = []
|
||||
var filter := ""
|
||||
var search := ""
|
||||
var sort_mode := 0
|
||||
var show_missing := false
|
||||
var msg_label: Label
|
||||
var grid: CardGrid
|
||||
var deck_list: Control
|
||||
var deck_label: Label
|
||||
var elems_label: Label
|
||||
var valid_label: Label
|
||||
var active_label: Label
|
||||
var curve: Control
|
||||
var preview: CardView
|
||||
var search_box: LineEdit
|
||||
var sort_box: OptionButton
|
||||
var missing_box: CheckButton
|
||||
var slot_buttons: Array = []
|
||||
var b_activate: Button
|
||||
var b_autofill: Button
|
||||
var name_box: LineEdit
|
||||
|
||||
|
||||
class DeckRow extends Control:
|
||||
signal clicked(card)
|
||||
signal hover(card, entered: bool)
|
||||
var card: Cards.Card
|
||||
var count: int
|
||||
var lit := false
|
||||
|
||||
func _init(c: Cards.Card, n: int, sz: Vector2) -> void:
|
||||
card = c
|
||||
count = n
|
||||
size = sz
|
||||
custom_minimum_size = sz
|
||||
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
mouse_entered.connect(func():
|
||||
lit = true
|
||||
queue_redraw()
|
||||
hover.emit(card, true))
|
||||
mouse_exited.connect(func():
|
||||
lit = false
|
||||
queue_redraw()
|
||||
hover.emit(card, false))
|
||||
|
||||
func _gui_input(event: InputEvent) -> void:
|
||||
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||
accept_event()
|
||||
clicked.emit(card)
|
||||
|
||||
func _draw() -> void:
|
||||
if lit:
|
||||
draw_style_box(Style.box(Style.PANEL_LIGHT, 4), Rect2(Vector2.ZERO, size))
|
||||
for k in card.elements.size():
|
||||
draw_circle(Vector2(10 + k * 12, size.y / 2), 7, Cards.ELEMENT_COLORS[card.elements[k]])
|
||||
Style.text(self, "%dx %s" % [count, card.name], 15, Style.TEXT, Vector2(40, size.y / 2), "midleft")
|
||||
Style.text(self, str(card.cost), 15, Style.GOLD, Vector2(size.x - 10, size.y / 2), "midright", true)
|
||||
|
||||
|
||||
class EnergyCurve extends Control:
|
||||
var cards: Array = []
|
||||
|
||||
func _draw() -> void:
|
||||
Style.text(self, "Energiekurve", 12, Style.MUTED, Vector2(0, 0))
|
||||
var counts := {}
|
||||
for c in cards:
|
||||
var k: int = mini(c.cost, 8)
|
||||
counts[k] = counts.get(k, 0) + 1
|
||||
for i in range(1, 9):
|
||||
var n: int = counts.get(i, 0)
|
||||
var x := (i - 1) * 48
|
||||
draw_rect(Rect2(x, 104 - n * 5, 36, n * 5), Style.BLUE)
|
||||
if n > 0:
|
||||
Style.text(self, str(n), 11, Style.TEXT, Vector2(x + 18, 102 - n * 5), "midbottom")
|
||||
Style.text(self, str(i) + ("+" if i == 8 else ""), 12, Style.MUTED, Vector2(x + 18, 108), "midtop")
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
slot = app.profile.active_deck
|
||||
deck = app.profile.decks[slot].cards.duplicate()
|
||||
Style.label(self, "Deck bauen", 30, Style.GOLD, Rect2(20, 8, 400, 40), "left", true)
|
||||
Style.label(self, "Klick: Karte ins Deck. Klick in der Liste: Karte entfernen. Mausrad: scrollen.",
|
||||
14, Style.MUTED, Rect2(300, 18, 700, 24))
|
||||
Style.button(self, "Alle", Rect2(20, 55, 60, 34), set_filter.bind(""))
|
||||
for i in Cards.ELEMENTS.size():
|
||||
var e: String = Cards.ELEMENTS[i]
|
||||
Style.button(self, e, Rect2(88 + i * 76, 55, 70, 34), set_filter.bind(e), Style.darker(Cards.ELEMENT_COLORS[e], 0.8))
|
||||
search_box = LineEdit.new()
|
||||
search_box.position = Vector2(400, 55)
|
||||
search_box.size = Vector2(160, 34)
|
||||
search_box.placeholder_text = "Suche"
|
||||
search_box.text_changed.connect(func(t: String):
|
||||
search = t
|
||||
refresh())
|
||||
add_child(search_box)
|
||||
sort_box = OptionButton.new()
|
||||
sort_box.position = Vector2(570, 55)
|
||||
sort_box.size = Vector2(130, 34)
|
||||
for s in SORTS:
|
||||
sort_box.add_item(s)
|
||||
sort_box.item_selected.connect(func(i: int):
|
||||
sort_mode = i
|
||||
refresh())
|
||||
add_child(sort_box)
|
||||
missing_box = CheckButton.new()
|
||||
missing_box.position = Vector2(710, 58)
|
||||
missing_box.text = "Fehlende"
|
||||
missing_box.toggled.connect(func(on: bool):
|
||||
show_missing = on
|
||||
refresh())
|
||||
add_child(missing_box)
|
||||
grid = CardGrid.new(Rect2(20, 100, CardGrid.COLS * (CardGrid.CW + CardGrid.GAP) + 10, 690))
|
||||
grid.card_clicked.connect(add)
|
||||
grid.card_hover.connect(show_preview)
|
||||
add_child(grid)
|
||||
# Rechte Seite: Slots, Deckliste, Kurve
|
||||
Style.panel(self, Rect2(845, 50, 420, 680))
|
||||
for i in Profile.DECK_SLOTS:
|
||||
slot_buttons.append(Style.button(self, str(i + 1), Rect2(860 + i * 46, 58, 40, 32), select_slot.bind(i)))
|
||||
b_activate = Style.button(self, "Aktiv setzen", Rect2(1100, 58, 150, 32), activate)
|
||||
name_box = LineEdit.new()
|
||||
name_box.position = Vector2(860, 98)
|
||||
name_box.size = Vector2(200, 30)
|
||||
name_box.max_length = 20
|
||||
name_box.text_submitted.connect(func(t: String): rename(t))
|
||||
name_box.focus_exited.connect(func(): rename(name_box.text))
|
||||
add_child(name_box)
|
||||
deck_label = Style.label(self, "", 22, Style.TEXT, Rect2(1070, 98, 120, 30), "left", true)
|
||||
active_label = Style.label(self, "", 14, Style.GREEN, Rect2(1180, 104, 75, 24), "right")
|
||||
b_autofill = Style.button(self, "Auto-Ausfüllen", Rect2(1100, 154, 150, 26), autofill)
|
||||
b_autofill.add_theme_font_size_override("font_size", 13)
|
||||
elems_label = Style.label(self, "", 16, Style.MUTED, Rect2(860, 130, 390, 24))
|
||||
valid_label = Style.label(self, "", 14, Style.GREEN, Rect2(860, 156, 235, 22))
|
||||
deck_list = Control.new()
|
||||
deck_list.position = Vector2(860, 182)
|
||||
deck_list.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(deck_list)
|
||||
curve = EnergyCurve.new()
|
||||
curve.position = Vector2(860, 604)
|
||||
curve.size = Vector2(400, 125)
|
||||
curve.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(curve)
|
||||
msg_label = Style.label(self, "", 16, Style.GREEN, Rect2(860, 736, 400, 24))
|
||||
Style.button(self, "Speichern", Rect2(860, 740, 180, 44), save_deck, Color("285a3c"))
|
||||
Style.button(self, "Leeren", Rect2(1050, 740, 90, 44), clear_deck)
|
||||
Style.button(self, "Zurück", Rect2(1150, 740, 110, 44), func(): app.show_screen(MenuScreen.new()))
|
||||
preview = CardView.new(Cards.all()[0], Vector2(200, 280))
|
||||
preview.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
preview.visible = false
|
||||
add_child(preview)
|
||||
refresh()
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE and not search_box.has_focus() and not name_box.has_focus():
|
||||
app.show_screen(MenuScreen.new())
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if preview.visible:
|
||||
var pos := get_local_mouse_position() + Vector2(16, 16)
|
||||
pos.x = clampf(pos.x, 0, 1280 - preview.size.x)
|
||||
pos.y = clampf(pos.y, 0, 800 - preview.size.y)
|
||||
preview.position = pos
|
||||
|
||||
|
||||
func set_filter(e: String) -> void:
|
||||
filter = e
|
||||
refresh()
|
||||
|
||||
|
||||
func select_slot(i: int) -> void:
|
||||
slot = i
|
||||
deck = app.profile.decks[slot].cards.duplicate()
|
||||
say("", true)
|
||||
refresh()
|
||||
|
||||
|
||||
func rename(t: String) -> void:
|
||||
if t.strip_edges() == app.profile.deck_name(slot):
|
||||
return
|
||||
var err: String = app.profile.rename_deck(slot, t)
|
||||
if err == "":
|
||||
app.save()
|
||||
say("Umbenannt in %s" % app.profile.deck_name(slot), true)
|
||||
else:
|
||||
say(err, false)
|
||||
refresh()
|
||||
|
||||
|
||||
func autofill() -> void:
|
||||
app.profile.decks[slot].cards = deck.duplicate()
|
||||
var err: String = app.profile.autofill_deck(slot)
|
||||
deck = app.profile.decks[slot].cards.duplicate()
|
||||
if err == "":
|
||||
app.save()
|
||||
say("Aufgefüllt und gespeichert", true)
|
||||
else:
|
||||
say(err, false)
|
||||
refresh()
|
||||
|
||||
|
||||
func activate() -> void:
|
||||
var ok: Array = Game.validate_deck(deck_cards(), app.profile.collection)
|
||||
if not ok[0]:
|
||||
say("Erst ein spielbares Deck speichern", false)
|
||||
return
|
||||
app.profile.decks[slot].cards = deck.duplicate()
|
||||
app.profile.active_deck = slot
|
||||
app.save()
|
||||
say("%s ist jetzt das aktive Deck" % app.profile.deck_name(slot), true)
|
||||
refresh()
|
||||
|
||||
|
||||
## Karten der Sammlung (oder alle, wenn Fehlende gezeigt werden), gefiltert und sortiert.
|
||||
func cards() -> Array:
|
||||
var out: Array = Cards.all().duplicate() if show_missing else app.profile.owned_cards()
|
||||
if filter != "":
|
||||
out = out.filter(func(c): return filter in c.elements)
|
||||
if search != "":
|
||||
var q := search.to_lower()
|
||||
out = out.filter(func(c): return q in c.name.to_lower() or q in c.text().to_lower() or q in c.combo().to_lower())
|
||||
match sort_mode:
|
||||
0:
|
||||
out.sort_custom(func(a, b): return a.cost < b.cost or (a.cost == b.cost and a.name < b.name))
|
||||
1:
|
||||
out.sort_custom(func(a, b): return a.name < b.name)
|
||||
3:
|
||||
out.sort_custom(func(a, b):
|
||||
var ra := Cards.RARITIES.find(a.rarity)
|
||||
var rb := Cards.RARITIES.find(b.rarity)
|
||||
return ra > rb or (ra == rb and a.cost < b.cost))
|
||||
return out
|
||||
|
||||
|
||||
func say(m: String, ok: bool) -> void:
|
||||
msg_label.text = m
|
||||
msg_label.add_theme_color_override("font_color", Style.GREEN if ok else Style.RED)
|
||||
|
||||
|
||||
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)
|
||||
elif deck.size() >= Game.DECK_SIZE:
|
||||
say("Das Deck ist voll (%d)" % Game.DECK_SIZE, false)
|
||||
elif have >= mini(owned, Game.MAX_COPIES):
|
||||
say("Nicht mehr Kopien möglich (max. %d)" % mini(owned, Game.MAX_COPIES), false)
|
||||
else:
|
||||
var elems := Game.deck_elements(deck_cards())
|
||||
for e in card.elements:
|
||||
if not (e in elems):
|
||||
elems.append(e)
|
||||
if elems.size() > Game.MAX_DECK_ELEMENTS:
|
||||
say("Höchstens %d Elemente im Deck" % Game.MAX_DECK_ELEMENTS, false)
|
||||
else:
|
||||
deck.append(card.id)
|
||||
app.sfx.play("click")
|
||||
say("", true)
|
||||
refresh()
|
||||
|
||||
|
||||
func remove(card: Cards.Card) -> void:
|
||||
deck.erase(card.id)
|
||||
say("", true)
|
||||
refresh()
|
||||
|
||||
|
||||
func clear_deck() -> void:
|
||||
deck = []
|
||||
refresh()
|
||||
|
||||
|
||||
func save_deck() -> void:
|
||||
var ok: Array = Game.validate_deck(deck_cards(), app.profile.collection)
|
||||
if ok[0]:
|
||||
app.profile.decks[slot].cards = deck.duplicate()
|
||||
app.save()
|
||||
say("%s gespeichert" % app.profile.deck_name(slot), true)
|
||||
else:
|
||||
say(ok[1], false)
|
||||
refresh()
|
||||
|
||||
|
||||
func deck_cards() -> Array:
|
||||
var out := []
|
||||
for cid in deck:
|
||||
out.append(Cards.by_id(cid))
|
||||
return out
|
||||
|
||||
|
||||
func show_preview(card: Cards.Card, entered: bool) -> void:
|
||||
if entered:
|
||||
preview.card = card
|
||||
preview.visible = true
|
||||
preview.queue_redraw()
|
||||
elif preview.card == card:
|
||||
preview.visible = false
|
||||
|
||||
|
||||
func refresh() -> void:
|
||||
var p: Profile = app.profile
|
||||
grid.fill(cards(),
|
||||
func(c):
|
||||
var owned: int = p.collection.get(c.id, 0)
|
||||
return "fehlt" if owned == 0 else "%d/%d im Deck" % [deck.count(c.id), owned],
|
||||
func(c):
|
||||
var owned: int = p.collection.get(c.id, 0)
|
||||
return owned == 0 or deck.count(c.id) >= mini(owned, Game.MAX_COPIES))
|
||||
for i in slot_buttons.size():
|
||||
var b: Button = slot_buttons[i]
|
||||
var col: Color = Style.GOLD if i == slot else (Style.GREEN if p.deck_valid(i)[0] else Style.MUTED)
|
||||
b.add_theme_stylebox_override("normal", Style.box(Style.PANEL_LIGHT if i != slot else Style.darker(Style.GOLD, 0.45), 6, col, 2))
|
||||
b.add_theme_stylebox_override("hover", Style.box(Style.lighter(Style.PANEL_LIGHT), 6, col, 2))
|
||||
active_label.text = "aktiv" if slot == p.active_deck else ""
|
||||
# Deckliste
|
||||
Style.clear(deck_list)
|
||||
var counts := {}
|
||||
for cid in deck:
|
||||
counts[cid] = counts.get(cid, 0) + 1
|
||||
var listed: Array = counts.keys().map(func(cid): return Cards.by_id(cid))
|
||||
listed.sort_custom(func(a, b): return a.cost < b.cost or (a.cost == b.cost and a.name < b.name))
|
||||
for i in listed.size():
|
||||
var c: Cards.Card = listed[i]
|
||||
var row := DeckRow.new(c, counts[c.id], Vector2(400, 20))
|
||||
row.position = Vector2(0, i * 21)
|
||||
row.clicked.connect(remove)
|
||||
row.hover.connect(show_preview)
|
||||
deck_list.add_child(row)
|
||||
var dc := deck_cards()
|
||||
if not name_box.has_focus():
|
||||
name_box.text = p.deck_name(slot)
|
||||
deck_label.text = "%d/%d" % [deck.size(), Game.DECK_SIZE]
|
||||
deck_label.add_theme_color_override("font_color", Style.GREEN if deck.size() == Game.DECK_SIZE else Style.TEXT)
|
||||
var elems := Game.deck_elements(dc)
|
||||
elems.sort_custom(func(a, b): return Cards.ELEMENTS.find(a) < Cards.ELEMENTS.find(b))
|
||||
elems_label.text = "Elemente: " + (", ".join(elems) if not elems.is_empty() else "–")
|
||||
var ok: Array = Game.validate_deck(dc, p.collection)
|
||||
valid_label.text = "spielbar" if ok[0] else ok[1]
|
||||
valid_label.add_theme_color_override("font_color", Style.GREEN if ok[0] else Style.ORANGE)
|
||||
curve.cards = dc
|
||||
curve.queue_redraw()
|
||||
1
scripts/ui/deck_screen.gd.uid
Normal file
1
scripts/ui/deck_screen.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://yg8pbj56mib5
|
||||
111
scripts/ui/hotseat_screen.gd
Normal file
111
scripts/ui/hotseat_screen.gd
Normal file
@@ -0,0 +1,111 @@
|
||||
class_name HotseatScreen
|
||||
extends Control
|
||||
## Lokal zu mehreren am selben Rechner: Spielerzahl, KI-Zahl und Decks wählen.
|
||||
|
||||
var app
|
||||
var n_humans := 2
|
||||
var n_ai := 0
|
||||
var deck_choice: Array = [] # je Mensch: Slot-Index oder -1 für Zufallsdeck
|
||||
var human_buttons: Array = []
|
||||
var ai_buttons: Array = []
|
||||
var deck_rows: VBoxContainer
|
||||
var b_start: Button
|
||||
var hint: Label
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
var col := Style.column(self, 760, 40, 10)
|
||||
Style.clabel(col, "Lokal zu mehreren", 36, Style.GOLD, "center", true)
|
||||
Style.clabel(col, "Alle spielen am selben Rechner und reichen die Maus weiter. Jeder gegen jeden, ohne Münzen.", 16, Style.MUTED)
|
||||
Style.spacer(col, 10)
|
||||
Style.clabel(col, "Spieler", 20, Style.TEXT, "center", true)
|
||||
var hr := Style.row(col, 10)
|
||||
hr.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
for n in range(2, 5):
|
||||
human_buttons.append(Style.cbutton(hr, "%d Spieler" % n, Vector2(150, 44), func(): set_humans(n)))
|
||||
Style.clabel(col, "KI-Gegner", 20, Style.TEXT, "center", true)
|
||||
var ar := Style.row(col, 10)
|
||||
ar.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
for n in range(0, 5):
|
||||
ai_buttons.append(Style.cbutton(ar, "%d KI" % n if n > 0 else "keine KI", Vector2(120, 44), func(): set_ai(n)))
|
||||
Style.spacer(col, 6)
|
||||
Style.clabel(col, "Decks", 20, Style.TEXT, "center", true)
|
||||
deck_rows = VBoxContainer.new()
|
||||
deck_rows.add_theme_constant_override("separation", 6)
|
||||
col.add_child(deck_rows)
|
||||
Style.spacer(col, 6)
|
||||
hint = Style.clabel(col, "", 15, Style.ORANGE)
|
||||
var br := Style.row(col, 20)
|
||||
br.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
b_start = Style.cbutton(br, "Start", Vector2(200, 48), start, Color("285a3c"))
|
||||
Style.cbutton(br, "Zurück", Vector2(160, 48), func(): app.show_screen(MenuScreen.new()))
|
||||
for i in 4:
|
||||
deck_choice.append(-1)
|
||||
var p: Profile = app.profile
|
||||
if p.deck_valid()[0]:
|
||||
deck_choice[0] = p.active_deck
|
||||
refresh()
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
|
||||
app.show_screen(MenuScreen.new())
|
||||
|
||||
|
||||
func set_humans(n: int) -> void:
|
||||
n_humans = n
|
||||
n_ai = mini(n_ai, Game.MAX_OPPONENTS + 1 - n_humans)
|
||||
refresh()
|
||||
|
||||
|
||||
func set_ai(n: int) -> void:
|
||||
n_ai = n
|
||||
refresh()
|
||||
|
||||
|
||||
func valid_slots() -> Array:
|
||||
var out := []
|
||||
for i in Profile.DECK_SLOTS:
|
||||
if app.profile.deck_valid(i)[0]:
|
||||
out.append(i)
|
||||
return out
|
||||
|
||||
|
||||
func refresh() -> void:
|
||||
var p: Profile = app.profile
|
||||
for i in human_buttons.size():
|
||||
human_buttons[i].disabled = (i + 2) == n_humans
|
||||
for i in ai_buttons.size():
|
||||
ai_buttons[i].disabled = i == n_ai or n_humans + i > Game.MAX_OPPONENTS + 1
|
||||
Style.clear(deck_rows)
|
||||
var slots := valid_slots()
|
||||
for h in n_humans:
|
||||
var r := Style.row(deck_rows, 12)
|
||||
r.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
Style.clabel(r, "Spieler %d" % (h + 1), 16, Style.TEXT, "right", true, 140)
|
||||
var ob := OptionButton.new()
|
||||
ob.custom_minimum_size = Vector2(340, 36)
|
||||
ob.add_item("Zufallsdeck", -1)
|
||||
for slot in slots:
|
||||
var elems := Game.deck_elements(p.deck_cards(slot))
|
||||
ob.add_item("%s (%s)" % [p.deck_name(slot), ", ".join(elems)], slot)
|
||||
ob.select(ob.get_item_index(deck_choice[h]) if deck_choice[h] >= 0 and slots.has(deck_choice[h]) else 0)
|
||||
var idx := h
|
||||
ob.item_selected.connect(func(i: int): deck_choice[idx] = ob.get_item_id(i))
|
||||
r.add_child(ob)
|
||||
hint.text = "%d am Tisch" % (n_humans + n_ai)
|
||||
|
||||
|
||||
func start() -> void:
|
||||
var p: Profile = app.profile
|
||||
var r := Game.make_rng()
|
||||
var decks := []
|
||||
for h in n_humans:
|
||||
var slot: int = deck_choice[h]
|
||||
if slot >= 0 and p.deck_valid(slot)[0]:
|
||||
decks.append(p.deck_cards(slot))
|
||||
else:
|
||||
var pool: Array = Cards.ELEMENTS.duplicate()
|
||||
Game.shuffle(pool, r)
|
||||
decks.append(Game.build_opponent_deck(r, [pool[0], pool[1]]))
|
||||
app.show_screen(BattleScreen.hotseat(decks, n_ai))
|
||||
1
scripts/ui/hotseat_screen.gd.uid
Normal file
1
scripts/ui/hotseat_screen.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://crlne4y0uiygl
|
||||
112
scripts/ui/lobby_screen.gd
Normal file
112
scripts/ui/lobby_screen.gd
Normal file
@@ -0,0 +1,112 @@
|
||||
class_name LobbyScreen
|
||||
extends Control
|
||||
## Netzwerkspiel im lokalen Netz: Spiel eröffnen oder beitreten.
|
||||
|
||||
var app
|
||||
var net: Net
|
||||
var status_label: Label
|
||||
var list_label: Label
|
||||
var ip_box: LineEdit
|
||||
var b_host: Button
|
||||
var b_join: Button
|
||||
var b_start: Button
|
||||
var ai_buttons: Array = []
|
||||
var n_ai := 0
|
||||
var addr_label: Label
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
net = app.net
|
||||
net.my_name = "Spieler"
|
||||
net.my_deck = app.profile.deck.duplicate()
|
||||
var col := Style.column(self, 760, 40, 10)
|
||||
Style.clabel(col, "Netzwerkspiel", 36, Style.GOLD, "center", true)
|
||||
Style.clabel(col, "Im selben Netz: einer eröffnet, die anderen treten mit seiner Adresse bei. Jeder gegen jeden, ohne Münzen.", 16, Style.MUTED)
|
||||
Style.spacer(col, 10)
|
||||
var hr := Style.row(col, 12)
|
||||
hr.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
b_host = Style.cbutton(hr, "Spiel eröffnen", Vector2(200, 46), _host, Color("285a3c"))
|
||||
ip_box = LineEdit.new()
|
||||
ip_box.custom_minimum_size = Vector2(220, 46)
|
||||
ip_box.placeholder_text = "Adresse des Hosts"
|
||||
ip_box.text = "127.0.0.1"
|
||||
hr.add_child(ip_box)
|
||||
b_join = Style.cbutton(hr, "Beitreten", Vector2(160, 46), _join)
|
||||
addr_label = Style.clabel(col, "", 14, Style.MUTED)
|
||||
status_label = Style.clabel(col, "", 16, Style.BLUE)
|
||||
Style.spacer(col, 6)
|
||||
Style.clabel(col, "Am Tisch", 20, Style.TEXT, "center", true)
|
||||
list_label = Style.clabel(col, "", 16, Style.TEXT)
|
||||
Style.spacer(col, 6)
|
||||
Style.clabel(col, "KI-Gegner (nur Host)", 16, Style.MUTED)
|
||||
var ar := Style.row(col, 10)
|
||||
ar.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
for n in range(0, 5):
|
||||
ai_buttons.append(Style.cbutton(ar, "%d KI" % n if n > 0 else "keine KI", Vector2(120, 40), func(): _set_ai(n)))
|
||||
Style.spacer(col, 10)
|
||||
var br := Style.row(col, 20)
|
||||
br.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
b_start = Style.cbutton(br, "Start", Vector2(200, 48), _start, Color("285a3c"))
|
||||
Style.cbutton(br, "Zurück", Vector2(160, 48), _back)
|
||||
net.lobby_changed.connect(refresh)
|
||||
net.started.connect(_started)
|
||||
net.ended.connect(func(_reason): refresh())
|
||||
refresh()
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
if net.lobby_changed.is_connected(refresh):
|
||||
net.lobby_changed.disconnect(refresh)
|
||||
if net.started.is_connected(_started):
|
||||
net.started.disconnect(_started)
|
||||
|
||||
|
||||
func _host() -> void:
|
||||
net.host()
|
||||
refresh()
|
||||
|
||||
|
||||
func _join() -> void:
|
||||
net.join(ip_box.text.strip_edges())
|
||||
refresh()
|
||||
|
||||
|
||||
func _set_ai(n: int) -> void:
|
||||
n_ai = n
|
||||
refresh()
|
||||
|
||||
|
||||
func _start() -> void:
|
||||
net.start_battle(n_ai)
|
||||
|
||||
|
||||
func _started(seat: int) -> void:
|
||||
app.show_screen(BattleScreen.online(seat))
|
||||
|
||||
|
||||
func _back() -> void:
|
||||
net.leave()
|
||||
app.show_screen(MenuScreen.new())
|
||||
|
||||
|
||||
func refresh() -> void:
|
||||
status_label.text = net.status
|
||||
var lines: Array[String] = []
|
||||
var ids: Array = net.names.keys()
|
||||
ids.sort()
|
||||
for id in ids:
|
||||
lines.append("%s%s" % [net.names[id], " (Host)" if id == 1 else ""])
|
||||
list_label.text = "\n".join(lines) if not lines.is_empty() else "–"
|
||||
b_host.disabled = net.connected
|
||||
b_join.disabled = net.connected
|
||||
ip_box.editable = not net.connected
|
||||
var humans := ids.size()
|
||||
b_start.disabled = not (net.is_host and humans >= 2 and humans + n_ai <= Game.MAX_OPPONENTS + 1)
|
||||
for i in ai_buttons.size():
|
||||
ai_buttons[i].disabled = not net.is_host or i == n_ai or humans + i > Game.MAX_OPPONENTS + 1
|
||||
addr_label.text = ("Deine Adressen: " + ", ".join(net.local_addresses())) if net.is_host else ""
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE and not ip_box.has_focus():
|
||||
_back()
|
||||
1
scripts/ui/lobby_screen.gd.uid
Normal file
1
scripts/ui/lobby_screen.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bhnnxjh6rsxnf
|
||||
52
scripts/ui/main.gd
Normal file
52
scripts/ui/main.gd
Normal file
@@ -0,0 +1,52 @@
|
||||
extends Control
|
||||
## Einstieg: hält den Spielstand und wechselt zwischen den Bildschirmen.
|
||||
|
||||
var profile: Profile
|
||||
var screen: Control
|
||||
var save_path: String = Profile.SAVE_PATH
|
||||
var sfx: Sfx
|
||||
var net: Net
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
theme = Style.make_theme()
|
||||
var bg := Style.Backdrop.new()
|
||||
bg.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
bg.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(bg)
|
||||
sfx = Sfx.new()
|
||||
add_child(sfx)
|
||||
# Fester Pfad /root/Net: Netzwerkaufrufe werden über den Knotenpfad zugestellt,
|
||||
# der bei Host und Mitspielern gleich sein muss.
|
||||
net = Net.new()
|
||||
net.name = "Net"
|
||||
get_tree().root.add_child.call_deferred(net)
|
||||
profile = Profile.load_or_new(save_path)
|
||||
profile.refresh_daily()
|
||||
sfx.set_volume(profile.settings.volume)
|
||||
sfx.set_music_volume(profile.settings.music)
|
||||
set_language(profile.settings.language)
|
||||
show_screen(MenuScreen.new())
|
||||
|
||||
|
||||
func set_language(code: String) -> void:
|
||||
TranslationServer.set_locale(code)
|
||||
|
||||
|
||||
func show_screen(s: Control) -> void:
|
||||
if screen != null:
|
||||
screen.queue_free()
|
||||
screen = s
|
||||
s.app = self
|
||||
s.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
add_child(s)
|
||||
|
||||
|
||||
func save() -> void:
|
||||
profile.save(save_path)
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
if net != null and net.is_inside_tree():
|
||||
net.leave()
|
||||
net.queue_free()
|
||||
1
scripts/ui/main.gd.uid
Normal file
1
scripts/ui/main.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bkltqeiif4e0j
|
||||
84
scripts/ui/menu_screen.gd
Normal file
84
scripts/ui/menu_screen.gd
Normal file
@@ -0,0 +1,84 @@
|
||||
class_name MenuScreen
|
||||
extends Control
|
||||
|
||||
var app
|
||||
var b_single: Button
|
||||
var b_campaign: Button
|
||||
var b_hotseat: Button
|
||||
var b_online: Button
|
||||
var b_tutorial: Button
|
||||
var coins_label: Label
|
||||
var coll_label: Label
|
||||
var deck_label: Label
|
||||
var daily_label: Label
|
||||
var warn_label: Label
|
||||
var notice_label: Label
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
var col := Style.column(self, 640, 24, 5)
|
||||
Style.clabel(col, "Dinger", 64, Style.GOLD, "center", true)
|
||||
Style.clabel(col, "Ein Kartenspiel: jeder gegen jeden", 22, Style.MUTED)
|
||||
Style.spacer(col, 8)
|
||||
coins_label = Style.clabel(col, "", 20, Style.GOLD)
|
||||
coll_label = Style.clabel(col, "", 16, Style.MUTED)
|
||||
deck_label = Style.clabel(col, "", 16, Style.MUTED)
|
||||
daily_label = Style.clabel(col, "", 16, Style.BLUE)
|
||||
Style.spacer(col, 8)
|
||||
var grid := GridContainer.new()
|
||||
grid.columns = 2
|
||||
grid.add_theme_constant_override("h_separation", 12)
|
||||
grid.add_theme_constant_override("v_separation", 8)
|
||||
var center := CenterContainer.new()
|
||||
center.add_child(grid)
|
||||
col.add_child(center)
|
||||
var sz := Vector2(270, 44)
|
||||
var p0: Profile = app.profile
|
||||
b_tutorial = Style.cbutton(grid, "Anleitung", sz, func(): app.show_screen(BattleScreen.guided()),
|
||||
Color("5a4628") if not p0.tutorial_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_hotseat = Style.cbutton(grid, "Lokal zu mehreren", sz, func(): app.show_screen(HotseatScreen.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, "Erfolge", sz, func(): app.show_screen(AchievementsScreen.new()))
|
||||
Style.cbutton(grid, "Regeln", sz, func(): app.show_screen(RulesScreen.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)
|
||||
warn_label = Style.clabel(col, "", 16, Style.RED)
|
||||
notice_label = Style.clabel(col, "", 16, Style.GREEN)
|
||||
notice_label.autowrap_mode = TextServer.AUTOWRAP_WORD
|
||||
refresh()
|
||||
|
||||
|
||||
func refresh() -> void:
|
||||
var p: Profile = app.profile
|
||||
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:
|
||||
total += p.collection[cid]
|
||||
coins_label.text = "Münzen: %d · Staub: %d" % [p.coins, p.dust]
|
||||
coll_label.text = "Sammlung: %d von %d Karten (%d insgesamt) · Kampagne: %d/%d" % [
|
||||
p.distinct_owned(), Cards.all().size(), total, p.campaign_stage, Campaign.STAGES.size()]
|
||||
var elems := Game.deck_elements(p.deck_cards())
|
||||
deck_label.text = "Aktives Deck: %s (%s)" % [p.deck_name(), ", ".join(elems) if not elems.is_empty() else "leer"]
|
||||
var task := p.daily_task()
|
||||
if p.daily.done:
|
||||
daily_label.text = "Tagesaufgabe erledigt: %s ✓" % task.text
|
||||
else:
|
||||
daily_label.text = "Tagesaufgabe: %s (%d/%d) · +%d Münzen" % [task.text, p.daily.progress, task.goal, task.reward]
|
||||
warn_label.text = "" if ok[0] else "Aktives Deck nicht spielbar: " + 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)
|
||||
var notes := p.take_notices()
|
||||
notice_label.text = " · ".join(notes)
|
||||
if not notes.is_empty():
|
||||
app.save()
|
||||
1
scripts/ui/menu_screen.gd.uid
Normal file
1
scripts/ui/menu_screen.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://3ib6kcyl21f
|
||||
29
scripts/ui/opponent_select_screen.gd
Normal file
29
scripts/ui/opponent_select_screen.gd
Normal file
@@ -0,0 +1,29 @@
|
||||
class_name OpponentSelectScreen
|
||||
extends Control
|
||||
## Einzelspieler: Auswahl, gegen wie viele Gegner gekämpft wird.
|
||||
|
||||
var app
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
var col := Style.column(self, 900, 50, 14)
|
||||
Style.clabel(col, "Einzelspieler", 36, Style.GOLD, "center", true)
|
||||
Style.clabel(col, "Wie viele Gegner? Jeder kämpft gegen jeden, alle nach denselben Regeln mit %d Leben." % Game.START_LIFE, 18, Style.MUTED)
|
||||
Style.clabel(col, "Du gewinnst, wenn alle anderen besiegt sind. Mehr Gegner, mehr Chaos, mehr Münzen.", 16, Style.MUTED)
|
||||
Style.spacer(col, 10)
|
||||
for i in range(1, Game.MAX_OPPONENTS + 1):
|
||||
var n := i
|
||||
var r := Style.row(col, 20)
|
||||
r.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
Style.cbutton(r, "%d Gegner" % n, Vector2(200, 56), func(): app.show_screen(BattleScreen.new(n)), Color("285a3c"))
|
||||
var info := ("Duell" if n == 1 else "%d Spieler am Tisch" % (n + 1)) + " · Sieg: %d Münzen" % Profile.REWARD_WIN[n]
|
||||
Style.clabel(r, info, 16, Style.MUTED, "left", false, 420)
|
||||
Style.spacer(col, 20)
|
||||
var br := Style.row(col)
|
||||
br.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
Style.cbutton(br, "Zurück", Vector2(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())
|
||||
1
scripts/ui/opponent_select_screen.gd.uid
Normal file
1
scripts/ui/opponent_select_screen.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cjpjji3gxmxvf
|
||||
25
scripts/ui/rules_screen.gd
Normal file
25
scripts/ui/rules_screen.gd
Normal file
@@ -0,0 +1,25 @@
|
||||
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())
|
||||
1
scripts/ui/rules_screen.gd.uid
Normal file
1
scripts/ui/rules_screen.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bkemrytc2pjem
|
||||
121
scripts/ui/settings_screen.gd
Normal file
121
scripts/ui/settings_screen.gd
Normal file
@@ -0,0 +1,121 @@
|
||||
class_name SettingsScreen
|
||||
extends Control
|
||||
|
||||
var app
|
||||
var volume: HSlider
|
||||
var music: HSlider
|
||||
var speed: HSlider
|
||||
var language: OptionButton
|
||||
var music_label: Label
|
||||
var anim: CheckButton
|
||||
var volume_label: Label
|
||||
var speed_label: Label
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
var s: Dictionary = app.profile.settings
|
||||
var col := Style.column(self, 700, 50, 24)
|
||||
Style.clabel(col, "Einstellungen", 36, Style.GOLD, "center", true)
|
||||
Style.spacer(col, 30)
|
||||
var grid := GridContainer.new()
|
||||
grid.columns = 3
|
||||
grid.add_theme_constant_override("h_separation", 24)
|
||||
grid.add_theme_constant_override("v_separation", 30)
|
||||
col.add_child(grid)
|
||||
Style.clabel(grid, "Lautstärke", 20, Style.TEXT, "left", false, 200)
|
||||
volume = HSlider.new()
|
||||
volume.custom_minimum_size = Vector2(280, 24)
|
||||
volume.min_value = 0
|
||||
volume.max_value = 1
|
||||
volume.step = 0.05
|
||||
volume.value = s.volume
|
||||
volume.value_changed.connect(_on_volume)
|
||||
grid.add_child(volume)
|
||||
volume_label = Style.clabel(grid, "", 18, Style.MUTED, "left", false, 100)
|
||||
Style.clabel(grid, "Musik", 20, Style.TEXT, "left", false, 200)
|
||||
music = HSlider.new()
|
||||
music.custom_minimum_size = Vector2(280, 24)
|
||||
music.min_value = 0
|
||||
music.max_value = 1
|
||||
music.step = 0.05
|
||||
music.value = s.music
|
||||
music.value_changed.connect(_on_music)
|
||||
grid.add_child(music)
|
||||
music_label = Style.clabel(grid, "", 18, Style.MUTED, "left", false, 100)
|
||||
Style.clabel(grid, "KI-Tempo", 20, Style.TEXT, "left", false, 200)
|
||||
speed = HSlider.new()
|
||||
speed.custom_minimum_size = Vector2(280, 24)
|
||||
speed.min_value = 0.5
|
||||
speed.max_value = 3.0
|
||||
speed.step = 0.25
|
||||
speed.value = s.ai_speed
|
||||
speed.value_changed.connect(_on_speed)
|
||||
grid.add_child(speed)
|
||||
speed_label = Style.clabel(grid, "", 18, Style.MUTED, "left", false, 100)
|
||||
Style.clabel(grid, "Animationen", 20, Style.TEXT, "left", false, 200)
|
||||
anim = CheckButton.new()
|
||||
anim.button_pressed = s.animations
|
||||
anim.toggled.connect(_on_anim)
|
||||
grid.add_child(anim)
|
||||
Style.clabel(grid, "", 18, Style.MUTED, "left", false, 100)
|
||||
Style.clabel(grid, "Sprache", 20, Style.TEXT, "left", false, 200)
|
||||
language = OptionButton.new()
|
||||
language.custom_minimum_size = Vector2(280, 36)
|
||||
language.add_item("Deutsch", 0)
|
||||
language.add_item("English (Oberfläche)", 1)
|
||||
language.select(1 if s.language == "en" else 0)
|
||||
language.item_selected.connect(_on_language)
|
||||
grid.add_child(language)
|
||||
Style.clabel(grid, "", 18, Style.MUTED, "left", false, 100)
|
||||
Style.spacer(col, 20)
|
||||
Style.clabel(col, "Der Spielstand liegt unter %s" % ProjectSettings.globalize_path(Profile.SAVE_PATH), 13, Style.MUTED)
|
||||
Style.spacer(col, 60)
|
||||
var r := Style.row(col)
|
||||
r.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
Style.cbutton(r, "Zurück", Vector2(160, 44), func(): app.show_screen(MenuScreen.new()))
|
||||
_labels()
|
||||
|
||||
|
||||
func _labels() -> void:
|
||||
volume_label.text = "%d %%" % int(round(volume.value * 100))
|
||||
music_label.text = "%d %%" % int(round(music.value * 100))
|
||||
speed_label.text = "%.2fx" % speed.value
|
||||
|
||||
|
||||
func _on_music(v: float) -> void:
|
||||
app.profile.settings.music = v
|
||||
app.sfx.set_music_volume(v)
|
||||
app.save()
|
||||
_labels()
|
||||
|
||||
|
||||
func _on_language(i: int) -> void:
|
||||
var code := "en" if i == 1 else "de"
|
||||
app.profile.settings.language = code
|
||||
app.save()
|
||||
app.set_language(code)
|
||||
app.show_screen(SettingsScreen.new())
|
||||
|
||||
|
||||
func _on_volume(v: float) -> void:
|
||||
app.profile.settings.volume = v
|
||||
app.sfx.set_volume(v)
|
||||
app.sfx.play("click")
|
||||
app.save()
|
||||
_labels()
|
||||
|
||||
|
||||
func _on_speed(v: float) -> void:
|
||||
app.profile.settings.ai_speed = v
|
||||
app.save()
|
||||
_labels()
|
||||
|
||||
|
||||
func _on_anim(on: bool) -> void:
|
||||
app.profile.settings.animations = on
|
||||
app.save()
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
|
||||
app.show_screen(MenuScreen.new())
|
||||
1
scripts/ui/settings_screen.gd.uid
Normal file
1
scripts/ui/settings_screen.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://i4f1frwa2cmm
|
||||
107
scripts/ui/sfx.gd
Normal file
107
scripts/ui/sfx.gd
Normal file
@@ -0,0 +1,107 @@
|
||||
class_name Sfx
|
||||
extends Node
|
||||
## Kurze, zur Laufzeit erzeugte Töne (keine Audiodateien nötig).
|
||||
|
||||
static var inst: Sfx
|
||||
|
||||
var streams := {}
|
||||
var players: Array[AudioStreamPlayer] = []
|
||||
var volume := 0.8
|
||||
var music_player: AudioStreamPlayer
|
||||
var music_volume := 0.5
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
inst = self
|
||||
streams["click"] = _tone(880, 0.05, "sine")
|
||||
streams["play"] = _tone(440, 0.14, "sine", 660)
|
||||
streams["attack"] = _tone(220, 0.16, "square", 90)
|
||||
streams["damage"] = _tone(140, 0.12, "square")
|
||||
streams["heal"] = _tone(523, 0.22, "sine", 784)
|
||||
streams["combo"] = _tone(660, 0.2, "sine", 1100)
|
||||
streams["win"] = _tone(400, 0.6, "sine", 1200)
|
||||
streams["lose"] = _tone(320, 0.7, "square", 90)
|
||||
for i in 4:
|
||||
var p := AudioStreamPlayer.new()
|
||||
add_child(p)
|
||||
players.append(p)
|
||||
music_player = AudioStreamPlayer.new()
|
||||
music_player.stream = _music()
|
||||
add_child(music_player)
|
||||
set_music_volume(music_volume)
|
||||
|
||||
|
||||
## Leise Hintergrundmusik: Arpeggio über vier Akkorde, als Schleife erzeugt.
|
||||
func _music() -> AudioStreamWAV:
|
||||
var rate := 22050
|
||||
var chords := [[220.0, 261.63, 329.63], [174.61, 220.0, 261.63], [130.81, 164.81, 196.0], [196.0, 246.94, 293.66]]
|
||||
var note_len := 0.25
|
||||
var pattern := [0, 1, 2, 1, 0, 2, 1, 2]
|
||||
var total := chords.size() * pattern.size() * note_len
|
||||
var n := int(total * rate)
|
||||
var data := PackedByteArray()
|
||||
data.resize(n * 2)
|
||||
for i in n:
|
||||
var t := float(i) / rate
|
||||
var step := int(t / note_len)
|
||||
var chord: Array = chords[(step / pattern.size()) % chords.size()]
|
||||
var f: float = chord[pattern[step % pattern.size()]]
|
||||
var lt := t - step * note_len
|
||||
var env := minf(1.0, lt * 30.0) * maxf(0.0, 1.0 - lt / note_len)
|
||||
var v := sin(TAU * f * t) * 0.6 + sin(TAU * f * 2.0 * t) * 0.15 + sin(TAU * chord[0] * 0.5 * t) * 0.25
|
||||
data.encode_s16(i * 2, int(clampf(v * env * 0.25, -1.0, 1.0) * 32767))
|
||||
var wav := AudioStreamWAV.new()
|
||||
wav.format = AudioStreamWAV.FORMAT_16_BITS
|
||||
wav.mix_rate = rate
|
||||
wav.stereo = false
|
||||
wav.data = data
|
||||
wav.loop_mode = AudioStreamWAV.LOOP_FORWARD
|
||||
wav.loop_begin = 0
|
||||
wav.loop_end = n
|
||||
return wav
|
||||
|
||||
|
||||
func set_music_volume(v: float) -> void:
|
||||
music_volume = clampf(v, 0.0, 1.0)
|
||||
if music_volume <= 0.0:
|
||||
music_player.stop()
|
||||
else:
|
||||
music_player.volume_db = linear_to_db(music_volume * 0.6)
|
||||
if not music_player.playing:
|
||||
music_player.play()
|
||||
|
||||
|
||||
func set_volume(v: float) -> void:
|
||||
volume = clampf(v, 0.0, 1.0)
|
||||
|
||||
|
||||
func play(kind: String) -> void:
|
||||
if volume <= 0.0 or not streams.has(kind):
|
||||
return
|
||||
for p in players:
|
||||
if not p.playing:
|
||||
p.stream = streams[kind]
|
||||
p.volume_db = linear_to_db(volume)
|
||||
p.play()
|
||||
return
|
||||
|
||||
|
||||
func _tone(freq: float, dur: float, shape: String, freq_end: float = -1.0) -> AudioStreamWAV:
|
||||
var rate := 22050
|
||||
var n := int(dur * rate)
|
||||
var data := PackedByteArray()
|
||||
data.resize(n * 2)
|
||||
for i in n:
|
||||
var t := float(i) / rate
|
||||
var f := freq if freq_end < 0 else lerpf(freq, freq_end, t / dur)
|
||||
var env := 1.0 - t / dur
|
||||
var v := sin(TAU * f * t)
|
||||
if shape == "square":
|
||||
v = 1.0 if v >= 0 else -1.0
|
||||
data.encode_s16(i * 2, int(clampf(v * env * 0.4, -1.0, 1.0) * 32767))
|
||||
var wav := AudioStreamWAV.new()
|
||||
wav.format = AudioStreamWAV.FORMAT_16_BITS
|
||||
wav.mix_rate = rate
|
||||
wav.stereo = false
|
||||
wav.data = data
|
||||
return wav
|
||||
1
scripts/ui/sfx.gd.uid
Normal file
1
scripts/ui/sfx.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bprwiyidhn7ek
|
||||
365
scripts/ui/style.gd
Normal file
365
scripts/ui/style.gd
Normal file
@@ -0,0 +1,365 @@
|
||||
class_name Style
|
||||
## Farben, Schriften, Theme und Zeichenhilfen.
|
||||
|
||||
const FONT: FontFile = preload("res://assets/fonts/DejaVuSans.ttf")
|
||||
const FONT_BOLD: FontFile = preload("res://assets/fonts/DejaVuSans-Bold.ttf")
|
||||
|
||||
const BG := Color("1a1c24")
|
||||
const PANEL := Color("282b36")
|
||||
const PANEL_LIGHT := Color("3a3e4c")
|
||||
const TEXT := Color("ececf0")
|
||||
const MUTED := Color("9698a5")
|
||||
const GOLD := Color("f5c850")
|
||||
const GREEN := Color("5ad282")
|
||||
const RED := Color("e65555")
|
||||
const BLUE := Color("6eaaf0")
|
||||
const YELLOW := Color("ffe65a")
|
||||
const ORANGE := Color("ff963c")
|
||||
|
||||
|
||||
static func darker(c: Color, f: float = 0.65) -> Color:
|
||||
return Color(c.r * f, c.g * f, c.b * f, c.a)
|
||||
|
||||
|
||||
static func lighter(c: Color, d: float = 0.1) -> Color:
|
||||
return Color(minf(1, c.r + d), minf(1, c.g + d), minf(1, c.b + d), c.a)
|
||||
|
||||
|
||||
static func box(bg: Color, radius: int = 8, border_col = null, border_w: int = 0) -> StyleBoxFlat:
|
||||
var sb := StyleBoxFlat.new()
|
||||
sb.bg_color = bg
|
||||
sb.set_corner_radius_all(radius)
|
||||
if border_col != null:
|
||||
sb.border_color = border_col
|
||||
sb.set_border_width_all(border_w)
|
||||
return sb
|
||||
|
||||
|
||||
static func make_theme() -> Theme:
|
||||
var t := Theme.new()
|
||||
t.default_font = FONT
|
||||
t.default_font_size = 16
|
||||
t.set_stylebox("normal", "Button", box(PANEL_LIGHT, 8, MUTED, 2))
|
||||
t.set_stylebox("hover", "Button", box(lighter(PANEL_LIGHT), 8, MUTED, 2))
|
||||
t.set_stylebox("pressed", "Button", box(darker(PANEL_LIGHT), 8, MUTED, 2))
|
||||
t.set_stylebox("disabled", "Button", box(darker(PANEL, 0.9), 8, PANEL, 2))
|
||||
t.set_stylebox("focus", "Button", StyleBoxEmpty.new())
|
||||
for name in ["font_color", "font_hover_color", "font_pressed_color", "font_focus_color", "font_hover_pressed_color"]:
|
||||
t.set_color(name, "Button", TEXT)
|
||||
t.set_color("font_disabled_color", "Button", MUTED)
|
||||
t.set_font_size("font_size", "Button", 18)
|
||||
t.set_color("font_color", "Label", TEXT)
|
||||
t.set_stylebox("panel", "Panel", box(PANEL, 8))
|
||||
t.set_stylebox("normal", "RichTextLabel", StyleBoxEmpty.new())
|
||||
t.set_stylebox("focus", "RichTextLabel", StyleBoxEmpty.new())
|
||||
t.set_stylebox("panel", "ScrollContainer", StyleBoxEmpty.new())
|
||||
t.set_stylebox("normal", "LineEdit", box(PANEL_LIGHT, 6, MUTED, 1))
|
||||
t.set_stylebox("focus", "LineEdit", box(PANEL_LIGHT, 6, BLUE, 2))
|
||||
t.set_color("font_color", "LineEdit", TEXT)
|
||||
t.set_color("font_placeholder_color", "LineEdit", MUTED)
|
||||
t.set_stylebox("normal", "OptionButton", box(PANEL_LIGHT, 6, MUTED, 1))
|
||||
t.set_stylebox("hover", "OptionButton", box(lighter(PANEL_LIGHT), 6, MUTED, 1))
|
||||
t.set_stylebox("pressed", "OptionButton", box(darker(PANEL_LIGHT), 6, MUTED, 1))
|
||||
t.set_stylebox("focus", "OptionButton", StyleBoxEmpty.new())
|
||||
t.set_color("font_color", "OptionButton", TEXT)
|
||||
t.set_color("font_color", "CheckButton", TEXT)
|
||||
t.set_stylebox("focus", "CheckButton", StyleBoxEmpty.new())
|
||||
return t
|
||||
|
||||
|
||||
# --- Knoten-Hilfen -------------------------------------------------------------
|
||||
|
||||
static func panel(parent: Node, rect: Rect2, bg: Color = PANEL, radius: int = 8, border_col = null, border_w: int = 0) -> Panel:
|
||||
var p := Panel.new()
|
||||
p.position = rect.position
|
||||
p.size = rect.size
|
||||
p.add_theme_stylebox_override("panel", box(bg, radius, border_col, border_w))
|
||||
p.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
parent.add_child(p)
|
||||
return p
|
||||
|
||||
|
||||
static func label(parent: Node, text: String, size: int, color: Color, rect: Rect2, align: String = "left", bold: bool = false) -> Label:
|
||||
var l := Label.new()
|
||||
l.text = text
|
||||
l.position = rect.position
|
||||
l.size = rect.size
|
||||
l.add_theme_font_size_override("font_size", size)
|
||||
l.add_theme_color_override("font_color", color)
|
||||
if bold:
|
||||
l.add_theme_font_override("font", FONT_BOLD)
|
||||
match align:
|
||||
"center":
|
||||
l.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
"right":
|
||||
l.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||
l.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
parent.add_child(l)
|
||||
return l
|
||||
|
||||
|
||||
static func button(parent: Node, text: String, rect: Rect2, on_pressed: Callable, color = null) -> Button:
|
||||
var b := Button.new()
|
||||
b.text = text
|
||||
b.position = rect.position
|
||||
b.size = rect.size
|
||||
b.pressed.connect(on_pressed)
|
||||
if color != null:
|
||||
b.add_theme_stylebox_override("normal", box(color, 8, MUTED, 2))
|
||||
b.add_theme_stylebox_override("hover", box(lighter(color), 8, MUTED, 2))
|
||||
b.add_theme_stylebox_override("pressed", box(darker(color), 8, MUTED, 2))
|
||||
parent.add_child(b)
|
||||
return b
|
||||
|
||||
|
||||
## Vertikal zentrierte Spalte (Container-Layout für Menübildschirme).
|
||||
static func column(parent: Node, width: float, top: float, sep: int = 12) -> VBoxContainer:
|
||||
var v := VBoxContainer.new()
|
||||
v.anchor_left = 0.5
|
||||
v.anchor_right = 0.5
|
||||
v.anchor_top = 0
|
||||
v.anchor_bottom = 0
|
||||
v.offset_left = -width / 2
|
||||
v.offset_right = width / 2
|
||||
v.offset_top = top
|
||||
v.add_theme_constant_override("separation", sep)
|
||||
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
parent.add_child(v)
|
||||
return v
|
||||
|
||||
|
||||
static func row(parent: Node, sep: int = 12) -> HBoxContainer:
|
||||
var h := HBoxContainer.new()
|
||||
h.add_theme_constant_override("separation", sep)
|
||||
h.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
parent.add_child(h)
|
||||
return h
|
||||
|
||||
|
||||
## Label in einem Container.
|
||||
static func clabel(parent: Node, text: String, size: int, color: Color, align: String = "center", bold: bool = false, min_w: float = 0) -> Label:
|
||||
var l := label(parent, text, size, color, Rect2(0, 0, 0, 0), align, bold)
|
||||
l.size_flags_horizontal = Control.SIZE_EXPAND_FILL if min_w == 0 else Control.SIZE_FILL
|
||||
if min_w > 0:
|
||||
l.custom_minimum_size = Vector2(min_w, 0)
|
||||
return l
|
||||
|
||||
|
||||
## Knopf in einem Container.
|
||||
static func cbutton(parent: Node, text: String, size: Vector2, on_pressed: Callable, color = null) -> Button:
|
||||
var b := button(parent, text, Rect2(Vector2.ZERO, size), on_pressed, color)
|
||||
b.custom_minimum_size = size
|
||||
return b
|
||||
|
||||
|
||||
static func spacer(parent: Node, h: float) -> Control:
|
||||
var c := Control.new()
|
||||
c.custom_minimum_size = Vector2(0, h)
|
||||
c.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
parent.add_child(c)
|
||||
return c
|
||||
|
||||
|
||||
static func clear(node: Node) -> void:
|
||||
for c in node.get_children():
|
||||
node.remove_child(c)
|
||||
c.queue_free()
|
||||
|
||||
|
||||
# --- Zeichenhilfen für _draw() ---------------------------------------------------
|
||||
|
||||
static func text(ci: CanvasItem, s: String, size: int, color: Color, pos: Vector2, anchor: String = "topleft", bold: bool = false) -> void:
|
||||
var font: Font = FONT_BOLD if bold else FONT
|
||||
var dim := font.get_string_size(s, HORIZONTAL_ALIGNMENT_LEFT, -1, size)
|
||||
var x := pos.x
|
||||
var y := pos.y
|
||||
match anchor:
|
||||
"center":
|
||||
x -= dim.x / 2
|
||||
y -= dim.y / 2
|
||||
"midtop":
|
||||
x -= dim.x / 2
|
||||
"topright":
|
||||
x -= dim.x
|
||||
"midleft":
|
||||
y -= dim.y / 2
|
||||
"midright":
|
||||
x -= dim.x
|
||||
y -= dim.y / 2
|
||||
"bottomleft":
|
||||
y -= dim.y
|
||||
"bottomright":
|
||||
x -= dim.x
|
||||
y -= dim.y
|
||||
"midbottom":
|
||||
x -= dim.x / 2
|
||||
y -= dim.y
|
||||
ci.draw_string(font, Vector2(x, y + font.get_ascent(size)), s, HORIZONTAL_ALIGNMENT_LEFT, -1, size, color)
|
||||
|
||||
|
||||
static func fit(s: String, bold: bool, size: int, max_w: float) -> String:
|
||||
var font: Font = FONT_BOLD if bold else FONT
|
||||
while font.get_string_size(s, HORIZONTAL_ALIGNMENT_LEFT, -1, size).x > max_w and s.length() > 3:
|
||||
s = s.substr(0, s.length() - 2) + "…"
|
||||
return s
|
||||
|
||||
|
||||
static func border(ci: CanvasItem, rect: Rect2, color: Color, width: int, radius: int) -> void:
|
||||
var sb := StyleBoxFlat.new()
|
||||
sb.draw_center = false
|
||||
sb.border_color = color
|
||||
sb.set_border_width_all(width)
|
||||
sb.set_corner_radius_all(radius)
|
||||
ci.draw_style_box(sb, rect)
|
||||
|
||||
|
||||
static func fill_elements(ci: CanvasItem, rect: Rect2, elements: Array, radius: int = 8) -> void:
|
||||
var a: Color = Cards.ELEMENT_COLORS[elements[0]]
|
||||
if elements.size() == 1:
|
||||
ci.draw_style_box(box(a, radius), rect)
|
||||
elif elements[0] == elements[1]:
|
||||
# Doppeltes Element: gleiche Farbe mit zweitem, hellem Innenrahmen
|
||||
ci.draw_style_box(box(a, radius), rect)
|
||||
border(ci, rect.grow(-5), lighter(a, 0.27), 2, maxi(2, radius - 3))
|
||||
else:
|
||||
var b: Color = Cards.ELEMENT_COLORS[elements[1]]
|
||||
var half := floorf(rect.size.x / 2)
|
||||
var left := StyleBoxFlat.new()
|
||||
left.bg_color = a
|
||||
left.corner_radius_top_left = radius
|
||||
left.corner_radius_bottom_left = radius
|
||||
var right := StyleBoxFlat.new()
|
||||
right.bg_color = b
|
||||
right.corner_radius_top_right = radius
|
||||
right.corner_radius_bottom_right = radius
|
||||
ci.draw_style_box(left, Rect2(rect.position, Vector2(half, rect.size.y)))
|
||||
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 alle gegnerischen Wesen, Blitz 2 Schaden",
|
||||
" an den schwächsten Gegner, Schlamm heilt dich um 3, Sturm zieht 1 Karte, Sand gibt dem neuen Wesen Schild.",
|
||||
"",
|
||||
"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("")
|
||||
lines.append("Werkstatt: Karten zerlegen bringt Staub, aus Staub entstehen neue Karten. Fünf Deck-Slots.")
|
||||
lines.append("Lokal zu mehreren: bis zu vier Menschen am selben Rechner, dazu KI, ohne Münzen.")
|
||||
lines.append("Mulligan: Im ersten eigenen Zug darfst du einmal Handkarten zurücklegen und nachziehen.")
|
||||
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:
|
||||
var variant := absi(hash(card.id)) % 3
|
||||
var col := lighter(Cards.ELEMENT_COLORS[card.elements[0]], 0.38)
|
||||
col.a = 0.95
|
||||
if card.is_trap():
|
||||
# Falle: Kreuz im Kreis
|
||||
ci.draw_arc(center, size * 0.5, 0, TAU, 32, col, maxf(2, size * 0.08))
|
||||
var d := size * 0.28
|
||||
ci.draw_line(center + Vector2(-d, -d), center + Vector2(d, d), col, maxf(2, size * 0.1))
|
||||
ci.draw_line(center + Vector2(-d, d), center + Vector2(d, -d), col, maxf(2, size * 0.1))
|
||||
return
|
||||
if card.kind == "Zauber":
|
||||
_star(ci, center, size * 0.55, Color(col, 0.35))
|
||||
var elems: Array = card.elements
|
||||
if elems.size() >= 2 and elems[0] != elems[1]:
|
||||
_element_glyph(ci, elems[0], center + Vector2(-size * 0.28, 0), size * 0.62, variant, col)
|
||||
var col2 := lighter(Cards.ELEMENT_COLORS[elems[1]], 0.38)
|
||||
col2.a = 0.95
|
||||
_element_glyph(ci, elems[1], center + Vector2(size * 0.28, 0), size * 0.62, variant, col2)
|
||||
else:
|
||||
_element_glyph(ci, elems[0], center, size, variant, col)
|
||||
if elems.size() >= 2:
|
||||
ci.draw_arc(center, size * 0.6, 0, TAU, 32, Color(col, 0.7), 2)
|
||||
if elems.size() >= 3:
|
||||
ci.draw_arc(center, size * 0.72, 0, TAU, 40, Color(col, 0.5), 2)
|
||||
|
||||
|
||||
static func _star(ci: CanvasItem, c: Vector2, r: float, col: Color) -> void:
|
||||
var pts := PackedVector2Array()
|
||||
for i in 8:
|
||||
var a := TAU * i / 8 - PI / 2
|
||||
var rr := r if i % 2 == 0 else r * 0.45
|
||||
pts.append(c + Vector2(cos(a), sin(a)) * rr)
|
||||
ci.draw_colored_polygon(pts, col)
|
||||
|
||||
|
||||
static func _element_glyph(ci: CanvasItem, element: String, c: Vector2, size: float, variant: int, col: Color) -> void:
|
||||
var h := size / 2
|
||||
match element:
|
||||
"Feuer":
|
||||
var n := 1 + variant
|
||||
for i in n:
|
||||
var off := Vector2((i - (n - 1) / 2.0) * size * 0.32, 0)
|
||||
var s := 1.0 if i == n / 2 else 0.7
|
||||
_flame(ci, c + off, h * s, col)
|
||||
"Wasser":
|
||||
for i in 2 + variant % 2:
|
||||
var y := c.y + (i - 0.5) * size * 0.3
|
||||
for k in 3:
|
||||
var x := c.x - size * 0.4 + k * size * 0.4
|
||||
ci.draw_arc(Vector2(x, y), size * 0.2, PI, TAU, 12, col, maxf(2, size * 0.09))
|
||||
"Erde":
|
||||
var n := 1 + variant
|
||||
for i in n:
|
||||
var off := (i - (n - 1) / 2.0) * size * 0.4
|
||||
var s := 1.0 if i == n / 2 else 0.65
|
||||
var pts := PackedVector2Array([c + Vector2(off - h * s, h * 0.7), c + Vector2(off, -h * s), c + Vector2(off + h * s, h * 0.7)])
|
||||
ci.draw_colored_polygon(pts, col)
|
||||
ci.draw_line(c + Vector2(-h, h * 0.7), c + Vector2(h, h * 0.7), col, maxf(2, size * 0.08))
|
||||
"Luft":
|
||||
for i in 2 + variant:
|
||||
var r := h * (1.0 - i * 0.28)
|
||||
ci.draw_arc(c + Vector2(i * size * 0.06, 0), r, PI * 0.2 + i * 0.4, PI * 1.5 + i * 0.4, 20, col, maxf(2, size * 0.09))
|
||||
|
||||
|
||||
static func _flame(ci: CanvasItem, c: Vector2, h: float, col: Color) -> void:
|
||||
var w := h * 0.75
|
||||
var pts := PackedVector2Array([
|
||||
c + Vector2(0, -h), c + Vector2(w * 0.55, -h * 0.25), c + Vector2(w * 0.4, h * 0.45),
|
||||
c + Vector2(0, h), c + Vector2(-w * 0.4, h * 0.45), c + Vector2(-w * 0.55, -h * 0.25)])
|
||||
ci.draw_colored_polygon(pts, col)
|
||||
var inner := PackedVector2Array([
|
||||
c + Vector2(0, -h * 0.35), c + Vector2(w * 0.25, h * 0.35), c + Vector2(0, h * 0.75), c + Vector2(-w * 0.25, h * 0.35)])
|
||||
ci.draw_colored_polygon(inner, Color(1.0, 0.9, 0.5, 0.75))
|
||||
|
||||
|
||||
## Dezenter Hintergrund: Farbverlauf und schräge Linien.
|
||||
class Backdrop extends Control:
|
||||
func _draw() -> void:
|
||||
var sz := size
|
||||
draw_rect(Rect2(Vector2.ZERO, sz), Style.BG)
|
||||
var c := Vector2(sz.x * 0.5, sz.y * 0.35)
|
||||
for i in range(8, 0, -1):
|
||||
var r := sz.x * 0.09 * i
|
||||
draw_circle(c, r, Color(0.16, 0.18, 0.26, 0.05))
|
||||
for k in range(-20, 40):
|
||||
var x := k * 80.0
|
||||
draw_line(Vector2(x, 0), Vector2(x + sz.y * 0.6, sz.y), Color(1, 1, 1, 0.012), 1.0)
|
||||
1
scripts/ui/style.gd.uid
Normal file
1
scripts/ui/style.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://diaenhpjc2b3i
|
||||
151
scripts/ui/workshop_screen.gd
Normal file
151
scripts/ui/workshop_screen.gd
Normal file
@@ -0,0 +1,151 @@
|
||||
class_name WorkshopScreen
|
||||
extends Control
|
||||
## Werkstatt: Karten zerlegen (Staub) und herstellen.
|
||||
|
||||
var app
|
||||
var filter := ""
|
||||
var search := ""
|
||||
var selected: Cards.Card = null
|
||||
var grid: CardGrid
|
||||
var search_box: LineEdit
|
||||
var dust_label: Label
|
||||
var msg_label: Label
|
||||
var preview_holder: Control
|
||||
var info_label: Label
|
||||
var b_dismantle: Button
|
||||
var b_craft: Button
|
||||
var b_extras: Button
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
Style.label(self, "Werkstatt", 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(""))
|
||||
for i in Cards.ELEMENTS.size():
|
||||
var e: String = Cards.ELEMENTS[i]
|
||||
Style.button(self, e, Rect2(88 + i * 76, 55, 70, 34), set_filter.bind(e), Style.darker(Cards.ELEMENT_COLORS[e], 0.8))
|
||||
search_box = LineEdit.new()
|
||||
search_box.position = Vector2(400, 55)
|
||||
search_box.size = Vector2(200, 34)
|
||||
search_box.placeholder_text = "Suche"
|
||||
search_box.text_changed.connect(func(t: String):
|
||||
search = t
|
||||
refresh())
|
||||
add_child(search_box)
|
||||
grid = CardGrid.new(Rect2(20, 100, CardGrid.COLS * (CardGrid.CW + CardGrid.GAP) + 10, 690))
|
||||
grid.card_clicked.connect(select)
|
||||
add_child(grid)
|
||||
Style.panel(self, Rect2(845, 50, 420, 680))
|
||||
dust_label = Style.label(self, "", 22, Style.BLUE, Rect2(860, 58, 390, 30), "left", true)
|
||||
preview_holder = Control.new()
|
||||
preview_holder.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(preview_holder)
|
||||
info_label = Style.label(self, "", 14, Style.MUTED, Rect2(860, 396, 390, 66), "center")
|
||||
b_dismantle = Style.button(self, "", Rect2(860, 470, 390, 44), dismantle, Color("5a3c28"))
|
||||
b_craft = Style.button(self, "", Rect2(860, 524, 390, 44), craft, Color("285a3c"))
|
||||
b_extras = Style.button(self, "", Rect2(860, 600, 390, 44), dismantle_extras)
|
||||
msg_label = Style.label(self, "", 15, Style.GREEN, Rect2(860, 660, 390, 48), "center")
|
||||
Style.button(self, "Zurück", Rect2(1150, 740, 110, 44), func(): app.show_screen(MenuScreen.new()))
|
||||
refresh()
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE and not search_box.has_focus():
|
||||
app.show_screen(MenuScreen.new())
|
||||
|
||||
|
||||
func set_filter(e: String) -> void:
|
||||
filter = e
|
||||
refresh()
|
||||
|
||||
|
||||
func cards() -> Array:
|
||||
var out: Array = Cards.all().duplicate()
|
||||
if filter != "":
|
||||
out = out.filter(func(c): return filter in c.elements)
|
||||
if search != "":
|
||||
var q := search.to_lower()
|
||||
out = out.filter(func(c): return q in c.name.to_lower() or q in c.text().to_lower() or q in c.combo().to_lower())
|
||||
return out
|
||||
|
||||
|
||||
func select(card: Cards.Card) -> void:
|
||||
selected = card
|
||||
msg_label.text = ""
|
||||
refresh()
|
||||
|
||||
|
||||
func say(m: String, ok: bool) -> void:
|
||||
msg_label.text = m
|
||||
msg_label.add_theme_color_override("font_color", Style.GREEN if ok else Style.RED)
|
||||
|
||||
|
||||
func dismantle() -> void:
|
||||
if selected == null:
|
||||
return
|
||||
var err: String = app.profile.dismantle(selected.id)
|
||||
if err == "":
|
||||
app.sfx.play("click")
|
||||
app.save()
|
||||
say("%s zerlegt (+%d Staub)" % [selected.name, Profile.DUST_DISMANTLE[selected.rarity]], true)
|
||||
else:
|
||||
say(err, false)
|
||||
refresh()
|
||||
|
||||
|
||||
func craft() -> void:
|
||||
if selected == null:
|
||||
return
|
||||
var err: String = app.profile.craft(selected.id)
|
||||
if err == "":
|
||||
app.sfx.play("play")
|
||||
app.save()
|
||||
var notes: Array[String] = app.profile.take_notices()
|
||||
say("%s hergestellt" % selected.name + (" · " + " · ".join(notes) if not notes.is_empty() else ""), true)
|
||||
else:
|
||||
say(err, false)
|
||||
refresh()
|
||||
|
||||
|
||||
func dismantle_extras() -> void:
|
||||
var got: int = app.profile.dismantle_extras()
|
||||
if got > 0:
|
||||
app.save()
|
||||
say("Überschuss zerlegt: +%d Staub" % got, true)
|
||||
else:
|
||||
say("Kein Überschuss vorhanden", false)
|
||||
refresh()
|
||||
|
||||
|
||||
func refresh() -> void:
|
||||
var p: Profile = app.profile
|
||||
dust_label.text = "Staub: %d" % p.dust
|
||||
grid.fill(cards(),
|
||||
func(c): return "%dx" % p.collection.get(c.id, 0) if p.collection.get(c.id, 0) > 0 else "fehlt",
|
||||
func(c): return p.collection.get(c.id, 0) == 0)
|
||||
Style.clear(preview_holder)
|
||||
if selected == null:
|
||||
info_label.text = "Keine Karte gewählt"
|
||||
b_dismantle.text = "Zerlegen"
|
||||
b_craft.text = "Herstellen"
|
||||
b_dismantle.disabled = true
|
||||
b_craft.disabled = true
|
||||
else:
|
||||
var v := CardView.new(selected, Vector2(200, 280))
|
||||
v.position = Vector2(955, 100)
|
||||
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
preview_holder.add_child(v)
|
||||
var owned: int = p.collection.get(selected.id, 0)
|
||||
var st: Dictionary = p.card_stats.get(selected.id, {"played": 0, "games": 0, "wins": 0})
|
||||
info_label.text = "%s · %s\nIm Besitz: %dx · Gespielt %dx · %d Partien, %d Siege" % [
|
||||
selected.combo(), selected.rarity, owned, st.played, st.games, st.wins]
|
||||
b_dismantle.text = "Zerlegen (+%d Staub)" % Profile.DUST_DISMANTLE[selected.rarity]
|
||||
b_craft.text = "Herstellen (%d Staub)" % Profile.DUST_CRAFT[selected.rarity]
|
||||
b_dismantle.disabled = owned == 0
|
||||
b_craft.disabled = p.dust < Profile.DUST_CRAFT[selected.rarity]
|
||||
var extras := 0
|
||||
for cid in p.collection:
|
||||
extras += maxi(0, p.collection[cid] - Game.MAX_COPIES)
|
||||
b_extras.text = "Überschuss zerlegen (%d Karten)" % extras
|
||||
b_extras.disabled = extras == 0
|
||||
1
scripts/ui/workshop_screen.gd.uid
Normal file
1
scripts/ui/workshop_screen.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cf42h6bsh8lvg
|
||||
Reference in New Issue
Block a user