695 lines
20 KiB
GDScript
695 lines
20 KiB
GDScript
class_name BoardView3D
|
|
extends Node3D
|
|
|
|
## 3D-Arena: 7x8 Hexfelder als Zylinder-Primitive, Einheiten als Box/Kegel/Kugel.
|
|
## Reine Darstellung, die Spiellogik liegt in Combat und PlayerState.
|
|
|
|
signal hex_clicked(hex: Vector2i, button: int)
|
|
|
|
const COLS := Combat.ARENA_COLS
|
|
const ROWS := Combat.ARENA_ROWS
|
|
const TILE := 1.0
|
|
const TILE_H := 0.18
|
|
const MOVE_LERP := 8.0
|
|
const BENCH_GAP := 1.62 ## Abstand der Bankplätze
|
|
const BENCH_OFFSET := 1.95 ## Abstand der Bank hinter der letzten Reihe
|
|
|
|
var game: Game = null
|
|
var camera: Camera3D = null
|
|
## Brett eines anderen Spielers, das in der Planung oben gezeigt wird.
|
|
var scout_player: PlayerState = null
|
|
var selected_slot := {} ## {"kind": "board"/"bench", ...}
|
|
var hover_slot := {}
|
|
## 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
|
|
|
|
## Was das 2D-Overlay über die Einheiten zeichnen soll.
|
|
var draw_units: Array = []
|
|
|
|
var _tiles := {} ## Vector2i -> MeshInstance3D
|
|
var _tile_mats := {} ## Vector2i -> StandardMaterial3D
|
|
var _bench_tiles := {} ## int -> MeshInstance3D
|
|
var _bench_mats := {}
|
|
var _bench_z := 0.0
|
|
var _units := {} ## key -> Node3D
|
|
var _dying := {} ## key -> Restzeit der Sterbe-Animation
|
|
const DEATH_TIME := 0.45
|
|
var _shots: Array = [] ## fliegende Geschosse {"node", "from", "to", "t", "dur"}
|
|
var shots_total := 0 ## Zähler aller Geschosse, für Tests
|
|
var _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
|
|
var _tile_mesh: CylinderMesh
|
|
var _ring_mesh: TorusMesh
|
|
|
|
|
|
func _ready() -> void:
|
|
_tile_mesh = CylinderMesh.new()
|
|
_tile_mesh.radial_segments = 6
|
|
_tile_mesh.rings = 1
|
|
_tile_mesh.top_radius = TILE * 0.92
|
|
_tile_mesh.bottom_radius = TILE * 0.92
|
|
_tile_mesh.height = TILE_H
|
|
_ring_mesh = TorusMesh.new()
|
|
_ring_mesh.inner_radius = 0.50
|
|
_ring_mesh.outer_radius = 0.66
|
|
_ring_mesh.ring_segments = 8
|
|
_ring_mesh.rings = 24
|
|
_shot_mesh = SphereMesh.new()
|
|
_shot_mesh.radius = 0.11
|
|
_shot_mesh.height = 0.22
|
|
_shot_mesh.radial_segments = 8
|
|
_shot_mesh.rings = 4
|
|
_slash_mesh = BoxMesh.new()
|
|
_slash_mesh.size = Vector3(0.12, 0.12, 0.7)
|
|
_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)
|
|
|
|
|
|
# --- Geometrie -------------------------------------------------------------
|
|
|
|
func _build_tiles() -> void:
|
|
var lo := Vector2(INF, INF)
|
|
var hi := Vector2(-INF, -INF)
|
|
for r in ROWS:
|
|
for c in COLS:
|
|
var p := Hex.to_pixel(Vector2i(c, r), TILE)
|
|
lo = Vector2(minf(lo.x, p.x), minf(lo.y, p.y))
|
|
hi = Vector2(maxf(hi.x, p.x), maxf(hi.y, p.y))
|
|
var mid := (lo + hi) * 0.5
|
|
_origin = Vector3(mid.x, 0.0, mid.y)
|
|
|
|
for r in ROWS:
|
|
for c in COLS:
|
|
var h := Vector2i(c, r)
|
|
var mi := MeshInstance3D.new()
|
|
mi.mesh = _tile_mesh
|
|
mi.position = world_pos(h)
|
|
var m := StandardMaterial3D.new()
|
|
m.roughness = 0.9
|
|
m.metallic = 0.0
|
|
mi.material_override = m
|
|
add_child(mi)
|
|
_tiles[h] = mi
|
|
_tile_mats[h] = m
|
|
|
|
|
|
func _build_bench() -> void:
|
|
var max_z := -INF
|
|
for key in _tiles:
|
|
max_z = maxf(max_z, world_pos(key).z)
|
|
_bench_z = max_z + TILE * BENCH_OFFSET
|
|
var pad := CylinderMesh.new()
|
|
pad.radial_segments = 6
|
|
pad.rings = 1
|
|
pad.top_radius = 0.62
|
|
pad.bottom_radius = 0.62
|
|
pad.height = 0.12
|
|
for i in Data.BENCH_SLOTS:
|
|
var mi := MeshInstance3D.new()
|
|
mi.mesh = pad
|
|
mi.position = bench_pos(i)
|
|
var m := StandardMaterial3D.new()
|
|
m.roughness = 0.9
|
|
mi.material_override = m
|
|
add_child(mi)
|
|
_bench_tiles[i] = mi
|
|
_bench_mats[i] = m
|
|
|
|
|
|
func bench_pos(index: int) -> Vector3:
|
|
var half := float(Data.BENCH_SLOTS - 1) * 0.5
|
|
return Vector3((float(index) - half) * BENCH_GAP, 0.0, _bench_z)
|
|
|
|
|
|
## Feld oder Bankplatz unter einem Weltpunkt.
|
|
func slot_at_world(p: Vector3) -> Dictionary:
|
|
var flat := Vector2(p.x, p.z)
|
|
var best := {}
|
|
var best_d := TILE * 0.98
|
|
for key in _tiles:
|
|
var w := world_pos(key)
|
|
var d := flat.distance_to(Vector2(w.x, w.z))
|
|
if d < best_d:
|
|
best_d = d
|
|
best = {"kind": "board", "hex": to_board(key), "arena": key}
|
|
for i in Data.BENCH_SLOTS:
|
|
var b := bench_pos(i)
|
|
var d2 := flat.distance_to(Vector2(b.x, b.z))
|
|
if d2 < best_d:
|
|
best_d = d2
|
|
best = {"kind": "bench", "index": i}
|
|
return best
|
|
|
|
|
|
## Bildschirmposition eines Feldes oder Bankplatzes (für Tests und Anzeige).
|
|
func slot_screen(slot: Dictionary) -> Vector2:
|
|
if camera == null:
|
|
return Vector2.ZERO
|
|
return camera.unproject_position(to_global(slot_world(slot)) + Vector3(0.0, TILE_H * 0.5, 0.0))
|
|
|
|
|
|
func slot_world(slot: Dictionary) -> Vector3:
|
|
if slot.is_empty():
|
|
return Vector3.ZERO
|
|
if slot["kind"] == "bench":
|
|
return bench_pos(slot["index"])
|
|
return world_pos(from_board(slot["hex"]))
|
|
|
|
|
|
func world_pos(hex: Vector2i) -> Vector3:
|
|
var p := Hex.to_pixel(hex, TILE)
|
|
return Vector3(p.x, 0.0, p.y) - _origin
|
|
|
|
|
|
## Nächstes Hexfeld zu einem Weltpunkt, oder (-1,-1).
|
|
func hex_at_world(p: Vector3) -> Vector2i:
|
|
var best := Vector2i(-1, -1)
|
|
var best_d := TILE * 0.98
|
|
for key in _tiles:
|
|
var h: Vector2i = key
|
|
var w := world_pos(h)
|
|
var d := Vector2(p.x, p.z).distance_to(Vector2(w.x, w.z))
|
|
if d < best_d:
|
|
best_d = d
|
|
best = h
|
|
return best
|
|
|
|
|
|
## Bildschirmposition eines Feldes (für Overlay und Tests).
|
|
func screen_pos(hex: Vector2i, height: float = 0.0) -> Vector2:
|
|
if camera == null:
|
|
return Vector2.ZERO
|
|
return camera.unproject_position(world_pos(hex) + Vector3(0.0, height, 0.0))
|
|
|
|
|
|
## Acht Eckpunkte des Spielfeldes, oben mit Platz für hohe Einheiten.
|
|
func corner_points() -> Array:
|
|
var lo := Vector2(INF, INF)
|
|
var hi := Vector2(-INF, -INF)
|
|
for key in _tiles:
|
|
var w := world_pos(key)
|
|
lo = Vector2(minf(lo.x, w.x), minf(lo.y, w.z))
|
|
hi = Vector2(maxf(hi.x, w.x), maxf(hi.y, w.z))
|
|
for i in Data.BENCH_SLOTS:
|
|
var b := bench_pos(i)
|
|
lo = Vector2(minf(lo.x, b.x), minf(lo.y, b.z))
|
|
hi = Vector2(maxf(hi.x, b.x), maxf(hi.y, b.z))
|
|
lo -= Vector2(TILE, TILE)
|
|
hi += Vector2(TILE, TILE)
|
|
var out := []
|
|
for y in [0.0, 1.5]:
|
|
out.append(Vector3(lo.x, y, lo.y))
|
|
out.append(Vector3(hi.x, y, lo.y))
|
|
out.append(Vector3(lo.x, y, hi.y))
|
|
out.append(Vector3(hi.x, y, hi.y))
|
|
return out
|
|
|
|
|
|
static func to_board(h: Vector2i) -> Vector2i:
|
|
return Vector2i(h.x, h.y - 4)
|
|
|
|
|
|
static func from_board(h: Vector2i) -> Vector2i:
|
|
return Vector2i(h.x, h.y + 4)
|
|
|
|
|
|
func is_own_half(h: Vector2i) -> bool:
|
|
return h.y >= 4 and h.y < ROWS and h.x >= 0 and h.x < COLS
|
|
|
|
|
|
# --- Bildaufbau ------------------------------------------------------------
|
|
|
|
func _process(delta: float) -> void:
|
|
if game == null:
|
|
return
|
|
_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.
|
|
func spawn_attack(from: Vector3, to: Vector3, ranged: bool, col: Color) -> void:
|
|
var mi := MeshInstance3D.new()
|
|
mi.mesh = _shot_mesh if ranged else _slash_mesh
|
|
var m := StandardMaterial3D.new()
|
|
m.albedo_color = col
|
|
m.emission_enabled = true
|
|
m.emission = col
|
|
m.emission_energy_multiplier = 1.8
|
|
mi.material_override = m
|
|
var a := from + Vector3(0.0, 0.7, 0.0)
|
|
var b := to + Vector3(0.0, 0.6, 0.0)
|
|
if not ranged:
|
|
# Der Hieb startet vor dem Angreifer und endet am Ziel.
|
|
a = a.lerp(b, 0.35)
|
|
mi.position = a
|
|
if (b - a).length() > 0.01:
|
|
mi.look_at_from_position(a, b, Vector3.UP)
|
|
add_child(mi)
|
|
shots_total += 1
|
|
_shots.append({"node": mi, "from": a, "to": b, "t": 0.0, "dur": 0.2 if ranged else 0.09})
|
|
|
|
|
|
func _update_shots(delta: float) -> void:
|
|
var keep := []
|
|
for sh in _shots:
|
|
sh["t"] += delta
|
|
var f: float = sh["t"] / sh["dur"]
|
|
var node: MeshInstance3D = sh["node"]
|
|
if f >= 1.0:
|
|
node.queue_free()
|
|
continue
|
|
var pos: Vector3 = sh["from"].lerp(sh["to"], f)
|
|
pos.y += sin(f * PI) * 0.35
|
|
node.position = pos
|
|
keep.append(sh)
|
|
_shots = keep
|
|
|
|
|
|
func _update_tiles() -> void:
|
|
for key in _tiles:
|
|
var h: Vector2i = key
|
|
var own: bool = h.y >= 4
|
|
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
|
|
elif own and not drag_active and _same_slot(slot, selected_slot):
|
|
col = Color(0.35, 0.52, 0.75)
|
|
emit = 0.30
|
|
elif own and drag_active:
|
|
col = Color(0.24, 0.42, 0.29)
|
|
emit = 0.16
|
|
_paint(_tile_mats[h], col, emit)
|
|
|
|
for i in Data.BENCH_SLOTS:
|
|
var bslot := {"kind": "bench", "index": i}
|
|
var bcol := Color(0.24, 0.24, 0.30)
|
|
var bemit := 0.0
|
|
if _is_hover(bslot):
|
|
bcol = Color(0.48, 0.56, 0.72)
|
|
bemit = 0.40
|
|
elif not drag_active and _same_slot(bslot, selected_slot):
|
|
bcol = Color(0.38, 0.44, 0.60)
|
|
bemit = 0.28
|
|
elif drag_active:
|
|
bcol = Color(0.26, 0.40, 0.32)
|
|
bemit = 0.16
|
|
_paint(_bench_mats[i], bcol, bemit)
|
|
|
|
|
|
func _paint(m: StandardMaterial3D, col: Color, emit: float) -> void:
|
|
m.albedo_color = col
|
|
m.emission_enabled = emit > 0.0
|
|
m.emission = col
|
|
m.emission_energy_multiplier = emit
|
|
|
|
|
|
func _is_hover(slot: Dictionary) -> bool:
|
|
return game.phase == Game.Phase.PLANNING and _same_slot(slot, hover_slot)
|
|
|
|
|
|
static func _same_slot(a: Dictionary, b: Dictionary) -> bool:
|
|
if a.is_empty() or b.is_empty() or a["kind"] != b["kind"]:
|
|
return false
|
|
if a["kind"] == "bench":
|
|
return a["index"] == b["index"]
|
|
return a["hex"] == b["hex"]
|
|
|
|
|
|
func _update_units(delta: float) -> void:
|
|
var wanted := {}
|
|
draw_units.clear()
|
|
if game.phase == Game.Phase.COMBAT and game.player_combat != null:
|
|
var my_team := game.human_team_in_combat()
|
|
for u in game.player_combat.units:
|
|
if not u.alive:
|
|
continue
|
|
var key := "c%d" % u.get_instance_id()
|
|
wanted[key] = {
|
|
"avatar": u.avatar,
|
|
"world": world_pos(_mirror(u.pos, my_team)),
|
|
"hp": u.hp / u.max_hp,
|
|
"mana": u.mana / u.max_mana,
|
|
"side": 0 if u.team == my_team else 1,
|
|
"cast": u.cast_flash > 0.0,
|
|
"hit": u.hit_flash > 0.0,
|
|
"burn": u.burn_timer > 0.0,
|
|
"slow": u.slow_timer > 0.0 and u.slow > 0.0,
|
|
"stun": u.stun_timer > 0.0,
|
|
"shield": clampf(u.shield / maxf(1.0, u.max_hp), 0.0, 1.0),
|
|
"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:
|
|
var a: Avatar = game.human.board[bh]
|
|
wanted["a%d" % a.id] = {
|
|
"avatar": a, "world": world_pos(from_board(bh)), "hp": 1.0, "mana": 0.0,
|
|
"side": 0, "cast": false, "hit": false,
|
|
}
|
|
if scout_player != null and scout_player.alive:
|
|
for bh in scout_player.board:
|
|
var sa: Avatar = scout_player.board[bh]
|
|
wanted["s%d" % sa.id] = {
|
|
"avatar": sa, "world": world_pos(Combat._arena_pos(bh, 1)), "hp": 1.0,
|
|
"mana": 0.0, "side": 1, "cast": false, "hit": false,
|
|
}
|
|
# Die Bank ist immer sichtbar, auch während des Kampfes.
|
|
for i in game.human.bench.size():
|
|
var ba = game.human.bench[i]
|
|
if ba == null:
|
|
continue
|
|
wanted["a%d" % ba.id] = {
|
|
"avatar": ba, "world": bench_pos(i), "hp": 1.0, "mana": 0.0,
|
|
"side": 0, "cast": false, "hit": false,
|
|
}
|
|
# Der gezogene Avatar folgt dem Zeiger.
|
|
var drag_key := ""
|
|
if drag_active and drag_avatar_id >= 0:
|
|
drag_key = "a%d" % drag_avatar_id
|
|
if wanted.has(drag_key):
|
|
wanted[drag_key]["world"] = drag_world
|
|
wanted[drag_key]["dragged"] = true
|
|
|
|
for key in _units.keys():
|
|
if wanted.has(key):
|
|
continue
|
|
var node: Node3D = _units[key]
|
|
if not _dying.has(key):
|
|
# Im Kampf schrumpft die Einheit weg, in der Planung verschwindet sie sofort.
|
|
if game.phase == Game.Phase.COMBAT and key.begins_with("c"):
|
|
_dying[key] = DEATH_TIME
|
|
else:
|
|
node.queue_free()
|
|
_units.erase(key)
|
|
continue
|
|
_dying[key] -= delta
|
|
var t: float = maxf(0.0, _dying[key] / DEATH_TIME)
|
|
node.scale = Vector3(t, t * 0.6, t)
|
|
node.position.y = -0.4 * (1.0 - t)
|
|
if _dying[key] <= 0.0:
|
|
node.queue_free()
|
|
_units.erase(key)
|
|
_dying.erase(key)
|
|
|
|
for key in wanted:
|
|
var d: Dictionary = wanted[key]
|
|
var node: Node3D = _units.get(key)
|
|
if node == null:
|
|
node = _make_unit(d["avatar"], d["side"])
|
|
node.position = d["world"]
|
|
add_child(node)
|
|
_units[key] = node
|
|
var target: Vector3 = d["world"]
|
|
if d.get("dragged", false):
|
|
# Beim Ziehen ohne Glättung, sonst schleift der Avatar hinterher
|
|
# und sieht nicht angehoben aus.
|
|
node.position = target
|
|
else:
|
|
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()
|
|
var col := base
|
|
if d.get("slow", false):
|
|
col = col.lerp(Color(0.55, 0.8, 1.0), 0.45)
|
|
if d.get("stun", false):
|
|
col = col.lerp(Color(1.0, 0.95, 0.4), 0.5)
|
|
if d["hit"]:
|
|
col = col.lightened(0.5)
|
|
mat.albedo_color = col
|
|
var dragged: bool = d.get("dragged", false)
|
|
var emit := 0.12
|
|
if d["cast"]:
|
|
emit = 1.6
|
|
elif d.get("burn", false):
|
|
emit = 0.9
|
|
mat.emission = Color(1.0, 0.45, 0.15)
|
|
elif dragged:
|
|
emit = 0.45
|
|
else:
|
|
mat.emission = base
|
|
mat.emission_energy_multiplier = emit
|
|
var sc := 1.12 if (d["cast"] or dragged) else 1.0
|
|
if d.get("stun", false):
|
|
node.scale = Vector3(sc * 1.08, sc * 0.8, sc * 1.08)
|
|
else:
|
|
node.scale = Vector3.ONE * sc
|
|
draw_units.append({
|
|
"pos": node.position, "avatar": d["avatar"], "hp": d["hp"],
|
|
"mana": d["mana"], "side": d["side"], "cast": d["cast"],
|
|
})
|
|
|
|
|
|
## 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:
|
|
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.
|
|
func mirror_for_view(p: Vector2i, my_team: int) -> Vector2i:
|
|
return _mirror(p, my_team)
|
|
|
|
|
|
func _mirror(p: Vector2i, my_team: int) -> Vector2i:
|
|
if my_team == 0:
|
|
return p
|
|
return Vector2i(COLS - 1 - p.x, ROWS - 1 - p.y)
|
|
|
|
|
|
## Kleines Kennzeichen des Hauptelements auf oder neben dem Körper.
|
|
func _add_ornament(root: Node3D, a: Avatar, body_y: float) -> void:
|
|
var e := a.primary_element()
|
|
if e < 0:
|
|
return
|
|
var col: Color = Data.ELEMENT_COLORS[e]
|
|
var m := StandardMaterial3D.new()
|
|
m.albedo_color = col.lightened(0.15)
|
|
m.emission_enabled = true
|
|
m.emission = col
|
|
m.emission_energy_multiplier = 0.5
|
|
var top := body_y + 0.5
|
|
match e:
|
|
Data.Element.FEUER:
|
|
# Flamme: kleiner Kegel oben.
|
|
var c := CylinderMesh.new()
|
|
c.top_radius = 0.0
|
|
c.bottom_radius = 0.16
|
|
c.height = 0.38
|
|
c.radial_segments = 8
|
|
_orn(root, c, m, Vector3(0.0, top + 0.18, 0.0))
|
|
Data.Element.WASSER:
|
|
# Tropfen: flache Kugel oben.
|
|
var sp := SphereMesh.new()
|
|
sp.radius = 0.16
|
|
sp.height = 0.22
|
|
_orn(root, sp, m, Vector3(0.0, top + 0.12, 0.0))
|
|
Data.Element.WIND:
|
|
# Finne: dünne, hohe Platte.
|
|
var b := BoxMesh.new()
|
|
b.size = Vector3(0.05, 0.42, 0.28)
|
|
_orn(root, b, m, Vector3(0.0, top + 0.2, 0.0))
|
|
Data.Element.BLITZ:
|
|
# Zacke: Prisma oben.
|
|
var pr := PrismMesh.new()
|
|
pr.size = Vector3(0.28, 0.4, 0.12)
|
|
_orn(root, pr, m, Vector3(0.0, top + 0.2, 0.0))
|
|
Data.Element.ERDE:
|
|
# Schultern: zwei Würfel seitlich.
|
|
for sx in [-1.0, 1.0]:
|
|
var bx := BoxMesh.new()
|
|
bx.size = Vector3(0.22, 0.22, 0.22)
|
|
_orn(root, bx, m, Vector3(sx * 0.5, body_y + 0.1, 0.0))
|
|
Data.Element.EIS:
|
|
# Kristall: umgedrehtes Prisma oben.
|
|
var cr := PrismMesh.new()
|
|
cr.size = Vector3(0.22, 0.42, 0.22)
|
|
var n := _orn(root, cr, m, Vector3(0.0, top + 0.2, 0.0))
|
|
n.rotation_degrees = Vector3(180.0, 45.0, 0.0)
|
|
|
|
|
|
func _orn(root: Node3D, mesh: Mesh, mat: Material, pos: Vector3) -> MeshInstance3D:
|
|
var mi := MeshInstance3D.new()
|
|
mi.mesh = mesh
|
|
mi.material_override = mat
|
|
mi.position = pos
|
|
root.add_child(mi)
|
|
return mi
|
|
|
|
|
|
func _make_unit(a: Avatar, side: int) -> Node3D:
|
|
var root := Node3D.new()
|
|
|
|
var body := MeshInstance3D.new()
|
|
var mat := StandardMaterial3D.new()
|
|
mat.albedo_color = a.color()
|
|
mat.roughness = 0.45
|
|
mat.metallic = 0.15
|
|
mat.emission_enabled = true
|
|
mat.emission = a.color()
|
|
mat.emission_energy_multiplier = 0.12
|
|
body.material_override = mat
|
|
match a.role:
|
|
Data.Role.TANK:
|
|
var box := BoxMesh.new()
|
|
box.size = Vector3(0.78, 0.78, 0.78)
|
|
body.mesh = box
|
|
body.position.y = 0.48
|
|
Data.Role.DPS:
|
|
var cone := CylinderMesh.new()
|
|
cone.top_radius = 0.0
|
|
cone.bottom_radius = 0.46
|
|
cone.height = 1.05
|
|
cone.radial_segments = 12
|
|
body.mesh = cone
|
|
body.position.y = 0.62
|
|
_:
|
|
var sph := SphereMesh.new()
|
|
sph.radius = 0.42
|
|
sph.height = 0.84
|
|
sph.radial_segments = 16
|
|
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)
|
|
|
|
# Bodenring: weiss = eigenes Team, rot = Gegner.
|
|
var ring := MeshInstance3D.new()
|
|
ring.mesh = _ring_mesh
|
|
ring.position.y = 0.10
|
|
var rm := StandardMaterial3D.new()
|
|
rm.albedo_color = Color(0.95, 0.95, 1.0) if side == 0 else Color(0.92, 0.32, 0.32)
|
|
rm.emission_enabled = true
|
|
rm.emission = rm.albedo_color
|
|
rm.emission_energy_multiplier = 0.5
|
|
ring.material_override = rm
|
|
root.add_child(ring)
|
|
|
|
# Zweit- und Drittelement als schmalere Ringe darüber.
|
|
for i in range(1, a.elements.size()):
|
|
var er := MeshInstance3D.new()
|
|
var em := TorusMesh.new()
|
|
em.inner_radius = 0.30 + 0.10 * float(i)
|
|
em.outer_radius = em.inner_radius + 0.07
|
|
em.ring_segments = 6
|
|
em.rings = 20
|
|
er.mesh = em
|
|
er.position.y = 0.18 + 0.16 * float(i)
|
|
var m2 := StandardMaterial3D.new()
|
|
m2.albedo_color = Data.ELEMENT_COLORS[a.elements[i]]
|
|
m2.emission_enabled = true
|
|
m2.emission = m2.albedo_color
|
|
m2.emission_energy_multiplier = 0.6
|
|
er.material_override = m2
|
|
root.add_child(er)
|
|
return root
|