update
This commit is contained in:
@@ -20,6 +20,10 @@ var camera: Camera3D = null
|
||||
var scout_player: PlayerState = null
|
||||
var selected_slot := {} ## {"kind": "board"/"bench", ...}
|
||||
var hover_slot := {}
|
||||
## Reichweite der gewählten Einheit: Mitte (Arena-Feld), Radius, Farbe. Radius 0 = aus.
|
||||
var range_center := Vector2i(-1, -1)
|
||||
var range_radius := 0
|
||||
var range_color := Color(1.0, 0.7, 0.3)
|
||||
var drag_active := false
|
||||
var drag_avatar_id := -1
|
||||
var drag_world := Vector3.ZERO
|
||||
@@ -37,6 +41,10 @@ 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 _effects: Array = [] ## Fähigkeitsformen {"node", "t", "dur", "grow"}
|
||||
var effects_total := 0
|
||||
var _ring_fx: TorusMesh
|
||||
var _disc_fx: CylinderMesh
|
||||
var _shot_mesh: SphereMesh
|
||||
var _slash_mesh: BoxMesh
|
||||
var _origin := Vector3.ZERO
|
||||
@@ -63,6 +71,16 @@ func _ready() -> void:
|
||||
_shot_mesh.rings = 4
|
||||
_slash_mesh = BoxMesh.new()
|
||||
_slash_mesh.size = Vector3(0.12, 0.12, 0.7)
|
||||
_ring_fx = TorusMesh.new()
|
||||
_ring_fx.inner_radius = 0.55
|
||||
_ring_fx.outer_radius = 0.7
|
||||
_ring_fx.ring_segments = 8
|
||||
_ring_fx.rings = 28
|
||||
_disc_fx = CylinderMesh.new()
|
||||
_disc_fx.top_radius = 0.9
|
||||
_disc_fx.bottom_radius = 0.9
|
||||
_disc_fx.height = 0.06
|
||||
_disc_fx.radial_segments = 6
|
||||
_build_tiles()
|
||||
_build_bench()
|
||||
set_process(true)
|
||||
@@ -228,6 +246,59 @@ func _process(delta: float) -> void:
|
||||
_update_tiles()
|
||||
_update_units(delta)
|
||||
_update_shots(delta)
|
||||
_update_effects(delta)
|
||||
|
||||
|
||||
## Sichtbare Fähigkeit: Tank = wachsender Schildring, Schütze = Fläche am Ziel,
|
||||
## Heiler = aufsteigender Heilring. Farbe nach Element.
|
||||
func spawn_effect(world: Vector3, target: Vector3, role: int, col: Color) -> void:
|
||||
var mi := MeshInstance3D.new()
|
||||
var m := StandardMaterial3D.new()
|
||||
m.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
m.albedo_color = Color(col, 0.75)
|
||||
m.emission_enabled = true
|
||||
m.emission = col
|
||||
m.emission_energy_multiplier = 1.4
|
||||
mi.material_override = m
|
||||
var fx := {"node": mi, "t": 0.0, "dur": 0.45, "rise": 0.0, "grow": 1.0}
|
||||
match role:
|
||||
Data.Role.TANK:
|
||||
mi.mesh = _ring_fx
|
||||
mi.position = world + Vector3(0.0, 0.3, 0.0)
|
||||
fx["grow"] = 1.6
|
||||
Data.Role.DPS:
|
||||
mi.mesh = _disc_fx
|
||||
mi.position = target + Vector3(0.0, 0.12, 0.0)
|
||||
mi.scale = Vector3(0.3, 1.0, 0.3)
|
||||
fx["grow"] = 1.4
|
||||
fx["dur"] = 0.35
|
||||
_:
|
||||
mi.mesh = _ring_fx
|
||||
mi.position = world + Vector3(0.0, 0.1, 0.0)
|
||||
fx["rise"] = 1.2
|
||||
fx["grow"] = 1.2
|
||||
fx["dur"] = 0.6
|
||||
add_child(mi)
|
||||
effects_total += 1
|
||||
_effects.append(fx)
|
||||
|
||||
|
||||
func _update_effects(delta: float) -> void:
|
||||
var keep := []
|
||||
for fx in _effects:
|
||||
fx["t"] += delta
|
||||
var f: float = fx["t"] / fx["dur"]
|
||||
var node: MeshInstance3D = fx["node"]
|
||||
if f >= 1.0:
|
||||
node.queue_free()
|
||||
continue
|
||||
var g: float = lerpf(1.0, fx["grow"], f) if fx["grow"] >= 1.0 else 1.0
|
||||
node.scale = Vector3(g, 1.0, g) if node.mesh == _disc_fx else Vector3(g, g, g)
|
||||
node.position.y += fx["rise"] * delta
|
||||
var m: StandardMaterial3D = node.material_override
|
||||
m.albedo_color.a = 0.75 * (1.0 - f)
|
||||
keep.append(fx)
|
||||
_effects = keep
|
||||
|
||||
|
||||
## Sichtbarer Angriff: Fernkampf schickt eine Kugel, Nahkampf einen kurzen Hieb.
|
||||
@@ -276,6 +347,11 @@ func _update_tiles() -> void:
|
||||
var slot := {"kind": "board", "hex": to_board(h)}
|
||||
var col := Color(0.20, 0.23, 0.31) if own else Color(0.28, 0.18, 0.20)
|
||||
var emit := 0.0
|
||||
var in_range := range_radius > 0 and range_center.x >= 0 \
|
||||
and Hex.distance(h, range_center) <= range_radius and h != range_center
|
||||
if in_range and not drag_active:
|
||||
col = col.lerp(range_color, 0.45)
|
||||
emit = 0.22
|
||||
if own and _is_hover(slot):
|
||||
col = Color(0.42, 0.60, 0.82)
|
||||
emit = 0.40
|
||||
@@ -343,6 +419,7 @@ func _update_units(delta: float) -> void:
|
||||
"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),
|
||||
"face": world_pos(_mirror(u.target.pos, my_team)) if (u.target != null and u.target.alive) else null,
|
||||
}
|
||||
else:
|
||||
for bh in game.human.board:
|
||||
@@ -410,7 +487,23 @@ func _update_units(delta: float) -> void:
|
||||
# und sieht nicht angehoben aus.
|
||||
node.position = target
|
||||
else:
|
||||
node.position = node.position.lerp(target, clampf(delta * MOVE_LERP, 0.0, 1.0))
|
||||
var flat_from := Vector3(node.position.x, 0.0, node.position.z)
|
||||
var flat_to := Vector3(target.x, 0.0, target.z)
|
||||
var moving := flat_from.distance_to(flat_to) > 0.05
|
||||
var next := node.position.lerp(target, clampf(delta * MOVE_LERP, 0.0, 1.0))
|
||||
if moving:
|
||||
# Blick in Laufrichtung, kleines Wippen wie ein Schritt.
|
||||
var dirv := flat_to - flat_from
|
||||
node.rotation.y = lerp_angle(node.rotation.y, atan2(dirv.x, dirv.z), clampf(delta * 10.0, 0.0, 1.0))
|
||||
var body: MeshInstance3D = node.get_meta("body")
|
||||
body.position.y = body.get_meta("base_y", body.position.y) + absf(sin(Time.get_ticks_msec() / 90.0)) * 0.12
|
||||
elif d.get("face", null) != null:
|
||||
var fd: Vector3 = d["face"] - flat_to
|
||||
if fd.length() > 0.05:
|
||||
node.rotation.y = lerp_angle(node.rotation.y, atan2(fd.x, fd.z), clampf(delta * 6.0, 0.0, 1.0))
|
||||
var body2: MeshInstance3D = node.get_meta("body")
|
||||
body2.position.y = body2.get_meta("base_y", body2.position.y)
|
||||
node.position = next
|
||||
var body: MeshInstance3D = node.get_meta("body")
|
||||
var mat: StandardMaterial3D = body.material_override
|
||||
var base: Color = d["avatar"].color()
|
||||
@@ -446,8 +539,15 @@ func _update_units(delta: float) -> void:
|
||||
|
||||
|
||||
## Knoten eines Avatars, oder null. Wird von den Tests genutzt.
|
||||
## Findet auch Kampfeinheiten, die unter einem anderen Schlüssel liegen.
|
||||
func unit_node(avatar_id: int) -> Node3D:
|
||||
return _units.get("a%d" % avatar_id)
|
||||
var direct = _units.get("a%d" % avatar_id)
|
||||
if direct != null:
|
||||
return direct
|
||||
for n in _units.values():
|
||||
if n.get_meta("avatar_id", -1) == avatar_id:
|
||||
return n
|
||||
return null
|
||||
|
||||
|
||||
## Für das Overlay: Arena-Feld so spiegeln, wie das Brett gezeichnet wird.
|
||||
@@ -555,8 +655,10 @@ func _make_unit(a: Avatar, side: int) -> Node3D:
|
||||
sph.rings = 10
|
||||
body.mesh = sph
|
||||
body.position.y = 0.55
|
||||
body.set_meta("base_y", body.position.y)
|
||||
root.add_child(body)
|
||||
root.set_meta("body", body)
|
||||
root.set_meta("avatar_id", a.id)
|
||||
|
||||
_add_ornament(root, a, body.position.y)
|
||||
|
||||
|
||||
93
scripts/ui/CameraRig.gd
Normal file
93
scripts/ui/CameraRig.gd
Normal file
@@ -0,0 +1,93 @@
|
||||
class_name CameraRig
|
||||
extends Node3D
|
||||
|
||||
## Kamera über dem Brett: passt sich der freien Bildmitte an, lässt sich
|
||||
## zoomen und um die Brettmitte drehen.
|
||||
|
||||
const FOV := 44.0
|
||||
const PITCH := 52.0 ## Grad über der Waagerechten
|
||||
const ZOOM_MIN := 0.6
|
||||
const ZOOM_MAX := 1.5
|
||||
|
||||
var camera: Camera3D
|
||||
var zoom := 1.0
|
||||
var yaw := 0.0
|
||||
var _center := Vector3.ZERO
|
||||
var _dir := Vector3.ZERO
|
||||
var _dist := 20.0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
camera = Camera3D.new()
|
||||
camera.fov = FOV
|
||||
camera.look_at_from_position(Vector3(0.0, 16.0, 13.0), Vector3.ZERO, Vector3.UP)
|
||||
add_child(camera)
|
||||
|
||||
|
||||
## Legt die Kamera so, dass alle Punkte in das Zielrechteck passen.
|
||||
func fit(points: Array, target: Rect2) -> void:
|
||||
if target.size.x <= 0.0 or target.size.y <= 0.0 or points.is_empty():
|
||||
return
|
||||
var pitch := deg_to_rad(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(points)
|
||||
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(points)
|
||||
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)
|
||||
_center = center
|
||||
_dir = dir
|
||||
_dist = dist
|
||||
apply()
|
||||
|
||||
|
||||
## Nutzeranteil: Zoom und Drehung um die Brettmitte.
|
||||
func apply() -> void:
|
||||
var dir := _dir.rotated(Vector3.UP, deg_to_rad(yaw))
|
||||
camera.look_at_from_position(_center + dir * _dist * zoom, _center, Vector3.UP)
|
||||
|
||||
|
||||
func zoom_by(delta: float) -> void:
|
||||
zoom = clampf(zoom + delta, ZOOM_MIN, ZOOM_MAX)
|
||||
apply()
|
||||
|
||||
|
||||
func rotate_by(deg: float) -> void:
|
||||
yaw = fmod(yaw + deg, 360.0)
|
||||
apply()
|
||||
|
||||
|
||||
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, plane_y: float):
|
||||
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 := (plane_y - from.y) / dir.y
|
||||
if t < 0.0:
|
||||
return null
|
||||
return from + dir * t
|
||||
1
scripts/ui/CameraRig.gd.uid
Normal file
1
scripts/ui/CameraRig.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bet73kf858xae
|
||||
@@ -9,6 +9,10 @@ const FLOAT_RISE := 42.0
|
||||
|
||||
var board: BoardView3D = null
|
||||
var sfx: Sfx = null
|
||||
## Tooltip zu einer Einheit, gesetzt von Main (Maus über Einheit oder Shop-Karte).
|
||||
var tooltip_avatar: Avatar = null
|
||||
var tooltip_pos := Vector2.ZERO
|
||||
var tooltip_note := ""
|
||||
var _font: Font
|
||||
var _floaters: Array = [] ## {"world": Vector3, "text": String, "col": Color, "age": float}
|
||||
var _combat_seen: Combat = null
|
||||
@@ -54,12 +58,19 @@ func _pull_hits() -> void:
|
||||
_floaters.append({"world": world, "text": str(h["amount"]) + "!",
|
||||
"col": Color(1.0, 0.85, 0.3), "age": 0.0})
|
||||
if sfx: sfx.play("krit")
|
||||
"counter":
|
||||
# Konter-Treffer: Element oder Rolle war stark gegen das Ziel.
|
||||
_floaters.append({"world": world, "text": str(h["amount"]) + " ▲",
|
||||
"col": Color(1.0, 0.6, 0.9), "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")
|
||||
var tgt := board.world_pos(board.mirror_for_view(h["target"], my_team))
|
||||
board.spawn_effect(world, tgt, h["role"], Data.ELEMENT_COLORS[h["element"]])
|
||||
"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"]])
|
||||
@@ -120,3 +131,4 @@ func _draw() -> void:
|
||||
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)
|
||||
|
||||
|
||||
@@ -32,7 +32,8 @@ func build(x: float, y: float, w: float, h: float) -> void:
|
||||
traits_box = VBoxContainer.new()
|
||||
info_box = VBoxContainer.new()
|
||||
log_box = VBoxContainer.new()
|
||||
for b in [traits_box, info_box, log_box]:
|
||||
# Die gewählte Einheit zuerst: dort sitzt der Aufwerten-Knopf, der muss ohne Scrollen erreichbar sein.
|
||||
for b in [info_box, traits_box, log_box]:
|
||||
b.add_theme_constant_override("separation", 3)
|
||||
b.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
col.add_child(b)
|
||||
@@ -76,11 +77,17 @@ func _refresh_traits(game: Game) -> void:
|
||||
dot.color = col if tier > 0 else col.darkened(0.55)
|
||||
dot.custom_minimum_size = Vector2(12, 12)
|
||||
row.add_child(dot)
|
||||
var tr: Dictionary = Data.ELEMENT_TRAITS[e]
|
||||
var txt := " %s %d" % [Data.ELEMENT_NAMES[e], c]
|
||||
if tier > 0:
|
||||
txt += " - " + Data.ELEMENT_TRAITS[e]["desc"][tier - 1]
|
||||
txt += " - " + tr["desc"][tier - 1]
|
||||
else:
|
||||
txt += " (ab 2 aktiv)"
|
||||
# Ausblick: was die nächste Stufe bringt.
|
||||
if tier < tr["steps"].size():
|
||||
txt += "\n nächste Stufe bei %d: %s" % [tr["steps"][tier], tr["desc"][tier]]
|
||||
else:
|
||||
txt += "\n höchste Stufe"
|
||||
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)
|
||||
@@ -114,6 +121,7 @@ func _refresh_info(game: Game, a: Avatar, hex: Vector2i) -> void:
|
||||
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.counter_text(), 12, Color(1.0, 0.75, 0.9)))
|
||||
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)))
|
||||
|
||||
@@ -9,14 +9,11 @@ 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 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
|
||||
var rig: CameraRig
|
||||
var camera: Camera3D
|
||||
var ui_layer: CanvasLayer
|
||||
var ui_root: Control
|
||||
@@ -30,14 +27,13 @@ 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
|
||||
|
||||
# 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
|
||||
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 := {}
|
||||
@@ -51,7 +47,6 @@ var picker_title: Label
|
||||
var picker_sub: Label
|
||||
var picker_cb: Callable
|
||||
|
||||
var overlay: Control ## Start-, Pause- oder Endbildschirm
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -72,17 +67,42 @@ func _ready() -> void:
|
||||
|
||||
_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)
|
||||
|
||||
game.start()
|
||||
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()
|
||||
_show_start()
|
||||
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()
|
||||
@@ -99,6 +119,8 @@ func _process(delta: float) -> void:
|
||||
if game.phase != before:
|
||||
_after_combat()
|
||||
_refresh()
|
||||
if game.phase == Game.Phase.PLANNING and game.human.alive:
|
||||
game.save()
|
||||
|
||||
|
||||
func _after_combat() -> void:
|
||||
@@ -106,6 +128,8 @@ func _after_combat() -> void:
|
||||
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 -----------------------------------------------------------------
|
||||
@@ -117,8 +141,7 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||
return
|
||||
if event is InputEventMouseMotion:
|
||||
if event.button_mask & MOUSE_BUTTON_MASK_MIDDLE:
|
||||
cam_yaw += event.relative.x * 0.3
|
||||
_apply_camera()
|
||||
rig.rotate_by(event.relative.x * 0.3)
|
||||
return
|
||||
_on_mouse_move(event.position)
|
||||
return
|
||||
@@ -134,21 +157,24 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||
_on_right_click(event.position)
|
||||
MOUSE_BUTTON_WHEEL_UP:
|
||||
if event.pressed:
|
||||
_zoom(-0.1)
|
||||
rig.zoom_by(-0.1)
|
||||
MOUSE_BUTTON_WHEEL_DOWN:
|
||||
if event.pressed:
|
||||
_zoom(0.1)
|
||||
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: _rotate(-YAW_STEP)
|
||||
KEY_E: _rotate(YAW_STEP)
|
||||
KEY_Q: rig.rotate_by(-YAW_STEP)
|
||||
KEY_E: rig.rotate_by(YAW_STEP)
|
||||
KEY_ESCAPE:
|
||||
_cancel_drag()
|
||||
sel_slot = {}
|
||||
@@ -160,6 +186,43 @@ func _on_mouse_move(pos: Vector2) -> void:
|
||||
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:
|
||||
@@ -220,6 +283,7 @@ func _on_right_click(pos: Vector2) -> void:
|
||||
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 = {}
|
||||
@@ -262,87 +326,24 @@ func _build_world() -> void:
|
||||
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)
|
||||
rig = CameraRig.new()
|
||||
add_child(rig)
|
||||
camera = rig.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.
|
||||
## 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)
|
||||
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)
|
||||
_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:
|
||||
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)
|
||||
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):
|
||||
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)
|
||||
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:
|
||||
@@ -452,6 +453,8 @@ func _on_ready() -> void:
|
||||
game.skip_combat()
|
||||
_after_combat()
|
||||
_refresh()
|
||||
if game.phase == Game.Phase.PLANNING and game.human.alive:
|
||||
game.save()
|
||||
|
||||
|
||||
func _on_speed() -> void:
|
||||
@@ -526,6 +529,27 @@ func _on_buy(index: int, quick: bool) -> void:
|
||||
_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()
|
||||
@@ -535,32 +559,12 @@ func _on_upgrade() -> void:
|
||||
var options := p.available_new_elements(hex)
|
||||
var previews := []
|
||||
for e in options:
|
||||
previews.append("+ " + _element_effect(e) + " (halbe Stärke)")
|
||||
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: " + _star_effect(a.role, a.star + 1))
|
||||
|
||||
|
||||
func _star_effect(role: int, star: int) -> String:
|
||||
match role:
|
||||
Data.Role.TANK:
|
||||
return "Schild und spottet 3 Gegner" if star == 2 else "Schild, Spott und 25% Reflexion"
|
||||
Data.Role.DPS:
|
||||
return "Ziel plus Splash auf Nachbarn" if star == 2 else "Ziel plus Fläche im Radius 2"
|
||||
_:
|
||||
return "heilt 3 Verbündete" if star == 2 else "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"
|
||||
"Fähigkeit wird zu: " + Data.star_effect(a.role, a.star + 1))
|
||||
|
||||
|
||||
func _on_scout(p: PlayerState) -> void:
|
||||
@@ -582,69 +586,92 @@ func _refresh() -> void:
|
||||
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:
|
||||
_show_gameover()
|
||||
Game.delete_save()
|
||||
if overlays.kind() != "gameover":
|
||||
overlays.show_gameover(game)
|
||||
|
||||
|
||||
# --- Overlays: Start, Pause, Ende --------------------------------------------
|
||||
|
||||
func _overlay_box(title: String) -> VBoxContainer:
|
||||
if overlay != null:
|
||||
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(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
|
||||
## 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)
|
||||
|
||||
|
||||
func _close_overlay() -> void:
|
||||
if overlay != null:
|
||||
overlay.queue_free()
|
||||
overlay = null
|
||||
## 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
|
||||
|
||||
|
||||
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
|
||||
## 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."
|
||||
|
||||
|
||||
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)
|
||||
# --- Start, Pause, Ausscheiden, Neustart ------------------------------------
|
||||
|
||||
|
||||
func _start_game() -> void:
|
||||
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
|
||||
_close_overlay()
|
||||
overlays.close()
|
||||
sfx.play("runde")
|
||||
|
||||
|
||||
@@ -655,39 +682,42 @@ func _toggle_pause() -> void:
|
||||
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)
|
||||
game.save()
|
||||
overlays.show_pause()
|
||||
else:
|
||||
_close_overlay()
|
||||
overlays.close()
|
||||
|
||||
|
||||
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("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():
|
||||
_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)
|
||||
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()
|
||||
|
||||
161
scripts/ui/Overlays.gd
Normal file
161
scripts/ui/Overlays.gd
Normal file
@@ -0,0 +1,161 @@
|
||||
class_name Overlays
|
||||
extends Control
|
||||
|
||||
## Start-, Pause-, Ausscheide- und Endbildschirm. Meldet Knöpfe per Signal.
|
||||
|
||||
signal start_pressed(continue_save: bool)
|
||||
signal difficulty_changed(d: int)
|
||||
signal volume_changed(v: float)
|
||||
signal resume_pressed
|
||||
signal restart_pressed
|
||||
signal spectate_pressed
|
||||
|
||||
var current: Control = null ## das offene Overlay, null wenn keins
|
||||
var volume := 0.8
|
||||
var difficulty := Data.Difficulty.NORMAL
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
|
||||
|
||||
func is_open() -> bool:
|
||||
return current != null
|
||||
|
||||
|
||||
func kind() -> String:
|
||||
return current.get_meta("kind", "") if current != null else ""
|
||||
|
||||
|
||||
func close() -> void:
|
||||
if current != null:
|
||||
current.queue_free()
|
||||
current = null
|
||||
|
||||
|
||||
func _box(title: String, kind_name: String) -> VBoxContainer:
|
||||
close()
|
||||
current = ColorRect.new()
|
||||
current.color = Color(0, 0, 0, 0.72)
|
||||
current.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
current.set_meta("kind", kind_name)
|
||||
add_child(current)
|
||||
var v := VBoxContainer.new()
|
||||
v.position = Vector2(560, 240)
|
||||
v.custom_minimum_size = Vector2(480, 0)
|
||||
v.add_theme_constant_override("separation", 12)
|
||||
current.add_child(v)
|
||||
v.add_child(UiKit.label(title, 32))
|
||||
return v
|
||||
|
||||
|
||||
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 = volume
|
||||
sl.custom_minimum_size = Vector2(220, 20)
|
||||
sl.focus_mode = Control.FOCUS_NONE
|
||||
sl.value_changed.connect(func(v):
|
||||
volume = v
|
||||
volume_changed.emit(v))
|
||||
h.add_child(sl)
|
||||
return h
|
||||
|
||||
|
||||
func _difficulty_row() -> HBoxContainer:
|
||||
var h := HBoxContainer.new()
|
||||
h.add_theme_constant_override("separation", 8)
|
||||
h.add_child(UiKit.label("Bots", 14))
|
||||
for d in Data.DIFFICULTY_NAMES.size():
|
||||
var b := UiKit.button(Data.DIFFICULTY_NAMES[d],
|
||||
Color(0.30, 0.50, 0.38) if d == difficulty else Color(0.25, 0.28, 0.36))
|
||||
b.custom_minimum_size = Vector2(90, 28)
|
||||
b.pressed.connect(func():
|
||||
difficulty = d
|
||||
difficulty_changed.emit(d))
|
||||
h.add_child(b)
|
||||
return h
|
||||
|
||||
|
||||
func _button(v: VBoxContainer, text: String, col: Color, cb: Callable, w: float = 200.0) -> Button:
|
||||
var b := UiKit.button(text, col)
|
||||
b.custom_minimum_size = Vector2(w, 36)
|
||||
b.pressed.connect(cb)
|
||||
v.add_child(b)
|
||||
return b
|
||||
|
||||
|
||||
func show_start(has_save: bool) -> void:
|
||||
var v := _box("Elements Autochess", "start")
|
||||
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 · 1–5 kaufen · D würfeln · F einfrieren\\nX Level · Z Verkauf rückgängig · Q/E drehen · Mausrad zoomen · P Pause · Leertaste Kampf",
|
||||
13, Color(0.7, 0.72, 0.8)))
|
||||
v.add_child(UiKit.label(
|
||||
"Konter: Wasser > Feuer > Eis > Wind > Erde > Blitz > Wasser (±20%) · Tank > Schütze > Heiler > Tank (±15%)",
|
||||
13, Color(1.0, 0.75, 0.9)))
|
||||
v.add_child(_difficulty_row())
|
||||
v.add_child(_volume_row())
|
||||
if has_save:
|
||||
_button(v, "Weiterspielen", Color(0.28, 0.5, 0.36), func(): start_pressed.emit(true))
|
||||
_button(v, "Neue Partie", Color(0.25, 0.28, 0.36), func(): start_pressed.emit(false))
|
||||
else:
|
||||
_button(v, "Spiel starten", Color(0.28, 0.5, 0.36), func(): start_pressed.emit(false))
|
||||
|
||||
|
||||
func show_pause() -> void:
|
||||
var v := _box("Pause", "pause")
|
||||
v.add_child(UiKit.label("Der Spielstand wurde gespeichert.", 13, Color(0.7, 0.72, 0.8)))
|
||||
v.add_child(_volume_row())
|
||||
_button(v, "Weiter [P]", Color(0.28, 0.5, 0.36), func(): resume_pressed.emit())
|
||||
|
||||
|
||||
func show_eliminated(placement: int, round_no: int) -> void:
|
||||
var v := _box("Ausgeschieden", "eliminated")
|
||||
v.add_child(UiKit.label("Platz %d von 8 nach Runde %d" % [placement, round_no], 20,
|
||||
Color(0.95, 0.75, 0.6)))
|
||||
var h := HBoxContainer.new()
|
||||
h.add_theme_constant_override("separation", 10)
|
||||
var watch := UiKit.button("Zuschauen", Color(0.25, 0.28, 0.36))
|
||||
watch.custom_minimum_size = Vector2(160, 36)
|
||||
watch.pressed.connect(func(): spectate_pressed.emit())
|
||||
h.add_child(watch)
|
||||
var again := UiKit.button("Neues Spiel", Color(0.28, 0.5, 0.36))
|
||||
again.custom_minimum_size = Vector2(160, 36)
|
||||
again.pressed.connect(func(): restart_pressed.emit())
|
||||
h.add_child(again)
|
||||
v.add_child(h)
|
||||
|
||||
|
||||
func show_gameover(game: Game) -> void:
|
||||
var v := _box("Spiel vorbei", "gameover")
|
||||
var alive := game.alive_players()
|
||||
var win: String = alive[0].pname if not alive.is_empty() else "-"
|
||||
v.add_child(UiKit.label("Sieger: %s" % win, 20, Color(1.0, 0.85, 0.35)))
|
||||
var p := game.human
|
||||
var place := p.placement if p.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 st := p.stats
|
||||
var best := "-"
|
||||
var best_star := 0
|
||||
for u in p.board_units():
|
||||
if u.star > best_star:
|
||||
best_star = u.star
|
||||
best = u.display_name()
|
||||
var lines := [
|
||||
"Runden: %d Siege: %d Niederlagen: %d" % [game.round_no, st["wins"], st["losses"]],
|
||||
"Gold verdient: %d ausgegeben: %d" % [st["gold_earned"], st["spent"]],
|
||||
"Höchste Stufe: %d-Star Level: %d" % [st["max_star"], p.level],
|
||||
"Stärkste Einheit: %s" % best,
|
||||
]
|
||||
for l in lines:
|
||||
v.add_child(UiKit.label(l, 14, Color(0.85, 0.88, 0.95)))
|
||||
_button(v, "Neues Spiel", Color(0.28, 0.5, 0.36), func(): restart_pressed.emit())
|
||||
1
scripts/ui/Overlays.gd.uid
Normal file
1
scripts/ui/Overlays.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bsb44pe2nytsf
|
||||
@@ -6,6 +6,9 @@ extends Control
|
||||
signal buy_requested(index: int, quick: bool)
|
||||
signal reroll_pressed
|
||||
signal lock_pressed
|
||||
signal card_hovered(index: int, screen_pos: Vector2)
|
||||
signal card_left
|
||||
signal undo_pressed
|
||||
|
||||
const PANEL_BG := Color(0.14, 0.15, 0.19)
|
||||
const SLOT_BG := Color(0.20, 0.21, 0.26)
|
||||
@@ -15,6 +18,8 @@ var btn_reroll: Button
|
||||
var btn_lock: Button
|
||||
var lbl_fodder: Label
|
||||
var lbl_hint: Label
|
||||
var lbl_tip: Label ## Erstrunden-Hilfe
|
||||
var btn_undo: Button ## Rückgängig nach einem Verkauf, verschwindet nach 5 s
|
||||
|
||||
|
||||
func build(x: float, y: float, w: float, h: float) -> void:
|
||||
@@ -44,6 +49,13 @@ func build(x: float, y: float, w: float, h: float) -> void:
|
||||
head.add_child(lbl_fodder)
|
||||
lbl_hint = UiKit.label("", 13, Color(0.95, 0.85, 0.45))
|
||||
head.add_child(lbl_hint)
|
||||
lbl_tip = UiKit.label("", 13, Color(0.6, 0.95, 0.75))
|
||||
head.add_child(lbl_tip)
|
||||
btn_undo = UiKit.button("Verkauf rückgängig [Z]", Color(0.55, 0.40, 0.25))
|
||||
btn_undo.custom_minimum_size = Vector2(210, 26)
|
||||
btn_undo.visible = false
|
||||
btn_undo.pressed.connect(func(): undo_pressed.emit())
|
||||
head.add_child(btn_undo)
|
||||
var spacer := Control.new()
|
||||
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
head.add_child(spacer)
|
||||
@@ -76,9 +88,11 @@ func refresh(game: Game) -> void:
|
||||
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 = UiKit.button("[%d] %s\n%d Gold\n%s" % [i + 1, 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.mouse_entered.connect(func(): card_hovered.emit(i, b.global_position + Vector2(b.size.x, -8.0)))
|
||||
b.mouse_exited.connect(func(): card_left.emit())
|
||||
b.custom_minimum_size = Vector2(196, 88)
|
||||
row.add_child(b)
|
||||
var mine := []
|
||||
@@ -111,3 +125,15 @@ func hint(text: String) -> void:
|
||||
get_tree().create_timer(2.5).timeout.connect(func():
|
||||
if is_instance_valid(lbl_hint) and lbl_hint.text == text:
|
||||
lbl_hint.text = "")
|
||||
|
||||
|
||||
## Zeigt den Rückgängig-Knopf für fünf Sekunden.
|
||||
func show_undo() -> void:
|
||||
btn_undo.visible = true
|
||||
get_tree().create_timer(5.0).timeout.connect(func():
|
||||
if is_instance_valid(btn_undo):
|
||||
btn_undo.visible = false)
|
||||
|
||||
|
||||
func hide_undo() -> void:
|
||||
btn_undo.visible = false
|
||||
|
||||
51
scripts/ui/TooltipView.gd
Normal file
51
scripts/ui/TooltipView.gd
Normal file
@@ -0,0 +1,51 @@
|
||||
class_name TooltipView
|
||||
extends Control
|
||||
|
||||
## Zeichnet den Einheiten-Tooltip über allen Panels. Die Daten liegen im HudOverlay.
|
||||
|
||||
var hud: HudOverlay = null
|
||||
var _font: Font
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_font = ThemeDB.fallback_font
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
set_process(true)
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
if hud == null or hud.tooltip_avatar == null:
|
||||
return
|
||||
var a := hud.tooltip_avatar
|
||||
var st := a.base_stats()
|
||||
var lines := []
|
||||
lines.append("%s %d-Star" % [a.display_name(), a.star])
|
||||
if hud.tooltip_note != "":
|
||||
lines.append(hud.tooltip_note)
|
||||
lines.append("Leben %d Angriff %d Rüstung %d%%" % [int(st["hp"]), int(st["atk"]), int(st["armor"] * 100.0)])
|
||||
var extra := "Tempo %.2f Reichweite %d" % [st["aspd"], st["range"]]
|
||||
if st["crit"] > 0.0:
|
||||
extra += " Krit %d%%" % int(st["crit"] * 100.0)
|
||||
lines.append(extra)
|
||||
lines.append(a.counter_text())
|
||||
for l in a.ability_text().split("\n"):
|
||||
lines.append(l)
|
||||
var w := 0.0
|
||||
for l in lines:
|
||||
w = maxf(w, _font.get_string_size(l, HORIZONTAL_ALIGNMENT_LEFT, -1, 12).x)
|
||||
w += 20.0
|
||||
var h := 8.0 + 17.0 * lines.size()
|
||||
var vp := get_viewport_rect().size
|
||||
var pos := hud.tooltip_pos + Vector2(16.0, 16.0)
|
||||
pos.x = clampf(pos.x, 8.0, vp.x - w - 8.0)
|
||||
pos.y = clampf(pos.y, 8.0, vp.y - h - 8.0)
|
||||
draw_rect(Rect2(pos, Vector2(w, h)), Color(0.08, 0.09, 0.12, 0.96))
|
||||
draw_rect(Rect2(pos, Vector2(w, h)), a.color(), false, 2.0)
|
||||
for i in lines.size():
|
||||
var col := a.color() if i == 0 else Color(0.9, 0.9, 0.95)
|
||||
draw_string(_font, pos + Vector2(10.0, 18.0 + 17.0 * float(i)), lines[i],
|
||||
HORIZONTAL_ALIGNMENT_LEFT, -1, 12, col)
|
||||
1
scripts/ui/TooltipView.gd.uid
Normal file
1
scripts/ui/TooltipView.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c3rgifc7gtsxv
|
||||
Reference in New Issue
Block a user