update
This commit is contained in:
@@ -4,6 +4,8 @@ extends RefCounted
|
||||
|
||||
var screen # BattleScreen
|
||||
var enabled := true
|
||||
var banners: Array = [] # wartende Ansagen [Text, Farbe]
|
||||
var banner_running := false
|
||||
|
||||
|
||||
func _init(s) -> void:
|
||||
@@ -25,6 +27,7 @@ func play_events(evs: Array, centers: Dictionary) -> void:
|
||||
var atk: CreatureView = screen.tiles.get(ev.attacker.uid)
|
||||
lunge(atk, center_of(ev.target, centers))
|
||||
flash_target(ev.target)
|
||||
shake()
|
||||
delay = 0.12
|
||||
"damage":
|
||||
sfx("damage")
|
||||
@@ -43,7 +46,7 @@ func play_events(evs: Array, centers: Dictionary) -> void:
|
||||
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)
|
||||
popup("%s!" % tr(ev.name), Style.GOLD, where + Vector2(0, -30), 0.1)
|
||||
"play":
|
||||
sfx("play")
|
||||
"fuse":
|
||||
@@ -51,7 +54,7 @@ func play_events(evs: Array, centers: Dictionary) -> void:
|
||||
popup("Verschmolzen!", Style.PURPLE, screen.target_center({"kind": "player", "index": ev.player}), 0.0)
|
||||
"field":
|
||||
sfx("combo")
|
||||
popup("Feld: %s" % Cards.FIELD_NAMES[ev.element], Cards.ELEMENT_COLORS[ev.element], Vector2(640, 196), 0.0)
|
||||
banner("%s: %s" % [tr(Cards.FIELD_NAMES[ev.element]), tr(Cards.FIELD_TEXT[ev.element])], Cards.ELEMENT_COLORS[ev.element])
|
||||
"buy":
|
||||
sfx("click") # die Karte landet sichtbar auf der Hand, das reicht
|
||||
"reroll":
|
||||
@@ -165,3 +168,108 @@ func move_to(tile: CreatureView, pos: Vector2) -> void:
|
||||
screen.create_tween().tween_property(tile, "position", pos, 0.2)
|
||||
else:
|
||||
tile.position = pos
|
||||
|
||||
|
||||
## Ein Wesen fliegt von der Handkachel auf seinen Platz.
|
||||
func fly_in(tile: CreatureView, from: Rect2) -> void:
|
||||
if not enabled:
|
||||
return
|
||||
tile.busy = true
|
||||
tile.position = from.position
|
||||
tile.scale = from.size / tile.size
|
||||
var tw := tile.create_tween().set_parallel(true)
|
||||
tw.tween_property(tile, "position", tile.slot, 0.22).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
||||
tw.tween_property(tile, "scale", Vector2.ONE, 0.22).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
|
||||
tw.chain().tween_callback(func():
|
||||
tile.busy = false
|
||||
tile.position = tile.slot)
|
||||
|
||||
|
||||
## Eine gekaufte Karte fliegt aus dem Laden auf die Hand.
|
||||
func fly_card(card: Cards.Card, from: Rect2, to: Rect2, rarity: String) -> void:
|
||||
if not enabled:
|
||||
return
|
||||
var v := CardView.new(card, from.size, false, rarity)
|
||||
v.position = from.position
|
||||
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
v.pivot_offset = Vector2.ZERO
|
||||
screen.fx_layer.add_child(v)
|
||||
var tw: Tween = screen.create_tween().set_parallel(true)
|
||||
tw.tween_property(v, "position", to.position, 0.25).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN_OUT)
|
||||
tw.tween_property(v, "scale", to.size / from.size, 0.25).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN_OUT)
|
||||
tw.tween_property(v, "modulate:a", 0.0, 0.1).set_delay(0.2)
|
||||
tw.chain().tween_callback(v.queue_free)
|
||||
|
||||
|
||||
## Ladenkarten werden nacheinander ausgeteilt.
|
||||
func deal(v: CardView, i: int) -> void:
|
||||
if not enabled:
|
||||
return
|
||||
v.modulate.a = 0.0
|
||||
v.position.y += 24
|
||||
var tw: Tween = screen.create_tween().set_parallel(true)
|
||||
tw.tween_property(v, "modulate:a", 1.0, 0.15).set_delay(i * 0.05)
|
||||
tw.tween_property(v, "position:y", v.position.y - 24, 0.2).set_delay(i * 0.05).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
|
||||
|
||||
|
||||
## Kurzes Zittern des Bretts beim Treffer.
|
||||
func shake() -> void:
|
||||
if not enabled:
|
||||
return
|
||||
var layer: Control = screen.tile_layer
|
||||
var tw: Tween = screen.create_tween()
|
||||
for i in 3:
|
||||
tw.tween_property(layer, "position", Vector2(3 if i % 2 == 0 else -3, 0), 0.03)
|
||||
tw.tween_property(layer, "position", Vector2.ZERO, 0.03)
|
||||
|
||||
|
||||
## Ansage quer über die Mitte: Zugbeginn, Feldwechsel. Mehrere folgen nacheinander.
|
||||
func banner(text: String, color: Color) -> void:
|
||||
if not enabled:
|
||||
return
|
||||
banners.append([text, color])
|
||||
if not banner_running:
|
||||
_next_banner()
|
||||
|
||||
|
||||
func _next_banner() -> void:
|
||||
if banners.is_empty() or not is_instance_valid(screen) or not screen.is_inside_tree():
|
||||
banner_running = false
|
||||
return
|
||||
banner_running = true
|
||||
var next: Array = banners.pop_front()
|
||||
var text: String = next[0]
|
||||
var color: Color = next[1]
|
||||
var delay := 0.0
|
||||
var band := ColorRect.new()
|
||||
band.color = Color(0, 0, 0, 0.55)
|
||||
band.position = Vector2(0, 300)
|
||||
band.size = Vector2(1280, 90)
|
||||
band.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
band.scale.y = 0.0
|
||||
band.pivot_offset = Vector2(640, 45)
|
||||
screen.fx_layer.add_child(band)
|
||||
var l := Label.new()
|
||||
l.text = text
|
||||
l.add_theme_font_size_override("font_size", 40)
|
||||
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", 8)
|
||||
l.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
screen.fx_layer.add_child(l)
|
||||
l.reset_size()
|
||||
l.position = Vector2(640, 345) - l.size / 2
|
||||
l.pivot_offset = l.size / 2
|
||||
l.scale = Vector2(0.6, 0.6)
|
||||
l.modulate.a = 0.0
|
||||
var tw: Tween = screen.create_tween().set_parallel(true)
|
||||
tw.tween_property(band, "scale:y", 1.0, 0.15).set_delay(delay).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
||||
tw.tween_property(l, "scale", Vector2.ONE, 0.25).set_delay(delay).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
|
||||
tw.tween_property(l, "modulate:a", 1.0, 0.12).set_delay(delay)
|
||||
tw.tween_property(l, "modulate:a", 0.0, 0.3).set_delay(delay + 0.8)
|
||||
tw.tween_property(band, "scale:y", 0.0, 0.2).set_delay(delay + 0.9)
|
||||
tw.chain().tween_callback(func():
|
||||
l.queue_free()
|
||||
band.queue_free()
|
||||
_next_banner())
|
||||
|
||||
@@ -1,42 +1,71 @@
|
||||
class_name BattleLayout
|
||||
## Feste Bildschirmaufteilung des Kampfes (1280x800).
|
||||
## Feste Bildschirmaufteilung des Duells (1280x800), von unten nach oben: Laden mit Neuwürfeln
|
||||
## und Zug beenden, Hand, eigenes Brett (mit eigener Leiste links), Feldband, oben der Gegner
|
||||
## mit seinem Brett.
|
||||
|
||||
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)
|
||||
const B_GIVEUP := Rect2(10, 10, 96, 32)
|
||||
const B_LOG := Rect2(112, 10, 96, 32)
|
||||
const OPP_BOX := Rect2(330, 10, 620, 56)
|
||||
const FIELD := Rect2(240, 226, 800, 32)
|
||||
const MSG := Rect2(240, 262, 800, 22)
|
||||
const OWN_PANEL := Rect2(240, 288, 800, 160)
|
||||
const STATUS := Rect2(20, 288, 210, 160)
|
||||
const B_FUSE := Rect2(1050, 298, 210, 40)
|
||||
const B_UNDO := Rect2(1050, 346, 210, 40)
|
||||
const OWN_X := 345
|
||||
const HAND_W := 96
|
||||
const HAND_H := 62
|
||||
const HAND_GAP := 4
|
||||
const HAND_Y := 458
|
||||
const SHOP_Y := 532
|
||||
const SHOP_W := 150
|
||||
const SHOP_H := 205
|
||||
const B_REROLL := Rect2(20, 606, 200, 52)
|
||||
const B_END := Rect2(1060, 600, 200, 60)
|
||||
const LOG_PANEL := Rect2(240, 60, 800, 560)
|
||||
const PREVIEW_SIZE := Vector2(180, 252)
|
||||
const CENTER_X := 240
|
||||
const CENTER_W := 800
|
||||
|
||||
|
||||
static func own_slot(j: int) -> Rect2:
|
||||
return Rect2(OWN_X + j * 120, 250, 110, 140)
|
||||
return Rect2(OWN_X + j * 120, 298, 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])
|
||||
## Brett des Gegners unter seinem Kasten.
|
||||
static func opp_slot(j: int) -> Rect2:
|
||||
return Rect2(OWN_X + j * 120, 76, 110, 140)
|
||||
|
||||
|
||||
## Laden: fünf Angebote über der Hand.
|
||||
## Laden: fünf große Angebote unten, wie in TFT.
|
||||
static func shop_rect(i: int) -> Rect2:
|
||||
return Rect2(340 + i * 121, 487, 115, 112)
|
||||
return Rect2(245 + i * (SHOP_W + 10), SHOP_Y, SHOP_W, SHOP_H)
|
||||
|
||||
|
||||
## Hand: flache Kacheln zwischen Brett und Laden (die Bank).
|
||||
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)
|
||||
var x := CENTER_X + (CENTER_W - total) / 2
|
||||
return Rect2(x + i * (HAND_W + HAND_GAP), HAND_Y, HAND_W, HAND_H)
|
||||
|
||||
|
||||
## Schwebende Vorschau neben der gezeigten Karte: über Hand- und Ladenkarten, sonst rechts
|
||||
## davon (links, wenn rechts kein Platz ist); nie aus dem Bild.
|
||||
static func preview_rect(anchor: Rect2) -> Rect2:
|
||||
if anchor.position.y >= HAND_Y - 10:
|
||||
var x := clampf(anchor.get_center().x - PREVIEW_SIZE.x / 2, 10, 1270 - PREVIEW_SIZE.x)
|
||||
return Rect2(Vector2(x, anchor.position.y - 12 - PREVIEW_SIZE.y), PREVIEW_SIZE)
|
||||
var x := anchor.end.x + 12
|
||||
if x + PREVIEW_SIZE.x > 1270:
|
||||
x = anchor.position.x - 12 - PREVIEW_SIZE.x
|
||||
var y := clampf(anchor.get_center().y - PREVIEW_SIZE.y / 2, 10, 800 - PREVIEW_SIZE.y - 10)
|
||||
return Rect2(Vector2(x, y), PREVIEW_SIZE)
|
||||
|
||||
|
||||
## Zusatzzeile (Resonanz, Feld, Feldkombo) neben der Vorschau, rechts davon oder links.
|
||||
static func info_rect(preview: Rect2) -> Rect2:
|
||||
var w := 230.0
|
||||
var x := preview.end.x + 8
|
||||
if x + w > 1275:
|
||||
x = preview.position.x - 8 - w
|
||||
return Rect2(x, preview.position.y + preview.size.y / 2 - 40, w, 80)
|
||||
|
||||
@@ -4,7 +4,6 @@ extends Control
|
||||
## Modi: Einzelspieler, Kampagne, Netzwerk (Host, Mitspieler oder Zuschauer).
|
||||
|
||||
var app
|
||||
var n_opp: int
|
||||
var stage := -1 # Kampagnenstufe oder -1
|
||||
var net_play := false # Netzwerkspiel (auch als Zuschauer)
|
||||
var online_seat := -1 # Netzwerk: eigener Platz, Zuschauer -1
|
||||
@@ -24,8 +23,6 @@ var ai_copy: Game # Spielkopie, auf der die KI rechnet
|
||||
var ai_box := {} # Ergebnis der Berechnung (vom Thread befüllt)
|
||||
var reward := -1
|
||||
var ended := false
|
||||
var zoom_player = null
|
||||
var preview_timer := 0.0
|
||||
var pad: BattleGamepad
|
||||
var gp_zone: String:
|
||||
get:
|
||||
@@ -36,7 +33,6 @@ var gp_index: int:
|
||||
var skip_ai := false # Gegnerzüge im Schnelldurchlauf
|
||||
var smart_ai := true # Vorausschau-KI (Leicht: einfache KI)
|
||||
var undo_state: Game = null # Zustand vor der letzten rückgängig machbaren Aktion
|
||||
var saved_game: Game = null # fortgesetzter Kampf (siehe resume)
|
||||
var undo_log_len := 0
|
||||
var undo_t := 0.0
|
||||
|
||||
@@ -54,11 +50,12 @@ var hero_layer: Control
|
||||
var tile_layer: Control
|
||||
var hand_layer: Control
|
||||
var shop_layer: Control
|
||||
var zoom_layer: Control
|
||||
var log_overlay: Control
|
||||
var b_log: Button
|
||||
var status_panel: Panel
|
||||
var fx_layer: Control
|
||||
var log_box: RichTextLabel
|
||||
var preview_holder: Control
|
||||
var bar_panel: Panel
|
||||
var name_label: Label
|
||||
var life_label: Label
|
||||
var energy_label: Label
|
||||
@@ -69,38 +66,35 @@ var b_end: Button
|
||||
var b_fuse: Button
|
||||
var b_reroll: Button
|
||||
var b_giveup: Button
|
||||
var field_label: Label
|
||||
var field_view: FieldView
|
||||
var link_view: Links # Resonanzlinien unter den Feldreihen
|
||||
var info_label: Label # Vorschau: Resonanz, Feld, Feldkombo
|
||||
var board_panel: Panel # eigenes Brett, färbt sich mit dem Feld
|
||||
var arrivals := {} # uid -> Rect2: Wesen, das gerade aus der Hand aufs Brett fliegt
|
||||
var banner_turn := -1 # Zug, für den die Zugansage schon lief
|
||||
var deal_shop := false # nächster Aufbau teilt den Laden animiert aus
|
||||
var overlay: Control
|
||||
var result_label: Label
|
||||
var reward_label: Label
|
||||
var notice_label: Label
|
||||
var confirm: Control
|
||||
var b_save_leave: Button
|
||||
var cursor_layer: Control
|
||||
var wait_label: Label
|
||||
var b_skip: Button
|
||||
var b_undo: Button
|
||||
|
||||
|
||||
func _init(n: int = 2, campaign_stage: int = -1) -> void:
|
||||
n_opp = n
|
||||
func _init(campaign_stage: int = -1) -> void:
|
||||
stage = campaign_stage
|
||||
|
||||
|
||||
static func online(seat: int) -> BattleScreen:
|
||||
var b := BattleScreen.new(0, -1)
|
||||
var b := BattleScreen.new(-1)
|
||||
b.net_play = true
|
||||
b.online_seat = seat
|
||||
return b
|
||||
|
||||
|
||||
## Unterbrochenen Kampf aus der Datei fortsetzen (siehe save_battle).
|
||||
static func resume(data: Dictionary) -> BattleScreen:
|
||||
var s := BattleScreen.new(int(data.get("n_opp", 1)), int(data.get("stage", -1)))
|
||||
s.saved_game = Game.from_snapshot(data.game)
|
||||
return s
|
||||
|
||||
|
||||
func is_online() -> bool:
|
||||
return net_play
|
||||
|
||||
@@ -137,13 +131,11 @@ func _ready() -> void:
|
||||
net.snapshot_received.connect(_on_snapshot)
|
||||
wait_label.visible = true
|
||||
return # der erste Zustand kommt vom Host
|
||||
elif saved_game != null:
|
||||
game = saved_game
|
||||
elif stage >= 0:
|
||||
var r := Game.make_rng()
|
||||
game = Game.new_battle_with(Campaign.opponents(stage), r, false, own_rules())
|
||||
game = Game.new_battle_with(Campaign.opponent(stage), r, false, own_rules())
|
||||
else:
|
||||
game = Game.new_battle(n_opp, Game.make_rng(), own_rules())
|
||||
game = Game.new_battle(Game.make_rng(), own_rules())
|
||||
smart_ai = bool(app.profile.difficulty().smart) or not counts_for_profile()
|
||||
_begin()
|
||||
|
||||
@@ -160,7 +152,6 @@ func own_rules() -> Dictionary:
|
||||
|
||||
|
||||
func _begin() -> void:
|
||||
n_opp = game.players.size() - 1
|
||||
if is_online():
|
||||
perspective = game.players[maxi(online_seat, 0)]
|
||||
else:
|
||||
@@ -170,67 +161,94 @@ func _begin() -> void:
|
||||
if is_online() and net.is_host:
|
||||
net.broadcast()
|
||||
game.events.clear()
|
||||
b_save_leave.visible = resumable()
|
||||
save_battle()
|
||||
refresh()
|
||||
announce_turn()
|
||||
|
||||
|
||||
func _build_static() -> void:
|
||||
Style.panel(self, Rect2(10, 205, 320, 290))
|
||||
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, 258)
|
||||
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)
|
||||
b_giveup = Style.button(self, "Aufgeben", Rect2(10, 553, 150, 40), func(): confirm.visible = true)
|
||||
Style.panel(self, Rect2(950, 205, 320, 380))
|
||||
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, "", 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)
|
||||
income_label = Style.label(self, "", 16, Style.MUTED, Rect2(760, 424, 170, 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"))
|
||||
b_skip = Style.button(self, "Gegnerzüge überspringen", Rect2(1005, 527, 210, 48), func(): skip_ai = true)
|
||||
var L := BattleLayout
|
||||
# oben links: Aufgeben und Verlauf, klein
|
||||
b_giveup = Style.button(self, "Aufgeben", L.B_GIVEUP, func(): confirm.visible = true)
|
||||
b_giveup.add_theme_font_size_override("font_size", 13)
|
||||
b_log = Style.button(self, "Verlauf", L.B_LOG, toggle_log)
|
||||
b_log.add_theme_font_size_override("font_size", 13)
|
||||
# Mitte: Feldband, Meldung, eigenes Brett mit fünf Plätzen, eigene Leiste links daneben
|
||||
Style.panel(self, L.FIELD, Style.darker(Style.PANEL, 0.9), 8)
|
||||
field_view = FieldView.new(L.FIELD)
|
||||
add_child(field_view)
|
||||
msg_label = Style.label(self, "", 15, Style.ORANGE, L.MSG, "center", true)
|
||||
board_panel = Style.panel(self, L.OWN_PANEL, Style.darker(Style.PANEL, 0.8), 10)
|
||||
var slots := Control.new()
|
||||
slots.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
slots.draw.connect(func():
|
||||
for j in Game.MAX_BOARD:
|
||||
Style.border(slots, L.own_slot(j), Color(1, 1, 1, 0.08), 2, 8)
|
||||
Style.border(slots, L.opp_slot(j), Color(1, 1, 1, 0.05), 2, 8))
|
||||
add_child(slots)
|
||||
status_panel = Style.panel(self, L.STATUS)
|
||||
var sx: float = L.STATUS.position.x
|
||||
var sy: float = L.STATUS.position.y
|
||||
name_label = Style.label(self, "", 18, Style.TEXT, Rect2(sx + 12, sy + 8, 190, 26), "left", true)
|
||||
name_label.clip_text = true
|
||||
life_label = Style.label(self, "", 26, Style.RED, Rect2(sx + 12, sy + 40, 190, 34), "left", true)
|
||||
energy_label = Style.label(self, "", 26, Style.BLUE, Rect2(sx + 12, sy + 78, 100, 34), "left", true)
|
||||
income_label = Style.label(self, "", 15, Style.MUTED, Rect2(sx + 100, sy + 88, 100, 22), "left")
|
||||
status_label = Style.label(self, "", 13, Style.MUTED, Rect2(sx + 12, sy + 124, 190, 22), "left")
|
||||
b_fuse = Style.button(self, "Verschmelzen", L.B_FUSE, toggle_fuse, Color("3c2858"))
|
||||
b_fuse.add_theme_font_size_override("font_size", 15)
|
||||
b_fuse.visible = false
|
||||
b_undo = Style.button(self, "Rückgängig", L.B_UNDO, undo)
|
||||
b_undo.add_theme_font_size_override("font_size", 15)
|
||||
b_undo.visible = false
|
||||
# unten: Laden mit Neuwürfeln links und Zug beenden rechts
|
||||
b_reroll = Style.button(self, "", L.B_REROLL, do_reroll, Color("285a5a"))
|
||||
b_reroll.add_theme_font_size_override("font_size", 15)
|
||||
b_end = Style.button(self, "Zug beenden", L.B_END, end_turn, Style.PRIMARY)
|
||||
b_end.add_theme_font_size_override("font_size", 20)
|
||||
b_skip = Style.button(self, "Gegnerzüge überspringen", L.B_END, func(): skip_ai = true)
|
||||
b_skip.add_theme_font_size_override("font_size", 14)
|
||||
b_skip.visible = false
|
||||
b_undo = Style.button(self, "Rückgängig", Rect2(170, 553, 150, 40), undo)
|
||||
b_undo.visible = false
|
||||
field_label = Style.label(self, "", 15, Style.MUTED, Rect2(340, 211, 600, 24), "center")
|
||||
b_fuse = Style.button(self, "Verschmelzen", Rect2(10, 503, 150, 40), toggle_fuse, Color("3c2858"))
|
||||
b_fuse.visible = false
|
||||
b_reroll = Style.button(self, "", Rect2(170, 503, 150, 40), do_reroll, Color("285a5a"))
|
||||
b_reroll.add_theme_font_size_override("font_size", 14)
|
||||
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()
|
||||
link_view = Links.new()
|
||||
add_child(link_view)
|
||||
tile_layer = _layer()
|
||||
hand_layer = _layer()
|
||||
shop_layer = _layer()
|
||||
preview_holder = _layer()
|
||||
zoom_layer = _layer()
|
||||
info_label = Style.label(self, "", 13, Style.TEXT, Rect2(0, 0, 220, 60), "center")
|
||||
info_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
info_label.visible = false
|
||||
fx_layer = _layer()
|
||||
cursor_layer = _layer()
|
||||
# Verlauf als Überlagerung
|
||||
log_overlay = Style.overlay(self)
|
||||
log_overlay.get_child(0).gui_input.connect(func(ev):
|
||||
if ev is InputEventMouseButton and ev.pressed:
|
||||
log_overlay.visible = false)
|
||||
Style.panel(log_overlay, L.LOG_PANEL, Style.PANEL, 10, Style.MUTED, 2)
|
||||
Style.label(log_overlay, "Verlauf", 20, Style.GOLD, Rect2(L.LOG_PANEL.position + Vector2(20, 12), Vector2(300, 30)), "left", true)
|
||||
log_box = RichTextLabel.new()
|
||||
log_box.position = L.LOG_PANEL.position + Vector2(20, 50)
|
||||
log_box.size = L.LOG_PANEL.size - Vector2(40, 110)
|
||||
log_box.bbcode_enabled = true
|
||||
log_box.scroll_following = true
|
||||
log_box.add_theme_font_size_override("normal_font_size", 14)
|
||||
log_box.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
log_overlay.add_child(log_box)
|
||||
Style.button(log_overlay, "Schließen", Rect2(L.LOG_PANEL.end.x - 180, L.LOG_PANEL.end.y - 52, 160, 40), func(): log_overlay.visible = false)
|
||||
# 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 = Style.overlay(self)
|
||||
Style.panel(confirm, Rect2(340, 290, 600, 200), Style.PANEL, 10, Style.MUTED, 2)
|
||||
Style.label(confirm, "Wirklich aufgeben?", 26, Style.TEXT, Rect2(340, 312, 600, 40), "center", true)
|
||||
Style.button(confirm, "Ja, aufgeben", Rect2(355, 420, 180, 46), leave, Color("5a2828"))
|
||||
b_save_leave = Style.button(confirm, "Speichern und zurück", Rect2(550, 420, 180, 46), save_and_leave, Color("285a3c"))
|
||||
b_save_leave.add_theme_font_size_override("font_size", 14)
|
||||
Style.button(confirm, "Weiterspielen", Rect2(745, 420, 180, 46), func(): confirm.visible = false)
|
||||
confirm = Style.confirm(self, "Wirklich aufgeben?", "", "Ja, aufgeben", leave, "Weiterspielen")
|
||||
|
||||
|
||||
func toggle_log() -> void:
|
||||
log_overlay.visible = not log_overlay.visible
|
||||
|
||||
|
||||
func _layer() -> Control:
|
||||
@@ -244,54 +262,27 @@ func _overlay() -> Control:
|
||||
return Style.overlay(self)
|
||||
|
||||
|
||||
## Einzelspieler-Kämpfe (auch Kampagne) lassen sich unterbrechen und fortsetzen.
|
||||
func resumable() -> bool:
|
||||
return counts_for_profile()
|
||||
|
||||
|
||||
func save_battle() -> void:
|
||||
if not resumable() or game == null or game.winner != "":
|
||||
return
|
||||
var d := {"version": 1, "n_opp": n_opp, "stage": stage, "game": game.snapshot(-1)}
|
||||
var f := FileAccess.open(app.battle_save_path(), FileAccess.WRITE)
|
||||
if f != null:
|
||||
f.store_string(JSON.stringify(d))
|
||||
f.close()
|
||||
|
||||
|
||||
func save_and_leave() -> void:
|
||||
save_battle()
|
||||
app.show_screen(MenuScreen.new())
|
||||
|
||||
|
||||
func leave() -> void:
|
||||
if is_online():
|
||||
net.leave()
|
||||
if resumable() and game != null and game.winner == "":
|
||||
app.clear_battle_save() # aufgegeben
|
||||
app.show_screen(MenuScreen.new())
|
||||
|
||||
|
||||
# --- Layout-Helfer ------------------------------------------------------------------
|
||||
|
||||
## Der Gegner aus Sicht der eigenen Perspektive (Zuschauer: der zweite Spieler).
|
||||
func opponent() -> Game.Player:
|
||||
return game.enemy_of(perspective)
|
||||
|
||||
|
||||
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())
|
||||
return [opponent()]
|
||||
|
||||
|
||||
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)
|
||||
return BattleLayout.opp_slot(j)
|
||||
|
||||
|
||||
func ref_key(ref: Dictionary) -> String:
|
||||
@@ -306,47 +297,37 @@ func target_center(ref: Dictionary) -> Vector2:
|
||||
return Vector2(-1, -1)
|
||||
var p: Game.Player = game.players[ref.index]
|
||||
if p == perspective:
|
||||
return BattleLayout.BAR.position + BattleLayout.BAR.size / 2
|
||||
return BattleLayout.STATUS.get_center()
|
||||
if hero_panels.has(p):
|
||||
var panel: Panel = hero_panels[p]
|
||||
return panel.position + panel.size / 2
|
||||
return Vector2(-1, -1)
|
||||
|
||||
|
||||
## Kasten des Gegners oben in der Mitte; ein Klick zielt auf den Helden.
|
||||
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 - (58 if c else 20), 26), "left", true)
|
||||
labels.name.clip_text = true
|
||||
labels.name.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
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)
|
||||
var o := opponent()
|
||||
var box := BattleLayout.OPP_BOX
|
||||
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, 18, Style.TEXT, Rect2(box.position.x + 8, box.position.y + 4, box.size.x - 16, 22), "left", true)
|
||||
labels.name.clip_text = true
|
||||
labels.name.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
labels["life"] = Style.label(hero_layer, "", 20, Style.RED, Rect2(box.position.x + 8, box.position.y + 30, 120, 24), "left", true)
|
||||
labels["stats"] = Style.label(hero_layer, "", 14, Style.MUTED, Rect2(box.position.x + 80, box.position.y + 34, box.size.x - 88, 20), "right")
|
||||
hero_labels[o] = labels
|
||||
|
||||
|
||||
# --- Ablauf und Eingabe ------------------------------------------------------------------
|
||||
@@ -358,7 +339,7 @@ func my_turn() -> bool:
|
||||
|
||||
|
||||
func any_overlay() -> bool:
|
||||
return confirm.visible or overlay.visible
|
||||
return confirm.visible or overlay.visible or log_overlay.visible
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
@@ -376,7 +357,9 @@ func _input(event: InputEvent) -> void:
|
||||
KEY_ESCAPE:
|
||||
if confirm.visible:
|
||||
confirm.visible = false
|
||||
elif selected != null or pending != null or fusing or zoom_player != null:
|
||||
elif log_overlay.visible:
|
||||
log_overlay.visible = false
|
||||
elif selected != null or pending != null or fusing:
|
||||
cancel_all()
|
||||
elif not any_overlay():
|
||||
confirm.visible = true
|
||||
@@ -402,7 +385,6 @@ func cancel_all() -> void:
|
||||
if confirm.visible:
|
||||
confirm.visible = false
|
||||
cancel()
|
||||
zoom_player = null
|
||||
refresh()
|
||||
|
||||
|
||||
@@ -412,10 +394,6 @@ func _process(delta: float) -> void:
|
||||
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 undo_t > 0.0:
|
||||
undo_t -= delta
|
||||
if undo_t <= 0.0:
|
||||
@@ -477,10 +455,9 @@ func _finish() -> void:
|
||||
var won := game.winner == "human"
|
||||
if counts_for_profile():
|
||||
var factor: float = float(app.profile.difficulty().reward) if stage < 0 else 1.0
|
||||
reward = app.profile.record_battle(won, n_opp, stage, factor)
|
||||
reward = app.profile.record_battle(won, stage, factor)
|
||||
app.save()
|
||||
app.sfx.play("win" if won else "lose")
|
||||
app.clear_battle_save()
|
||||
else:
|
||||
var wp := game.winner_player
|
||||
var mine: bool = wp != null and wp == perspective
|
||||
@@ -506,10 +483,13 @@ func play(card: Cards.Card, target) -> void:
|
||||
return
|
||||
var before: Game = game.clone() if not is_online() else null
|
||||
var log_len := game.log.size()
|
||||
var from := _hand_rect_of(card)
|
||||
var err := game.play_card(perspective, card, target)
|
||||
if err != "":
|
||||
say(tr(err))
|
||||
return
|
||||
if from.size.x > 0 and not perspective.board.is_empty() and perspective.board[-1].card == card:
|
||||
arrivals[perspective.board[-1].uid] = from
|
||||
# Rückgängig nur, wenn nichts weiter passiert ist (keine Effekte und keine Kombos)
|
||||
if before != null and game.events.size() == 1 and game.events[0].type == "play":
|
||||
undo_state = before
|
||||
@@ -534,9 +514,15 @@ func do_buy(slot: int) -> void:
|
||||
if is_client():
|
||||
net.send_action({"type": "buy", "slot": slot})
|
||||
return
|
||||
var from := BattleLayout.shop_rect(slot)
|
||||
var offer = perspective.shop[slot] if slot < perspective.shop.size() else null
|
||||
var err := game.buy(perspective, slot)
|
||||
if err != "":
|
||||
say(tr(err))
|
||||
elif offer != null:
|
||||
fx.sfx("buy")
|
||||
fx.fly_card(offer, from, BattleLayout.hand_rect(perspective.hand.size(), perspective.hand.size() - 1),
|
||||
Cards.RARITIES[int(perspective.looks.get(offer.id, 0))])
|
||||
|
||||
|
||||
## Würfelt den Laden neu (kostet Mana).
|
||||
@@ -551,6 +537,8 @@ func do_reroll() -> void:
|
||||
var err := game.reroll(perspective)
|
||||
if err != "":
|
||||
say(tr(err))
|
||||
else:
|
||||
deal_shop = true
|
||||
after_action()
|
||||
|
||||
|
||||
@@ -596,7 +584,6 @@ func end_turn() -> void:
|
||||
return
|
||||
cancel()
|
||||
clear_undo()
|
||||
zoom_player = null
|
||||
if is_client():
|
||||
net.send_action({"type": "end"})
|
||||
return
|
||||
@@ -629,7 +616,6 @@ func on_click(kind: String, obj) -> void:
|
||||
play(card, obj)
|
||||
else:
|
||||
say(tr("Abgebrochen"))
|
||||
zoom_player = null
|
||||
after_action()
|
||||
return
|
||||
if selected != null:
|
||||
@@ -643,7 +629,6 @@ func on_click(kind: String, obj) -> void:
|
||||
var err := g.attack(cr, obj)
|
||||
if err != "":
|
||||
say(tr(err))
|
||||
zoom_player = null
|
||||
after_action()
|
||||
return
|
||||
match kind:
|
||||
@@ -677,17 +662,121 @@ func on_tile_click(cr: Game.Creature) -> void:
|
||||
on_click("enemy", cr)
|
||||
|
||||
|
||||
func show_preview(card, entered: bool, owner_looks: Dictionary = {}) -> void:
|
||||
## Zugansage, wenn der eigene Zug beginnt.
|
||||
func announce_turn() -> void:
|
||||
if game == null or not my_turn() or banner_turn == game.turn:
|
||||
return
|
||||
banner_turn = game.turn
|
||||
fx.banner(tr("Dein Zug"), Style.GOLD)
|
||||
fx.sfx("turn")
|
||||
deal_shop = true
|
||||
refresh()
|
||||
|
||||
|
||||
func _hand_rect_of(card: Cards.Card) -> Rect2:
|
||||
for v in hand_views:
|
||||
if v.card == card:
|
||||
return Rect2(v.position, v.size)
|
||||
return Rect2()
|
||||
|
||||
|
||||
## Vorschau schwebt neben der gezeigten Karte (anchor), darunter die Zusatzzeile (info).
|
||||
func show_preview(card, entered: bool, owner_looks: Dictionary = {}, info: String = "", anchor: Rect2 = Rect2()) -> void:
|
||||
Style.clear(preview_holder)
|
||||
info_label.visible = entered and info != ""
|
||||
info_label.text = info if entered else ""
|
||||
if entered and card != null:
|
||||
var v := CardView.new(card, Vector2(210, 294), false, Cards.RARITIES[int(owner_looks.get(card.id, 0))])
|
||||
v.position = Vector2(1005, 215)
|
||||
var r := BattleLayout.preview_rect(anchor)
|
||||
var v := CardView.new(card, r.size, false, Cards.RARITIES[int(owner_looks.get(card.id, 0))])
|
||||
v.position = r.position
|
||||
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
preview_holder.add_child(v)
|
||||
var ir := BattleLayout.info_rect(r)
|
||||
info_label.position = ir.position
|
||||
info_label.size = ir.size
|
||||
|
||||
|
||||
func show_hand_preview(card: Cards.Card, entered: bool) -> void:
|
||||
show_preview(card, entered, perspective.looks)
|
||||
## Eigene Hand- und Ladenkarte: zeigt, was das Ausspielen jetzt brächte, und markiert den Kombo-Partner.
|
||||
func show_hand_preview(card: Cards.Card, entered: bool, anchor: Rect2 = Rect2()) -> void:
|
||||
var f := game.forecast(perspective, card)
|
||||
var lines: Array[String] = []
|
||||
var parts: Array[String] = []
|
||||
if f.resonance > 0:
|
||||
parts.append(tr("Resonanz +%d") % f.resonance)
|
||||
if f.field > 0:
|
||||
parts.append(tr("Feld +%d") % f.field)
|
||||
if not parts.is_empty():
|
||||
lines.append(" · ".join(parts))
|
||||
if f.combo != "":
|
||||
lines.append(tr("Feldkombo %s mit %s: %s") % [tr(f.combo), f.partner.card.title(), tr(Cards.FIELD_COMBO_TEXT[f.combo])])
|
||||
for t in tiles.values():
|
||||
t.mark = ""
|
||||
t.queue_redraw()
|
||||
if entered and f.partner != null and tiles.has(f.partner.uid):
|
||||
tiles[f.partner.uid].mark = f.combo
|
||||
tiles[f.partner.uid].queue_redraw()
|
||||
show_preview(card, entered, perspective.looks, "\n".join(lines), anchor)
|
||||
|
||||
|
||||
## Hand- und Ladenkarte tragen ihren Bonus und ihre Feldkombo als Vorschau.
|
||||
func apply_forecast(v: CardView) -> void:
|
||||
var f := game.forecast(perspective, v.card)
|
||||
v.bonus = f.resonance + f.field
|
||||
v.tag = f.combo
|
||||
|
||||
|
||||
## Was den Angriff eines Wesens gerade hebt.
|
||||
func creature_info(cr: Game.Creature) -> String:
|
||||
var parts: Array[String] = []
|
||||
if cr.resonance > 0:
|
||||
parts.append(tr("Resonanz +%d") % cr.resonance)
|
||||
if cr.field_bonus > 0:
|
||||
parts.append(tr("Feld +%d") % cr.field_bonus)
|
||||
var perk := cr.power() - cr.attack - cr.resonance - cr.field_bonus
|
||||
if perk > 0:
|
||||
parts.append(tr("Merkmal +%d") % perk)
|
||||
return " · ".join(parts)
|
||||
|
||||
|
||||
## Elementvorteil gegen ein mögliches Ziel: des gewählten Angreifers oder eines Schadenszaubers auf der Hand.
|
||||
func matchup_for(cr: Game.Creature, valid: Array) -> int:
|
||||
if selected != null and game.can_attack(selected, cr)[0]:
|
||||
return Game.matchup_bonus(selected, cr)
|
||||
if pending != null and cr in valid and pending.effects.any(func(fx): return fx.kind == "damage" and fx.target in Cards.CHOSEN_TARGETS):
|
||||
return Cards.matchup(pending.elements, cr.card.elements)
|
||||
return 0
|
||||
|
||||
|
||||
## Resonanzgruppen je Reihe: Wesen mit gleichem Element werden unter der Reihe verbunden.
|
||||
func resonance_groups() -> Array:
|
||||
var out := []
|
||||
for p in game.players:
|
||||
var k := 0
|
||||
for e in Cards.ELEMENTS:
|
||||
var xs: Array[float] = []
|
||||
for j in p.board.size():
|
||||
if e in p.board[j].card.elements:
|
||||
xs.append(slot_rect(p, j).get_center().x)
|
||||
if xs.size() < 2:
|
||||
continue
|
||||
out.append({"color": Style.lighter(Cards.ELEMENT_COLORS[e], 0.25), "y": slot_rect(p, 0).end.y + 4 + k * 6, "xs": xs})
|
||||
k += 1
|
||||
return out
|
||||
|
||||
|
||||
class Links extends Control:
|
||||
var groups: Array = []
|
||||
|
||||
func _init() -> void:
|
||||
set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
|
||||
func _draw() -> void:
|
||||
for gr in groups:
|
||||
var y: float = gr.y
|
||||
draw_line(Vector2(gr.xs[0], y), Vector2(gr.xs[-1], y), gr.color, 3.0)
|
||||
for x in gr.xs:
|
||||
draw_circle(Vector2(x, y), 4.0, gr.color)
|
||||
|
||||
|
||||
# --- Netzwerk -------------------------------------------------------------------------------
|
||||
@@ -744,7 +833,7 @@ func after_action() -> void:
|
||||
centers[ref_key(ev[key])] = target_center(ev[key])
|
||||
refresh()
|
||||
fx.play_events(evs, centers)
|
||||
save_battle()
|
||||
announce_turn()
|
||||
|
||||
|
||||
# --- Ansicht angleichen --------------------------------------------------------------------
|
||||
@@ -781,8 +870,7 @@ func refresh() -> void:
|
||||
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)
|
||||
labels.stats.text = ("H %d ◆ %d" % [o.hand.size(), o.energy]) if compact() \
|
||||
else ("Hand %d ◆ %d (+%d)" % [o.hand.size(), o.energy, o.max_energy])
|
||||
labels.stats.text = "Hand %d ◆ %d (+%d)" % [o.hand.size(), o.energy, o.max_energy]
|
||||
else:
|
||||
labels.life.text = "besiegt"
|
||||
labels.life.add_theme_color_override("font_color", Style.MUTED)
|
||||
@@ -798,10 +886,11 @@ func refresh() -> void:
|
||||
if tiles.has(cr.uid):
|
||||
tile = tiles[cr.uid]
|
||||
tile.creature = cr
|
||||
if tile.size != rect.size:
|
||||
tile.size = rect.size
|
||||
if tile.size != rect.size: # erst die Mindestgröße, sonst bleibt die Kachel groß
|
||||
tile.custom_minimum_size = rect.size
|
||||
tile.size = rect.size
|
||||
tile.pivot_offset = rect.size / 2
|
||||
tile.set_process(Style.rarity_animated(tile.rarity()))
|
||||
else:
|
||||
tile = CreatureView.new(cr, rect.size)
|
||||
tile.position = rect.position
|
||||
@@ -809,13 +898,17 @@ func refresh() -> void:
|
||||
tiles[cr.uid] = tile
|
||||
tile_layer.add_child(tile)
|
||||
var looks: Dictionary = p.looks
|
||||
tile.hover.connect(func(card, entered): show_preview(card, entered, looks))
|
||||
tile.hover.connect(func(card, entered): show_preview(card, entered, looks, creature_info(tile.creature) if entered else "", Rect2(tile.slot, tile.size)))
|
||||
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)
|
||||
if arrivals.has(cr.uid):
|
||||
fx.fly_in(tile, arrivals[cr.uid])
|
||||
arrivals.erase(cr.uid)
|
||||
else:
|
||||
fx.pop_in(tile)
|
||||
tile.slot = rect.position
|
||||
if not tile.busy and tile.position != rect.position:
|
||||
fx.move_to(tile, rect.position)
|
||||
@@ -835,6 +928,8 @@ func refresh() -> void:
|
||||
w = 3
|
||||
tile.border = b
|
||||
tile.width = w
|
||||
tile.mark = ""
|
||||
tile.matchup = matchup_for(cr, valid)
|
||||
tile.queue_redraw()
|
||||
if p == h:
|
||||
own_views.append(tile)
|
||||
@@ -845,6 +940,8 @@ func refresh() -> void:
|
||||
var t: CreatureView = tiles[uid]
|
||||
tiles.erase(uid)
|
||||
fx.fade_out(t)
|
||||
link_view.groups = resonance_groups()
|
||||
link_view.queue_redraw()
|
||||
|
||||
Style.clear(shop_layer)
|
||||
if not spectating():
|
||||
@@ -856,17 +953,22 @@ func refresh() -> void:
|
||||
continue
|
||||
var sv := CardView.new(offer, sr.size, false, Cards.RARITIES[int(h.looks.get(offer.id, 0))])
|
||||
sv.position = sr.position
|
||||
sv.lift = mine
|
||||
apply_forecast(sv)
|
||||
sv.dim = not mine or h.energy < offer.cost or h.hand.size() >= Game.MAX_HAND
|
||||
sv.hover.connect(show_hand_preview)
|
||||
sv.hover.connect(func(c, e): show_hand_preview(c, e, Rect2(sv.position, sv.size)))
|
||||
var slot := i
|
||||
sv.set_meta("slot", slot) # der Platz im Laden, auch für das Gamepad
|
||||
sv.clicked.connect(func(_view): on_click("shop", slot))
|
||||
shop_layer.add_child(sv)
|
||||
shop_views.append(sv)
|
||||
if deal_shop:
|
||||
fx.deal(sv, i)
|
||||
deal_shop = false
|
||||
|
||||
Style.clear(hand_layer)
|
||||
if spectating():
|
||||
Style.label(hand_layer, "Zuschauer: Hände sind verdeckt", 18, Style.MUTED, Rect2(0, 690, 1280, 30), "center")
|
||||
Style.label(hand_layer, "Zuschauer: Hände sind verdeckt", 18, Style.MUTED, Rect2(240, BattleLayout.HAND_Y + 18, 800, 30), "center")
|
||||
else:
|
||||
for i in h.hand.size():
|
||||
var card: Cards.Card = h.hand[i]
|
||||
@@ -874,12 +976,14 @@ func refresh() -> void:
|
||||
var r := BattleLayout.hand_rect(h.hand.size(), i)
|
||||
var v := CardView.new(card, r.size, false, Cards.RARITIES[int(h.looks.get(card.id, 0))])
|
||||
v.position = r.position
|
||||
v.lift = mine
|
||||
apply_forecast(v)
|
||||
if card == pending:
|
||||
v.border = Style.YELLOW
|
||||
elif playable:
|
||||
v.border = Style.GREEN
|
||||
v.dim = not playable
|
||||
v.hover.connect(show_hand_preview)
|
||||
v.hover.connect(func(c, e): show_hand_preview(c, e, Rect2(v.position, v.size)))
|
||||
v.clicked.connect(func(_view): on_click("hand", card))
|
||||
hand_layer.add_child(v)
|
||||
hand_views.append(v)
|
||||
@@ -887,7 +991,11 @@ func refresh() -> void:
|
||||
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))
|
||||
status_panel.add_theme_stylebox_override("panel", Style.box(Style.PANEL, 8, bar_border, 3 if bar_border != null else 0))
|
||||
var board_col := Style.darker(Style.PANEL, 0.8)
|
||||
if g.field_element != "":
|
||||
board_col = board_col.lerp(Cards.ELEMENT_COLORS[g.field_element], 0.22) # das Feld färbt das Brett
|
||||
board_panel.add_theme_stylebox_override("panel", Style.box(board_col, 10))
|
||||
name_label.text = h.name
|
||||
life_label.text = "♥ %d" % maxi(h.life, 0)
|
||||
energy_label.text = "◆ %d" % h.energy
|
||||
@@ -902,18 +1010,10 @@ func refresh() -> void:
|
||||
b_undo.visible = undo_state != null and mine
|
||||
b_fuse.visible = mine and not spectating() and h.board.size() >= 2
|
||||
b_reroll.visible = mine and not spectating()
|
||||
b_reroll.text = tr("Neu würfeln (%d)") % Game.REROLL_COST
|
||||
b_reroll.text = tr("Neu würfeln ◆ %d") % Game.REROLL_COST
|
||||
b_reroll.disabled = h.energy < Game.REROLL_COST
|
||||
b_fuse.text = tr("Abbrechen") if fusing else tr("Verschmelzen")
|
||||
if g.field_element == "":
|
||||
var parts: Array[String] = []
|
||||
for e in Cards.ELEMENTS:
|
||||
parts.append("%s %d" % [tr(e), int(g.field_charge.get(e, 0))])
|
||||
field_label.text = tr("Feld: neutral") + " · " + " ".join(parts)
|
||||
field_label.add_theme_color_override("font_color", Style.MUTED)
|
||||
else:
|
||||
field_label.text = "%s: %s" % [tr(Cards.FIELD_NAMES[g.field_element]), tr(Cards.FIELD_TEXT[g.field_element])]
|
||||
field_label.add_theme_color_override("font_color", Cards.ELEMENT_COLORS[g.field_element])
|
||||
field_view.update_from(g)
|
||||
var lines: Array[String] = []
|
||||
var start := maxi(0, g.log.size() - 60)
|
||||
for i in range(start, g.log.size()):
|
||||
@@ -929,10 +1029,6 @@ func refresh() -> void:
|
||||
log_box.text = "\n".join(lines)
|
||||
pad.draw_cursor()
|
||||
|
||||
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():
|
||||
@@ -951,30 +1047,3 @@ func refresh() -> void:
|
||||
overlay.visible = true
|
||||
confirm.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 ◆ %d (+%d)" % [o.name, o.title, o.life, o.hand.size(), o.energy, 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())
|
||||
|
||||
@@ -12,6 +12,8 @@ var msg_label: Label
|
||||
var b_buy: Button
|
||||
var b_all: Button
|
||||
var slots: Control
|
||||
var fx_layer: Control
|
||||
var fresh := {} # gerade aufgedeckt: wird umgedreht
|
||||
|
||||
|
||||
class CardBack extends Control:
|
||||
@@ -47,15 +49,18 @@ func slot_rect(i: int) -> Rect2:
|
||||
|
||||
|
||||
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.header(self, "Booster", func(): app.show_screen(MenuScreen.new()))
|
||||
coins_label = Style.chip(self, "", Rect2(860, 26, 240, 38))
|
||||
fx_layer = Control.new()
|
||||
fx_layer.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(fx_layer)
|
||||
slots = Control.new()
|
||||
slots.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(slots)
|
||||
msg_label = Style.label(self, "", 17, Style.RED, Rect2(0, 700, 1280, 22), "center")
|
||||
b_buy = Style.button(self, "Booster kaufen (%d Münzen)" % Profile.BOOSTER_COST, Rect2(290, 720, 330, 48), buy, Color("5a4628"))
|
||||
b_all = Style.button(self, "Alle aufdecken", Rect2(640, 720, 200, 48), reveal_all)
|
||||
Style.button(self, "Zurück", Rect2(860, 720, 130, 48), func(): app.show_screen(MenuScreen.new()))
|
||||
msg_label = Style.label(self, "", 17, Style.RED, Rect2(0, 640, 1280, 22), "center")
|
||||
b_buy = Style.button(self, "Booster kaufen (%d Münzen)" % Profile.BOOSTER_COST, Rect2(370, 690, 330, 52), buy, Color("5a4628"))
|
||||
b_buy.add_theme_font_size_override("font_size", 20)
|
||||
b_all = Style.button(self, "Alle aufdecken", Rect2(720, 690, 200, 52), reveal_all)
|
||||
refresh()
|
||||
|
||||
|
||||
@@ -81,16 +86,44 @@ func buy() -> void:
|
||||
|
||||
func reveal(i: int) -> void:
|
||||
revealed[i] = true
|
||||
app.sfx.play("click")
|
||||
fresh = {i: 0.0}
|
||||
app.sfx.play("combo" if cards[i].rarity != "gewöhnlich" else "click")
|
||||
refresh()
|
||||
|
||||
|
||||
func reveal_all() -> void:
|
||||
fresh = {}
|
||||
var k := 0
|
||||
for i in Profile.BOOSTER_SIZE:
|
||||
if not revealed.has(i):
|
||||
fresh[i] = k * 0.12
|
||||
k += 1
|
||||
revealed[i] = true
|
||||
app.sfx.play("play")
|
||||
refresh()
|
||||
|
||||
|
||||
## Umdrehen: die Karte wächst aus der Mitte; seltene Stufen blitzen in ihrer Farbe auf.
|
||||
func flip(v: CardView, delay: float, rarity: String) -> void:
|
||||
if not Style.animations:
|
||||
return
|
||||
v.scale.x = 0.0
|
||||
var tw := create_tween()
|
||||
tw.tween_interval(delay)
|
||||
tw.tween_property(v, "scale:x", 1.0, 0.18).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
|
||||
if rarity == "gewöhnlich":
|
||||
return
|
||||
var burst := Style.panel(fx_layer, Rect2(v.position, v.size).grow(6), Color(Cards.RARITY_COLORS[rarity], 0.6), 12)
|
||||
burst.pivot_offset = burst.size / 2
|
||||
burst.modulate.a = 0.0
|
||||
var bt := create_tween()
|
||||
bt.tween_interval(delay + 0.1)
|
||||
bt.tween_property(burst, "modulate:a", 1.0, 0.05)
|
||||
bt.parallel().tween_property(burst, "scale", Vector2(1.25, 1.25), 0.45).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
||||
bt.tween_property(burst, "modulate:a", 0.0, 0.3)
|
||||
bt.tween_callback(burst.queue_free)
|
||||
|
||||
|
||||
func refresh() -> void:
|
||||
var p: Profile = app.profile
|
||||
coins_label.text = "Münzen: %d" % p.coins
|
||||
@@ -107,6 +140,8 @@ func refresh() -> void:
|
||||
var v := CardView.new(c, r.size, false, res.rarity)
|
||||
v.position = r.position
|
||||
slots.add_child(v)
|
||||
if fresh.has(i):
|
||||
flip(v, fresh[i], res.rarity)
|
||||
Style.label(slots, tr(res.rarity), 16, Cards.RARITY_COLORS[res.rarity], Rect2(r.position.x, r.end.y + 8, r.size.x, 24), "center", true)
|
||||
if res.upgraded:
|
||||
Style.label(slots, "Aufgewertet", 16, Style.GREEN, Rect2(r.position.x, r.end.y + 32, r.size.x, 24), "center", true)
|
||||
@@ -117,3 +152,4 @@ func refresh() -> void:
|
||||
back.position = r.position
|
||||
back.clicked.connect(reveal.bind(i))
|
||||
slots.add_child(back)
|
||||
fresh = {}
|
||||
|
||||
@@ -1,32 +1,37 @@
|
||||
class_name CampaignScreen
|
||||
extends Control
|
||||
## Kampagne als Pfad: zehn Stationen je Kapitel, darunter die gewählte Stufe im Detail.
|
||||
|
||||
var app
|
||||
var chapter := 1
|
||||
var selected := 0 # gewählte Stufe (Index über alle Kapitel)
|
||||
var perk_box: OptionButton
|
||||
var node_buttons: Array = []
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
var p: Profile = app.profile
|
||||
chapter = mini(Campaign.chapter(p.campaign_stage), Campaign.CHAPTERS.size())
|
||||
selected = mini(p.campaign_stage, Campaign.total() - 1)
|
||||
build()
|
||||
|
||||
|
||||
func build() -> void:
|
||||
Style.clear(self)
|
||||
node_buttons = []
|
||||
var p: Profile = app.profile
|
||||
var col := Style.column(self, 1000, 16, 5)
|
||||
Style.clabel(col, "Kampagne", 34, Style.GOLD, "center", true)
|
||||
var tabs := Style.row(col, 10)
|
||||
tabs.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
Style.header(self, "Kampagne", func(): app.show_screen(MenuScreen.new()))
|
||||
var n := Campaign.STAGES.size()
|
||||
var x := 40
|
||||
for c in Campaign.CHAPTERS.size():
|
||||
var b := Style.cbutton(tabs, "Kapitel %d" % (c + 1), Vector2(140, 36), set_chapter.bind(c + 1), Color("285a3c") if chapter == c + 1 else null)
|
||||
b.disabled = p.campaign_stage < c * Campaign.STAGES.size()
|
||||
var b := Style.button(self, "Kapitel %d" % (c + 1), Rect2(x, 88, 150, 38), set_chapter.bind(c + 1), Style.PRIMARY if chapter == c + 1 else null)
|
||||
b.disabled = p.campaign_stage < c * n
|
||||
x += 160
|
||||
if p.perk_unlocked():
|
||||
var pr := Style.row(tabs, 8)
|
||||
Style.clabel(pr, "Dein Merkmal:", 15, Style.MUTED, "right", false, 110)
|
||||
Style.label(self, "Dein Merkmal:", 15, Style.MUTED, Rect2(760, 96, 130, 24), "right")
|
||||
perk_box = OptionButton.new()
|
||||
perk_box.custom_minimum_size = Vector2(300, 36)
|
||||
perk_box.position = Vector2(900, 88)
|
||||
perk_box.size = Vector2(340, 38)
|
||||
perk_box.add_item("keins")
|
||||
var keys: Array = Game.PERKS.keys()
|
||||
for k in keys:
|
||||
@@ -35,54 +40,90 @@ func build() -> void:
|
||||
perk_box.item_selected.connect(func(i: int):
|
||||
p.perk = "" if i == 0 else keys[i - 1]
|
||||
app.save())
|
||||
pr.add_child(perk_box)
|
||||
var offset := (chapter - 1) * Campaign.STAGES.size()
|
||||
for k in Campaign.STAGES.size():
|
||||
add_child(perk_box)
|
||||
# Pfad mit Stationen
|
||||
var offset := (chapter - 1) * n
|
||||
var y := 190.0
|
||||
var step := (1280 - 200) / float(n - 1)
|
||||
var centers: Array[Vector2] = []
|
||||
for k in n:
|
||||
centers.append(Vector2(100 + k * step, y))
|
||||
var path := Control.new()
|
||||
path.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
path.draw.connect(func():
|
||||
for k in n - 1:
|
||||
var done: bool = offset + k + 1 <= p.campaign_stage
|
||||
path.draw_line(centers[k], centers[k + 1], Style.GREEN if done else Style.PANEL_LIGHT, 4.0))
|
||||
add_child(path)
|
||||
for k in n:
|
||||
var i := offset + k
|
||||
var st := Campaign.stage(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 pad := MarginContainer.new() # Innenabstand, damit Name und Knopf nicht am Rahmen kleben
|
||||
for side in ["left", "right"]:
|
||||
pad.add_theme_constant_override("margin_" + side, 14)
|
||||
panel.add_child(pad)
|
||||
var r := Style.row(pad, 10)
|
||||
r.custom_minimum_size = Vector2(0, 50)
|
||||
var nm := Style.clabel(r, "%s · %s" % [st.name, st.title], 16, Style.TEXT if not done else Style.MUTED, "left", true, 420)
|
||||
nm.size_flags_horizontal = Control.SIZE_EXPAND_FILL # schiebt die Punkte und den Knopf nach rechts
|
||||
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 m in elems.size():
|
||||
dots.draw_circle(Vector2(10 + m * 18, dots.size.y / 2), 7, Cards.ELEMENT_COLORS[elems[m]]))
|
||||
r.add_child(dots)
|
||||
if done:
|
||||
Style.cbutton(r, "Wiederholen", Vector2(130, 36), _start.bind(i))
|
||||
elif next:
|
||||
Style.cbutton(r, "Kämpfen", Vector2(130, 36), _start.bind(i), Color("285a3c"))
|
||||
else:
|
||||
var b := Style.cbutton(r, "Gesperrt", Vector2(130, 36), func(): pass)
|
||||
b.disabled = true
|
||||
var b := Button.new()
|
||||
b.text = str(i + 1)
|
||||
b.position = centers[k] - Vector2(24, 24)
|
||||
b.size = Vector2(48, 48)
|
||||
var base: Color = Style.PRIMARY if done else (Color("5a4628") if next else Style.PANEL)
|
||||
var edge: Color = Style.GOLD if i == selected else (Style.GREEN if done else Style.MUTED)
|
||||
b.add_theme_stylebox_override("normal", Style.box(base, 24, edge, 3 if i == selected else 2))
|
||||
b.add_theme_stylebox_override("hover", Style.box(Style.lighter(base), 24, edge, 3))
|
||||
b.add_theme_stylebox_override("pressed", Style.box(Style.darker(base), 24, edge, 3))
|
||||
b.add_theme_stylebox_override("disabled", Style.box(Style.darker(Style.PANEL, 0.8), 24, Style.PANEL_LIGHT, 2))
|
||||
b.add_theme_stylebox_override("focus", Style.box(Color(0, 0, 0, 0), 24, Style.GOLD, 3))
|
||||
b.disabled = i > p.campaign_stage
|
||||
b.pressed.connect(func():
|
||||
selected = i
|
||||
build())
|
||||
Style.juice(b)
|
||||
add_child(b)
|
||||
node_buttons.append(b)
|
||||
if next and Style.animations:
|
||||
b.pivot_offset = b.size / 2
|
||||
var tw := b.create_tween().set_loops()
|
||||
tw.tween_property(b, "scale", Vector2(1.1, 1.1), 0.6).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
|
||||
tw.tween_property(b, "scale", Vector2.ONE, 0.6).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
|
||||
# Gewählte Stufe
|
||||
var st := Campaign.stage(selected)
|
||||
var done := selected < p.campaign_stage
|
||||
var next := selected == p.campaign_stage
|
||||
Style.panel(self, Rect2(240, 270, 800, 300))
|
||||
Style.label(self, "%s · %s" % [st.name, st.title], 28, Style.TEXT, Rect2(270, 290, 740, 40), "center", true)
|
||||
var dots := Control.new()
|
||||
dots.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
var elems: Array = st.elements
|
||||
dots.draw.connect(func():
|
||||
var w := elems.size() * 26 - 6
|
||||
for m in elems.size():
|
||||
dots.draw_circle(Vector2(640 - w / 2.0 + 10 + m * 26, 348), 10, Cards.ELEMENT_COLORS[elems[m]]))
|
||||
add_child(dots)
|
||||
Style.label(self, "%s · %s" % [tr("%d Leben") % int(st.life), tr("%d Münzen") % st.reward], 16, Style.MUTED, Rect2(270, 370, 740, 24), "center")
|
||||
var perks: Array = st.perks
|
||||
var py := 402
|
||||
for k in perks:
|
||||
Style.label(self, "★ " + tr(Game.PERKS[k]), 15, Style.GOLD, Rect2(270, py, 740, 22), "center")
|
||||
py += 24
|
||||
if done:
|
||||
Style.button(self, "Wiederholen", Rect2(540, 500, 200, 50), _start.bind(selected))
|
||||
elif next:
|
||||
var b := Style.button(self, "Kämpfen", Rect2(540, 500, 200, 50), _start.bind(selected), Style.PRIMARY)
|
||||
b.add_theme_font_size_override("font_size", 20)
|
||||
else:
|
||||
var b := Style.button(self, "Gesperrt", Rect2(540, 500, 200, 50), func(): pass)
|
||||
b.disabled = true
|
||||
if p.campaign_stage >= Campaign.total():
|
||||
Style.clabel(col, "Alle Kapitel abgeschlossen", 16, Style.GREEN, "center", true)
|
||||
var br := Style.row(col)
|
||||
br.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
Style.cbutton(br, "Zurück", Vector2(160, 40), func(): app.show_screen(MenuScreen.new()))
|
||||
Style.label(self, "Alle Kapitel abgeschlossen", 16, Style.GREEN, Rect2(0, 600, 1280, 24), "center", true)
|
||||
|
||||
|
||||
func set_chapter(c: int) -> void:
|
||||
chapter = c
|
||||
var p: Profile = app.profile
|
||||
var first := (c - 1) * Campaign.STAGES.size()
|
||||
selected = p.campaign_stage if Campaign.chapter(mini(p.campaign_stage, Campaign.total() - 1)) == c else first
|
||||
build()
|
||||
|
||||
|
||||
func _start(stage: int) -> void:
|
||||
app.show_screen(BattleScreen.new(0, stage))
|
||||
app.show_screen(BattleScreen.new(stage))
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
class_name CardGrid
|
||||
extends ScrollContainer
|
||||
## Scrollbares Kartenraster mit Beschriftung unter jeder Karte.
|
||||
## Scrollbares Kartenraster.
|
||||
|
||||
signal card_clicked(card: Cards.Card)
|
||||
|
||||
const COLS := 7
|
||||
const CW := 100
|
||||
const CH := 140
|
||||
const GAP := 12
|
||||
const COLS := 6
|
||||
const CW := 112
|
||||
const CH := 157
|
||||
const GAP := 14
|
||||
|
||||
var grid: GridContainer
|
||||
|
||||
@@ -24,19 +24,16 @@ func _init(rect: Rect2) -> void:
|
||||
add_child(grid)
|
||||
|
||||
|
||||
## cards: Karten; label_fn(card) -> String; dim_fn(card) -> bool; rarity_fn(card) -> String (Optik)
|
||||
func fill(cards: Array, label_fn: Callable, dim_fn: Callable, rarity_fn: Callable = Callable()) -> void:
|
||||
## cards: Karten; rarity_fn(card) -> String: die Stufe, die die Karte zeigt (Optik)
|
||||
func fill(cards: Array, rarity_fn: Callable) -> void:
|
||||
Style.clear(grid)
|
||||
for c in cards:
|
||||
var cell := Control.new()
|
||||
cell.custom_minimum_size = Vector2(CW, CH + 16)
|
||||
cell.custom_minimum_size = Vector2(CW, CH)
|
||||
cell.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
var v := CardView.new(c, Vector2(CW, CH), true, rarity_fn.call(c) if rarity_fn.is_valid() else "gewöhnlich")
|
||||
v.dim = dim_fn.call(c)
|
||||
var v := CardView.new(c, Vector2(CW, CH), true, rarity_fn.call(c))
|
||||
v.clicked.connect(func(_view): card_clicked.emit(c))
|
||||
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)
|
||||
|
||||
|
||||
|
||||
@@ -1,25 +1,37 @@
|
||||
class_name CardView
|
||||
extends Control
|
||||
## Zeichnet eine Karte (Hand, Sammlung, Booster, Vorschau).
|
||||
## Zeichnet eine Karte (Laden, Hand, Sammlung, Booster, Vorschau).
|
||||
## Aufbau wie bei Hearthstone: Kostenkristall oben links, Bild, Namensband, Schlüsselwörter als
|
||||
## Symbole, Effekttext, Angriff und Leben als Kreise unten. Das Element steht im Hintergrund,
|
||||
## die Seltenheit in Rahmen, Glanz und Edelstein. Sehr flache Karten (Hand) zeigen nur das Nötigste.
|
||||
|
||||
signal clicked(view: CardView)
|
||||
signal hover(card: Cards.Card, entered: bool)
|
||||
|
||||
var card: Cards.Card
|
||||
var border = null # Color oder null (dann Seltenheitsfarbe)
|
||||
var rarity := "gewöhnlich" # nur Optik: Rahmenfarbe der eigenen Stufe
|
||||
var border = null # Color oder null (dann Rahmen der Seltenheitsstufe)
|
||||
var rarity := "gewöhnlich" # nur Optik
|
||||
var dim := false
|
||||
var bonus := 0 # Vorschau im Kampf: Angriff, den die Karte auf dem Feld dazubekäme
|
||||
var tag := "" # Vorschau im Kampf: Feldkombo, die das Ausspielen auslöst
|
||||
var lift := false # hebt sich beim Zeigen an (Hand und Laden)
|
||||
|
||||
|
||||
## focusable: per Gamepad oder Tab anwählbar (Sammlung, Booster); A bzw. Enter wirkt wie ein Klick.
|
||||
func _init(c: Cards.Card, sz: Vector2, focusable: bool = false, rar: String = "gewöhnlich") -> void:
|
||||
card = c
|
||||
rarity = rar
|
||||
set_process(Style.rarity_animated(rarity)) # bewegter Glanz
|
||||
custom_minimum_size = sz
|
||||
size = sz
|
||||
pivot_offset = sz / 2
|
||||
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
mouse_entered.connect(func(): hover.emit(card, true))
|
||||
mouse_exited.connect(func(): hover.emit(card, false))
|
||||
mouse_entered.connect(func():
|
||||
hover.emit(card, true)
|
||||
_lift(true))
|
||||
mouse_exited.connect(func():
|
||||
hover.emit(card, false)
|
||||
_lift(false))
|
||||
if focusable:
|
||||
focus_mode = Control.FOCUS_ALL
|
||||
focus_entered.connect(func():
|
||||
@@ -30,6 +42,17 @@ func _init(c: Cards.Card, sz: Vector2, focusable: bool = false, rar: String = "g
|
||||
queue_redraw())
|
||||
|
||||
|
||||
func _lift(up: bool) -> void:
|
||||
if not lift or not Style.animations:
|
||||
return
|
||||
z_index = 1 if up else 0
|
||||
create_tween().tween_property(self, "scale", Vector2(1.07, 1.07) if up else Vector2.ONE, 0.1).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _gui_input(event: InputEvent) -> void:
|
||||
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||
accept_event()
|
||||
@@ -41,47 +64,135 @@ func _gui_input(event: InputEvent) -> void:
|
||||
|
||||
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)
|
||||
var seed := hash(card.id)
|
||||
var frame: Color = border if border != null else Style.rarity_frame(rarity, seed)
|
||||
if rarity != "gewöhnlich":
|
||||
Style.glow(self, rect, Style.rarity_color(rarity, seed), 8, 8 if size.x >= 180 else 6)
|
||||
Style.fill_elements(self, rect, card.elements, 8)
|
||||
var bcol: Color = border if border != null else Cards.RARITY_COLORS[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 name_left := 2 * r + 10
|
||||
var nm := Style.fit(card.title(), true, f_name, size.x - name_left - 6)
|
||||
Style.text(self, nm, f_name, Style.TEXT, Vector2((name_left + size.x - 6) / 2.0, 4), "midtop", true)
|
||||
if mid:
|
||||
var sub: String = tr(card.combo()) if card.is_combo() else ""
|
||||
Style.text(self, sub, f_text, Color("e6e6e6"), Vector2(size.x / 2, 2 * r + 4), "midtop")
|
||||
# Textfeld: Höhe folgt dem umbrochenen Text und der Schriftgröße, das Bild bekommt den Rest
|
||||
var tsize := Style.fs(f_text)
|
||||
var text_w: float = size.x - 20
|
||||
var needed: float = Style.FONT.get_multiline_string_size(card.text(), HORIZONTAL_ALIGNMENT_LEFT, text_w, tsize).y + 8 if card.text() != "" else size.y * 0.42
|
||||
var box_bottom: float = size.y - (28 if big else 22)
|
||||
var band_top: float = 2 * r + 4 + (f_text + 6 if mid else 0) # unter Name und Kombo
|
||||
var box_top: float = maxf(maxf(size.y * 0.30, band_top), box_bottom - maxf(needed, size.y * 0.42))
|
||||
var box := Rect2(6, box_top, size.x - 12, box_bottom - box_top)
|
||||
# Bild
|
||||
var band_bottom: float = box_top - 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)
|
||||
draw_rect(box, Color(0.06, 0.06, 0.09, 0.67))
|
||||
var max_lines := maxi(1, int((box.size.y - 6) / Style.FONT.get_height(tsize))) # große Schrift: lieber kürzen als überlaufen
|
||||
draw_multiline_string(Style.FONT, Vector2(box.position.x + 4, box.position.y + 4 + Style.FONT.get_ascent(tsize)),
|
||||
card.text(), HORIZONTAL_ALIGNMENT_LEFT, box.size.x - 8, tsize, max_lines, Style.TEXT)
|
||||
# Werte
|
||||
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 size.y < 90:
|
||||
_draw_compact(rect, frame, seed)
|
||||
else:
|
||||
_draw_full(rect, frame, seed)
|
||||
if dim:
|
||||
draw_rect(rect, Color(0, 0, 0, 0.47))
|
||||
if has_focus():
|
||||
Style.border(self, rect, Style.GOLD, 4, 8)
|
||||
|
||||
|
||||
## Volle Karte: Laden, Sammlung, Booster, Vorschau.
|
||||
func _draw_full(rect: Rect2, frame: Color, seed: int) -> void:
|
||||
var big := size.x >= 180
|
||||
var mid := size.x >= 140
|
||||
var f_name := 17 if big else (14 if mid else 11)
|
||||
var f_text := 13 if big else (11 if mid else 10)
|
||||
var f_stat := 20 if big else (17 if mid else 14)
|
||||
var r_cost := 17.0 if big else (14.0 if mid else 12.0)
|
||||
var r_stat := 18.0 if big else (15.0 if mid else 12.0)
|
||||
var icon := 11.0 if big else (9.0 if mid else 7.0)
|
||||
# Bild: das Elementsymbol füllt den oberen Teil
|
||||
var band_top: float = size.y * 0.36
|
||||
Style.glyph(self, card, Vector2(size.x / 2, band_top * 0.56), minf(size.x * 0.62, band_top * 0.95), 0.4)
|
||||
Style.rarity_sheen(self, rect, rarity, seed)
|
||||
# Namensband über die volle Breite
|
||||
var band_h: float = Style.fs(f_name) + 10
|
||||
var band := Rect2(0, band_top, size.x, band_h)
|
||||
draw_rect(band, Color(0.05, 0.05, 0.08, 0.82))
|
||||
var nf := Style.fit_size(card.title(), true, f_name, size.x - 12)
|
||||
Style.text(self, Style.fit(card.title(), true, Style.fs(nf), size.x - 12), nf, Style.TEXT, Vector2(size.x / 2, band.get_center().y), "center", true)
|
||||
var y := band.end.y + 4
|
||||
# Kombo eines verschmolzenen Wesens
|
||||
if card.is_combo():
|
||||
Style.text(self, tr(card.combo()), f_text, Color("e6e6e6"), Vector2(size.x / 2, y), "midtop")
|
||||
y += Style.fs(f_text) + 4
|
||||
# Schlüsselwörter als Symbole, auf großen Karten mit Wort
|
||||
var kws: Array[String] = []
|
||||
for k in Cards.KEYWORDS:
|
||||
if k in card.keywords:
|
||||
kws.append(k)
|
||||
if not kws.is_empty():
|
||||
var with_words := big
|
||||
var widths: Array[float] = []
|
||||
var total := 0.0
|
||||
for k in kws:
|
||||
var w: float = icon * 2 + 6 + (Style.FONT.get_string_size(tr(k), HORIZONTAL_ALIGNMENT_LEFT, -1, Style.fs(f_text)).x + 8 if with_words else 0)
|
||||
widths.append(w)
|
||||
total += w + 6
|
||||
if with_words and total > size.x - 12:
|
||||
with_words = false
|
||||
total = kws.size() * (icon * 2 + 12)
|
||||
var x := (size.x - total + 6) / 2
|
||||
for i in kws.size():
|
||||
var k := kws[i]
|
||||
Style.keyword_icon(self, k, Vector2(x + icon, y + icon + 2), icon)
|
||||
if with_words:
|
||||
Style.text(self, tr(k), f_text, Style.TEXT, Vector2(x + icon * 2 + 6, y + icon + 2), "midleft")
|
||||
x += widths[i] + 6
|
||||
else:
|
||||
x += icon * 2 + 12
|
||||
y += icon * 2 + 8
|
||||
# Effekttext: so hoch wie nötig, endet über den Wertkreisen
|
||||
var box := Rect2()
|
||||
var fx := card.effect_text()
|
||||
var box_bottom: float = size.y - r_stat * 2 - 4
|
||||
if fx != "" and box_bottom - y > Style.fs(f_text) + 6:
|
||||
var tsize := Style.fs(f_text)
|
||||
var heading := big
|
||||
var needed: float = Style.FONT.get_multiline_string_size(fx, HORIZONTAL_ALIGNMENT_LEFT, size.x - 20, tsize).y + 8 + (tsize + 2 if heading else 0)
|
||||
var box_top: float = maxf(y, y + (box_bottom - y - needed) / 2)
|
||||
box = Rect2(6, box_top, size.x - 12, box_bottom - box_top)
|
||||
draw_rect(box, Color(0.06, 0.06, 0.09, 0.72))
|
||||
var ty := box.position.y + 4
|
||||
if heading:
|
||||
Style.text(self, tr("Beim Ausspielen"), f_text - 2, Style.MUTED, Vector2(box.position.x + 4, ty), "topleft")
|
||||
ty += tsize + 2
|
||||
var max_lines := maxi(1, int((box.end.y - ty - 2) / Style.FONT.get_height(tsize)))
|
||||
draw_multiline_string(Style.FONT, Vector2(box.position.x + 4, ty + Style.FONT.get_ascent(tsize)),
|
||||
fx, HORIZONTAL_ALIGNMENT_LEFT, box.size.x - 8, tsize, max_lines, Style.TEXT)
|
||||
# Werte springen aus den unteren Ecken, dazwischen der Edelstein
|
||||
Style.stat_circle(self, Vector2(r_stat - 2, size.y - r_stat + 2), r_stat, str(card.attack), Style.AMBER, f_stat)
|
||||
Style.stat_circle(self, Vector2(size.x - r_stat + 2, size.y - r_stat + 2), r_stat, str(card.health), Style.CRIMSON, f_stat)
|
||||
if bonus > 0:
|
||||
Style.text_outlined(self, "+%d" % bonus, f_text, Style.GREEN, Vector2(r_stat * 2 + 4, size.y - r_stat + 2), "midleft", 3)
|
||||
Style.rarity_gem(self, rarity, Vector2(size.x / 2, size.y - r_stat + 1), 8 if big else 6, seed)
|
||||
Style.border(self, rect, frame, 3 if big else 2, 8)
|
||||
Style.cost_crystal(self, Vector2(r_cost + 2, r_cost + 2), r_cost, card.cost, f_stat)
|
||||
if tag != "":
|
||||
_draw_tag(Vector2(size.x - 6, 6), f_text - 1)
|
||||
Style.rarity_sparkle(self, rect, rarity, seed, box)
|
||||
|
||||
|
||||
## Flache Handkachel: Kosten, Name, Symbole, Werte.
|
||||
func _draw_compact(rect: Rect2, frame: Color, seed: int) -> void:
|
||||
Style.rarity_sheen(self, rect, rarity, seed)
|
||||
Style.border(self, rect, frame, 2, 8)
|
||||
Style.cost_crystal(self, Vector2(13, 13), 11, card.cost, 13)
|
||||
var name_w: float = size.x - 30 - (12 if rarity != "gewöhnlich" else 0)
|
||||
var nf := Style.fit_size(card.title(), true, 12, name_w)
|
||||
Style.text(self, Style.fit(card.title(), true, Style.fs(nf), name_w), nf, Style.TEXT, Vector2(28, 13), "midleft", true)
|
||||
Style.rarity_gem(self, rarity, Vector2(size.x - 9, 13), 5, seed)
|
||||
if tag != "":
|
||||
var w := Style.FONT_BOLD.get_string_size(tr(tag), HORIZONTAL_ALIGNMENT_LEFT, -1, Style.fs(9)).x + 12
|
||||
_draw_tag(Vector2(size.x / 2 + w / 2, size.y - 21), 9)
|
||||
else:
|
||||
var kws: Array[String] = []
|
||||
for k in Cards.KEYWORDS:
|
||||
if k in card.keywords:
|
||||
kws.append(k)
|
||||
var x := size.x / 2 - (kws.size() * 18 - 4) / 2.0
|
||||
for k in kws:
|
||||
Style.keyword_icon(self, k, Vector2(x + 6, size.y - 13), 6)
|
||||
x += 18
|
||||
Style.stat_circle(self, Vector2(11, size.y - 11), 10, str(card.attack), Style.AMBER, 12)
|
||||
Style.stat_circle(self, Vector2(size.x - 11, size.y - 11), 10, str(card.health), Style.CRIMSON, 12)
|
||||
if bonus > 0:
|
||||
Style.text_outlined(self, "+%d" % bonus, 10, Style.GREEN, Vector2(4, size.y - 29), "midleft", 3)
|
||||
Style.rarity_sparkle(self, rect, rarity, seed)
|
||||
|
||||
|
||||
## Goldenes Schild mit der Feldkombo, rechts oben.
|
||||
func _draw_tag(top_right: Vector2, font: int) -> void:
|
||||
var w := Style.FONT_BOLD.get_string_size(tr(tag), HORIZONTAL_ALIGNMENT_LEFT, -1, Style.fs(font)).x + 12
|
||||
var h := Style.fs(font) + 6
|
||||
var r := Rect2(top_right - Vector2(w, 0), Vector2(w, h))
|
||||
draw_style_box(Style.box(Color("14141c"), 6, Style.GOLD, 1), r)
|
||||
Style.text(self, tr(tag), font, Style.GOLD, r.get_center(), "center", true)
|
||||
|
||||
@@ -18,33 +18,32 @@ var b_upgrade: Button
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
Style.label(self, "Sammlung", 30, Style.GOLD, Rect2(20, 8, 400, 40), "left", true)
|
||||
Style.button(self, "Alle", Rect2(20, 55, 60, 34), set_filter.bind(""))
|
||||
Style.header(self, "Sammlung", func(): app.show_screen(MenuScreen.new()))
|
||||
Style.button(self, "Alle", Rect2(40, 86, 60, 34), set_filter.bind(""))
|
||||
for i in Cards.ELEMENTS.size():
|
||||
var e: String = Cards.ELEMENTS[i]
|
||||
var fb := Style.button(self, e, Rect2(88 + i * 82, 55, 78, 34), set_filter.bind(e), Style.darker(Cards.ELEMENT_COLORS[e], 0.8))
|
||||
var fb := Style.button(self, e, Rect2(108 + i * 82, 86, 78, 34), set_filter.bind(e), Style.darker(Cards.ELEMENT_COLORS[e], 0.8))
|
||||
fb.add_theme_font_size_override("font_size", 14)
|
||||
search_box = LineEdit.new()
|
||||
search_box.position = Vector2(424, 55)
|
||||
search_box.size = Vector2(200, 34)
|
||||
search_box.position = Vector2(444, 86)
|
||||
search_box.size = Vector2(220, 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 = CardGrid.new(Rect2(40, 132, CardGrid.COLS * (CardGrid.CW + CardGrid.GAP) + 10, 650))
|
||||
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)
|
||||
Style.panel(self, Rect2(820, 86, 420, 696))
|
||||
dust_label = Style.chip(self, "", Rect2(860, 100, 340, 36), Style.BLUE)
|
||||
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_upgrade = Style.button(self, "", Rect2(860, 480, 390, 44), upgrade, Color("285a3c"))
|
||||
b_dismantle = Style.button(self, "", Rect2(860, 534, 390, 44), dismantle, Color("5a3c28"))
|
||||
msg_label = Style.label(self, "", 15, Style.GREEN, Rect2(860, 600, 390, 48), "center")
|
||||
Style.button(self, "Zurück", Rect2(1150, 740, 110, 44), func(): app.show_screen(MenuScreen.new()))
|
||||
info_label = Style.label(self, "", 15, Style.MUTED, Rect2(840, 476, 380, 30), "center")
|
||||
b_upgrade = Style.button(self, "", Rect2(840, 530, 380, 46), upgrade, Style.PRIMARY)
|
||||
b_dismantle = Style.button(self, "", Rect2(840, 586, 380, 46), dismantle, Color("5a3c28"))
|
||||
msg_label = Style.label(self, "", 15, Style.GREEN, Rect2(840, 650, 380, 48), "center")
|
||||
refresh()
|
||||
|
||||
|
||||
@@ -58,6 +57,7 @@ func set_filter(e: String) -> void:
|
||||
refresh()
|
||||
|
||||
|
||||
## Aufgewertete Karten zuerst, sonst in Kartenreihenfolge.
|
||||
func cards() -> Array:
|
||||
var out: Array = Cards.all().duplicate()
|
||||
if filter != "":
|
||||
@@ -65,6 +65,11 @@ func cards() -> Array:
|
||||
if search != "":
|
||||
var q := search.to_lower()
|
||||
out = out.filter(func(c): return q in c.name.to_lower() or q in c.title().to_lower() or q in c.text().to_lower() or q in c.combo().to_lower())
|
||||
var p: Profile = app.profile
|
||||
var order := {}
|
||||
for i in out.size():
|
||||
order[out[i].id] = i
|
||||
out.sort_custom(func(a, b): return p.level(a.id) > p.level(b.id) if p.level(a.id) != p.level(b.id) else order[a.id] < order[b.id])
|
||||
return out
|
||||
|
||||
|
||||
@@ -109,7 +114,7 @@ func dismantle() -> void:
|
||||
func refresh() -> void:
|
||||
var p: Profile = app.profile
|
||||
dust_label.text = "Staub: %d" % p.dust
|
||||
grid.fill(cards(), func(c): return tr(p.rarity(c.id)), func(_c): return false, func(c): return p.rarity(c.id))
|
||||
grid.fill(cards(), func(c): return p.rarity(c.id))
|
||||
Style.clear(preview_holder)
|
||||
if selected == null:
|
||||
info_label.text = "Keine Karte gewählt"
|
||||
@@ -118,8 +123,8 @@ func refresh() -> void:
|
||||
b_upgrade.disabled = true
|
||||
b_dismantle.disabled = true
|
||||
return
|
||||
var v := CardView.new(selected, Vector2(200, 280), false, p.rarity(selected.id))
|
||||
v.position = Vector2(955, 100)
|
||||
var v := CardView.new(selected, Vector2(220, 308), false, p.rarity(selected.id))
|
||||
v.position = Vector2(920, 152)
|
||||
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
preview_holder.add_child(v)
|
||||
var lvl := p.level(selected.id)
|
||||
|
||||
@@ -11,6 +11,8 @@ var width := 2
|
||||
var flash := false # kurz rot: wurde gerade angegriffen
|
||||
var slot: Vector2 # Zielposition im Raster
|
||||
var busy := false # wird gerade animiert (kein Neupositionieren)
|
||||
var mark := "" # Feldkombo, die dieses Wesen als Partner auslösen würde
|
||||
var matchup := 0 # Elementvorteil des gewählten Angreifers gegen dieses Wesen (+1/-1)
|
||||
|
||||
|
||||
func _init(cr: Game.Creature, sz: Vector2, b = null, w: int = 2) -> void:
|
||||
@@ -20,58 +22,93 @@ func _init(cr: Game.Creature, sz: Vector2, b = null, w: int = 2) -> void:
|
||||
custom_minimum_size = sz
|
||||
size = sz
|
||||
pivot_offset = sz / 2
|
||||
set_process(Style.rarity_animated(rarity())) # bewegter Glanz
|
||||
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 _process(_delta: float) -> void:
|
||||
queue_redraw()
|
||||
|
||||
|
||||
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 rarity() -> String:
|
||||
var owner = creature.owner()
|
||||
return Cards.RARITIES[int(owner.looks.get(creature.card.id, 0)) if owner != null else 0]
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
var rect := Rect2(Vector2.ZERO, size)
|
||||
var small := size.x < 100
|
||||
var mini := size.x < 60
|
||||
var rar := rarity()
|
||||
var seed := hash(creature.card.id) + creature.uid
|
||||
if rar != "gewöhnlich":
|
||||
Style.glow(self, rect, Style.rarity_color(rar, seed), 6, 4 if mini else 6)
|
||||
Style.fill_elements(self, rect, creature.card.elements, 6)
|
||||
var owner = creature.owner()
|
||||
var lvl: int = int(owner.looks.get(creature.card.id, 0)) if owner != null else 0
|
||||
var bcol: Color = border if border != null else Cards.RARITY_COLORS[Cards.RARITIES[lvl]]
|
||||
if not mini:
|
||||
Style.glyph(self, creature.card, Vector2(size.x / 2, size.y * 0.5), size.x * 0.75, 0.3)
|
||||
Style.rarity_sheen(self, rect, rar, seed)
|
||||
var bcol: Color = border if border != null else Style.rarity_frame(rar, seed)
|
||||
if flash:
|
||||
bcol = Style.RED
|
||||
Style.border(self, rect, bcol, 4 if flash else width, 6)
|
||||
if mark != "":
|
||||
Style.border(self, rect.grow(3), Style.GOLD, 3, 8)
|
||||
if mini:
|
||||
_draw_mini()
|
||||
Style.rarity_sparkle(self, rect, rar, seed)
|
||||
return
|
||||
var fs := 11 if small else 13
|
||||
var nm := Style.fit(creature.card.title(), 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")
|
||||
if creature.field_bonus > 0:
|
||||
badges.append("Feld")
|
||||
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 + creature.field_bonus > 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)
|
||||
var name_w: float = size.x - (30 if matchup != 0 else 6) # Platz für den Vorteilskreis oben rechts
|
||||
var nf := Style.fit_size(creature.card.title(), true, fs, name_w)
|
||||
draw_rect(Rect2(2, 2, size.x - 4, Style.fs(fs) + 8), Color(0.05, 0.05, 0.08, 0.7))
|
||||
Style.text(self, Style.fit(creature.card.title(), true, Style.fs(nf), name_w), nf, Style.TEXT, Vector2(3 + name_w / 2, 6), "midtop", true)
|
||||
# Schlüsselwörter als Symbole
|
||||
var icon := 7.0 if small else 9.0
|
||||
var kws: Array[String] = []
|
||||
for k in Cards.KEYWORDS:
|
||||
if k in creature.card.keywords:
|
||||
kws.append(k)
|
||||
if creature.shield and not ("Schild" in kws):
|
||||
kws.append("Schild")
|
||||
var x := size.x / 2 - (kws.size() * (icon * 2 + 6) - 6) / 2.0
|
||||
var y := Style.fs(fs) + 14 + icon
|
||||
for k in kws:
|
||||
Style.keyword_icon(self, k, Vector2(x + icon, y), icon)
|
||||
x += icon * 2 + 6
|
||||
_draw_stats(15 if small else 17, 10 if small else 12, 12.0 if small else 14.0)
|
||||
Style.rarity_gem(self, rar, Vector2(size.x / 2, size.y - 9), 5, seed)
|
||||
if mark != "":
|
||||
var w := Style.FONT_BOLD.get_string_size(tr(mark), HORIZONTAL_ALIGNMENT_LEFT, -1, Style.fs(11)).x + 14
|
||||
draw_style_box(Style.box(Color("14141c"), 6, Style.GOLD, 1), Rect2(size.x / 2 - w / 2, -9, w, 18))
|
||||
Style.text(self, tr(mark), 11, Style.GOLD, Vector2(size.x / 2, 0), "center", true)
|
||||
if creature.frozen:
|
||||
draw_rect(rect, Color(0.55, 0.78, 1.0, 0.35))
|
||||
Style.text_outlined(self, "❄", 22 if small else 28, Color("dff2ff"), size / 2, "center", 3)
|
||||
Style.rarity_sparkle(self, rect, rar, seed)
|
||||
|
||||
|
||||
## Angriff (grün mit „+n“, wenn Resonanz, Feld oder Merkmale ihn heben), Leben, Elementvorteil.
|
||||
func _draw_stats(f: int, f_small: int, r: float) -> void:
|
||||
var extra := creature.power() - creature.attack
|
||||
var atk_col := Style.GREEN if extra > 0 else Style.AMBER
|
||||
Style.stat_circle(self, Vector2(r - 1, size.y - r + 1), r, str(creature.power()), atk_col, f)
|
||||
if extra > 0:
|
||||
Style.text_outlined(self, "+%d" % extra, f_small, Style.GREEN, Vector2(r * 2 + 3, size.y - r + 1), "midleft", 3)
|
||||
var hp_col := Style.CRIMSON if creature.health < creature.max_health else Color("b84a4a")
|
||||
Style.stat_circle(self, Vector2(size.x - r + 1, size.y - r + 1), r, str(creature.health), hp_col, f)
|
||||
if matchup != 0:
|
||||
var c := Vector2(size.x - 12, 12)
|
||||
draw_circle(c, 11, Color("14141c"))
|
||||
Style.text(self, ("+%d" % matchup) if matchup > 0 else ("−%d" % -matchup), f_small + 1,
|
||||
Style.GREEN if matchup > 0 else Style.RED, c, "center", true)
|
||||
|
||||
|
||||
## Sehr kleine Kacheln (4 oder 5 Gegner): nur Kürzel und Werte, Details per Vorschau oder Lupe.
|
||||
@@ -84,15 +121,14 @@ func _draw_mini() -> void:
|
||||
if creature.attacks_per_turn() == 2:
|
||||
letters.append("2")
|
||||
if creature.frozen:
|
||||
letters.append("E")
|
||||
if creature.resonance > 0:
|
||||
letters.append("R")
|
||||
if creature.field_bonus > 0:
|
||||
letters.append("F")
|
||||
letters.append("❄")
|
||||
Style.text(self, " ".join(letters), 10, Color("ebebf5"), Vector2(size.x / 2, 3), "midtop", true)
|
||||
var atk_col := Style.GREEN if creature.resonance + creature.field_bonus > 0 else Style.YELLOW
|
||||
Style.text(self, str(creature.power()), 13, atk_col, Vector2(4, size.y - 2), "bottomleft", true)
|
||||
var extra := creature.power() - creature.attack
|
||||
Style.text_outlined(self, str(creature.power()), 13, Style.GREEN if extra > 0 else Style.YELLOW, Vector2(9, size.y - 9), "center", 3)
|
||||
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)
|
||||
Style.text_outlined(self, str(creature.health), 13, hp_col, Vector2(size.x - 9, size.y - 9), "center", 3)
|
||||
if matchup != 0:
|
||||
Style.text(self, ("+%d" % matchup) if matchup > 0 else ("−%d" % -matchup), 10,
|
||||
Style.GREEN if matchup > 0 else Style.RED, Vector2(size.x - 3, 2), "topright", true)
|
||||
if creature.frozen:
|
||||
draw_rect(Rect2(Vector2.ZERO, size), Color(0.55, 0.78, 1.0, 0.35))
|
||||
|
||||
53
scripts/ui/field_view.gd
Normal file
53
scripts/ui/field_view.gd
Normal file
@@ -0,0 +1,53 @@
|
||||
class_name FieldView
|
||||
extends Control
|
||||
## Das gemeinsame Feld: je Element vier Ladepunkte, bei vollen Punkten kippt das Feld.
|
||||
## Das aktive Feld steht farbig mit seinem Bonus da.
|
||||
|
||||
var element := ""
|
||||
var charge := {}
|
||||
|
||||
|
||||
func _init(rect: Rect2) -> void:
|
||||
position = rect.position
|
||||
size = rect.size
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
|
||||
|
||||
func update_from(g: Game) -> void:
|
||||
element = g.field_element
|
||||
charge = g.field_charge.duplicate()
|
||||
queue_redraw()
|
||||
|
||||
|
||||
## Der Inhalt als Text (Tests, Protokoll).
|
||||
func text() -> String:
|
||||
var parts: Array[String] = []
|
||||
for e in Cards.ELEMENTS:
|
||||
if e == element:
|
||||
parts.append("%s: +1 %s" % [tr(Cards.FIELD_NAMES[e]), tr("Angriff")])
|
||||
else:
|
||||
parts.append("%s %d/%d" % [tr(e), int(charge.get(e, 0)), Cards.FIELD_CHARGE])
|
||||
return " · ".join(parts)
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
var w := size.x / Cards.ELEMENTS.size()
|
||||
for i in Cards.ELEMENTS.size():
|
||||
var e: String = Cards.ELEMENTS[i]
|
||||
var col: Color = Cards.ELEMENT_COLORS[e]
|
||||
var x0 := i * w
|
||||
var cy := size.y / 2
|
||||
if e == element:
|
||||
draw_style_box(Style.box(Color(col, 0.35), 6, col, 2), Rect2(x0 + 2, 0, w - 4, size.y))
|
||||
Style.text(self, "%s +1 %s" % [tr(Cards.FIELD_NAMES[e]), tr("Angriff")], 13, Style.lighter(col, 0.4), Vector2(x0 + w / 2, cy), "center", true)
|
||||
continue
|
||||
Style.text(self, tr(e), 13, Style.MUTED, Vector2(x0 + 12, cy), "midleft")
|
||||
var n := int(charge.get(e, 0))
|
||||
var px := x0 + 12 + Style.FONT.get_string_size(tr(e), HORIZONTAL_ALIGNMENT_LEFT, -1, Style.fs(13)).x + 14
|
||||
for k in Cards.FIELD_CHARGE:
|
||||
var c := Vector2(px + k * 13, cy)
|
||||
if k < n:
|
||||
draw_circle(c, 4.5, col)
|
||||
else:
|
||||
draw_circle(c, 4.5, Color(0, 0, 0, 0.35))
|
||||
draw_arc(c, 4.5, 0, TAU, 16, Color(col, 0.6), 1.0)
|
||||
1
scripts/ui/field_view.gd.uid
Normal file
1
scripts/ui/field_view.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://i4rdq5rmcyuk
|
||||
@@ -11,8 +11,6 @@ var b_host: Button
|
||||
var b_join: Button
|
||||
var b_watch: Button
|
||||
var b_start: Button
|
||||
var ai_buttons: Array = []
|
||||
var n_ai := 0
|
||||
var addr_label: Label
|
||||
|
||||
|
||||
@@ -20,9 +18,8 @@ func _ready() -> void:
|
||||
net = app.net
|
||||
net.my_name = "Spieler"
|
||||
net.my_pick = {"looks": app.profile.looks()}
|
||||
var col := Style.column(self, 760, 40, 10)
|
||||
Style.clabel(col, "Netzwerkspiel", 36, Style.GOLD, "center", true)
|
||||
Style.spacer(col, 10)
|
||||
Style.header(self, "Netzwerkspiel", _back)
|
||||
var col := Style.column(self, 760, 110, 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"))
|
||||
@@ -38,17 +35,10 @@ func _ready() -> void:
|
||||
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", 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())
|
||||
@@ -79,13 +69,8 @@ func _watch() -> void:
|
||||
refresh()
|
||||
|
||||
|
||||
func _set_ai(n: int) -> void:
|
||||
n_ai = n
|
||||
refresh()
|
||||
|
||||
|
||||
func _start() -> void:
|
||||
net.start_battle(n_ai)
|
||||
net.start_battle()
|
||||
|
||||
|
||||
func _started(seat: int) -> void:
|
||||
@@ -112,9 +97,8 @@ func refresh() -> void:
|
||||
b_watch.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
|
||||
b_start.disabled = not net.is_host
|
||||
b_start.text = tr("Start") if humans >= 2 else tr("Start gegen die KI")
|
||||
addr_label.text = ("Deine Adressen: " + ", ".join(net.local_addresses())) if net.is_host else ""
|
||||
|
||||
|
||||
|
||||
@@ -32,13 +32,13 @@ func apply_settings() -> void:
|
||||
sfx.set_music_volume(profile.settings.music)
|
||||
set_language(profile.settings.language)
|
||||
Style.font_scale = 1.2 if profile.settings.get("big_font", false) else 1.0
|
||||
Style.animations = bool(profile.settings.get("animations", true))
|
||||
theme = Style.make_theme()
|
||||
|
||||
|
||||
## Neues Profil: Sammlung und Fortschritt werden verworfen.
|
||||
func reset_profile() -> void:
|
||||
profile = Profile.starter()
|
||||
clear_battle_save()
|
||||
save()
|
||||
apply_settings()
|
||||
|
||||
@@ -54,6 +54,9 @@ func show_screen(s: Control) -> void:
|
||||
s.app = self
|
||||
s.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
add_child(s)
|
||||
if Style.animations:
|
||||
s.modulate.a = 0.0
|
||||
create_tween().tween_property(s, "modulate:a", 1.0, 0.15)
|
||||
if pad_mode:
|
||||
focus_first_button.call_deferred()
|
||||
|
||||
@@ -107,16 +110,6 @@ func save() -> void:
|
||||
profile.save(save_path)
|
||||
|
||||
|
||||
## Datei für einen unterbrochenen Kampf (neben dem Spielstand).
|
||||
func battle_save_path() -> String:
|
||||
return save_path.get_basename() + "_kampf.json"
|
||||
|
||||
|
||||
func clear_battle_save() -> void:
|
||||
if FileAccess.file_exists(battle_save_path()):
|
||||
DirAccess.remove_absolute(ProjectSettings.globalize_path(battle_save_path()))
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
if net != null and net.is_inside_tree():
|
||||
net.leave()
|
||||
|
||||
@@ -1,53 +1,55 @@
|
||||
class_name MenuScreen
|
||||
extends Control
|
||||
## Hauptmenü: links Titel und Spielarten, rechts drei Karten der eigenen Sammlung als Schaufenster.
|
||||
|
||||
var app
|
||||
var b_resume: Button
|
||||
var coins_label: Label
|
||||
var coll_label: Label
|
||||
var notice_label: Label
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
var col := Style.column(self, 640, 40, 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)
|
||||
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)
|
||||
if FileAccess.file_exists(app.battle_save_path()):
|
||||
b_resume = Style.cbutton(grid, "Kampf fortsetzen", sz, _resume, Color("5a4628"))
|
||||
Style.cbutton(grid, "Einzelspieler", sz, func(): app.show_screen(OpponentSelectScreen.new()), Color("285a3c"))
|
||||
Style.cbutton(grid, "Kampagne", sz, func(): app.show_screen(CampaignScreen.new()), Color("285a3c"))
|
||||
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, "Sammlung", sz, func(): app.show_screen(CollectionScreen.new()))
|
||||
Style.cbutton(grid, "Einstellungen", sz, func(): app.show_screen(SettingsScreen.new()))
|
||||
Style.cbutton(grid, "Beenden", sz, func(): get_tree().quit())
|
||||
Style.spacer(col, 10)
|
||||
notice_label = Style.clabel(col, "", 16, Style.GREEN)
|
||||
var p: Profile = app.profile
|
||||
Style.label(self, "Dinger", 76, Style.GOLD, Rect2(80, 64, 600, 90), "left", true)
|
||||
Style.label(self, "Ein Kartenspiel: Duell um das Feld", 22, Style.MUTED, Rect2(84, 156, 600, 30), "left")
|
||||
coins_label = Style.chip(self, "", Rect2(930, 20, 310, 38))
|
||||
coll_label = Style.label(self, "", 15, Style.MUTED, Rect2(700, 64, 540, 24), "right")
|
||||
var y := 230
|
||||
for entry in [["Einzelspieler", func(): app.show_screen(OpponentSelectScreen.new())],
|
||||
["Kampagne", func(): app.show_screen(CampaignScreen.new())],
|
||||
["Netzwerkspiel", func(): app.show_screen(LobbyScreen.new())]]:
|
||||
var b := Style.button(self, entry[0], Rect2(80, y, 380, 62), entry[1], Style.PRIMARY)
|
||||
b.add_theme_font_size_override("font_size", 24)
|
||||
y += 74
|
||||
y += 18
|
||||
var x := 80
|
||||
for entry in [["Booster öffnen", func(): app.show_screen(BoosterScreen.new())],
|
||||
["Sammlung", func(): app.show_screen(CollectionScreen.new())],
|
||||
["Einstellungen", func(): app.show_screen(SettingsScreen.new())],
|
||||
["Beenden", func(): get_tree().quit()]]:
|
||||
Style.button(self, entry[0], Rect2(x, y, 166, 44), entry[1])
|
||||
x += 176
|
||||
notice_label = Style.label(self, "", 16, Style.GREEN, Rect2(80, y + 70, 760, 60), "left")
|
||||
notice_label.autowrap_mode = TextServer.AUTOWRAP_WORD
|
||||
_showcase(p)
|
||||
refresh()
|
||||
|
||||
|
||||
## Unterbrochenen Kampf laden; eine unlesbare Datei wird verworfen.
|
||||
func _resume() -> void:
|
||||
var json := JSON.new()
|
||||
var text := FileAccess.get_file_as_string(app.battle_save_path())
|
||||
if json.parse(text) != OK or not (json.data is Dictionary) or not json.data.has("game"):
|
||||
app.clear_battle_save()
|
||||
app.show_screen(MenuScreen.new())
|
||||
return
|
||||
app.show_screen(BattleScreen.resume(json.data))
|
||||
## Drei Karten der Sammlung, die höchsten Stufen zuerst, leicht gefächert.
|
||||
func _showcase(p: Profile) -> void:
|
||||
var cards: Array = Cards.all().duplicate()
|
||||
cards.sort_custom(func(a, b):
|
||||
if p.level(a.id) != p.level(b.id):
|
||||
return p.level(a.id) > p.level(b.id)
|
||||
return hash(a.id) % 97 < hash(b.id) % 97)
|
||||
var spots := [[Vector2(830, 300), -0.14], [Vector2(950, 270), 0.0], [Vector2(1070, 300), 0.14]]
|
||||
for i in 3:
|
||||
var c: Cards.Card = cards[i]
|
||||
var v := CardView.new(c, Vector2(180, 252), false, p.rarity(c.id))
|
||||
v.position = spots[i][0]
|
||||
v.rotation = spots[i][1]
|
||||
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(v)
|
||||
|
||||
|
||||
func refresh() -> void:
|
||||
|
||||
@@ -1,79 +1,54 @@
|
||||
class_name OpponentSelectScreen
|
||||
extends Control
|
||||
## Einzelspieler: Schwierigkeit und Gegnerzahl wählen. Die Karten kommen im Kampf
|
||||
## Einzelspieler: Schwierigkeit wählen und ein Duell starten. Die Karten kommen im Kampf
|
||||
## aus dem Laden (siehe DeckGen.shop_cards).
|
||||
|
||||
var app
|
||||
var diff_buttons := {}
|
||||
var count_slider: HSlider
|
||||
var count_label: Label
|
||||
var info_label: Label
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
var col := Style.column(self, 1000, 50, 12)
|
||||
Style.clabel(col, "Einzelspieler", 36, Style.GOLD, "center", true)
|
||||
Style.spacer(col, 8)
|
||||
Style.header(self, "Einzelspieler", func(): app.show_screen(MenuScreen.new()))
|
||||
var col := Style.column(self, 1000, 170, 12)
|
||||
Style.clabel(col, "Schwierigkeit", 20, Style.TEXT, "center", true)
|
||||
var dr := Style.row(col, 10)
|
||||
dr.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
for key in Profile.DIFFICULTIES:
|
||||
var k: String = key
|
||||
diff_buttons[k] = Style.cbutton(dr, Profile.DIFFICULTIES[k].name, Vector2(150, 40), func(): set_difficulty(k))
|
||||
Style.spacer(col, 20)
|
||||
count_label = Style.clabel(col, "", 20, Style.TEXT, "center", true)
|
||||
var sr := Style.row(col, 12)
|
||||
sr.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
Style.clabel(sr, "1", 15, Style.MUTED, "right", false, 20)
|
||||
count_slider = HSlider.new()
|
||||
count_slider.min_value = 1
|
||||
count_slider.max_value = Game.MAX_OPPONENTS
|
||||
count_slider.step = 1
|
||||
count_slider.value = 1
|
||||
count_slider.tick_count = Game.MAX_OPPONENTS
|
||||
count_slider.ticks_on_borders = true
|
||||
count_slider.custom_minimum_size = Vector2(420, 32)
|
||||
count_slider.value_changed.connect(func(_v): refresh())
|
||||
sr.add_child(count_slider)
|
||||
Style.clabel(sr, str(Game.MAX_OPPONENTS), 15, Style.MUTED, "left", false, 20)
|
||||
info_label = Style.clabel(col, "", 15, Style.MUTED)
|
||||
Style.spacer(col, 24)
|
||||
var br := Style.row(col, 16)
|
||||
br.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
Style.cbutton(br, "Starten", Vector2(190, 46), start, Color("285a3c"))
|
||||
Style.cbutton(br, "Zurück", Vector2(150, 46), func(): app.show_screen(MenuScreen.new()))
|
||||
var b := Style.cbutton(br, "Starten", Vector2(260, 56), start, Style.PRIMARY)
|
||||
b.add_theme_font_size_override("font_size", 22)
|
||||
refresh()
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if event.is_action_pressed("ui_cancel"):
|
||||
app.show_screen(MenuScreen.new())
|
||||
|
||||
|
||||
func set_difficulty(key: String) -> void:
|
||||
app.profile.settings.difficulty = key
|
||||
app.save()
|
||||
refresh()
|
||||
|
||||
|
||||
func opponents() -> int:
|
||||
return int(count_slider.value)
|
||||
|
||||
|
||||
func start() -> void:
|
||||
app.show_screen(BattleScreen.new(opponents()))
|
||||
app.show_screen(BattleScreen.new())
|
||||
|
||||
|
||||
func refresh() -> void:
|
||||
var p: Profile = app.profile
|
||||
for key in diff_buttons:
|
||||
_mark(diff_buttons[key], key == p.settings.difficulty)
|
||||
var n := opponents()
|
||||
count_label.text = tr("1 Gegner") if n == 1 else tr("%d Gegner") % n
|
||||
info_label.text = (tr("Duell") if n == 1 else tr("%d am Tisch") % (n + 1)) + " · " + tr("%d Münzen") % Profile.REWARD_WIN[n]
|
||||
var d := p.difficulty()
|
||||
info_label.text = tr("Gegner mit %d Leben · %d Münzen bei Sieg") % [int(d.life), int(round(Profile.REWARD_WIN * float(d.reward)))]
|
||||
|
||||
|
||||
func _mark(b: Button, active: bool, col: Color = Color("285a3c")) -> void:
|
||||
var base: Color = col if active else Style.PANEL_LIGHT
|
||||
b.add_theme_stylebox_override("normal", Style.box(base, 8, Style.GOLD if active else Style.MUTED, 2))
|
||||
b.add_theme_stylebox_override("hover", Style.box(Style.lighter(base), 8, Style.GOLD if active else Style.MUTED, 2))
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if event.is_action_pressed("ui_cancel"):
|
||||
app.show_screen(MenuScreen.new())
|
||||
|
||||
@@ -16,9 +16,8 @@ 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)
|
||||
Style.header(self, "Einstellungen", func(): app.show_screen(MenuScreen.new()))
|
||||
var col := Style.column(self, 700, 110, 24)
|
||||
var grid := GridContainer.new()
|
||||
grid.columns = 3
|
||||
grid.add_theme_constant_override("h_separation", 24)
|
||||
@@ -80,7 +79,6 @@ func _ready() -> void:
|
||||
var r := Style.row(col, 16)
|
||||
r.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
Style.cbutton(r, "Profil zurücksetzen", Vector2(200, 44), func(): confirm.visible = true, Color("5a2828"))
|
||||
Style.cbutton(r, "Zurück", Vector2(160, 44), func(): app.show_screen(MenuScreen.new()))
|
||||
confirm = Style.confirm(self, "Profil wirklich zurücksetzen?", "Sammlung, Münzen und Fortschritt gehen verloren.",
|
||||
"Ja, zurücksetzen", _reset, "Abbrechen", Style.RED)
|
||||
_labels()
|
||||
@@ -135,6 +133,7 @@ func _on_speed(v: float) -> void:
|
||||
|
||||
func _on_anim(on: bool) -> void:
|
||||
app.profile.settings.animations = on
|
||||
Style.animations = on
|
||||
app.save()
|
||||
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ func _ready() -> void:
|
||||
streams["damage"] = _tone(140, 0.12, "square")
|
||||
streams["heal"] = _tone(523, 0.22, "sine", 784)
|
||||
streams["combo"] = _tone(660, 0.2, "sine", 1100)
|
||||
streams["buy"] = _tone(1200, 0.08, "sine", 1700)
|
||||
streams["turn"] = _tone(520, 0.3, "sine", 1040)
|
||||
streams["win"] = _tone(400, 0.6, "sine", 1200)
|
||||
streams["lose"] = _tone(320, 0.7, "square", 90)
|
||||
for i in 4:
|
||||
|
||||
@@ -4,6 +4,7 @@ class_name Style
|
||||
const FONT: FontFile = preload("res://assets/fonts/DejaVuSans.ttf")
|
||||
const FONT_BOLD: FontFile = preload("res://assets/fonts/DejaVuSans-Bold.ttf")
|
||||
static var font_scale := 1.0 # Einstellung „Große Schrift“
|
||||
static var animations := true # Einstellung „Animationen“: der Glanz seltener Karten bewegt sich
|
||||
|
||||
const BG := Color("1a1c24")
|
||||
const PANEL := Color("282b36")
|
||||
@@ -17,6 +18,11 @@ const BLUE := Color("6eaaf0")
|
||||
const YELLOW := Color("ffe65a")
|
||||
const ORANGE := Color("ff963c")
|
||||
const PURPLE := Color("8c78dc")
|
||||
const AMBER := Color("f0b429") # Angriff
|
||||
const CRIMSON := Color("d64545") # Leben
|
||||
const MANA := Color("3a78d2") # Kostenkristall
|
||||
const PRIMARY := Color("285a3c") # Hauptaktion
|
||||
const DANGER := Color("5a2828")
|
||||
|
||||
|
||||
## Schriftgröße mit der Einstellung „Große Schrift“.
|
||||
@@ -80,7 +86,11 @@ static func panel(parent: Node, rect: Rect2, bg: Color = PANEL, radius: int = 8,
|
||||
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))
|
||||
var sb := box(bg, radius, border_col, border_w)
|
||||
sb.shadow_color = Color(0, 0, 0, 0.35)
|
||||
sb.shadow_size = 6
|
||||
sb.shadow_offset = Vector2(0, 3)
|
||||
p.add_theme_stylebox_override("panel", sb)
|
||||
p.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
parent.add_child(p)
|
||||
return p
|
||||
@@ -115,10 +125,35 @@ static func button(parent: Node, text: String, rect: Rect2, on_pressed: Callable
|
||||
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))
|
||||
juice(b)
|
||||
parent.add_child(b)
|
||||
return b
|
||||
|
||||
|
||||
## Knöpfe federn beim Drücken kurz ein.
|
||||
static func juice(b: Button) -> void:
|
||||
b.button_down.connect(func():
|
||||
if animations:
|
||||
b.pivot_offset = b.size / 2
|
||||
b.create_tween().tween_property(b, "scale", Vector2(0.96, 0.96), 0.05))
|
||||
b.button_up.connect(func():
|
||||
if animations:
|
||||
b.pivot_offset = b.size / 2
|
||||
b.create_tween().tween_property(b, "scale", Vector2.ONE, 0.12).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT))
|
||||
|
||||
|
||||
## Kopfzeile eines Unterbildschirms: Titel links, Zurück rechts oben.
|
||||
static func header(parent: Node, title: String, on_back: Callable) -> Button:
|
||||
label(parent, title, 34, GOLD, Rect2(40, 22, 700, 46), "left", true)
|
||||
return button(parent, "Zurück", Rect2(1120, 26, 120, 40), on_back)
|
||||
|
||||
|
||||
## Kleine Kachel mit Text, etwa für Münzen und Staub.
|
||||
static func chip(parent: Node, text: String, rect: Rect2, color: Color = GOLD) -> Label:
|
||||
panel(parent, rect, darker(PANEL, 0.9), rect.size.y / 2, PANEL_LIGHT, 1)
|
||||
return label(parent, text, 17, color, rect, "center", true)
|
||||
|
||||
|
||||
## 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()
|
||||
@@ -230,6 +265,33 @@ static func text(ci: CanvasItem, s: String, size_in: int, color: Color, pos: Vec
|
||||
ci.draw_string(font, Vector2(x, y + font.get_ascent(size)), s, HORIZONTAL_ALIGNMENT_LEFT, -1, size, color)
|
||||
|
||||
|
||||
## Text mit dunklem Rand: bleibt auf jeder Farbe lesbar (Werte, Kosten).
|
||||
static func text_outlined(ci: CanvasItem, s: String, size_in: int, color: Color, pos: Vector2, anchor: String = "center", outline: int = 4) -> void:
|
||||
var size := fs(size_in)
|
||||
var dim := FONT_BOLD.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
|
||||
"midleft":
|
||||
y -= dim.y / 2
|
||||
var at := Vector2(x, y + FONT_BOLD.get_ascent(size))
|
||||
ci.draw_string_outline(FONT_BOLD, at, s, HORIZONTAL_ALIGNMENT_LEFT, -1, size, outline, Color(0.05, 0.05, 0.08, 0.95))
|
||||
ci.draw_string(FONT_BOLD, at, s, HORIZONTAL_ALIGNMENT_LEFT, -1, size, color)
|
||||
|
||||
|
||||
## Schriftgröße, mit der ein Text in eine Breite passt (bis zur Mindestgröße).
|
||||
static func fit_size(s: String, bold: bool, size: int, max_w: float, min_size: int = 9) -> int:
|
||||
var font: Font = FONT_BOLD if bold else FONT
|
||||
while size > min_size and font.get_string_size(s, HORIZONTAL_ALIGNMENT_LEFT, -1, fs(size)).x > max_w:
|
||||
size -= 1
|
||||
return size
|
||||
|
||||
|
||||
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:
|
||||
@@ -246,6 +308,88 @@ static func border(ci: CanvasItem, rect: Rect2, color: Color, width: int, radius
|
||||
ci.draw_style_box(sb, rect)
|
||||
|
||||
|
||||
# --- Seltenheit als Aussehen ------------------------------------------------------
|
||||
|
||||
## Ab episch bewegt sich der Glanz; dann zeichnet die Karte jeden Frame neu.
|
||||
static func rarity_animated(rar: String) -> bool:
|
||||
return animations and Cards.RARITIES.find(rar) >= 2
|
||||
|
||||
|
||||
## Farbe einer Stufe; mythisch schillert über die Zeit durch alle Farben.
|
||||
static func rarity_color(rar: String, seed: int = 0) -> Color:
|
||||
if rar == "mythisch" and animations:
|
||||
return Color.from_hsv(fmod(Time.get_ticks_msec() / 1000.0 * 0.12 + (seed % 97) / 97.0, 1.0), 0.55, 1.0)
|
||||
return Cards.RARITY_COLORS[rar]
|
||||
|
||||
|
||||
## Rahmenfarbe einer Stufe: gewöhnlich bleibt schlicht, alles darüber trägt seine Farbe.
|
||||
static func rarity_frame(rar: String, seed: int = 0) -> Color:
|
||||
return Color("3a3e4c") if rar == "gewöhnlich" else rarity_color(rar, seed)
|
||||
|
||||
|
||||
## Lichtstreifen wie auf einer Folienkarte: selten steht er, ab episch wandert er über die Karte.
|
||||
static func rarity_sheen(ci: CanvasItem, rect: Rect2, rar: String, seed: int) -> void:
|
||||
var lvl := Cards.RARITIES.find(rar)
|
||||
if lvl < 1:
|
||||
return
|
||||
var phase := (seed % 100) / 100.0
|
||||
if lvl >= 2 and animations:
|
||||
phase = fmod(Time.get_ticks_msec() / 2500.0 + phase, 1.0)
|
||||
var frame := PackedVector2Array([rect.position, Vector2(rect.end.x, rect.position.y), rect.end, Vector2(rect.position.x, rect.end.y)])
|
||||
var w := rect.size.x * 0.34
|
||||
var k := rect.size.y * 0.75 # Schräge
|
||||
var x0 := rect.position.x - w + (rect.size.x + w + k) * phase
|
||||
for layer in [[1.0, 0.07], [0.55, 0.09], [0.22, 0.13]]:
|
||||
var lw: float = w * layer[0]
|
||||
var lx := x0 + (w - lw) / 2
|
||||
var band := PackedVector2Array([Vector2(lx, rect.position.y), Vector2(lx + lw, rect.position.y), Vector2(lx + lw - k, rect.end.y), Vector2(lx - k, rect.end.y)])
|
||||
for poly in Geometry2D.intersect_polygons(band, frame):
|
||||
ci.draw_colored_polygon(poly, Color(1, 1, 1, layer[1]))
|
||||
|
||||
|
||||
## Funkeln ab legendär: kleine Sterne, die aufblitzen und vergehen; ganz zum Schluss zeichnen.
|
||||
## avoid: Bereich, der frei bleibt (der Kartentext).
|
||||
static func rarity_sparkle(ci: CanvasItem, rect: Rect2, rar: String, seed: int, avoid: Rect2 = Rect2()) -> void:
|
||||
var lvl := Cards.RARITIES.find(rar)
|
||||
if lvl < 3:
|
||||
return
|
||||
var t := Time.get_ticks_msec() / 1000.0
|
||||
var n := 5 if lvl == 3 else 9
|
||||
for i in n:
|
||||
var h := absi(hash(Vector2i(seed, i)))
|
||||
var p := rect.position + Vector2(8 + (h % 1000) / 1000.0 * (rect.size.x - 16), 8 + ((h / 1000) % 1000) / 1000.0 * (rect.size.y - 16))
|
||||
var tw := 0.5 + 0.5 * sin(t * 2.5 + i * 1.9 + seed % 7) if animations else 0.35 + 0.65 * ((i * 7 + seed) % 5) / 4.0
|
||||
if tw < 0.15 or avoid.has_point(p):
|
||||
continue
|
||||
var s := 2.0 + 4.0 * tw
|
||||
var col := Color(1, 1, 1, 0.3 + 0.65 * tw)
|
||||
ci.draw_line(p - Vector2(s, 0), p + Vector2(s, 0), col, 1.4)
|
||||
ci.draw_line(p - Vector2(0, s), p + Vector2(0, s), col, 1.4)
|
||||
ci.draw_circle(p, 1.3, col)
|
||||
|
||||
|
||||
## Weicher Schein um eine Karte höherer Stufe; vor dem Hintergrund zeichnen.
|
||||
static func glow(ci: CanvasItem, rect: Rect2, color: Color, radius: int = 8, spread: int = 8) -> void:
|
||||
var sb := StyleBoxFlat.new()
|
||||
sb.bg_color = Color(0, 0, 0, 0)
|
||||
sb.set_corner_radius_all(radius)
|
||||
sb.shadow_color = Color(color, 0.6)
|
||||
sb.shadow_size = spread
|
||||
ci.draw_style_box(sb, rect)
|
||||
|
||||
|
||||
## Edelstein in der Farbe der Stufe (ab selten).
|
||||
static func rarity_gem(ci: CanvasItem, rar: String, c: Vector2, s: float, seed: int = 0) -> void:
|
||||
if rar == "gewöhnlich":
|
||||
return
|
||||
var col := rarity_color(rar, seed)
|
||||
var diamond := func(h: float, k: Color) -> void:
|
||||
ci.draw_colored_polygon(PackedVector2Array([c + Vector2(0, -h), c + Vector2(h * 0.72, 0), c + Vector2(0, h), c + Vector2(-h * 0.72, 0)]), k)
|
||||
diamond.call(s + 2, Color("14141c"))
|
||||
diamond.call(s, col)
|
||||
ci.draw_colored_polygon(PackedVector2Array([c + Vector2(0, -s * 0.7), c + Vector2(s * 0.3, -s * 0.15), c + Vector2(-s * 0.35, -s * 0.1)]), Color(1, 1, 1, 0.55))
|
||||
|
||||
|
||||
## Hintergrund in Elementfarbe; ein verschmolzenes Wesen zeigt seine Elemente als Streifen.
|
||||
static func fill_elements(ci: CanvasItem, rect: Rect2, elements: Array, radius: int = 8) -> void:
|
||||
if elements.size() == 1:
|
||||
@@ -265,12 +409,63 @@ static func fill_elements(ci: CanvasItem, rect: Rect2, elements: Array, radius:
|
||||
ci.draw_style_box(sb, Rect2(rect.position + Vector2(floorf(i * w), 0), Vector2(ceilf(w), rect.size.y)))
|
||||
|
||||
|
||||
# --- Kartenteile ------------------------------------------------------
|
||||
|
||||
## Kostenkristall: sechseckig, blau, Zahl in der Mitte.
|
||||
static func cost_crystal(ci: CanvasItem, c: Vector2, r: float, cost: int, font: int) -> void:
|
||||
var pts := PackedVector2Array()
|
||||
var outer := PackedVector2Array()
|
||||
for i in 6:
|
||||
var a := -PI / 2 + i * PI / 3
|
||||
pts.append(c + Vector2(cos(a), sin(a)) * r)
|
||||
outer.append(c + Vector2(cos(a), sin(a)) * (r + 2))
|
||||
ci.draw_colored_polygon(outer, Color("14141c"))
|
||||
ci.draw_colored_polygon(pts, MANA)
|
||||
var shine := PackedVector2Array([pts[5], pts[0], c + Vector2(0, -r * 0.1), c + Vector2(-r * 0.55, r * 0.15)])
|
||||
ci.draw_colored_polygon(shine, Color(1, 1, 1, 0.22))
|
||||
text_outlined(ci, str(cost), font, Color.WHITE, c, "center", 3)
|
||||
|
||||
|
||||
## Wertkreis (Angriff gelb, Leben rot) mit Zahl.
|
||||
static func stat_circle(ci: CanvasItem, c: Vector2, r: float, value: String, color: Color, font: int) -> void:
|
||||
ci.draw_circle(c, r + 2, Color("14141c"))
|
||||
ci.draw_circle(c, r, color)
|
||||
ci.draw_circle(c + Vector2(-r * 0.25, -r * 0.3), r * 0.45, Color(1, 1, 1, 0.16))
|
||||
text_outlined(ci, value, font, Color.WHITE, c, "center", 3)
|
||||
|
||||
|
||||
## Schlüsselwort als Symbol auf dunklem Kreis: Wächter Mauer, Schild Schild, Flink Doppelpfeil,
|
||||
## Doppelschlag gekreuzte Klingen.
|
||||
static func keyword_icon(ci: CanvasItem, kw: String, c: Vector2, r: float, col: Color = Color("f2f2f6")) -> void:
|
||||
ci.draw_circle(c, r + 3, Color(0.05, 0.05, 0.08, 0.7))
|
||||
var w := maxf(1.5, r * 0.22)
|
||||
match kw:
|
||||
"Wächter":
|
||||
ci.draw_rect(Rect2(c + Vector2(-r * 0.75, -r * 0.15), Vector2(r * 1.5, r * 0.85)), col)
|
||||
for i in 3:
|
||||
ci.draw_rect(Rect2(c + Vector2(-r * 0.75 + i * r * 0.6, -r * 0.6), Vector2(r * 0.3, r * 0.5)), col)
|
||||
"Schild":
|
||||
var pts := PackedVector2Array([c + Vector2(-r * 0.7, -r * 0.65), c + Vector2(r * 0.7, -r * 0.65), c + Vector2(r * 0.7, r * 0.05),
|
||||
c + Vector2(0, r * 0.8), c + Vector2(-r * 0.7, r * 0.05)])
|
||||
ci.draw_colored_polygon(pts, col)
|
||||
ci.draw_line(c + Vector2(0, -r * 0.55), c + Vector2(0, r * 0.6), Color(0.05, 0.05, 0.08, 0.6), w)
|
||||
"Flink":
|
||||
for i in 2:
|
||||
var x := c.x - r * 0.55 + i * r * 0.55
|
||||
ci.draw_polyline(PackedVector2Array([Vector2(x, c.y - r * 0.6), Vector2(x + r * 0.5, c.y), Vector2(x, c.y + r * 0.6)]), col, w)
|
||||
"Doppelschlag":
|
||||
ci.draw_line(c + Vector2(-r * 0.65, r * 0.65), c + Vector2(r * 0.6, -r * 0.6), col, w)
|
||||
ci.draw_line(c + Vector2(r * 0.65, r * 0.65), c + Vector2(-r * 0.6, -r * 0.6), col, w)
|
||||
ci.draw_line(c + Vector2(-r * 0.7, r * 0.35), c + Vector2(-r * 0.35, r * 0.7), col, w)
|
||||
ci.draw_line(c + Vector2(r * 0.7, r * 0.35), c + Vector2(r * 0.35, r * 0.7), col, w)
|
||||
|
||||
|
||||
# --- Kartensymbole (prozedural, je Element) --------------------------
|
||||
|
||||
static func glyph(ci: CanvasItem, card, center: Vector2, size: float) -> void:
|
||||
static func glyph(ci: CanvasItem, card, center: Vector2, size: float, alpha: float = 0.95) -> void:
|
||||
var variant := absi(hash(card.id)) % 3
|
||||
var col := lighter(Cards.ELEMENT_COLORS[card.elements[0]], 0.38)
|
||||
col.a = 0.95
|
||||
col.a = alpha
|
||||
var elems: Array = card.elements
|
||||
if elems.size() == 1:
|
||||
_element_glyph(ci, elems[0], center, size, variant, col)
|
||||
@@ -278,7 +473,7 @@ static func glyph(ci: CanvasItem, card, center: Vector2, size: float) -> void:
|
||||
# Verschmolzen: die ersten beiden Elemente nebeneinander, ab dem dritten ein Ring
|
||||
_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
|
||||
col2.a = alpha
|
||||
_element_glyph(ci, elems[1], center + Vector2(size * 0.28, 0), size * 0.62, variant, col2)
|
||||
if elems.size() >= 3:
|
||||
ci.draw_arc(center, size * 0.72, 0, TAU, 40, Color(col, 0.5), 2)
|
||||
@@ -321,7 +516,7 @@ static func _flame(ci: CanvasItem, c: Vector2, h: float, col: Color) -> void:
|
||||
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))
|
||||
ci.draw_colored_polygon(inner, Color(1.0, 0.9, 0.5, 0.75 * col.a))
|
||||
|
||||
|
||||
## Dezenter Hintergrund: Farbverlauf und schräge Linien.
|
||||
|
||||
Reference in New Issue
Block a user