first commit
This commit is contained in:
843
scripts/ui/Main.gd
Normal file
843
scripts/ui/Main.gd
Normal file
@@ -0,0 +1,843 @@
|
||||
extends Node3D
|
||||
|
||||
## Spielszene: 3D-Arena plus Bedienoberfläche.
|
||||
## Alles wird im Code aus primitiven Meshes und Controls gebaut.
|
||||
|
||||
const TOP_H := 46.0
|
||||
const LEFT_W := 290.0 ## links: Elemente, Einheit, Rundenlog
|
||||
const RIGHT_W := 210.0 ## rechts: Spielerliste
|
||||
const BOTTOM_H := 138.0
|
||||
const TEXT_W := 244.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
|
||||
|
||||
var game := Game.new()
|
||||
var board: BoardView3D
|
||||
var camera: Camera3D
|
||||
var ui_layer: CanvasLayer
|
||||
var ui_root: Control
|
||||
var hud: HudOverlay
|
||||
var combat_speed := 1.0
|
||||
|
||||
# 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
|
||||
|
||||
# Auswahl und Ziehen
|
||||
var sel_slot := {}
|
||||
var drag_src := {}
|
||||
var drag_avatar: Avatar = null
|
||||
var drag_moved := false
|
||||
|
||||
# Element-Auswahl
|
||||
var picker: PopupPanel
|
||||
var picker_grid: GridContainer
|
||||
var picker_title: Label
|
||||
var picker_sub: Label
|
||||
var picker_cb: Callable
|
||||
|
||||
var overlay: Control
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_build_world()
|
||||
ui_layer = CanvasLayer.new()
|
||||
add_child(ui_layer)
|
||||
hud = HudOverlay.new()
|
||||
hud.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
hud.board = board
|
||||
ui_layer.add_child(hud)
|
||||
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_picker()
|
||||
|
||||
game.start()
|
||||
board.game = game
|
||||
_refresh()
|
||||
set_process(true)
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if game.phase == Game.Phase.COMBAT:
|
||||
var before := game.phase
|
||||
game.update_combat(delta * combat_speed)
|
||||
_refresh_header()
|
||||
if game.phase != before:
|
||||
_refresh()
|
||||
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if event is InputEventMouseMotion:
|
||||
_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
|
||||
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_ESCAPE:
|
||||
_cancel_drag()
|
||||
sel_slot = {}
|
||||
_refresh()
|
||||
|
||||
|
||||
func _on_mouse_move(pos: Vector2) -> void:
|
||||
var slot := _pick_slot(pos)
|
||||
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
|
||||
var slot := _pick_slot(pos)
|
||||
var a: Avatar = _avatar_at(slot)
|
||||
if a == null:
|
||||
sel_slot = {}
|
||||
_refresh()
|
||||
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()
|
||||
|
||||
|
||||
func _end_drag(pos: Vector2) -> void:
|
||||
if drag_avatar == null:
|
||||
return
|
||||
var dst := _pick_slot(pos)
|
||||
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 dst["kind"] == "board":
|
||||
_hint("Kaderplätze voll (%d)." % game.human.team_slots(game.round_no))
|
||||
else:
|
||||
sel_slot = dst
|
||||
_refresh()
|
||||
|
||||
|
||||
func _cancel_drag() -> void:
|
||||
drag_avatar = null
|
||||
drag_src = {}
|
||||
board.drag_active = false
|
||||
board.drag_avatar_id = -1
|
||||
|
||||
|
||||
func _on_right_click(pos: Vector2) -> void:
|
||||
if game.phase != Game.Phase.PLANNING:
|
||||
return
|
||||
_cancel_drag()
|
||||
var slot := _pick_slot(pos)
|
||||
if slot.is_empty():
|
||||
return
|
||||
if slot["kind"] == "bench":
|
||||
game.human.sell_bench(slot["index"], game.pool)
|
||||
else:
|
||||
game.human.unplace(slot["hex"])
|
||||
sel_slot = {}
|
||||
_refresh()
|
||||
|
||||
|
||||
func _avatar_at(slot: Dictionary):
|
||||
if slot.is_empty():
|
||||
return null
|
||||
if slot["kind"] == "bench":
|
||||
return game.human.bench[slot["index"]]
|
||||
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())
|
||||
|
||||
|
||||
func _build_world() -> void:
|
||||
var we := WorldEnvironment.new()
|
||||
var env := Environment.new()
|
||||
env.background_mode = Environment.BG_COLOR
|
||||
env.background_color = BG
|
||||
env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
|
||||
env.ambient_light_color = Color(0.44, 0.48, 0.62)
|
||||
env.ambient_light_energy = 0.9
|
||||
we.environment = env
|
||||
add_child(we)
|
||||
|
||||
var sun := DirectionalLight3D.new()
|
||||
sun.rotation_degrees = Vector3(-58.0, -35.0, 0.0)
|
||||
sun.light_energy = 1.15
|
||||
sun.shadow_enabled = true
|
||||
add_child(sun)
|
||||
|
||||
var fill := DirectionalLight3D.new()
|
||||
fill.rotation_degrees = Vector3(-22.0, 140.0, 0.0)
|
||||
fill.light_energy = 0.4
|
||||
fill.light_color = Color(0.72, 0.80, 1.0)
|
||||
add_child(fill)
|
||||
|
||||
board = BoardView3D.new()
|
||||
add_child(board)
|
||||
|
||||
camera = Camera3D.new()
|
||||
camera.fov = CAM_FOV
|
||||
camera.look_at_from_position(Vector3(0.0, 16.0, 13.0), Vector3.ZERO, Vector3.UP)
|
||||
add_child(camera)
|
||||
board.camera = camera
|
||||
get_viewport().size_changed.connect(_fit_camera)
|
||||
_fit_camera()
|
||||
|
||||
|
||||
## 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,
|
||||
vp.x - LEFT_W - RIGHT_W - 32.0, vp.y - TOP_H - BOTTOM_H - 32.0)
|
||||
if target.size.x <= 0.0 or target.size.y <= 0.0:
|
||||
return
|
||||
var pts := board.corner_points()
|
||||
var pitch := deg_to_rad(CAM_PITCH)
|
||||
var dir := Vector3(0.0, sin(pitch), cos(pitch))
|
||||
var dist := 20.0
|
||||
var center := Vector3.ZERO
|
||||
for i in 8:
|
||||
camera.look_at_from_position(center + dir * dist, center, Vector3.UP)
|
||||
var r := _project_rect(pts)
|
||||
if r.size.x <= 0.0 or r.size.y <= 0.0:
|
||||
return
|
||||
dist *= maxf(r.size.x / target.size.x, r.size.y / target.size.y)
|
||||
camera.look_at_from_position(center + dir * dist, center, Vector3.UP)
|
||||
r = _project_rect(pts)
|
||||
var err := r.get_center() - target.get_center()
|
||||
var o := camera.unproject_position(center)
|
||||
var kx := camera.unproject_position(center + Vector3(1.0, 0.0, 0.0)).x - o.x
|
||||
var kz := camera.unproject_position(center + Vector3(0.0, 0.0, 1.0)).y - o.y
|
||||
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)
|
||||
|
||||
|
||||
func _project_rect(points: Array) -> Rect2:
|
||||
var lo := Vector2(INF, INF)
|
||||
var hi := Vector2(-INF, -INF)
|
||||
for p in points:
|
||||
if camera.is_position_behind(p):
|
||||
return Rect2()
|
||||
var q := camera.unproject_position(p)
|
||||
lo = Vector2(minf(lo.x, q.x), minf(lo.y, q.y))
|
||||
hi = Vector2(maxf(hi.x, q.x), maxf(hi.y, q.y))
|
||||
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
|
||||
var from := camera.project_ray_origin(screen_pos)
|
||||
var dir := camera.project_ray_normal(screen_pos)
|
||||
if absf(dir.y) < 0.0001:
|
||||
return null
|
||||
var t := (BoardView3D.TILE_H * 0.5 - from.y) / dir.y
|
||||
if t < 0.0:
|
||||
return null
|
||||
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:
|
||||
return board.slot_world(fallback)
|
||||
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:
|
||||
return {}
|
||||
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)))
|
||||
|
||||
|
||||
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 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)))
|
||||
|
||||
shop_row = HBoxContainer.new()
|
||||
shop_row.add_theme_constant_override("separation", 8)
|
||||
v.add_child(shop_row)
|
||||
|
||||
|
||||
func _build_picker() -> void:
|
||||
picker = PopupPanel.new()
|
||||
picker.add_theme_stylebox_override("panel", UiKit.flat(Color(0.16, 0.17, 0.22)))
|
||||
var v := VBoxContainer.new()
|
||||
v.add_theme_constant_override("separation", 8)
|
||||
picker.add_child(v)
|
||||
picker_title = UiKit.label("Element wählen", 16)
|
||||
v.add_child(picker_title)
|
||||
picker_sub = UiKit.label("", 13, Color(0.75, 0.8, 0.95))
|
||||
v.add_child(picker_sub)
|
||||
picker_grid = GridContainer.new()
|
||||
picker_grid.columns = 3
|
||||
picker_grid.add_theme_constant_override("h_separation", 6)
|
||||
picker_grid.add_theme_constant_override("v_separation", 6)
|
||||
v.add_child(picker_grid)
|
||||
ui_root.add_child(picker)
|
||||
|
||||
|
||||
## Element-Auswahl. Mit `previews` steht neben jedem Knopf, was sich ändert.
|
||||
func _open_picker(title: String, options: Array, cb: Callable, previews: Array = [],
|
||||
subtitle: String = "") -> void:
|
||||
picker_title.text = title
|
||||
picker_sub.text = subtitle
|
||||
picker_sub.visible = subtitle != ""
|
||||
for c in picker_grid.get_children():
|
||||
c.queue_free()
|
||||
picker_cb = cb
|
||||
var counts := game.human.trait_counts()
|
||||
var with_preview := not previews.is_empty()
|
||||
picker_grid.columns = 2 if with_preview else 3
|
||||
for i in options.size():
|
||||
var e: int = options[i]
|
||||
var col: Color = Data.ELEMENT_COLORS[e]
|
||||
var b := UiKit.button("%s\n(%d im Team)" % [Data.ELEMENT_NAMES[e], counts.get(e, 0)],
|
||||
col, UiKit.ink(col))
|
||||
b.custom_minimum_size = Vector2(120, 52 if with_preview else 46)
|
||||
b.pressed.connect(func():
|
||||
picker.hide()
|
||||
picker_cb.call(e))
|
||||
picker_grid.add_child(b)
|
||||
if with_preview:
|
||||
var l := UiKit.label(previews[i], 12, Color(0.85, 0.88, 0.95))
|
||||
l.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
picker_grid.add_child(l)
|
||||
picker.reset_size()
|
||||
picker.popup_centered()
|
||||
|
||||
|
||||
# --- Aktionen --------------------------------------------------------------
|
||||
|
||||
func _on_ready() -> void:
|
||||
if game.phase == Game.Phase.PLANNING:
|
||||
_cancel_drag()
|
||||
sel_slot = {}
|
||||
game.start_combat()
|
||||
_refresh()
|
||||
elif game.phase == Game.Phase.COMBAT:
|
||||
game.skip_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)
|
||||
|
||||
|
||||
func _on_reroll() -> void:
|
||||
if game.phase != Game.Phase.PLANNING:
|
||||
return
|
||||
game.human.reroll(game.pool, game.rand)
|
||||
_refresh()
|
||||
|
||||
|
||||
func _on_buy(index: int) -> void:
|
||||
if game.phase != Game.Phase.PLANNING:
|
||||
return
|
||||
var p := game.human
|
||||
if index < 0 or index >= p.shop.size():
|
||||
return
|
||||
var entry: ShopEntry = p.shop[index]
|
||||
if entry.sold:
|
||||
return
|
||||
if p.free_bench_slot() < 0:
|
||||
_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).")
|
||||
_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.")
|
||||
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).")
|
||||
_refresh())
|
||||
|
||||
|
||||
func _on_upgrade() -> void:
|
||||
var p := game.human
|
||||
var hex := selected_board_hex()
|
||||
if hex.x < 0 or not p.can_upgrade(hex):
|
||||
return
|
||||
var a: Avatar = p.board[hex]
|
||||
var options := p.available_new_elements(hex)
|
||||
var previews := []
|
||||
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)
|
||||
_refresh(), previews,
|
||||
"Fähigkeit wird zu: " + _star_effect(a.role, a.star + 1))
|
||||
|
||||
|
||||
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"
|
||||
Data.Role.DPS:
|
||||
if star == 2:
|
||||
return "Ziel plus Splash auf Nachbarn"
|
||||
return "Ziel plus Fläche im Radius 2"
|
||||
_:
|
||||
if star == 2:
|
||||
return "heilt 3 Verbündete"
|
||||
return "heilt alle und schildet sie"
|
||||
|
||||
|
||||
func _element_effect(element: int) -> String:
|
||||
match element:
|
||||
Data.Element.FEUER: return "Brand"
|
||||
Data.Element.WASSER: return "Mana und stärkere Heilung"
|
||||
Data.Element.WIND: return "Tempo-Schub"
|
||||
Data.Element.BLITZ: return "Kettenblitz"
|
||||
Data.Element.ERDE: return "Betäubung"
|
||||
_: return "Verlangsamung"
|
||||
|
||||
|
||||
## Gewähltes Board-Feld, oder (-1,-1) wenn nichts oder ein Bankplatz gewählt ist.
|
||||
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 ---------------------------------------------------------------
|
||||
|
||||
func _refresh() -> void:
|
||||
_refresh_header()
|
||||
_refresh_players()
|
||||
_refresh_traits()
|
||||
_refresh_info()
|
||||
_refresh_log()
|
||||
_refresh_fodder()
|
||||
_refresh_shop()
|
||||
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
|
||||
|
||||
|
||||
## 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:
|
||||
if overlay != null:
|
||||
return
|
||||
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)
|
||||
overlay.add_child(v)
|
||||
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,
|
||||
Color(0.6, 0.95, 0.7) if place <= 4 else Color(0.95, 0.6, 0.6)))
|
||||
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
|
||||
game = Game.new()
|
||||
game.start()
|
||||
board.game = game
|
||||
_cancel_drag()
|
||||
sel_slot = {}
|
||||
_refresh())
|
||||
v.add_child(b)
|
||||
Reference in New Issue
Block a user