724 lines
20 KiB
GDScript
724 lines
20 KiB
GDScript
extends Node3D
|
|
|
|
## 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, Rückblick
|
|
const RIGHT_W := 210.0 ## rechts: Spielerliste
|
|
const BOTTOM_H := 152.0
|
|
const BG := Color(0.09, 0.10, 0.13)
|
|
const YAW_STEP := 15.0
|
|
|
|
var game := Game.new()
|
|
var board: BoardView3D
|
|
var rig: CameraRig
|
|
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 spectating := false ## nach dem Ausscheiden zuschauen
|
|
var eliminated_shown := false
|
|
var scout: PlayerState = null ## Spieler, dessen Brett gerade angesehen wird
|
|
var settings := Settings.new()
|
|
var overlays: Overlays
|
|
var _prev_combos: Array = [] ## für die Meldung neuer Kombinationen
|
|
var _prev_tiers := {} ## Element -> Trait-Stufe
|
|
|
|
# Auswahl und Ziehen
|
|
var sel_slot := {}
|
|
var drag_src := {}
|
|
var drag_avatar: Avatar = null
|
|
|
|
# Element-Auswahl
|
|
var picker: PopupPanel
|
|
var picker_grid: GridContainer
|
|
var picker_title: Label
|
|
var picker_sub: Label
|
|
var picker_cb: Callable
|
|
|
|
|
|
|
|
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)
|
|
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_panels()
|
|
_build_picker()
|
|
var tip := TooltipView.new()
|
|
tip.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
tip.hud = hud
|
|
ui_layer.add_child(tip)
|
|
shop.card_hovered.connect(_on_card_hovered)
|
|
shop.card_left.connect(_on_card_left)
|
|
shop.undo_pressed.connect(_on_undo)
|
|
|
|
settings.load_from_disk()
|
|
sfx.set_volume(settings.volume)
|
|
overlays = Overlays.new()
|
|
overlays.volume = settings.volume
|
|
overlays.difficulty = settings.difficulty
|
|
overlays.start_pressed.connect(_start_game)
|
|
overlays.difficulty_changed.connect(_on_difficulty)
|
|
overlays.volume_changed.connect(func(v):
|
|
settings.volume = v
|
|
sfx.set_volume(v)
|
|
settings.save_to_disk())
|
|
overlays.resume_pressed.connect(_toggle_pause)
|
|
overlays.restart_pressed.connect(_restart)
|
|
overlays.spectate_pressed.connect(_spectate)
|
|
ui_layer.add_child(overlays)
|
|
|
|
game.start(0, settings.difficulty)
|
|
board.game = game
|
|
_refresh()
|
|
overlays.show_start(Game.has_save())
|
|
set_process(true)
|
|
|
|
|
|
func _process(delta: float) -> void:
|
|
if not started or paused:
|
|
return
|
|
if spectating and game.phase == Game.Phase.PLANNING and game.plan_left > 3.0:
|
|
game.plan_left = 3.0
|
|
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)
|
|
top.refresh(game, combat_speed)
|
|
if game.phase != before:
|
|
_after_combat()
|
|
_refresh()
|
|
if game.phase == Game.Phase.PLANNING and game.human.alive:
|
|
game.save()
|
|
|
|
|
|
func _after_combat() -> void:
|
|
match game.human_last_result:
|
|
1: sfx.play("sieg")
|
|
-1: sfx.play("niederlage")
|
|
_: sfx.play("runde")
|
|
if not game.human.alive and not eliminated_shown and game.phase != Game.Phase.GAMEOVER:
|
|
_show_eliminated()
|
|
|
|
|
|
# --- 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:
|
|
rig.rotate_by(event.relative.x * 0.3)
|
|
return
|
|
_on_mouse_move(event.position)
|
|
return
|
|
if event is InputEventMouseButton:
|
|
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:
|
|
rig.zoom_by(-0.1)
|
|
MOUSE_BUTTON_WHEEL_DOWN:
|
|
if event.pressed:
|
|
rig.zoom_by(0.1)
|
|
return
|
|
if not (event is InputEventKey and event.pressed and not event.echo):
|
|
return
|
|
match event.keycode:
|
|
KEY_1, KEY_2, KEY_3, KEY_4, KEY_5:
|
|
_on_buy(event.keycode - KEY_1, false)
|
|
KEY_Z: _on_undo()
|
|
KEY_D: _on_reroll()
|
|
KEY_X: _on_buy_xp()
|
|
KEY_F: _on_lock()
|
|
KEY_SPACE: _on_ready()
|
|
KEY_P: _toggle_pause()
|
|
KEY_Q: rig.rotate_by(-YAW_STEP)
|
|
KEY_E: rig.rotate_by(YAW_STEP)
|
|
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)
|
|
hud.tooltip_avatar = null
|
|
return
|
|
_hover_unit(pos)
|
|
|
|
|
|
## Einheit unter der Maus, egal ob eigene, Gegner im Kampf oder angesehenes Brett.
|
|
func _hover_unit(pos: Vector2) -> void:
|
|
var best: Avatar = null
|
|
var best_d := 34.0
|
|
for d in board.draw_units:
|
|
var world: Vector3 = d["pos"]
|
|
if camera.is_position_behind(world):
|
|
continue
|
|
var sp := camera.unproject_position(world + Vector3(0.0, 0.6, 0.0))
|
|
var dist := sp.distance_to(pos)
|
|
if dist < best_d:
|
|
best_d = dist
|
|
best = d["avatar"]
|
|
hud.tooltip_avatar = best
|
|
hud.tooltip_pos = pos
|
|
hud.tooltip_note = ""
|
|
|
|
|
|
func _on_card_hovered(index: int, screen_pos: Vector2) -> void:
|
|
var p := game.human
|
|
if index < 0 or index >= p.shop.size() or p.shop[index].sold:
|
|
return
|
|
var e: ShopEntry = p.shop[index]
|
|
var element := e.element if e.element >= 0 else (p.last_element if p.last_element >= 0 else Data.Element.FEUER)
|
|
hud.tooltip_avatar = e.to_avatar(element)
|
|
hud.tooltip_note = "Werte als %s, Element wählst du beim Kauf" % Data.ELEMENT_NAMES[element] \
|
|
if e.element < 0 else "%d Gold" % e.price()
|
|
hud.tooltip_pos = screen_pos - Vector2(0.0, 120.0)
|
|
|
|
|
|
func _on_card_left() -> void:
|
|
hud.tooltip_avatar = null
|
|
|
|
|
|
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
|
|
board.drag_active = true
|
|
board.drag_avatar_id = a.id
|
|
board.drag_world = _drag_point(pos, slot)
|
|
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):
|
|
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):
|
|
if dst["kind"] == "board":
|
|
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()
|
|
|
|
|
|
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":
|
|
if game.human.sell_bench(slot["index"], game.pool):
|
|
sfx.play("verkauf")
|
|
shop.show_undo()
|
|
elif game.human.unplace(slot["hex"]):
|
|
sfx.play("setzen")
|
|
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"])
|
|
|
|
|
|
# --- Welt und Kamera ---------------------------------------------------------
|
|
|
|
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)
|
|
|
|
rig = CameraRig.new()
|
|
add_child(rig)
|
|
camera = rig.camera
|
|
board.camera = camera
|
|
get_viewport().size_changed.connect(_fit_camera)
|
|
_fit_camera()
|
|
|
|
|
|
## Das Spielfeld muss in die freie Bildmitte passen: Panels liegen links, rechts, unten.
|
|
func _fit_camera() -> void:
|
|
var vp := Vector2(get_viewport().get_visible_rect().size)
|
|
rig.fit(board.corner_points(), 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))
|
|
|
|
|
|
func _ground_point(screen_pos: Vector2):
|
|
var hit = rig.ground_point(screen_pos, BoardView3D.TILE_H * 0.5)
|
|
return null if hit == null else board.to_local(hit)
|
|
|
|
|
|
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)
|
|
|
|
|
|
func _pick_slot(screen_pos: Vector2) -> Dictionary:
|
|
var hit = _ground_point(screen_pos)
|
|
if hit == null:
|
|
return {}
|
|
return board.slot_at_world(hit)
|
|
|
|
|
|
# --- 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)
|
|
|
|
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)
|
|
|
|
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 = 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:
|
|
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 = {}
|
|
picker.hide()
|
|
game.start_combat()
|
|
sfx.play("runde")
|
|
_refresh()
|
|
elif game.phase == Game.Phase.COMBAT:
|
|
game.skip_combat()
|
|
_after_combat()
|
|
_refresh()
|
|
if game.phase == Game.Phase.PLANNING and game.human.alive:
|
|
game.save()
|
|
|
|
|
|
func _on_speed() -> void:
|
|
combat_speed = 1.0 if combat_speed >= 4.0 else combat_speed * 2.0
|
|
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
|
|
if game.human.reroll(game.pool, game.rand):
|
|
sfx.play("wuerfeln")
|
|
_refresh()
|
|
|
|
|
|
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
|
|
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:
|
|
shop.hint("Bank ist voll. Erst etwas verkaufen.")
|
|
return
|
|
if p.gold < entry.price():
|
|
return
|
|
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
|
|
var free: Array[int] = []
|
|
for e in Data.ELEMENT_COUNT:
|
|
if game.pool.element_left[e] > 0:
|
|
free.append(e)
|
|
if free.is_empty():
|
|
shop.hint("Kein Element mehr im Vorrat.")
|
|
return
|
|
_open_picker("Spezialisierung wählen", free, func(e):
|
|
if game.human.buy(index, game.pool, e):
|
|
sfx.play("kauf")
|
|
else:
|
|
shop.hint("Kauf nicht möglich (Vorrat leer).")
|
|
_refresh())
|
|
|
|
|
|
func _on_undo() -> void:
|
|
if game.phase != Game.Phase.PLANNING:
|
|
return
|
|
if game.human.undo_sell(game.pool):
|
|
sfx.play("kauf")
|
|
shop.hide_undo()
|
|
else:
|
|
shop.hint("Rückgängig geht nicht mehr.")
|
|
_refresh()
|
|
|
|
|
|
func _on_difficulty(d: int) -> void:
|
|
settings.difficulty = d
|
|
settings.save_to_disk()
|
|
game = Game.new()
|
|
game.start(0, d)
|
|
board.game = game
|
|
_refresh()
|
|
overlays.show_start(Game.has_save())
|
|
|
|
|
|
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("+ " + Data.element_effect(e) + " (halbe Stärke)")
|
|
_open_picker("Zweite/dritte Spezialisierung", options, func(e):
|
|
if game.human.upgrade(hex, e, game.pool):
|
|
sfx.play("aufwerten")
|
|
_refresh(), previews,
|
|
"Fähigkeit wird zu: " + Data.star_effect(a.role, a.star + 1))
|
|
|
|
|
|
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"]
|
|
|
|
|
|
# --- Anzeige -----------------------------------------------------------------
|
|
|
|
func _refresh() -> void:
|
|
top.refresh(game, combat_speed)
|
|
players.refresh(game, scout)
|
|
info.refresh(game, _avatar_at(sel_slot), selected_board_hex())
|
|
shop.refresh(game)
|
|
shop.lbl_tip.text = _tutorial_tip()
|
|
board.selected_slot = sel_slot
|
|
_refresh_range()
|
|
_announce_synergies()
|
|
if game.phase == Game.Phase.GAMEOVER:
|
|
Game.delete_save()
|
|
if overlays.kind() != "gameover":
|
|
overlays.show_gameover(game)
|
|
|
|
|
|
## Reichweite der gewählten Board-Einheit auf den Feldern zeigen.
|
|
func _refresh_range() -> void:
|
|
var hex := selected_board_hex()
|
|
var a: Avatar = _avatar_at(sel_slot)
|
|
if hex.x < 0 or a == null or game.phase != Game.Phase.PLANNING:
|
|
board.range_radius = 0
|
|
return
|
|
board.range_center = BoardView3D.from_board(hex)
|
|
if a.role == Data.Role.HEILER:
|
|
board.range_radius = Data.HEAL_RANGE
|
|
board.range_color = Color(0.45, 0.95, 0.55)
|
|
else:
|
|
board.range_radius = int(a.base_stats()["range"])
|
|
board.range_color = Color(1.0, 0.65, 0.3)
|
|
|
|
|
|
## Meldet neu aktive Kombinationen und Trait-Stufen.
|
|
func _announce_synergies() -> void:
|
|
var p := game.human
|
|
var combos := []
|
|
for c in p.active_combos():
|
|
combos.append(c["name"])
|
|
var tiers := {}
|
|
var counts := p.trait_counts()
|
|
for e in counts:
|
|
tiers[e] = Data.trait_tier(e, counts[e])
|
|
if game.phase == Game.Phase.PLANNING:
|
|
var msgs := []
|
|
for name in combos:
|
|
if not _prev_combos.has(name):
|
|
msgs.append("Kombination aktiv: %s" % name)
|
|
for e in tiers:
|
|
if tiers[e] > _prev_tiers.get(e, 0) and tiers[e] > 0:
|
|
msgs.append("%s-Trait Stufe %d: %s" % [Data.ELEMENT_NAMES[e], tiers[e],
|
|
Data.ELEMENT_TRAITS[e]["desc"][tiers[e] - 1]])
|
|
if not msgs.is_empty():
|
|
shop.hint(" · ".join(msgs))
|
|
sfx.play("level" if msgs[0].begins_with("Kombination") else "aufwerten")
|
|
_prev_combos = combos
|
|
_prev_tiers = tiers
|
|
|
|
|
|
## Erstrunden-Hilfe: sagt in den ersten zwei Runden, was als Nächstes dran ist.
|
|
func _tutorial_tip() -> String:
|
|
if game.round_no > 2 or game.phase != Game.Phase.PLANNING or not game.human.alive:
|
|
return ""
|
|
var p := game.human
|
|
var on_bench := 0
|
|
for a in p.bench:
|
|
if a != null:
|
|
on_bench += 1
|
|
if p.board.is_empty() and on_bench == 0:
|
|
return "Tipp: Kauf unten einen neutralen Avatar und wähl sein Element."
|
|
if p.board.is_empty():
|
|
return "Tipp: Zieh den Avatar von der Bank auf dein Feld (untere Hälfte)."
|
|
if p.board.size() < p.team_slots() and p.gold >= Data.PRICE_NONE:
|
|
return "Tipp: Du hast noch %d freie Kaderplätze. Tanks nach vorn." % (p.team_slots() - p.board.size())
|
|
if game.round_no == 1:
|
|
return "Tipp: Leertaste startet den Kampf. Gleiche Elemente ab 2 geben einen Trait."
|
|
return "Tipp: 3 gleiche Elemente machen einen 2-Star. Futter liegt auf der Bank."
|
|
|
|
|
|
# --- Start, Pause, Ausscheiden, Neustart ------------------------------------
|
|
|
|
func _start_game(continue_save: bool) -> void:
|
|
if continue_save:
|
|
var loaded := Game.load_from()
|
|
if loaded != null:
|
|
game = loaded
|
|
board.game = game
|
|
board.scout_player = null
|
|
scout = null
|
|
_refresh()
|
|
started = true
|
|
paused = false
|
|
overlays.close()
|
|
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()
|
|
game.save()
|
|
overlays.show_pause()
|
|
else:
|
|
overlays.close()
|
|
|
|
|
|
func _show_eliminated() -> void:
|
|
eliminated_shown = true
|
|
_cancel_drag()
|
|
picker.hide()
|
|
Game.delete_save()
|
|
overlays.show_eliminated(game.human.placement, game.round_no)
|
|
|
|
|
|
func _spectate() -> void:
|
|
spectating = true
|
|
overlays.close()
|
|
var best := game.ranking()
|
|
if not best.is_empty() and best[0].alive:
|
|
_on_scout(best[0])
|
|
|
|
|
|
func _restart() -> void:
|
|
overlays.close()
|
|
Game.delete_save()
|
|
game = Game.new()
|
|
game.start(0, settings.difficulty)
|
|
board.game = game
|
|
board.scout_player = null
|
|
scout = null
|
|
spectating = false
|
|
eliminated_shown = false
|
|
_prev_combos = []
|
|
_prev_tiers = {}
|
|
_cancel_drag()
|
|
sel_slot = {}
|
|
started = true
|
|
paused = false
|
|
_refresh()
|