update
This commit is contained in:
@@ -16,6 +16,8 @@ const BENCH_OFFSET := 1.95 ## Abstand der Bank hinter der letzten Reihe
|
||||
|
||||
var game: Game = null
|
||||
var camera: Camera3D = null
|
||||
## Brett eines anderen Spielers, das in der Planung oben gezeigt wird.
|
||||
var scout_player: PlayerState = null
|
||||
var selected_slot := {} ## {"kind": "board"/"bench", ...}
|
||||
var hover_slot := {}
|
||||
var drag_active := false
|
||||
@@ -31,6 +33,12 @@ var _bench_tiles := {} ## int -> MeshInstance3D
|
||||
var _bench_mats := {}
|
||||
var _bench_z := 0.0
|
||||
var _units := {} ## key -> Node3D
|
||||
var _dying := {} ## key -> Restzeit der Sterbe-Animation
|
||||
const DEATH_TIME := 0.45
|
||||
var _shots: Array = [] ## fliegende Geschosse {"node", "from", "to", "t", "dur"}
|
||||
var shots_total := 0 ## Zähler aller Geschosse, für Tests
|
||||
var _shot_mesh: SphereMesh
|
||||
var _slash_mesh: BoxMesh
|
||||
var _origin := Vector3.ZERO
|
||||
var _tile_mesh: CylinderMesh
|
||||
var _ring_mesh: TorusMesh
|
||||
@@ -48,6 +56,13 @@ func _ready() -> void:
|
||||
_ring_mesh.outer_radius = 0.66
|
||||
_ring_mesh.ring_segments = 8
|
||||
_ring_mesh.rings = 24
|
||||
_shot_mesh = SphereMesh.new()
|
||||
_shot_mesh.radius = 0.11
|
||||
_shot_mesh.height = 0.22
|
||||
_shot_mesh.radial_segments = 8
|
||||
_shot_mesh.rings = 4
|
||||
_slash_mesh = BoxMesh.new()
|
||||
_slash_mesh.size = Vector3(0.12, 0.12, 0.7)
|
||||
_build_tiles()
|
||||
_build_bench()
|
||||
set_process(true)
|
||||
@@ -212,6 +227,46 @@ func _process(delta: float) -> void:
|
||||
return
|
||||
_update_tiles()
|
||||
_update_units(delta)
|
||||
_update_shots(delta)
|
||||
|
||||
|
||||
## Sichtbarer Angriff: Fernkampf schickt eine Kugel, Nahkampf einen kurzen Hieb.
|
||||
func spawn_attack(from: Vector3, to: Vector3, ranged: bool, col: Color) -> void:
|
||||
var mi := MeshInstance3D.new()
|
||||
mi.mesh = _shot_mesh if ranged else _slash_mesh
|
||||
var m := StandardMaterial3D.new()
|
||||
m.albedo_color = col
|
||||
m.emission_enabled = true
|
||||
m.emission = col
|
||||
m.emission_energy_multiplier = 1.8
|
||||
mi.material_override = m
|
||||
var a := from + Vector3(0.0, 0.7, 0.0)
|
||||
var b := to + Vector3(0.0, 0.6, 0.0)
|
||||
if not ranged:
|
||||
# Der Hieb startet vor dem Angreifer und endet am Ziel.
|
||||
a = a.lerp(b, 0.35)
|
||||
mi.position = a
|
||||
if (b - a).length() > 0.01:
|
||||
mi.look_at_from_position(a, b, Vector3.UP)
|
||||
add_child(mi)
|
||||
shots_total += 1
|
||||
_shots.append({"node": mi, "from": a, "to": b, "t": 0.0, "dur": 0.2 if ranged else 0.09})
|
||||
|
||||
|
||||
func _update_shots(delta: float) -> void:
|
||||
var keep := []
|
||||
for sh in _shots:
|
||||
sh["t"] += delta
|
||||
var f: float = sh["t"] / sh["dur"]
|
||||
var node: MeshInstance3D = sh["node"]
|
||||
if f >= 1.0:
|
||||
node.queue_free()
|
||||
continue
|
||||
var pos: Vector3 = sh["from"].lerp(sh["to"], f)
|
||||
pos.y += sin(f * PI) * 0.35
|
||||
node.position = pos
|
||||
keep.append(sh)
|
||||
_shots = keep
|
||||
|
||||
|
||||
func _update_tiles() -> void:
|
||||
@@ -284,6 +339,10 @@ func _update_units(delta: float) -> void:
|
||||
"side": 0 if u.team == my_team else 1,
|
||||
"cast": u.cast_flash > 0.0,
|
||||
"hit": u.hit_flash > 0.0,
|
||||
"burn": u.burn_timer > 0.0,
|
||||
"slow": u.slow_timer > 0.0 and u.slow > 0.0,
|
||||
"stun": u.stun_timer > 0.0,
|
||||
"shield": clampf(u.shield / maxf(1.0, u.max_hp), 0.0, 1.0),
|
||||
}
|
||||
else:
|
||||
for bh in game.human.board:
|
||||
@@ -292,6 +351,13 @@ func _update_units(delta: float) -> void:
|
||||
"avatar": a, "world": world_pos(from_board(bh)), "hp": 1.0, "mana": 0.0,
|
||||
"side": 0, "cast": false, "hit": false,
|
||||
}
|
||||
if scout_player != null and scout_player.alive:
|
||||
for bh in scout_player.board:
|
||||
var sa: Avatar = scout_player.board[bh]
|
||||
wanted["s%d" % sa.id] = {
|
||||
"avatar": sa, "world": world_pos(Combat._arena_pos(bh, 1)), "hp": 1.0,
|
||||
"mana": 0.0, "side": 1, "cast": false, "hit": false,
|
||||
}
|
||||
# Die Bank ist immer sichtbar, auch während des Kampfes.
|
||||
for i in game.human.bench.size():
|
||||
var ba = game.human.bench[i]
|
||||
@@ -310,9 +376,25 @@ func _update_units(delta: float) -> void:
|
||||
wanted[drag_key]["dragged"] = true
|
||||
|
||||
for key in _units.keys():
|
||||
if not wanted.has(key):
|
||||
_units[key].queue_free()
|
||||
if wanted.has(key):
|
||||
continue
|
||||
var node: Node3D = _units[key]
|
||||
if not _dying.has(key):
|
||||
# Im Kampf schrumpft die Einheit weg, in der Planung verschwindet sie sofort.
|
||||
if game.phase == Game.Phase.COMBAT and key.begins_with("c"):
|
||||
_dying[key] = DEATH_TIME
|
||||
else:
|
||||
node.queue_free()
|
||||
_units.erase(key)
|
||||
continue
|
||||
_dying[key] -= delta
|
||||
var t: float = maxf(0.0, _dying[key] / DEATH_TIME)
|
||||
node.scale = Vector3(t, t * 0.6, t)
|
||||
node.position.y = -0.4 * (1.0 - t)
|
||||
if _dying[key] <= 0.0:
|
||||
node.queue_free()
|
||||
_units.erase(key)
|
||||
_dying.erase(key)
|
||||
|
||||
for key in wanted:
|
||||
var d: Dictionary = wanted[key]
|
||||
@@ -332,10 +414,31 @@ func _update_units(delta: float) -> void:
|
||||
var body: MeshInstance3D = node.get_meta("body")
|
||||
var mat: StandardMaterial3D = body.material_override
|
||||
var base: Color = d["avatar"].color()
|
||||
mat.albedo_color = base.lightened(0.5) if d["hit"] else base
|
||||
var col := base
|
||||
if d.get("slow", false):
|
||||
col = col.lerp(Color(0.55, 0.8, 1.0), 0.45)
|
||||
if d.get("stun", false):
|
||||
col = col.lerp(Color(1.0, 0.95, 0.4), 0.5)
|
||||
if d["hit"]:
|
||||
col = col.lightened(0.5)
|
||||
mat.albedo_color = col
|
||||
var dragged: bool = d.get("dragged", false)
|
||||
mat.emission_energy_multiplier = 1.6 if d["cast"] else (0.45 if dragged else 0.12)
|
||||
node.scale = Vector3.ONE * (1.12 if (d["cast"] or dragged) else 1.0)
|
||||
var emit := 0.12
|
||||
if d["cast"]:
|
||||
emit = 1.6
|
||||
elif d.get("burn", false):
|
||||
emit = 0.9
|
||||
mat.emission = Color(1.0, 0.45, 0.15)
|
||||
elif dragged:
|
||||
emit = 0.45
|
||||
else:
|
||||
mat.emission = base
|
||||
mat.emission_energy_multiplier = emit
|
||||
var sc := 1.12 if (d["cast"] or dragged) else 1.0
|
||||
if d.get("stun", false):
|
||||
node.scale = Vector3(sc * 1.08, sc * 0.8, sc * 1.08)
|
||||
else:
|
||||
node.scale = Vector3.ONE * sc
|
||||
draw_units.append({
|
||||
"pos": node.position, "avatar": d["avatar"], "hp": d["hp"],
|
||||
"mana": d["mana"], "side": d["side"], "cast": d["cast"],
|
||||
@@ -347,12 +450,77 @@ func unit_node(avatar_id: int) -> Node3D:
|
||||
return _units.get("a%d" % avatar_id)
|
||||
|
||||
|
||||
## Für das Overlay: Arena-Feld so spiegeln, wie das Brett gezeichnet wird.
|
||||
func mirror_for_view(p: Vector2i, my_team: int) -> Vector2i:
|
||||
return _mirror(p, my_team)
|
||||
|
||||
|
||||
func _mirror(p: Vector2i, my_team: int) -> Vector2i:
|
||||
if my_team == 0:
|
||||
return p
|
||||
return Vector2i(COLS - 1 - p.x, ROWS - 1 - p.y)
|
||||
|
||||
|
||||
## Kleines Kennzeichen des Hauptelements auf oder neben dem Körper.
|
||||
func _add_ornament(root: Node3D, a: Avatar, body_y: float) -> void:
|
||||
var e := a.primary_element()
|
||||
if e < 0:
|
||||
return
|
||||
var col: Color = Data.ELEMENT_COLORS[e]
|
||||
var m := StandardMaterial3D.new()
|
||||
m.albedo_color = col.lightened(0.15)
|
||||
m.emission_enabled = true
|
||||
m.emission = col
|
||||
m.emission_energy_multiplier = 0.5
|
||||
var top := body_y + 0.5
|
||||
match e:
|
||||
Data.Element.FEUER:
|
||||
# Flamme: kleiner Kegel oben.
|
||||
var c := CylinderMesh.new()
|
||||
c.top_radius = 0.0
|
||||
c.bottom_radius = 0.16
|
||||
c.height = 0.38
|
||||
c.radial_segments = 8
|
||||
_orn(root, c, m, Vector3(0.0, top + 0.18, 0.0))
|
||||
Data.Element.WASSER:
|
||||
# Tropfen: flache Kugel oben.
|
||||
var sp := SphereMesh.new()
|
||||
sp.radius = 0.16
|
||||
sp.height = 0.22
|
||||
_orn(root, sp, m, Vector3(0.0, top + 0.12, 0.0))
|
||||
Data.Element.WIND:
|
||||
# Finne: dünne, hohe Platte.
|
||||
var b := BoxMesh.new()
|
||||
b.size = Vector3(0.05, 0.42, 0.28)
|
||||
_orn(root, b, m, Vector3(0.0, top + 0.2, 0.0))
|
||||
Data.Element.BLITZ:
|
||||
# Zacke: Prisma oben.
|
||||
var pr := PrismMesh.new()
|
||||
pr.size = Vector3(0.28, 0.4, 0.12)
|
||||
_orn(root, pr, m, Vector3(0.0, top + 0.2, 0.0))
|
||||
Data.Element.ERDE:
|
||||
# Schultern: zwei Würfel seitlich.
|
||||
for sx in [-1.0, 1.0]:
|
||||
var bx := BoxMesh.new()
|
||||
bx.size = Vector3(0.22, 0.22, 0.22)
|
||||
_orn(root, bx, m, Vector3(sx * 0.5, body_y + 0.1, 0.0))
|
||||
Data.Element.EIS:
|
||||
# Kristall: umgedrehtes Prisma oben.
|
||||
var cr := PrismMesh.new()
|
||||
cr.size = Vector3(0.22, 0.42, 0.22)
|
||||
var n := _orn(root, cr, m, Vector3(0.0, top + 0.2, 0.0))
|
||||
n.rotation_degrees = Vector3(180.0, 45.0, 0.0)
|
||||
|
||||
|
||||
func _orn(root: Node3D, mesh: Mesh, mat: Material, pos: Vector3) -> MeshInstance3D:
|
||||
var mi := MeshInstance3D.new()
|
||||
mi.mesh = mesh
|
||||
mi.material_override = mat
|
||||
mi.position = pos
|
||||
root.add_child(mi)
|
||||
return mi
|
||||
|
||||
|
||||
func _make_unit(a: Avatar, side: int) -> Node3D:
|
||||
var root := Node3D.new()
|
||||
|
||||
@@ -390,6 +558,8 @@ func _make_unit(a: Avatar, side: int) -> Node3D:
|
||||
root.add_child(body)
|
||||
root.set_meta("body", body)
|
||||
|
||||
_add_ornament(root, a, body.position.y)
|
||||
|
||||
# Bodenring: weiss = eigenes Team, rot = Gegner.
|
||||
var ring := MeshInstance3D.new()
|
||||
ring.mesh = _ring_mesh
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
class_name HudOverlay
|
||||
extends Control
|
||||
|
||||
## Zeichnet Lebens- und Manabalken, Rolle und Sterne über die 3D-Einheiten.
|
||||
## Zeichnet Lebens- und Manabalken, Rolle, Sterne und Schadenszahlen
|
||||
## über die 3D-Einheiten. Spielt außerdem die Kampfgeräusche ab.
|
||||
|
||||
const FLOAT_TIME := 0.9
|
||||
const FLOAT_RISE := 42.0
|
||||
|
||||
var board: BoardView3D = null
|
||||
var sfx: Sfx = null
|
||||
var _font: Font
|
||||
var _floaters: Array = [] ## {"world": Vector3, "text": String, "col": Color, "age": float}
|
||||
var _combat_seen: Combat = null
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -13,10 +20,52 @@ func _ready() -> void:
|
||||
set_process(true)
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
func _process(delta: float) -> void:
|
||||
_pull_hits()
|
||||
for f in _floaters:
|
||||
f["age"] += delta
|
||||
_floaters = _floaters.filter(func(f): return f["age"] < FLOAT_TIME)
|
||||
queue_redraw()
|
||||
|
||||
|
||||
## Holt neue Treffer aus dem laufenden Spielerkampf und macht Zahlen und Töne daraus.
|
||||
func _pull_hits() -> void:
|
||||
if board == null or board.game == null:
|
||||
return
|
||||
var c: Combat = board.game.player_combat
|
||||
if c == null or not c.record_hits:
|
||||
_combat_seen = null
|
||||
return
|
||||
if c != _combat_seen:
|
||||
_combat_seen = c
|
||||
_floaters.clear()
|
||||
if c.hits.is_empty():
|
||||
return
|
||||
var my_team := board.game.human_team_in_combat()
|
||||
for h in c.hits:
|
||||
var world := board.world_pos(board.mirror_for_view(h["pos"], my_team))
|
||||
var mine: bool = h["team"] == my_team
|
||||
match h["kind"]:
|
||||
"dmg":
|
||||
_floaters.append({"world": world, "text": str(h["amount"]),
|
||||
"col": Color(1.0, 0.55, 0.45) if mine else Color(1.0, 0.95, 0.85), "age": 0.0})
|
||||
if sfx: sfx.play("treffer")
|
||||
"crit":
|
||||
_floaters.append({"world": world, "text": str(h["amount"]) + "!",
|
||||
"col": Color(1.0, 0.85, 0.3), "age": 0.0})
|
||||
if sfx: sfx.play("krit")
|
||||
"heal":
|
||||
_floaters.append({"world": world, "text": "+" + str(h["amount"]),
|
||||
"col": Color(0.55, 1.0, 0.6), "age": 0.0})
|
||||
if sfx: sfx.play("heilung")
|
||||
"cast":
|
||||
if sfx: sfx.play("zauber")
|
||||
"attack":
|
||||
var from := board.world_pos(board.mirror_for_view(h["from"], my_team))
|
||||
board.spawn_attack(from, world, h["ranged"], Data.ELEMENT_COLORS[h["element"]])
|
||||
c.hits.clear()
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
if board == null or board.camera == null:
|
||||
return
|
||||
@@ -30,7 +79,6 @@ func _draw() -> void:
|
||||
var a: Avatar = d["avatar"]
|
||||
var side: int = d["side"]
|
||||
|
||||
# Balkenbreite nach Entfernung, damit nahe Einheiten größer wirken.
|
||||
var bw: float = clampf(absf(foot.y - head.y) * 0.85, 26.0, 70.0)
|
||||
var bx := head.x - bw * 0.5
|
||||
var by := head.y - 4.0
|
||||
@@ -41,13 +89,34 @@ func _draw() -> void:
|
||||
draw_rect(Rect2(bx, by + 7.0, bw, 4.0), Color(0.05, 0.05, 0.07, 0.85))
|
||||
draw_rect(Rect2(bx, by + 7.0, bw * clampf(d["mana"], 0.0, 1.0), 4.0),
|
||||
Color(0.35, 0.62, 0.95))
|
||||
# Schild als heller Streifen über dem Lebensbalken.
|
||||
if d.get("shield", 0.0) > 0.0:
|
||||
draw_rect(Rect2(bx, by - 4.0, bw * clampf(d["shield"], 0.0, 1.0), 3.0), Color(0.85, 0.9, 1.0))
|
||||
|
||||
# Sterne
|
||||
for s in a.star:
|
||||
draw_circle(Vector2(bx + bw * 0.5 - 7.0 + 7.0 * float(s), by - 7.0), 2.6,
|
||||
draw_circle(Vector2(bx + bw * 0.5 - 7.0 + 7.0 * float(s), by - 9.0), 2.6,
|
||||
Color(1.0, 0.85, 0.25))
|
||||
# Rolle
|
||||
draw_string(_font, Vector2(bx + bw + 4.0, by + 7.0), Data.ROLE_SHORT[a.role],
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, 13, Color(0.9, 0.9, 0.95, 0.9))
|
||||
if d["cast"]:
|
||||
draw_arc(foot, bw * 0.55, 0.0, TAU, 24, Color(1.0, 0.95, 0.5, 0.9), 2.5)
|
||||
# Zustände als kleine Marken neben dem Balken.
|
||||
var marks := []
|
||||
if d.get("burn", false): marks.append(["B", Color(1.0, 0.5, 0.2)])
|
||||
if d.get("slow", false): marks.append(["S", Color(0.6, 0.85, 1.0)])
|
||||
if d.get("stun", false): marks.append(["Z", Color(1.0, 0.9, 0.3)])
|
||||
for i in marks.size():
|
||||
draw_string(_font, Vector2(bx - 12.0 - 11.0 * float(i), by + 7.0), marks[i][0],
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, 12, marks[i][1])
|
||||
|
||||
# Schwebende Zahlen
|
||||
for f in _floaters:
|
||||
var t: float = f["age"] / FLOAT_TIME
|
||||
var pos := cam.unproject_position(f["world"] + Vector3(0.0, 1.5, 0.0))
|
||||
pos.y -= t * FLOAT_RISE
|
||||
var col: Color = f["col"]
|
||||
col.a = 1.0 - t * t
|
||||
var size := 15 if f["text"].ends_with("!") else 13
|
||||
draw_string(_font, pos + Vector2(1, 1), f["text"], HORIZONTAL_ALIGNMENT_CENTER, -1, size,
|
||||
Color(0, 0, 0, col.a * 0.8))
|
||||
draw_string(_font, pos, f["text"], HORIZONTAL_ALIGNMENT_CENTER, -1, size, col)
|
||||
|
||||
171
scripts/ui/InfoColumn.gd
Normal file
171
scripts/ui/InfoColumn.gd
Normal file
@@ -0,0 +1,171 @@
|
||||
class_name InfoColumn
|
||||
extends Control
|
||||
|
||||
## Linke Spalte: Elemente im Team, Kombinationen, gewählte Einheit, Rückblick.
|
||||
|
||||
signal upgrade_pressed
|
||||
|
||||
const PANEL_BG := Color(0.14, 0.15, 0.19)
|
||||
|
||||
var traits_box: VBoxContainer
|
||||
var info_box: VBoxContainer
|
||||
var log_box: VBoxContainer
|
||||
var _text_w := 240.0
|
||||
|
||||
|
||||
func build(x: float, y: float, w: float, h: float) -> void:
|
||||
position = Vector2(x, y)
|
||||
size = Vector2(w, h)
|
||||
_text_w = w - 46.0
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
var p := UiKit.panel(PANEL_BG)
|
||||
p.size = size
|
||||
p.clip_contents = true
|
||||
add_child(p)
|
||||
var sc := ScrollContainer.new()
|
||||
sc.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
p.add_child(sc)
|
||||
var col := VBoxContainer.new()
|
||||
col.add_theme_constant_override("separation", 10)
|
||||
col.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
sc.add_child(col)
|
||||
traits_box = VBoxContainer.new()
|
||||
info_box = VBoxContainer.new()
|
||||
log_box = VBoxContainer.new()
|
||||
for b in [traits_box, info_box, log_box]:
|
||||
b.add_theme_constant_override("separation", 3)
|
||||
b.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
col.add_child(b)
|
||||
col.add_child(HSeparator.new())
|
||||
|
||||
|
||||
func refresh(game: Game, avatar: Avatar, hex: Vector2i) -> void:
|
||||
_refresh_traits(game)
|
||||
_refresh_info(game, avatar, hex)
|
||||
_refresh_log(game)
|
||||
|
||||
|
||||
func _wrapped(text: String, size: int, col: Color) -> Label:
|
||||
var l := UiKit.label(text, size, col)
|
||||
l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
l.custom_minimum_size = Vector2(_text_w, 0)
|
||||
l.size_flags_horizontal = Control.SIZE_FILL
|
||||
return l
|
||||
|
||||
|
||||
func _clear(box: Node) -> void:
|
||||
for c in box.get_children():
|
||||
box.remove_child(c)
|
||||
c.queue_free()
|
||||
|
||||
|
||||
func _refresh_traits(game: Game) -> void:
|
||||
_clear(traits_box)
|
||||
traits_box.add_child(UiKit.label("Elemente im Team", 15, Color(0.75, 0.8, 0.95)))
|
||||
var counts := game.human.trait_counts()
|
||||
var any := false
|
||||
for e in Data.ELEMENT_COUNT:
|
||||
var c: int = counts.get(e, 0)
|
||||
if c == 0:
|
||||
continue
|
||||
any = true
|
||||
var tier := Data.trait_tier(e, c)
|
||||
var col: Color = Data.ELEMENT_COLORS[e]
|
||||
var row := HBoxContainer.new()
|
||||
var dot := ColorRect.new()
|
||||
dot.color = col if tier > 0 else col.darkened(0.55)
|
||||
dot.custom_minimum_size = Vector2(12, 12)
|
||||
row.add_child(dot)
|
||||
var txt := " %s %d" % [Data.ELEMENT_NAMES[e], c]
|
||||
if tier > 0:
|
||||
txt += " - " + Data.ELEMENT_TRAITS[e]["desc"][tier - 1]
|
||||
else:
|
||||
txt += " (ab 2 aktiv)"
|
||||
var el := UiKit.label(txt, 13, Color(0.9, 0.9, 0.95) if tier > 0 else Color(0.55, 0.55, 0.6))
|
||||
el.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
el.custom_minimum_size = Vector2(_text_w - 20.0, 0)
|
||||
row.add_child(el)
|
||||
traits_box.add_child(row)
|
||||
if not any:
|
||||
traits_box.add_child(_wrapped("Noch nichts aufgestellt.", 13, Color(0.55, 0.55, 0.6)))
|
||||
traits_box.add_child(UiKit.label("\nKombinationen", 15, Color(0.75, 0.8, 0.95)))
|
||||
var keys := []
|
||||
for c in game.human.active_combos():
|
||||
keys.append(c["key"])
|
||||
for c in Data.COMBO_TRAITS:
|
||||
var on: bool = keys.has(c["key"])
|
||||
var t := "%s (%s+%s): %s" % [c["name"], Data.ELEMENT_NAMES[c["a"]],
|
||||
Data.ELEMENT_NAMES[c["b"]], c["desc"]]
|
||||
traits_box.add_child(_wrapped(t, 12, Color(0.6, 0.95, 0.7) if on else Color(0.45, 0.45, 0.5)))
|
||||
|
||||
|
||||
func _refresh_info(game: Game, a: Avatar, hex: Vector2i) -> void:
|
||||
_clear(info_box)
|
||||
info_box.add_child(UiKit.label("Einheit", 15, Color(0.75, 0.8, 0.95)))
|
||||
if a == null:
|
||||
info_box.add_child(_wrapped("Einheit auf dem Feld oder der Bank anklicken.", 13,
|
||||
Color(0.55, 0.55, 0.6)))
|
||||
return
|
||||
var s := a.base_stats()
|
||||
info_box.add_child(_wrapped("%s %d-Star" % [a.display_name(), a.star], 14, a.color()))
|
||||
info_box.add_child(_wrapped("%s: %s" % [Data.ELEMENT_NAMES[a.primary_element()],
|
||||
Data.PROFILE_HINT[a.primary_element()]], 12, Color(0.75, 0.8, 0.9)))
|
||||
var crit := " Krit %d%%" % int(s["crit"] * 100.0) if s["crit"] > 0.0 else ""
|
||||
info_box.add_child(_wrapped("Leben %d Angriff %d Rüstung %d%%\nTempo %.2f Reichweite %d%s"
|
||||
% [int(s["hp"]), int(s["atk"]), int(s["armor"] * 100.0), s["aspd"], s["range"], crit], 12,
|
||||
Color(0.9, 0.9, 0.95)))
|
||||
info_box.add_child(_wrapped(a.ability_text(), 12, Color(0.8, 0.85, 1.0)))
|
||||
if hex.x < 0:
|
||||
info_box.add_child(_wrapped("Zum Aufwerten aufs Feld ziehen.", 12, Color(0.85, 0.85, 0.9)))
|
||||
return
|
||||
var cost := a.upgrade_cost()
|
||||
if cost.is_empty():
|
||||
info_box.add_child(_wrapped("Maximale Stufe.", 12, Color(1.0, 0.85, 0.35)))
|
||||
return
|
||||
var p := game.human
|
||||
var missing := p.upgrade_missing(hex)
|
||||
var need := []
|
||||
for e in cost:
|
||||
need.append("%dx %s" % [cost[e], Data.ELEMENT_NAMES[e]])
|
||||
info_box.add_child(_wrapped("Aufwerten braucht: " + ", ".join(need), 12, Color(0.85, 0.85, 0.9)))
|
||||
if missing.is_empty():
|
||||
var b := UiKit.button("Aufwerten", Color(0.28, 0.55, 0.36))
|
||||
b.custom_minimum_size = Vector2(140, 26)
|
||||
b.pressed.connect(func(): upgrade_pressed.emit())
|
||||
info_box.add_child(b)
|
||||
else:
|
||||
var miss := []
|
||||
for e in missing:
|
||||
miss.append("%dx %s" % [missing[e], Data.ELEMENT_NAMES[e]])
|
||||
info_box.add_child(_wrapped("Es fehlt: " + ", ".join(miss), 12, Color(0.95, 0.6, 0.5)))
|
||||
|
||||
|
||||
func _refresh_log(game: Game) -> void:
|
||||
_clear(log_box)
|
||||
log_box.add_child(UiKit.label("Letzte Runde", 15, Color(0.75, 0.8, 0.95)))
|
||||
var r := game.human_recap
|
||||
if not r.is_empty():
|
||||
var verdict := "unentschieden"
|
||||
var col := Color(0.85, 0.85, 0.9)
|
||||
if r["result"] > 0:
|
||||
verdict = "gewonnen"
|
||||
col = Color(0.6, 0.95, 0.7)
|
||||
elif r["result"] < 0:
|
||||
verdict = "verloren (-%d Leben)" % r["damage"]
|
||||
col = Color(1.0, 0.6, 0.55)
|
||||
log_box.add_child(_wrapped("Gegen %s: %s" % [r["opponent"], verdict], 13, col))
|
||||
if r["total"] == 0:
|
||||
log_box.add_child(_wrapped("Du hattest keine Einheiten aufgestellt.", 12,
|
||||
Color(0.95, 0.75, 0.7)))
|
||||
else:
|
||||
log_box.add_child(_wrapped("%d von %d Einheiten überlebt, %.0f s" %
|
||||
[r["alive"], r["total"], r["duration"]], 12, Color(0.8, 0.8, 0.86)))
|
||||
var best := "Stärkste: %s (%d Schaden" % [r["best_name"], r["best_dealt"]]
|
||||
best += ", %d geheilt)" % r["best_healed"] if r["best_healed"] > 0 else ")"
|
||||
log_box.add_child(_wrapped(best, 12, Color(0.8, 0.8, 0.86)))
|
||||
log_box.add_child(_wrapped("Gefährlichster Gegner: %s (%d Schaden)" %
|
||||
[r["enemy_name"], r["enemy_dealt"]], 12, Color(0.95, 0.75, 0.7)))
|
||||
if game.last_round_log.is_empty() and r.is_empty():
|
||||
log_box.add_child(UiKit.label("-", 12, Color(0.55, 0.55, 0.6)))
|
||||
for line in game.last_round_log:
|
||||
log_box.add_child(_wrapped(line, 11, Color(0.6, 0.6, 0.68)))
|
||||
1
scripts/ui/InfoColumn.gd.uid
Normal file
1
scripts/ui/InfoColumn.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dr6yh7ynm7li8
|
||||
@@ -1,18 +1,19 @@
|
||||
extends Node3D
|
||||
|
||||
## Spielszene: 3D-Arena plus Bedienoberfläche.
|
||||
## Alles wird im Code aus primitiven Meshes und Controls gebaut.
|
||||
## Spielszene: 3D-Arena, Kamera, Eingabe, Overlays. Die Panels sind eigene
|
||||
## Klassen (TopBar, InfoColumn, PlayersPanel, ShopPanel) und melden sich
|
||||
## über Signale.
|
||||
|
||||
const TOP_H := 46.0
|
||||
const LEFT_W := 290.0 ## links: Elemente, Einheit, Rundenlog
|
||||
const LEFT_W := 290.0 ## links: Elemente, Einheit, Rückblick
|
||||
const RIGHT_W := 210.0 ## rechts: Spielerliste
|
||||
const BOTTOM_H := 138.0
|
||||
const TEXT_W := 244.0
|
||||
const BOTTOM_H := 152.0
|
||||
const BG := Color(0.09, 0.10, 0.13)
|
||||
const PANEL_BG := Color(0.14, 0.15, 0.19)
|
||||
const SLOT_BG := Color(0.20, 0.21, 0.26)
|
||||
const CAM_FOV := 44.0
|
||||
const CAM_PITCH := 52.0 ## Grad über der Waagerechten
|
||||
const ZOOM_MIN := 0.6
|
||||
const ZOOM_MAX := 1.5
|
||||
const YAW_STEP := 15.0
|
||||
|
||||
var game := Game.new()
|
||||
var board: BoardView3D
|
||||
@@ -20,33 +21,28 @@ var camera: Camera3D
|
||||
var ui_layer: CanvasLayer
|
||||
var ui_root: Control
|
||||
var hud: HudOverlay
|
||||
var sfx: Sfx
|
||||
var top: TopBar
|
||||
var info: InfoColumn
|
||||
var players: PlayersPanel
|
||||
var shop: ShopPanel
|
||||
|
||||
var combat_speed := 1.0
|
||||
var started := false
|
||||
var paused := false
|
||||
var scout: PlayerState = null ## Spieler, dessen Brett gerade angesehen wird
|
||||
|
||||
# Kopfzeile
|
||||
var lbl_round: Label
|
||||
var lbl_gold: Label
|
||||
var lbl_hp: Label
|
||||
var lbl_slots: Label
|
||||
var lbl_streak: Label
|
||||
var lbl_phase: Label
|
||||
var btn_ready: Button
|
||||
var btn_speed: Button
|
||||
|
||||
# Panels
|
||||
var players_box: VBoxContainer
|
||||
var traits_box: VBoxContainer
|
||||
var info_box: VBoxContainer
|
||||
var log_box: VBoxContainer
|
||||
var shop_row: HBoxContainer
|
||||
var lbl_fodder: Label
|
||||
var btn_reroll: Button
|
||||
var lbl_hint: Label
|
||||
# Kamera: Einpassung plus Nutzeranteil
|
||||
var _cam_center := Vector3.ZERO
|
||||
var _cam_dir := Vector3.ZERO
|
||||
var _cam_dist := 20.0
|
||||
var cam_zoom := 1.0
|
||||
var cam_yaw := 0.0
|
||||
|
||||
# Auswahl und Ziehen
|
||||
var sel_slot := {}
|
||||
var drag_src := {}
|
||||
var drag_avatar: Avatar = null
|
||||
var drag_moved := false
|
||||
|
||||
# Element-Auswahl
|
||||
var picker: PopupPanel
|
||||
@@ -55,7 +51,7 @@ var picker_title: Label
|
||||
var picker_sub: Label
|
||||
var picker_cb: Callable
|
||||
|
||||
var overlay: Control
|
||||
var overlay: Control ## Start-, Pause- oder Endbildschirm
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -66,54 +62,93 @@ func _ready() -> void:
|
||||
hud.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
hud.board = board
|
||||
ui_layer.add_child(hud)
|
||||
sfx = Sfx.new()
|
||||
add_child(sfx)
|
||||
hud.sfx = sfx
|
||||
ui_root = Control.new()
|
||||
ui_root.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
ui_root.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
ui_layer.add_child(ui_root)
|
||||
|
||||
_build_top()
|
||||
_build_left()
|
||||
_build_right()
|
||||
_build_bottom()
|
||||
_build_panels()
|
||||
_build_picker()
|
||||
|
||||
game.start()
|
||||
board.game = game
|
||||
_refresh()
|
||||
_show_start()
|
||||
set_process(true)
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if game.phase == Game.Phase.COMBAT:
|
||||
if not started or paused:
|
||||
return
|
||||
if game.phase == Game.Phase.PLANNING:
|
||||
if game.tick_planning(delta):
|
||||
_cancel_drag()
|
||||
sel_slot = {}
|
||||
picker.hide()
|
||||
sfx.play("runde")
|
||||
_refresh()
|
||||
else:
|
||||
top.refresh_timer(game)
|
||||
elif game.phase == Game.Phase.COMBAT:
|
||||
var before := game.phase
|
||||
game.update_combat(delta * combat_speed)
|
||||
_refresh_header()
|
||||
top.refresh(game, combat_speed)
|
||||
if game.phase != before:
|
||||
_after_combat()
|
||||
_refresh()
|
||||
|
||||
|
||||
func _after_combat() -> void:
|
||||
match game.human_last_result:
|
||||
1: sfx.play("sieg")
|
||||
-1: sfx.play("niederlage")
|
||||
_: sfx.play("runde")
|
||||
|
||||
|
||||
# --- Eingabe -----------------------------------------------------------------
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if not started or paused:
|
||||
if event is InputEventKey and event.pressed and event.keycode == KEY_P and started:
|
||||
_toggle_pause()
|
||||
return
|
||||
if event is InputEventMouseMotion:
|
||||
if event.button_mask & MOUSE_BUTTON_MASK_MIDDLE:
|
||||
cam_yaw += event.relative.x * 0.3
|
||||
_apply_camera()
|
||||
return
|
||||
_on_mouse_move(event.position)
|
||||
return
|
||||
if event is InputEventMouseButton:
|
||||
if event.button_index == MOUSE_BUTTON_LEFT:
|
||||
if event.pressed:
|
||||
_begin_drag(event.position)
|
||||
else:
|
||||
_end_drag(event.position)
|
||||
return
|
||||
if event.button_index == MOUSE_BUTTON_RIGHT and event.pressed:
|
||||
_on_right_click(event.position)
|
||||
return
|
||||
match event.button_index:
|
||||
MOUSE_BUTTON_LEFT:
|
||||
if event.pressed:
|
||||
_begin_drag(event.position)
|
||||
else:
|
||||
_end_drag(event.position)
|
||||
MOUSE_BUTTON_RIGHT:
|
||||
if event.pressed:
|
||||
_on_right_click(event.position)
|
||||
MOUSE_BUTTON_WHEEL_UP:
|
||||
if event.pressed:
|
||||
_zoom(-0.1)
|
||||
MOUSE_BUTTON_WHEEL_DOWN:
|
||||
if event.pressed:
|
||||
_zoom(0.1)
|
||||
return
|
||||
if not (event is InputEventKey and event.pressed and not event.echo):
|
||||
return
|
||||
match event.keycode:
|
||||
KEY_D:
|
||||
_on_reroll()
|
||||
KEY_SPACE, KEY_F:
|
||||
_on_ready()
|
||||
KEY_D: _on_reroll()
|
||||
KEY_X: _on_buy_xp()
|
||||
KEY_F: _on_lock()
|
||||
KEY_SPACE: _on_ready()
|
||||
KEY_P: _toggle_pause()
|
||||
KEY_Q: _rotate(-YAW_STEP)
|
||||
KEY_E: _rotate(YAW_STEP)
|
||||
KEY_ESCAPE:
|
||||
_cancel_drag()
|
||||
sel_slot = {}
|
||||
@@ -125,10 +160,8 @@ func _on_mouse_move(pos: Vector2) -> void:
|
||||
board.hover_slot = slot
|
||||
if drag_avatar != null:
|
||||
board.drag_world = _drag_point(pos, slot)
|
||||
drag_moved = true
|
||||
|
||||
|
||||
## Hebt einen Avatar an, wenn unter dem Zeiger einer liegt.
|
||||
func _begin_drag(pos: Vector2) -> void:
|
||||
if game.phase != Game.Phase.PLANNING:
|
||||
return
|
||||
@@ -140,11 +173,9 @@ func _begin_drag(pos: Vector2) -> void:
|
||||
return
|
||||
drag_src = slot
|
||||
drag_avatar = a
|
||||
drag_moved = false
|
||||
board.drag_active = true
|
||||
board.drag_avatar_id = a.id
|
||||
board.drag_world = _drag_point(pos, slot)
|
||||
# Während des Ziehens zeigt das Info-Panel den Avatar in der Hand.
|
||||
sel_slot = slot
|
||||
_refresh()
|
||||
|
||||
@@ -156,18 +187,19 @@ func _end_drag(pos: Vector2) -> void:
|
||||
var src := drag_src
|
||||
_cancel_drag()
|
||||
if dst.is_empty() or BoardView3D._same_slot(src, dst):
|
||||
# Kein Weg zurückgelegt: das war eine Auswahl.
|
||||
sel_slot = src
|
||||
_refresh()
|
||||
return
|
||||
if dst["kind"] == "board" and not board.is_own_half(BoardView3D.from_board(dst["hex"])):
|
||||
_refresh()
|
||||
return
|
||||
if not game.human.move_slot(src, dst, game.round_no):
|
||||
if not game.human.move_slot(src, dst):
|
||||
if dst["kind"] == "board":
|
||||
_hint("Kaderplätze voll (%d)." % game.human.team_slots(game.round_no))
|
||||
shop.hint("Kaderplätze voll (%d). XP kaufen für mehr." % game.human.team_slots())
|
||||
sfx.play("fehler")
|
||||
else:
|
||||
sel_slot = dst
|
||||
sfx.play("setzen")
|
||||
_refresh()
|
||||
|
||||
|
||||
@@ -186,9 +218,10 @@ func _on_right_click(pos: Vector2) -> void:
|
||||
if slot.is_empty():
|
||||
return
|
||||
if slot["kind"] == "bench":
|
||||
game.human.sell_bench(slot["index"], game.pool)
|
||||
else:
|
||||
game.human.unplace(slot["hex"])
|
||||
if game.human.sell_bench(slot["index"], game.pool):
|
||||
sfx.play("verkauf")
|
||||
elif game.human.unplace(slot["hex"]):
|
||||
sfx.play("setzen")
|
||||
sel_slot = {}
|
||||
_refresh()
|
||||
|
||||
@@ -201,82 +234,7 @@ func _avatar_at(slot: Dictionary):
|
||||
return game.human.board.get(slot["hex"])
|
||||
|
||||
|
||||
# --- Aufbau ----------------------------------------------------------------
|
||||
|
||||
func _add_panel(pos: Vector2, sz: Vector2) -> VBoxContainer:
|
||||
var p := UiKit.panel(PANEL_BG)
|
||||
p.position = pos
|
||||
p.size = sz
|
||||
ui_root.add_child(p)
|
||||
var v := VBoxContainer.new()
|
||||
v.add_theme_constant_override("separation", 4)
|
||||
p.add_child(v)
|
||||
return v
|
||||
|
||||
|
||||
func _build_top() -> void:
|
||||
var bar := ColorRect.new()
|
||||
bar.color = PANEL_BG
|
||||
bar.position = Vector2.ZERO
|
||||
bar.size = Vector2(1600, TOP_H)
|
||||
bar.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
ui_root.add_child(bar)
|
||||
|
||||
var h := HBoxContainer.new()
|
||||
h.position = Vector2(14, 9)
|
||||
h.size = Vector2(1570, 30)
|
||||
h.add_theme_constant_override("separation", 22)
|
||||
ui_root.add_child(h)
|
||||
|
||||
lbl_round = UiKit.label("Runde 1", 18)
|
||||
lbl_phase = UiKit.label("Planung", 16, Color(0.7, 0.85, 1.0))
|
||||
lbl_hp = UiKit.label("Leben 100", 16, Color(0.5, 0.9, 0.55))
|
||||
lbl_gold = UiKit.label("Gold 4", 16, Color(1.0, 0.85, 0.35))
|
||||
lbl_slots = UiKit.label("Kader 0/3", 16)
|
||||
lbl_streak = UiKit.label("Serie 0", 16, Color(0.85, 0.7, 1.0))
|
||||
for l in [lbl_round, lbl_phase, lbl_hp, lbl_gold, lbl_slots, lbl_streak]:
|
||||
h.add_child(l)
|
||||
|
||||
var spacer := Control.new()
|
||||
spacer.custom_minimum_size = Vector2(360, 0)
|
||||
h.add_child(spacer)
|
||||
|
||||
btn_speed = UiKit.button("Tempo 1x", Color(0.25, 0.28, 0.36))
|
||||
btn_speed.custom_minimum_size = Vector2(96, 28)
|
||||
btn_speed.pressed.connect(_on_speed)
|
||||
h.add_child(btn_speed)
|
||||
|
||||
btn_ready = UiKit.button("Kampf starten [Leer]", Color(0.24, 0.52, 0.34))
|
||||
btn_ready.custom_minimum_size = Vector2(190, 28)
|
||||
btn_ready.pressed.connect(_on_ready)
|
||||
h.add_child(btn_ready)
|
||||
|
||||
|
||||
## Linke Spalte: Elemente, gewählte Einheit und Rundenlog, scrollbar.
|
||||
func _build_left() -> void:
|
||||
var w := LEFT_W - 16.0
|
||||
var h := 900.0 - TOP_H - BOTTOM_H - 20.0
|
||||
var p := UiKit.panel(PANEL_BG)
|
||||
p.position = Vector2(8, TOP_H + 8)
|
||||
p.size = Vector2(w, h)
|
||||
p.clip_contents = true
|
||||
ui_root.add_child(p)
|
||||
var sc := ScrollContainer.new()
|
||||
sc.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
p.add_child(sc)
|
||||
var col := VBoxContainer.new()
|
||||
col.add_theme_constant_override("separation", 10)
|
||||
col.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
sc.add_child(col)
|
||||
traits_box = VBoxContainer.new()
|
||||
info_box = VBoxContainer.new()
|
||||
log_box = VBoxContainer.new()
|
||||
for b in [traits_box, info_box, log_box]:
|
||||
b.add_theme_constant_override("separation", 3)
|
||||
b.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
col.add_child(b)
|
||||
col.add_child(HSeparator.new())
|
||||
|
||||
# --- Welt und Kamera ---------------------------------------------------------
|
||||
|
||||
func _build_world() -> void:
|
||||
var we := WorldEnvironment.new()
|
||||
@@ -314,7 +272,6 @@ func _build_world() -> void:
|
||||
|
||||
|
||||
## Legt die Kamera so, dass das Spielfeld genau in die freie Bildmitte passt.
|
||||
## Links, rechts und unten liegen Bedienpanels darüber.
|
||||
func _fit_camera() -> void:
|
||||
var vp := Vector2(get_viewport().get_visible_rect().size)
|
||||
var target := Rect2(LEFT_W + 16.0, TOP_H + 16.0,
|
||||
@@ -341,10 +298,26 @@ func _fit_camera() -> void:
|
||||
if absf(kx) < 0.001 or absf(kz) < 0.001:
|
||||
break
|
||||
center += Vector3(err.x / kx, 0.0, err.y / kz)
|
||||
if err.length() < 1.0 and absf(1.0 - maxf(r.size.x / target.size.x,
|
||||
r.size.y / target.size.y)) < 0.01:
|
||||
break
|
||||
camera.look_at_from_position(center + dir * dist, center, Vector3.UP)
|
||||
_cam_center = center
|
||||
_cam_dir = dir
|
||||
_cam_dist = dist
|
||||
_apply_camera()
|
||||
|
||||
|
||||
## Nutzeranteil: Zoom und Drehung um die Brettmitte.
|
||||
func _apply_camera() -> void:
|
||||
var dir := _cam_dir.rotated(Vector3.UP, deg_to_rad(cam_yaw))
|
||||
camera.look_at_from_position(_cam_center + dir * _cam_dist * cam_zoom, _cam_center, Vector3.UP)
|
||||
|
||||
|
||||
func _zoom(delta: float) -> void:
|
||||
cam_zoom = clampf(cam_zoom + delta, ZOOM_MIN, ZOOM_MAX)
|
||||
_apply_camera()
|
||||
|
||||
|
||||
func _rotate(deg: float) -> void:
|
||||
cam_yaw = fmod(cam_yaw + deg, 360.0)
|
||||
_apply_camera()
|
||||
|
||||
|
||||
func _project_rect(points: Array) -> Rect2:
|
||||
@@ -359,7 +332,6 @@ func _project_rect(points: Array) -> Rect2:
|
||||
return Rect2(lo, hi - lo)
|
||||
|
||||
|
||||
## Punkt auf der Feldebene unter einer Bildschirmposition, oder null.
|
||||
func _ground_point(screen_pos: Vector2):
|
||||
if camera == null:
|
||||
return null
|
||||
@@ -373,7 +345,6 @@ func _ground_point(screen_pos: Vector2):
|
||||
return board.to_local(from + dir * t)
|
||||
|
||||
|
||||
## Position des gezogenen Avatars: auf Feldhöhe, damit er nicht schwebt.
|
||||
func _drag_point(screen_pos: Vector2, fallback: Dictionary) -> Vector3:
|
||||
var hit = _ground_point(screen_pos)
|
||||
if hit == null:
|
||||
@@ -381,7 +352,6 @@ func _drag_point(screen_pos: Vector2, fallback: Dictionary) -> Vector3:
|
||||
return Vector3(hit.x, 0.0, hit.z)
|
||||
|
||||
|
||||
## Feld oder Bankplatz unter einer Bildschirmposition.
|
||||
func _pick_slot(screen_pos: Vector2) -> Dictionary:
|
||||
var hit = _ground_point(screen_pos)
|
||||
if hit == null:
|
||||
@@ -389,45 +359,35 @@ func _pick_slot(screen_pos: Vector2) -> Dictionary:
|
||||
return board.slot_at_world(hit)
|
||||
|
||||
|
||||
## Rechte Spalte: die acht Spieler mit Leben und Serie.
|
||||
func _build_right() -> void:
|
||||
players_box = _add_panel(Vector2(1600.0 - RIGHT_W + 8.0, TOP_H + 8),
|
||||
Vector2(RIGHT_W - 16, 900 - TOP_H - BOTTOM_H - 20))
|
||||
players_box.add_child(UiKit.label("Spieler", 16, Color(0.75, 0.8, 0.95)))
|
||||
# --- Panels ------------------------------------------------------------------
|
||||
|
||||
func _build_panels() -> void:
|
||||
var vp := Vector2(1600, 900)
|
||||
top = TopBar.new()
|
||||
top.build(vp.x, TOP_H)
|
||||
top.ready_pressed.connect(_on_ready)
|
||||
top.xp_pressed.connect(_on_buy_xp)
|
||||
top.speed_pressed.connect(_on_speed)
|
||||
top.pause_pressed.connect(_toggle_pause)
|
||||
ui_root.add_child(top)
|
||||
|
||||
func _build_bottom() -> void:
|
||||
var y := 900.0 - BOTTOM_H
|
||||
var p := UiKit.panel(PANEL_BG)
|
||||
p.position = Vector2(8, y)
|
||||
p.size = Vector2(1584, BOTTOM_H - 10)
|
||||
ui_root.add_child(p)
|
||||
var v := VBoxContainer.new()
|
||||
v.add_theme_constant_override("separation", 6)
|
||||
p.add_child(v)
|
||||
var col_h := vp.y - TOP_H - BOTTOM_H - 20.0
|
||||
info = InfoColumn.new()
|
||||
info.build(8.0, TOP_H + 8.0, LEFT_W - 16.0, col_h)
|
||||
info.upgrade_pressed.connect(_on_upgrade)
|
||||
ui_root.add_child(info)
|
||||
|
||||
var head := HBoxContainer.new()
|
||||
head.add_theme_constant_override("separation", 16)
|
||||
v.add_child(head)
|
||||
head.add_child(UiKit.label("Shop", 13, Color(0.7, 0.72, 0.8)))
|
||||
btn_reroll = UiKit.button("Neu würfeln %d Gold [D]" % Data.REROLL_COST, Color(0.30, 0.32, 0.44))
|
||||
btn_reroll.custom_minimum_size = Vector2(200, 26)
|
||||
btn_reroll.pressed.connect(_on_reroll)
|
||||
head.add_child(btn_reroll)
|
||||
lbl_fodder = UiKit.label("", 13, Color(0.7, 0.72, 0.8))
|
||||
head.add_child(lbl_fodder)
|
||||
lbl_hint = UiKit.label("", 13, Color(0.95, 0.85, 0.45))
|
||||
head.add_child(lbl_hint)
|
||||
var spacer := Control.new()
|
||||
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
head.add_child(spacer)
|
||||
head.add_child(UiKit.label(
|
||||
"Avatare zwischen Bank und Feld ziehen · Rechtsklick: verkaufen bzw. zurück",
|
||||
13, Color(0.6, 0.62, 0.7)))
|
||||
players = PlayersPanel.new()
|
||||
players.build(vp.x - RIGHT_W + 8.0, TOP_H + 8.0, RIGHT_W - 16.0, col_h)
|
||||
players.scout_requested.connect(_on_scout)
|
||||
ui_root.add_child(players)
|
||||
|
||||
shop_row = HBoxContainer.new()
|
||||
shop_row.add_theme_constant_override("separation", 8)
|
||||
v.add_child(shop_row)
|
||||
shop = ShopPanel.new()
|
||||
shop.build(8.0, vp.y - BOTTOM_H, vp.x - 16.0, BOTTOM_H - 10.0)
|
||||
shop.buy_requested.connect(_on_buy)
|
||||
shop.reroll_pressed.connect(_on_reroll)
|
||||
shop.lock_pressed.connect(_on_lock)
|
||||
ui_root.add_child(shop)
|
||||
|
||||
|
||||
func _build_picker() -> void:
|
||||
@@ -478,32 +438,56 @@ func _open_picker(title: String, options: Array, cb: Callable, previews: Array =
|
||||
picker.popup_centered()
|
||||
|
||||
|
||||
# --- Aktionen --------------------------------------------------------------
|
||||
# --- Aktionen ----------------------------------------------------------------
|
||||
|
||||
func _on_ready() -> void:
|
||||
if game.phase == Game.Phase.PLANNING:
|
||||
_cancel_drag()
|
||||
sel_slot = {}
|
||||
picker.hide()
|
||||
game.start_combat()
|
||||
sfx.play("runde")
|
||||
_refresh()
|
||||
elif game.phase == Game.Phase.COMBAT:
|
||||
game.skip_combat()
|
||||
_after_combat()
|
||||
_refresh()
|
||||
|
||||
|
||||
func _on_speed() -> void:
|
||||
combat_speed = 1.0 if combat_speed >= 4.0 else combat_speed * 2.0
|
||||
btn_speed.text = "Tempo %dx" % int(combat_speed)
|
||||
top.refresh(game, combat_speed)
|
||||
|
||||
|
||||
func _on_buy_xp() -> void:
|
||||
if game.phase != Game.Phase.PLANNING:
|
||||
return
|
||||
var before := game.human.level
|
||||
if game.human.buy_xp():
|
||||
sfx.play("level" if game.human.level > before else "kauf")
|
||||
else:
|
||||
shop.hint("Nicht genug Gold für XP." if game.human.level < Data.MAX_LEVEL else "Maximales Level.")
|
||||
_refresh()
|
||||
|
||||
|
||||
func _on_reroll() -> void:
|
||||
if game.phase != Game.Phase.PLANNING:
|
||||
return
|
||||
game.human.reroll(game.pool, game.rand)
|
||||
if game.human.reroll(game.pool, game.rand):
|
||||
sfx.play("wuerfeln")
|
||||
_refresh()
|
||||
|
||||
|
||||
func _on_buy(index: int) -> void:
|
||||
func _on_lock() -> void:
|
||||
if game.phase != Game.Phase.PLANNING:
|
||||
return
|
||||
game.human.shop_locked = not game.human.shop_locked
|
||||
sfx.play("setzen")
|
||||
_refresh()
|
||||
|
||||
|
||||
## Kauf. `quick` (Rechtsklick) nimmt bei Neutral die zuletzt gewählte Spezialisierung.
|
||||
func _on_buy(index: int, quick: bool) -> void:
|
||||
if game.phase != Game.Phase.PLANNING:
|
||||
return
|
||||
var p := game.human
|
||||
@@ -513,26 +497,32 @@ func _on_buy(index: int) -> void:
|
||||
if entry.sold:
|
||||
return
|
||||
if p.free_bench_slot() < 0:
|
||||
_hint("Bank ist voll. Erst etwas verkaufen.")
|
||||
shop.hint("Bank ist voll. Erst etwas verkaufen.")
|
||||
return
|
||||
if p.gold < entry.price():
|
||||
return
|
||||
if entry.element >= 0:
|
||||
if not p.buy(index, game.pool, entry.element):
|
||||
_hint("Kauf nicht möglich (Vorrat leer).")
|
||||
var direct := entry.element
|
||||
if direct < 0 and quick and p.last_element >= 0 and game.pool.element_left[p.last_element] > 0:
|
||||
direct = p.last_element
|
||||
if direct >= 0:
|
||||
if p.buy(index, game.pool, direct):
|
||||
sfx.play("kauf")
|
||||
else:
|
||||
shop.hint("Kauf nicht möglich (Vorrat leer).")
|
||||
_refresh()
|
||||
return
|
||||
# Neutraler Avatar: Spezialisierung schon beim Kauf wählen.
|
||||
var free: Array[int] = []
|
||||
for e in Data.ELEMENT_COUNT:
|
||||
if game.pool.element_left[e] > 0:
|
||||
free.append(e)
|
||||
if free.is_empty():
|
||||
_hint("Kein Element mehr im Vorrat.")
|
||||
shop.hint("Kein Element mehr im Vorrat.")
|
||||
return
|
||||
_open_picker("Spezialisierung wählen", free, func(e):
|
||||
if not game.human.buy(index, game.pool, e):
|
||||
_hint("Kauf nicht möglich (Vorrat leer).")
|
||||
if game.human.buy(index, game.pool, e):
|
||||
sfx.play("kauf")
|
||||
else:
|
||||
shop.hint("Kauf nicht möglich (Vorrat leer).")
|
||||
_refresh())
|
||||
|
||||
|
||||
@@ -547,7 +537,8 @@ func _on_upgrade() -> void:
|
||||
for e in options:
|
||||
previews.append("+ " + _element_effect(e) + " (halbe Stärke)")
|
||||
_open_picker("Zweite/dritte Spezialisierung", options, func(e):
|
||||
game.human.upgrade(hex, e, game.pool)
|
||||
if game.human.upgrade(hex, e, game.pool):
|
||||
sfx.play("aufwerten")
|
||||
_refresh(), previews,
|
||||
"Fähigkeit wird zu: " + _star_effect(a.role, a.star + 1))
|
||||
|
||||
@@ -555,17 +546,11 @@ func _on_upgrade() -> void:
|
||||
func _star_effect(role: int, star: int) -> String:
|
||||
match role:
|
||||
Data.Role.TANK:
|
||||
if star == 2:
|
||||
return "Schild und spottet 3 Gegner"
|
||||
return "Schild, Spott und 25% Reflexion"
|
||||
return "Schild und spottet 3 Gegner" if star == 2 else "Schild, Spott und 25% Reflexion"
|
||||
Data.Role.DPS:
|
||||
if star == 2:
|
||||
return "Ziel plus Splash auf Nachbarn"
|
||||
return "Ziel plus Fläche im Radius 2"
|
||||
return "Ziel plus Splash auf Nachbarn" if star == 2 else "Ziel plus Fläche im Radius 2"
|
||||
_:
|
||||
if star == 2:
|
||||
return "heilt 3 Verbündete"
|
||||
return "heilt alle und schildet sie"
|
||||
return "heilt 3 Verbündete" if star == 2 else "heilt alle und schildet sie"
|
||||
|
||||
|
||||
func _element_effect(element: int) -> String:
|
||||
@@ -578,253 +563,115 @@ func _element_effect(element: int) -> String:
|
||||
_: return "Verlangsamung"
|
||||
|
||||
|
||||
## Gewähltes Board-Feld, oder (-1,-1) wenn nichts oder ein Bankplatz gewählt ist.
|
||||
func _on_scout(p: PlayerState) -> void:
|
||||
scout = null if (p == game.human or p == scout) else p
|
||||
board.scout_player = scout
|
||||
_refresh()
|
||||
|
||||
|
||||
func selected_board_hex() -> Vector2i:
|
||||
if sel_slot.is_empty() or sel_slot["kind"] != "board":
|
||||
return Vector2i(-1, -1)
|
||||
return sel_slot["hex"]
|
||||
|
||||
|
||||
func _hint(text: String) -> void:
|
||||
lbl_hint.text = text
|
||||
get_tree().create_timer(2.5).timeout.connect(func():
|
||||
if is_instance_valid(lbl_hint) and lbl_hint.text == text:
|
||||
lbl_hint.text = "")
|
||||
|
||||
|
||||
# --- Anzeige ---------------------------------------------------------------
|
||||
# --- Anzeige -----------------------------------------------------------------
|
||||
|
||||
func _refresh() -> void:
|
||||
_refresh_header()
|
||||
_refresh_players()
|
||||
_refresh_traits()
|
||||
_refresh_info()
|
||||
_refresh_log()
|
||||
_refresh_fodder()
|
||||
_refresh_shop()
|
||||
top.refresh(game, combat_speed)
|
||||
players.refresh(game, scout)
|
||||
info.refresh(game, _avatar_at(sel_slot), selected_board_hex())
|
||||
shop.refresh(game)
|
||||
board.selected_slot = sel_slot
|
||||
if game.phase == Game.Phase.GAMEOVER:
|
||||
_show_gameover()
|
||||
|
||||
|
||||
func _refresh_header() -> void:
|
||||
var p := game.human
|
||||
lbl_round.text = "Runde %d" % game.round_no
|
||||
lbl_hp.text = "Leben %d" % p.hp
|
||||
var interest := Data.interest(p.gold)
|
||||
lbl_gold.text = "Gold %d (+%d Zins)" % [p.gold, interest] if interest > 0 else "Gold %d" % p.gold
|
||||
lbl_slots.text = "Kader %d/%d" % [p.board.size(), p.team_slots(game.round_no)]
|
||||
var s := p.streak
|
||||
lbl_streak.text = "Serie %s%d (+%d Gold)" % ["+" if s > 0 else "", s, Data.streak_gold(s)]
|
||||
match game.phase:
|
||||
Game.Phase.PLANNING:
|
||||
lbl_phase.text = "Planung"
|
||||
btn_ready.text = "Kampf starten [Leer]"
|
||||
btn_ready.disabled = false
|
||||
Game.Phase.COMBAT:
|
||||
var opp := game.human_opponent()
|
||||
lbl_phase.text = "Kampf gegen %s" % (opp.pname if opp else "?")
|
||||
btn_ready.text = "Kampf überspringen"
|
||||
btn_ready.disabled = false
|
||||
_:
|
||||
lbl_phase.text = "Spiel vorbei"
|
||||
btn_ready.disabled = true
|
||||
# --- Overlays: Start, Pause, Ende --------------------------------------------
|
||||
|
||||
|
||||
## Label für das rechte Panel: bricht um und bleibt in der Panelbreite.
|
||||
func _wrapped(text: String, size: int, col: Color) -> Label:
|
||||
var l := UiKit.label(text, size, col)
|
||||
l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
l.custom_minimum_size = Vector2(TEXT_W, 0)
|
||||
l.size_flags_horizontal = Control.SIZE_FILL
|
||||
return l
|
||||
|
||||
|
||||
func _clear(box: Node, keep: int = 0) -> void:
|
||||
var kids := box.get_children()
|
||||
for i in range(keep, kids.size()):
|
||||
box.remove_child(kids[i])
|
||||
kids[i].queue_free()
|
||||
|
||||
|
||||
func _refresh_players() -> void:
|
||||
_clear(players_box, 1)
|
||||
for p in game.ranking():
|
||||
var row := VBoxContainer.new()
|
||||
row.add_theme_constant_override("separation", 1)
|
||||
var top := HBoxContainer.new()
|
||||
var col := Color(0.9, 0.9, 0.95) if p.alive else Color(0.5, 0.5, 0.55)
|
||||
if p == game.human:
|
||||
col = Color(0.6, 0.9, 1.0)
|
||||
var nm := UiKit.label(p.pname, 14, col)
|
||||
nm.custom_minimum_size = Vector2(88, 0)
|
||||
top.add_child(nm)
|
||||
top.add_child(UiKit.label("%d" % p.hp, 14, col))
|
||||
if p.streak != 0:
|
||||
top.add_child(UiKit.label(" Serie %s%d" % ["+" if p.streak > 0 else "", p.streak], 12,
|
||||
Color(0.85, 0.7, 1.0)))
|
||||
row.add_child(top)
|
||||
var bar := ColorRect.new()
|
||||
bar.color = Color(0.12, 0.13, 0.16)
|
||||
bar.custom_minimum_size = Vector2(RIGHT_W - 32, 6)
|
||||
var fill := ColorRect.new()
|
||||
fill.color = Color(0.35, 0.8, 0.45) if p.alive else Color(0.4, 0.4, 0.45)
|
||||
fill.size = Vector2((RIGHT_W - 32) * (float(p.hp) / float(Data.START_HP)), 6)
|
||||
fill.position = Vector2.ZERO
|
||||
bar.add_child(fill)
|
||||
row.add_child(bar)
|
||||
players_box.add_child(row)
|
||||
var opp := game.human_opponent()
|
||||
if opp != null and game.phase == Game.Phase.COMBAT:
|
||||
players_box.add_child(UiKit.label("\nGegner: " + opp.pname, 13, Color(1.0, 0.6, 0.6)))
|
||||
|
||||
|
||||
func _refresh_traits() -> void:
|
||||
_clear(traits_box)
|
||||
traits_box.add_child(UiKit.label("Elemente im Team", 15, Color(0.75, 0.8, 0.95)))
|
||||
var counts := game.human.trait_counts()
|
||||
var any := false
|
||||
for e in Data.ELEMENT_COUNT:
|
||||
var c: int = counts.get(e, 0)
|
||||
if c == 0:
|
||||
continue
|
||||
any = true
|
||||
var tier := Data.trait_tier(e, c)
|
||||
var col: Color = Data.ELEMENT_COLORS[e]
|
||||
var row := HBoxContainer.new()
|
||||
var dot := ColorRect.new()
|
||||
dot.color = col if tier > 0 else col.darkened(0.55)
|
||||
dot.custom_minimum_size = Vector2(12, 12)
|
||||
row.add_child(dot)
|
||||
var txt := " %s %d" % [Data.ELEMENT_NAMES[e], c]
|
||||
if tier > 0:
|
||||
txt += " - " + Data.ELEMENT_TRAITS[e]["desc"][tier - 1]
|
||||
else:
|
||||
txt += " (ab 2 aktiv)"
|
||||
var el := UiKit.label(txt, 13, Color(0.9, 0.9, 0.95) if tier > 0 else Color(0.55, 0.55, 0.6))
|
||||
el.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
el.custom_minimum_size = Vector2(TEXT_W - 20.0, 0)
|
||||
row.add_child(el)
|
||||
traits_box.add_child(row)
|
||||
if not any:
|
||||
traits_box.add_child(_wrapped("Noch nichts aufgestellt.", 13, Color(0.55, 0.55, 0.6)))
|
||||
traits_box.add_child(UiKit.label("\nKombinationen", 15, Color(0.75, 0.8, 0.95)))
|
||||
var active := game.human.active_combos()
|
||||
var keys := []
|
||||
for c in active:
|
||||
keys.append(c["key"])
|
||||
for c in Data.COMBO_TRAITS:
|
||||
var on: bool = keys.has(c["key"])
|
||||
var t := "%s (%s+%s): %s" % [c["name"], Data.ELEMENT_NAMES[c["a"]],
|
||||
Data.ELEMENT_NAMES[c["b"]], c["desc"]]
|
||||
traits_box.add_child(_wrapped(t, 12, Color(0.6, 0.95, 0.7) if on else Color(0.45, 0.45, 0.5)))
|
||||
|
||||
|
||||
func _refresh_info() -> void:
|
||||
_clear(info_box)
|
||||
info_box.add_child(UiKit.label("Einheit", 15, Color(0.75, 0.8, 0.95)))
|
||||
var p := game.human
|
||||
var a: Avatar = _selected_avatar()
|
||||
if a == null:
|
||||
info_box.add_child(_wrapped("Einheit auf dem Feld oder der Bank anklicken.", 13,
|
||||
Color(0.55, 0.55, 0.6)))
|
||||
return
|
||||
var s := a.base_stats()
|
||||
info_box.add_child(_wrapped("%s %d-Star" % [a.display_name(), a.star], 14, a.color()))
|
||||
info_box.add_child(_wrapped("Leben %d Angriff %d Rüstung %d%%\nTempo %.2f Reichweite %d"
|
||||
% [int(s["hp"]), int(s["atk"]), int(s["armor"] * 100.0), s["aspd"], s["range"]], 12,
|
||||
Color(0.9, 0.9, 0.95)))
|
||||
info_box.add_child(_wrapped(a.ability_text(), 12, Color(0.8, 0.85, 1.0)))
|
||||
|
||||
var hex := selected_board_hex()
|
||||
if hex.x < 0:
|
||||
info_box.add_child(_wrapped("Zum Aufwerten aufs Feld ziehen.", 12,
|
||||
Color(0.85, 0.85, 0.9)))
|
||||
return
|
||||
var cost := a.upgrade_cost()
|
||||
if cost.is_empty():
|
||||
info_box.add_child(_wrapped("Maximale Stufe.", 12, Color(1.0, 0.85, 0.35)))
|
||||
return
|
||||
var missing := p.upgrade_missing(hex)
|
||||
var need := []
|
||||
for e in cost:
|
||||
need.append("%dx %s" % [cost[e], Data.ELEMENT_NAMES[e]])
|
||||
info_box.add_child(_wrapped("Aufwerten braucht: " + ", ".join(need), 12,
|
||||
Color(0.85, 0.85, 0.9)))
|
||||
if missing.is_empty():
|
||||
var b := UiKit.button("Aufwerten", Color(0.28, 0.55, 0.36))
|
||||
b.custom_minimum_size = Vector2(140, 26)
|
||||
b.pressed.connect(_on_upgrade)
|
||||
info_box.add_child(b)
|
||||
else:
|
||||
var miss := []
|
||||
for e in missing:
|
||||
miss.append("%dx %s" % [missing[e], Data.ELEMENT_NAMES[e]])
|
||||
info_box.add_child(_wrapped("Es fehlt: " + ", ".join(miss), 12, Color(0.95, 0.6, 0.5)))
|
||||
|
||||
|
||||
func _selected_avatar():
|
||||
return _avatar_at(sel_slot)
|
||||
|
||||
|
||||
func _refresh_log() -> void:
|
||||
_clear(log_box)
|
||||
log_box.add_child(UiKit.label("Letzte Runde", 15, Color(0.75, 0.8, 0.95)))
|
||||
if game.last_round_log.is_empty():
|
||||
log_box.add_child(UiKit.label("-", 12, Color(0.55, 0.55, 0.6)))
|
||||
for line in game.last_round_log:
|
||||
log_box.add_child(_wrapped(line, 12, Color(0.8, 0.8, 0.86)))
|
||||
|
||||
|
||||
func _refresh_fodder() -> void:
|
||||
var fodder := game.human.fodder_counts()
|
||||
var parts := []
|
||||
for e in fodder:
|
||||
parts.append("%dx %s" % [fodder[e], Data.ELEMENT_NAMES[e]])
|
||||
lbl_fodder.text = "Futter auf der Bank: " + (", ".join(parts) if parts else "-")
|
||||
|
||||
|
||||
func _refresh_shop() -> void:
|
||||
_clear(shop_row)
|
||||
var p := game.human
|
||||
for i in p.shop.size():
|
||||
var e: ShopEntry = p.shop[i]
|
||||
var col := e.color()
|
||||
var txt := "verkauft"
|
||||
var b: Button
|
||||
if e.sold:
|
||||
b = UiKit.button(txt, SLOT_BG.darkened(0.3))
|
||||
b.disabled = true
|
||||
else:
|
||||
var note: String = "Element wählen" if e.element < 0 \
|
||||
else "noch %d im Vorrat" % game.pool.element_left[e.element]
|
||||
b = UiKit.button("%s\n%d Gold\n%s" % [e.label(), e.price(), note], col, UiKit.ink(col))
|
||||
b.disabled = p.gold < e.price() or game.phase != Game.Phase.PLANNING
|
||||
b.pressed.connect(_on_buy.bind(i))
|
||||
b.custom_minimum_size = Vector2(196, 74)
|
||||
shop_row.add_child(b)
|
||||
var mine := []
|
||||
for el in game.human.shop_elements(game.pool):
|
||||
mine.append("%s %d" % [Data.ELEMENT_NAMES[el], game.pool.element_left[el]])
|
||||
shop_row.add_child(UiKit.label(" Dein Shop zeigt Neutral\n und deine Elemente:\n %s"
|
||||
% [", ".join(mine) if mine else "noch keine aufgestellt"], 12,
|
||||
Color(0.7, 0.72, 0.8)))
|
||||
|
||||
|
||||
func _show_gameover() -> void:
|
||||
func _overlay_box(title: String) -> VBoxContainer:
|
||||
if overlay != null:
|
||||
return
|
||||
overlay.queue_free()
|
||||
overlay = ColorRect.new()
|
||||
overlay.color = Color(0, 0, 0, 0.72)
|
||||
overlay.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
ui_root.add_child(overlay)
|
||||
var v := VBoxContainer.new()
|
||||
v.position = Vector2(620, 320)
|
||||
v.add_theme_constant_override("separation", 10)
|
||||
v.position = Vector2(600, 280)
|
||||
v.custom_minimum_size = Vector2(400, 0)
|
||||
v.add_theme_constant_override("separation", 12)
|
||||
overlay.add_child(v)
|
||||
v.add_child(UiKit.label(title, 32))
|
||||
return v
|
||||
|
||||
|
||||
func _close_overlay() -> void:
|
||||
if overlay != null:
|
||||
overlay.queue_free()
|
||||
overlay = null
|
||||
|
||||
|
||||
func _volume_row() -> HBoxContainer:
|
||||
var h := HBoxContainer.new()
|
||||
h.add_theme_constant_override("separation", 10)
|
||||
h.add_child(UiKit.label("Lautstärke", 14))
|
||||
var sl := HSlider.new()
|
||||
sl.min_value = 0.0
|
||||
sl.max_value = 1.0
|
||||
sl.step = 0.05
|
||||
sl.value = 0.8
|
||||
sl.custom_minimum_size = Vector2(220, 20)
|
||||
sl.focus_mode = Control.FOCUS_NONE
|
||||
sl.value_changed.connect(func(v): sfx.set_volume(v))
|
||||
h.add_child(sl)
|
||||
return h
|
||||
|
||||
|
||||
func _show_start() -> void:
|
||||
var v := _overlay_box("Elements Autochess")
|
||||
v.add_child(UiKit.label("8 Spieler, 100 Leben, Elemente als Traits. Basis wie TFT.", 15,
|
||||
Color(0.8, 0.85, 0.95)))
|
||||
v.add_child(UiKit.label(
|
||||
"Ziehen: Bank ↔ Feld · Rechtsklick: verkaufen · D würfeln · F einfrieren · X Level\nQ/E drehen · Mausrad zoomen · P Pause · Leertaste Kampf",
|
||||
13, Color(0.7, 0.72, 0.8)))
|
||||
v.add_child(_volume_row())
|
||||
var b := UiKit.button("Spiel starten", Color(0.28, 0.5, 0.36))
|
||||
b.custom_minimum_size = Vector2(200, 38)
|
||||
b.pressed.connect(_start_game)
|
||||
v.add_child(b)
|
||||
|
||||
|
||||
func _start_game() -> void:
|
||||
started = true
|
||||
paused = false
|
||||
_close_overlay()
|
||||
sfx.play("runde")
|
||||
|
||||
|
||||
func _toggle_pause() -> void:
|
||||
if not started or game.phase == Game.Phase.GAMEOVER:
|
||||
return
|
||||
paused = not paused
|
||||
if paused:
|
||||
_cancel_drag()
|
||||
picker.hide()
|
||||
var v := _overlay_box("Pause")
|
||||
v.add_child(_volume_row())
|
||||
var b := UiKit.button("Weiter [P]", Color(0.28, 0.5, 0.36))
|
||||
b.custom_minimum_size = Vector2(200, 36)
|
||||
b.pressed.connect(_toggle_pause)
|
||||
v.add_child(b)
|
||||
else:
|
||||
_close_overlay()
|
||||
|
||||
|
||||
func _show_gameover() -> void:
|
||||
if overlay != null and overlay.has_meta("gameover"):
|
||||
return
|
||||
var v := _overlay_box("Spiel vorbei")
|
||||
overlay.set_meta("gameover", true)
|
||||
var alive := game.alive_players()
|
||||
var win: String = alive[0].pname if not alive.is_empty() else "-"
|
||||
v.add_child(UiKit.label("Spiel vorbei", 32))
|
||||
v.add_child(UiKit.label("Sieger: %s" % win, 20, Color(1.0, 0.85, 0.35)))
|
||||
var place := game.human.placement if game.human.placement > 0 else 1
|
||||
v.add_child(UiKit.label("Dein Platz: %d von 8" % place, 18,
|
||||
@@ -832,12 +679,15 @@ func _show_gameover() -> void:
|
||||
var b := UiKit.button("Neues Spiel", Color(0.28, 0.5, 0.36))
|
||||
b.custom_minimum_size = Vector2(180, 36)
|
||||
b.pressed.connect(func():
|
||||
overlay.queue_free()
|
||||
overlay = null
|
||||
_close_overlay()
|
||||
game = Game.new()
|
||||
game.start()
|
||||
board.game = game
|
||||
board.scout_player = null
|
||||
scout = null
|
||||
_cancel_drag()
|
||||
sel_slot = {}
|
||||
started = true
|
||||
paused = false
|
||||
_refresh())
|
||||
v.add_child(b)
|
||||
|
||||
76
scripts/ui/PlayersPanel.gd
Normal file
76
scripts/ui/PlayersPanel.gd
Normal file
@@ -0,0 +1,76 @@
|
||||
class_name PlayersPanel
|
||||
extends Control
|
||||
|
||||
## Rechte Spalte: die acht Spieler mit Leben, Serie und Klick zum Ansehen.
|
||||
|
||||
signal scout_requested(p: PlayerState)
|
||||
|
||||
const PANEL_BG := Color(0.14, 0.15, 0.19)
|
||||
|
||||
var box: VBoxContainer
|
||||
var _w := 0.0
|
||||
|
||||
|
||||
func build(x: float, y: float, w: float, h: float) -> void:
|
||||
_w = w
|
||||
position = Vector2(x, y)
|
||||
size = Vector2(w, h)
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
var p := UiKit.panel(PANEL_BG)
|
||||
p.size = size
|
||||
add_child(p)
|
||||
box = VBoxContainer.new()
|
||||
box.add_theme_constant_override("separation", 4)
|
||||
p.add_child(box)
|
||||
box.add_child(UiKit.label("Spieler · Klick = ansehen", 15, Color(0.75, 0.8, 0.95)))
|
||||
|
||||
|
||||
func refresh(game: Game, scout: PlayerState) -> void:
|
||||
var kids := box.get_children()
|
||||
for i in range(1, kids.size()):
|
||||
box.remove_child(kids[i])
|
||||
kids[i].queue_free()
|
||||
var next_opp := game.human_opponent()
|
||||
for p in game.ranking():
|
||||
var row := VBoxContainer.new()
|
||||
row.add_theme_constant_override("separation", 1)
|
||||
var top := HBoxContainer.new()
|
||||
var col := Color(0.9, 0.9, 0.95) if p.alive else Color(0.5, 0.5, 0.55)
|
||||
if p == game.human:
|
||||
col = Color(0.6, 0.9, 1.0)
|
||||
elif p == next_opp:
|
||||
col = Color(1.0, 0.75, 0.6)
|
||||
var nm := UiKit.button(p.pname, PANEL_BG if p != scout else Color(0.30, 0.34, 0.48), col)
|
||||
nm.custom_minimum_size = Vector2(96, 22)
|
||||
nm.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||
nm.disabled = not p.alive
|
||||
nm.pressed.connect(func(): scout_requested.emit(p))
|
||||
top.add_child(nm)
|
||||
top.add_child(UiKit.label("%d" % p.hp, 14, col))
|
||||
if p.streak != 0:
|
||||
top.add_child(UiKit.label(" %s%d" % ["+" if p.streak > 0 else "", p.streak], 12,
|
||||
Color(0.85, 0.7, 1.0)))
|
||||
row.add_child(top)
|
||||
var bar := ColorRect.new()
|
||||
bar.color = Color(0.12, 0.13, 0.16)
|
||||
bar.custom_minimum_size = Vector2(_w - 16, 6)
|
||||
var fill := ColorRect.new()
|
||||
fill.color = Color(0.35, 0.8, 0.45) if p.alive else Color(0.4, 0.4, 0.45)
|
||||
fill.size = Vector2((_w - 16) * (float(p.hp) / float(Data.START_HP)), 6)
|
||||
bar.add_child(fill)
|
||||
row.add_child(bar)
|
||||
box.add_child(row)
|
||||
if next_opp != null and game.phase == Game.Phase.PLANNING:
|
||||
box.add_child(_note("\nNächster Gegner: %s\nLevel %d, %d Einheiten" %
|
||||
[next_opp.pname, next_opp.level, next_opp.board.size()], Color(1.0, 0.75, 0.6)))
|
||||
elif next_opp != null:
|
||||
box.add_child(_note("\nGegner: " + next_opp.pname, Color(1.0, 0.6, 0.6)))
|
||||
if scout != null:
|
||||
box.add_child(_note("Oben: Brett von %s" % scout.pname, Color(0.75, 0.85, 1.0)))
|
||||
|
||||
|
||||
func _note(text: String, col: Color) -> Label:
|
||||
var l := UiKit.label(text, 13, col)
|
||||
l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
l.custom_minimum_size = Vector2(_w - 20, 0)
|
||||
return l
|
||||
1
scripts/ui/PlayersPanel.gd.uid
Normal file
1
scripts/ui/PlayersPanel.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bveden78b8xd1
|
||||
114
scripts/ui/Sfx.gd
Normal file
114
scripts/ui/Sfx.gd
Normal file
@@ -0,0 +1,114 @@
|
||||
class_name Sfx
|
||||
extends Node
|
||||
|
||||
## Primitive Klänge, im Code erzeugt: kurze Töne mit Hüllkurve.
|
||||
## Keine Audiodateien, passend zu den primitiven Meshes.
|
||||
|
||||
const RATE := 22050
|
||||
const HIT_COOLDOWN := 0.06
|
||||
|
||||
var _streams := {}
|
||||
var _players: Array = []
|
||||
var _next := 0
|
||||
var _hit_gate := 0.0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
for i in 8:
|
||||
var pl := AudioStreamPlayer.new()
|
||||
pl.volume_db = -8.0
|
||||
add_child(pl)
|
||||
_players.append(pl)
|
||||
_streams["kauf"] = _tone([[660.0, 0.07]], 0.5)
|
||||
_streams["verkauf"] = _tone([[520.0, 0.05], [330.0, 0.07]], 0.45)
|
||||
_streams["wuerfeln"] = _noise(0.04, 0.35)
|
||||
_streams["setzen"] = _tone([[180.0, 0.06]], 0.6, true)
|
||||
_streams["aufwerten"] = _tone([[523.0, 0.08], [659.0, 0.08], [784.0, 0.12]], 0.55)
|
||||
_streams["level"] = _tone([[440.0, 0.08], [880.0, 0.14]], 0.5)
|
||||
_streams["treffer"] = _noise(0.03, 0.22)
|
||||
_streams["krit"] = _tone([[220.0, 0.05]], 0.55, true)
|
||||
_streams["heilung"] = _tone([[988.0, 0.06]], 0.3)
|
||||
_streams["zauber"] = _tone([[880.0, 0.05], [1320.0, 0.09]], 0.45)
|
||||
_streams["sieg"] = _tone([[523.0, 0.1], [659.0, 0.1], [784.0, 0.1], [1046.0, 0.2]], 0.6)
|
||||
_streams["niederlage"] = _tone([[392.0, 0.12], [311.0, 0.12], [261.0, 0.24]], 0.6)
|
||||
_streams["runde"] = _tone([[440.0, 0.06], [440.0, 0.06]], 0.4)
|
||||
_streams["fehler"] = _tone([[200.0, 0.1]], 0.5, true)
|
||||
|
||||
|
||||
## Lautstärke 0..1, 0 schaltet stumm.
|
||||
func set_volume(linear: float) -> void:
|
||||
for pl in _players:
|
||||
pl.volume_db = linear_to_db(maxf(0.0001, linear)) - 8.0
|
||||
pl.stream_paused = false
|
||||
_muted = linear <= 0.001
|
||||
|
||||
|
||||
var _muted := false
|
||||
|
||||
|
||||
func play(name: String) -> void:
|
||||
if _muted:
|
||||
return
|
||||
if not _streams.has(name):
|
||||
return
|
||||
if name == "treffer":
|
||||
var now := Time.get_ticks_msec() / 1000.0
|
||||
if now < _hit_gate:
|
||||
return
|
||||
_hit_gate = now + HIT_COOLDOWN
|
||||
var pl: AudioStreamPlayer = _players[_next]
|
||||
_next = (_next + 1) % _players.size()
|
||||
pl.stream = _streams[name]
|
||||
pl.play()
|
||||
|
||||
|
||||
## Tonfolge [[frequenz, dauer], ...] mit Ein- und Ausblenden.
|
||||
func _tone(notes: Array, gain: float, square: bool = false) -> AudioStreamWAV:
|
||||
var total := 0.0
|
||||
for n in notes:
|
||||
total += n[1]
|
||||
var frames := int(total * RATE)
|
||||
var data := PackedByteArray()
|
||||
data.resize(frames * 2)
|
||||
var idx := 0
|
||||
for n in notes:
|
||||
var freq: float = n[0]
|
||||
var len_frames := int(n[1] * RATE)
|
||||
for i in len_frames:
|
||||
var t := float(i) / RATE
|
||||
var env := minf(1.0, float(i) / (0.005 * RATE)) * minf(1.0, float(len_frames - i) / (0.02 * RATE))
|
||||
var v := sin(TAU * freq * t)
|
||||
if square:
|
||||
v = 1.0 if v >= 0.0 else -1.0
|
||||
_put(data, idx, v * env * gain)
|
||||
idx += 1
|
||||
if idx >= frames:
|
||||
break
|
||||
return _wav(data)
|
||||
|
||||
|
||||
## Kurzes Rauschen für Treffer und Würfeln.
|
||||
func _noise(dur: float, gain: float) -> AudioStreamWAV:
|
||||
var frames := int(dur * RATE)
|
||||
var data := PackedByteArray()
|
||||
data.resize(frames * 2)
|
||||
var rng := RandomNumberGenerator.new()
|
||||
rng.seed = 1234
|
||||
for i in frames:
|
||||
var env := 1.0 - float(i) / float(frames)
|
||||
_put(data, i, rng.randf_range(-1.0, 1.0) * env * env * gain)
|
||||
return _wav(data)
|
||||
|
||||
|
||||
func _put(data: PackedByteArray, index: int, v: float) -> void:
|
||||
var s := int(clampf(v, -1.0, 1.0) * 32767.0)
|
||||
data.encode_s16(index * 2, s)
|
||||
|
||||
|
||||
func _wav(data: PackedByteArray) -> AudioStreamWAV:
|
||||
var w := AudioStreamWAV.new()
|
||||
w.format = AudioStreamWAV.FORMAT_16_BITS
|
||||
w.mix_rate = RATE
|
||||
w.stereo = false
|
||||
w.data = data
|
||||
return w
|
||||
1
scripts/ui/Sfx.gd.uid
Normal file
1
scripts/ui/Sfx.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ciqf6ibhibuqp
|
||||
113
scripts/ui/ShopPanel.gd
Normal file
113
scripts/ui/ShopPanel.gd
Normal file
@@ -0,0 +1,113 @@
|
||||
class_name ShopPanel
|
||||
extends Control
|
||||
|
||||
## Unten: Shop, Würfeln, Einfrieren, Futter und Hinweise.
|
||||
|
||||
signal buy_requested(index: int, quick: bool)
|
||||
signal reroll_pressed
|
||||
signal lock_pressed
|
||||
|
||||
const PANEL_BG := Color(0.14, 0.15, 0.19)
|
||||
const SLOT_BG := Color(0.20, 0.21, 0.26)
|
||||
|
||||
var row: HBoxContainer
|
||||
var btn_reroll: Button
|
||||
var btn_lock: Button
|
||||
var lbl_fodder: Label
|
||||
var lbl_hint: Label
|
||||
|
||||
|
||||
func build(x: float, y: float, w: float, h: float) -> void:
|
||||
position = Vector2(x, y)
|
||||
size = Vector2(w, h)
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
var p := UiKit.panel(PANEL_BG)
|
||||
p.size = size
|
||||
add_child(p)
|
||||
var v := VBoxContainer.new()
|
||||
v.add_theme_constant_override("separation", 6)
|
||||
p.add_child(v)
|
||||
|
||||
var head := HBoxContainer.new()
|
||||
head.add_theme_constant_override("separation", 14)
|
||||
v.add_child(head)
|
||||
head.add_child(UiKit.label("Shop", 13, Color(0.7, 0.72, 0.8)))
|
||||
btn_reroll = UiKit.button("Neu würfeln %d Gold [D]" % Data.REROLL_COST, Color(0.30, 0.32, 0.44))
|
||||
btn_reroll.custom_minimum_size = Vector2(200, 26)
|
||||
btn_reroll.pressed.connect(func(): reroll_pressed.emit())
|
||||
head.add_child(btn_reroll)
|
||||
btn_lock = UiKit.button("Einfrieren [F]", Color(0.30, 0.38, 0.50))
|
||||
btn_lock.custom_minimum_size = Vector2(150, 26)
|
||||
btn_lock.pressed.connect(func(): lock_pressed.emit())
|
||||
head.add_child(btn_lock)
|
||||
lbl_fodder = UiKit.label("", 13, Color(0.7, 0.72, 0.8))
|
||||
head.add_child(lbl_fodder)
|
||||
lbl_hint = UiKit.label("", 13, Color(0.95, 0.85, 0.45))
|
||||
head.add_child(lbl_hint)
|
||||
var spacer := Control.new()
|
||||
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
head.add_child(spacer)
|
||||
head.add_child(UiKit.label(
|
||||
"Ziehen: Bank ↔ Feld · Rechtsklick: verkaufen / zurück · Rechtsklick auf Neutral: wie zuletzt",
|
||||
13, Color(0.6, 0.62, 0.7)))
|
||||
|
||||
row = HBoxContainer.new()
|
||||
row.add_theme_constant_override("separation", 8)
|
||||
v.add_child(row)
|
||||
|
||||
|
||||
func refresh(game: Game) -> void:
|
||||
var p := game.human
|
||||
for c in row.get_children():
|
||||
row.remove_child(c)
|
||||
c.queue_free()
|
||||
for i in p.shop.size():
|
||||
var e: ShopEntry = p.shop[i]
|
||||
var b: Button
|
||||
if e.sold:
|
||||
b = UiKit.button("verkauft", SLOT_BG.darkened(0.3))
|
||||
b.disabled = true
|
||||
else:
|
||||
var col := e.color()
|
||||
var note: String
|
||||
if e.element < 0:
|
||||
note = "Element wählen"
|
||||
if p.last_element >= 0:
|
||||
note += "\nRechtsklick: %s" % Data.ELEMENT_NAMES[p.last_element]
|
||||
else:
|
||||
note = "noch %d im Vorrat" % game.pool.element_left[e.element]
|
||||
b = UiKit.button("%s\n%d Gold\n%s" % [e.label(), e.price(), note], col, UiKit.ink(col))
|
||||
b.disabled = p.gold < e.price() or game.phase != Game.Phase.PLANNING
|
||||
b.gui_input.connect(_on_card_input.bind(i))
|
||||
b.custom_minimum_size = Vector2(196, 88)
|
||||
row.add_child(b)
|
||||
var mine := []
|
||||
for el in p.shop_elements(game.pool):
|
||||
mine.append("%s %d" % [Data.ELEMENT_NAMES[el], game.pool.element_left[el]])
|
||||
row.add_child(UiKit.label(" Dein Shop zeigt Neutral\n und deine Elemente:\n %s"
|
||||
% [", ".join(mine) if mine else "noch keine aufgestellt"], 12, Color(0.7, 0.72, 0.8)))
|
||||
|
||||
var fodder := p.fodder_counts()
|
||||
var parts := []
|
||||
for e in fodder:
|
||||
parts.append("%dx %s" % [fodder[e], Data.ELEMENT_NAMES[e]])
|
||||
lbl_fodder.text = "Futter: " + (", ".join(parts) if parts else "-")
|
||||
btn_lock.text = "Eingefroren [F]" if p.shop_locked else "Einfrieren [F]"
|
||||
btn_lock.add_theme_stylebox_override("normal",
|
||||
UiKit.flat(Color(0.35, 0.55, 0.75) if p.shop_locked else Color(0.30, 0.38, 0.50)))
|
||||
btn_reroll.disabled = game.phase != Game.Phase.PLANNING or p.gold < Data.REROLL_COST
|
||||
|
||||
|
||||
func _on_card_input(event: InputEvent, index: int) -> void:
|
||||
if event is InputEventMouseButton and event.pressed:
|
||||
if event.button_index == MOUSE_BUTTON_LEFT:
|
||||
buy_requested.emit(index, false)
|
||||
elif event.button_index == MOUSE_BUTTON_RIGHT:
|
||||
buy_requested.emit(index, true)
|
||||
|
||||
|
||||
func hint(text: String) -> void:
|
||||
lbl_hint.text = text
|
||||
get_tree().create_timer(2.5).timeout.connect(func():
|
||||
if is_instance_valid(lbl_hint) and lbl_hint.text == text:
|
||||
lbl_hint.text = "")
|
||||
1
scripts/ui/ShopPanel.gd.uid
Normal file
1
scripts/ui/ShopPanel.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cg27viivxmuap
|
||||
120
scripts/ui/TopBar.gd
Normal file
120
scripts/ui/TopBar.gd
Normal file
@@ -0,0 +1,120 @@
|
||||
class_name TopBar
|
||||
extends Control
|
||||
|
||||
## Kopfzeile: Runde, Phase, Uhr, Gold, Kader, Serie, nächster Gegner, Knöpfe.
|
||||
|
||||
signal ready_pressed
|
||||
signal xp_pressed
|
||||
signal speed_pressed
|
||||
signal pause_pressed
|
||||
|
||||
const PANEL_BG := Color(0.14, 0.15, 0.19)
|
||||
|
||||
var lbl_round: Label
|
||||
var lbl_phase: Label
|
||||
var lbl_timer: Label
|
||||
var lbl_hp: Label
|
||||
var lbl_gold: Label
|
||||
var lbl_slots: Label
|
||||
var lbl_streak: Label
|
||||
var lbl_next: Label
|
||||
var btn_xp: Button
|
||||
var btn_speed: Button
|
||||
var btn_ready: Button
|
||||
var btn_pause: Button
|
||||
|
||||
|
||||
func build(width: float, height: float) -> void:
|
||||
position = Vector2.ZERO
|
||||
size = Vector2(width, height)
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
var bar := ColorRect.new()
|
||||
bar.color = PANEL_BG
|
||||
bar.size = size
|
||||
bar.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(bar)
|
||||
|
||||
var h := HBoxContainer.new()
|
||||
h.position = Vector2(14, 9)
|
||||
h.size = Vector2(width - 28, 30)
|
||||
h.add_theme_constant_override("separation", 18)
|
||||
add_child(h)
|
||||
|
||||
lbl_round = UiKit.label("Runde 1", 18)
|
||||
lbl_phase = UiKit.label("Planung", 16, Color(0.7, 0.85, 1.0))
|
||||
lbl_timer = UiKit.label("30 s", 16, Color(0.95, 0.85, 0.45))
|
||||
lbl_hp = UiKit.label("Leben 100", 16, Color(0.5, 0.9, 0.55))
|
||||
lbl_gold = UiKit.label("Gold 4", 16, Color(1.0, 0.85, 0.35))
|
||||
lbl_slots = UiKit.label("Kader 0/3", 16)
|
||||
lbl_streak = UiKit.label("Serie 0", 16, Color(0.85, 0.7, 1.0))
|
||||
lbl_next = UiKit.label("", 15, Color(1.0, 0.75, 0.6))
|
||||
for l in [lbl_round, lbl_phase, lbl_timer, lbl_hp, lbl_gold, lbl_slots, lbl_streak, lbl_next]:
|
||||
h.add_child(l)
|
||||
|
||||
btn_xp = UiKit.button("XP kaufen %d Gold [X]" % Data.XP_BUY_COST, Color(0.36, 0.30, 0.50))
|
||||
btn_xp.custom_minimum_size = Vector2(170, 28)
|
||||
btn_xp.pressed.connect(func(): xp_pressed.emit())
|
||||
h.add_child(btn_xp)
|
||||
|
||||
var spacer := Control.new()
|
||||
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
h.add_child(spacer)
|
||||
|
||||
btn_pause = UiKit.button("Pause [P]", Color(0.25, 0.28, 0.36))
|
||||
btn_pause.custom_minimum_size = Vector2(96, 28)
|
||||
btn_pause.pressed.connect(func(): pause_pressed.emit())
|
||||
h.add_child(btn_pause)
|
||||
|
||||
btn_speed = UiKit.button("Tempo 1x", Color(0.25, 0.28, 0.36))
|
||||
btn_speed.custom_minimum_size = Vector2(96, 28)
|
||||
btn_speed.pressed.connect(func(): speed_pressed.emit())
|
||||
h.add_child(btn_speed)
|
||||
|
||||
btn_ready = UiKit.button("Kampf starten [Leer]", Color(0.24, 0.52, 0.34))
|
||||
btn_ready.custom_minimum_size = Vector2(190, 28)
|
||||
btn_ready.pressed.connect(func(): ready_pressed.emit())
|
||||
h.add_child(btn_ready)
|
||||
|
||||
|
||||
func refresh(game: Game, combat_speed: float) -> void:
|
||||
var p := game.human
|
||||
lbl_round.text = "Runde %d" % game.round_no
|
||||
lbl_hp.text = "Leben %d" % p.hp
|
||||
var interest := Data.interest(p.gold)
|
||||
lbl_gold.text = "Gold %d (+%d Zins)" % [p.gold, interest] if interest > 0 else "Gold %d" % p.gold
|
||||
if p.level >= Data.MAX_LEVEL:
|
||||
lbl_slots.text = "Kader %d/%d (Level max)" % [p.board.size(), p.team_slots()]
|
||||
else:
|
||||
lbl_slots.text = "Kader %d/%d Level %d XP %d/%d" % [p.board.size(), p.team_slots(),
|
||||
p.level, p.xp, Data.xp_needed(p.level)]
|
||||
var s := p.streak
|
||||
lbl_streak.text = "Serie %s%d (+%d Gold)" % ["+" if s > 0 else "", s, Data.streak_gold(s)]
|
||||
btn_speed.text = "Tempo %dx" % int(combat_speed)
|
||||
btn_xp.disabled = game.phase != Game.Phase.PLANNING or not p.can_buy_xp()
|
||||
var opp := game.human_opponent()
|
||||
match game.phase:
|
||||
Game.Phase.PLANNING:
|
||||
lbl_phase.text = "Planung"
|
||||
lbl_next.text = "Nächster Gegner: %s" % opp.pname if opp else ""
|
||||
btn_ready.text = "Kampf starten [Leer]"
|
||||
btn_ready.disabled = false
|
||||
Game.Phase.COMBAT:
|
||||
lbl_phase.text = "Kampf gegen %s" % (opp.pname if opp else "?")
|
||||
lbl_next.text = ""
|
||||
btn_ready.text = "Kampf überspringen"
|
||||
btn_ready.disabled = false
|
||||
_:
|
||||
lbl_phase.text = "Spiel vorbei"
|
||||
lbl_next.text = ""
|
||||
btn_ready.disabled = true
|
||||
refresh_timer(game)
|
||||
|
||||
|
||||
func refresh_timer(game: Game) -> void:
|
||||
if game.phase == Game.Phase.PLANNING and game.timer_enabled:
|
||||
lbl_timer.text = "%d s" % int(ceil(game.plan_left))
|
||||
lbl_timer.add_theme_color_override("font_color",
|
||||
Color(1.0, 0.45, 0.4) if game.plan_left <= 5.0 else Color(0.95, 0.85, 0.45))
|
||||
lbl_timer.visible = true
|
||||
else:
|
||||
lbl_timer.visible = false
|
||||
1
scripts/ui/TopBar.gd.uid
Normal file
1
scripts/ui/TopBar.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bmlotka44lvsq
|
||||
Reference in New Issue
Block a user