698 lines
30 KiB
GDScript
698 lines
30 KiB
GDScript
class_name ABGBattleLiveArt
|
|
extends Control
|
|
|
|
## Engine-drawn battle chrome reconstructed from the approved BAT-002 master.
|
|
## Runtime labels, values and hit targets remain separate overlay nodes.
|
|
|
|
var live_state := "waiting_for_next_event"
|
|
var event_kind := ""
|
|
var source_side := ""
|
|
var target_side := ""
|
|
var source_anchor := Vector2(0.46, 0.60)
|
|
var target_anchor := Vector2(0.67, 0.58)
|
|
var ally_anchors: Array[Vector2] = []
|
|
var enemy_anchors: Array[Vector2] = []
|
|
var party_count := 0
|
|
var enemy_count := 0
|
|
var playback_paused := true
|
|
var playback_speed := 1
|
|
|
|
|
|
func _ready() -> void:
|
|
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
queue_redraw()
|
|
|
|
|
|
func _notification(what: int) -> void:
|
|
if what == NOTIFICATION_RESIZED:
|
|
queue_redraw()
|
|
|
|
|
|
func set_event_state(state: String, kind: String, source: String, target: String) -> void:
|
|
live_state = state
|
|
event_kind = kind
|
|
source_side = source
|
|
target_side = target
|
|
queue_redraw()
|
|
|
|
|
|
func set_presentation_state(
|
|
paused: bool,
|
|
speed: float,
|
|
allies: Array[Vector2],
|
|
enemies: Array[Vector2],
|
|
event_source: Vector2,
|
|
event_target: Vector2
|
|
) -> void:
|
|
playback_paused = paused
|
|
playback_speed = int(speed)
|
|
ally_anchors = allies.duplicate()
|
|
enemy_anchors = enemies.duplicate()
|
|
party_count = ally_anchors.size()
|
|
enemy_count = enemy_anchors.size()
|
|
source_anchor = event_source
|
|
target_anchor = event_target
|
|
queue_redraw()
|
|
|
|
|
|
func _draw() -> void:
|
|
if size.x <= 0.0 or size.y <= 0.0:
|
|
return
|
|
_draw_top_rails()
|
|
_draw_field_language()
|
|
_draw_beat()
|
|
_draw_bottom_rail()
|
|
|
|
|
|
func _draw_top_rails() -> void:
|
|
var w := size.x
|
|
var h := size.y
|
|
var gold := Color("#d6ad58", 0.98)
|
|
var gold_dim := Color("#78633f", 0.92)
|
|
var navy := Color("#101b2a", 0.96)
|
|
var navy_inner := Color("#18293b", 0.96)
|
|
var blue := Color("#315a87", 0.98)
|
|
var red := Color("#8b3d47", 0.98)
|
|
var ivory := Color("#f1e4c2", 0.98)
|
|
|
|
# Long asymmetric faction rails with pointed inner shoulders. The master is
|
|
# deliberately quiet here: the rail carries count/pips, while all readable
|
|
# values remain engine-owned overlay text.
|
|
var left_outer := PackedVector2Array([
|
|
Vector2(w * 0.050, h * 0.016), Vector2(w * 0.325, h * 0.016),
|
|
Vector2(w * 0.344, h * 0.090), Vector2(w * 0.337, h * 0.108),
|
|
Vector2(w * 0.318, h * 0.122), Vector2(w * 0.060, h * 0.108),
|
|
])
|
|
draw_colored_polygon(left_outer, gold_dim)
|
|
var left_inner := PackedVector2Array([
|
|
Vector2(w * 0.058, h * 0.024), Vector2(w * 0.319, h * 0.024),
|
|
Vector2(w * 0.335, h * 0.087), Vector2(w * 0.328, h * 0.100),
|
|
Vector2(w * 0.316, h * 0.111), Vector2(w * 0.058, h * 0.099),
|
|
])
|
|
draw_colored_polygon(left_inner, navy)
|
|
draw_polyline(left_outer + PackedVector2Array([left_outer[0]]), gold, 2.0)
|
|
draw_line(Vector2(w * 0.060, h * 0.091), Vector2(w * 0.321, h * 0.102), Color(gold, 0.42), 1.0)
|
|
draw_line(Vector2(w * 0.070, h * 0.032), Vector2(w * 0.314, h * 0.032), Color(gold, 0.22), 1.0)
|
|
draw_line(Vector2(w * 0.060, h * 0.100), Vector2(w * 0.305, h * 0.111), Color(gold, 0.18), 1.0)
|
|
draw_line(Vector2(w * 0.082, h * 0.043), Vector2(w * 0.314, h * 0.043), Color("#e8c878", 0.11), 1.0)
|
|
draw_circle(Vector2(w * 0.307, h * 0.101), maxf(1.5, w * 0.0022), Color("#efcf83", 0.72))
|
|
|
|
var right_outer := PackedVector2Array([
|
|
Vector2(w * 0.675, h * 0.016), Vector2(w * 0.950, h * 0.016),
|
|
Vector2(w * 0.950, h * 0.108), Vector2(w * 0.682, h * 0.122),
|
|
Vector2(w * 0.663, h * 0.108), Vector2(w * 0.656, h * 0.090),
|
|
])
|
|
draw_colored_polygon(right_outer, gold_dim)
|
|
var right_inner := PackedVector2Array([
|
|
Vector2(w * 0.681, h * 0.024), Vector2(w * 0.943, h * 0.024),
|
|
Vector2(w * 0.943, h * 0.099), Vector2(w * 0.678, h * 0.111),
|
|
Vector2(w * 0.666, h * 0.100), Vector2(w * 0.662, h * 0.087),
|
|
])
|
|
draw_colored_polygon(right_inner, navy)
|
|
draw_polyline(right_outer + PackedVector2Array([right_outer[0]]), gold, 2.0)
|
|
draw_line(Vector2(w * 0.679, h * 0.102), Vector2(w * 0.940, h * 0.091), Color(gold, 0.42), 1.0)
|
|
draw_line(Vector2(w * 0.686, h * 0.032), Vector2(w * 0.936, h * 0.032), Color(gold, 0.22), 1.0)
|
|
draw_line(Vector2(w * 0.695, h * 0.111), Vector2(w * 0.940, h * 0.100), Color(gold, 0.18), 1.0)
|
|
draw_line(Vector2(w * 0.686, h * 0.043), Vector2(w * 0.918, h * 0.043), Color("#e8c878", 0.11), 1.0)
|
|
draw_circle(Vector2(w * 0.693, h * 0.101), maxf(1.5, w * 0.0022), Color("#efcf83", 0.72))
|
|
|
|
# Hanging crest endcaps preserve the approved blue/red silhouette.
|
|
var left_cap := PackedVector2Array([
|
|
Vector2(w * 0.014, 0.0), Vector2(w * 0.073, 0.0),
|
|
Vector2(w * 0.073, h * 0.105), Vector2(w * 0.052, h * 0.144),
|
|
Vector2(w * 0.015, h * 0.112),
|
|
])
|
|
draw_colored_polygon(left_cap, blue)
|
|
draw_polyline(left_cap + PackedVector2Array([left_cap[0]]), gold, 2.0)
|
|
var right_cap := PackedVector2Array([
|
|
Vector2(w * 0.927, 0.0), Vector2(w * 0.986, 0.0),
|
|
Vector2(w * 0.985, h * 0.112), Vector2(w * 0.948, h * 0.144),
|
|
Vector2(w * 0.927, h * 0.105),
|
|
])
|
|
draw_colored_polygon(right_cap, red)
|
|
draw_polyline(right_cap + PackedVector2Array([right_cap[0]]), gold, 2.0)
|
|
draw_line(Vector2(w * 0.933, h * 0.014), Vector2(w * 0.933, h * 0.099), Color("#e8a26e", 0.24), 1.0)
|
|
_draw_crest(Vector2(w * 0.044, h * 0.057), minf(w, h) * 0.026, Color("#edf1ec"))
|
|
_draw_crest(Vector2(w * 0.956, h * 0.057), minf(w, h) * 0.026, Color("#f3e8dc"))
|
|
|
|
# Status pips remain inside the long rails instead of becoming cards. Keep
|
|
# the spacing stable for the small BAT-002 fixtures while allowing larger
|
|
# committed lines to use the same authored rail.
|
|
var ally_pip_count := mini(party_count, 10)
|
|
var enemy_pip_count := mini(enemy_count, 10)
|
|
for index in range(ally_pip_count):
|
|
_draw_diamond(Vector2(w * (0.145 + index * 0.034), h * 0.064), w * 0.0075, Color("#68b8e9"))
|
|
for index in range(enemy_pip_count):
|
|
_draw_diamond(Vector2(w * (0.855 - index * 0.034), h * 0.064), w * 0.0075, Color("#e05c5c"))
|
|
# The small ivory markers on the lower rail line are part of the approved
|
|
# rhythm and make the opposing tracks read as one family.
|
|
_draw_diamond(Vector2(w * 0.119, h * 0.096), w * 0.0065, Color("#f3e7c8"))
|
|
_draw_diamond(Vector2(w * 0.821, h * 0.096), w * 0.0065, Color("#f3e7c8"))
|
|
|
|
# One continuous ivory playback ribbon. Logical buttons sit transparently on top.
|
|
var ribbon_outer := PackedVector2Array([
|
|
Vector2(w * 0.390, 0.0), Vector2(w * 0.610, 0.0),
|
|
Vector2(w * 0.598, h * 0.103), Vector2(w * 0.402, h * 0.103),
|
|
])
|
|
draw_colored_polygon(ribbon_outer, gold_dim)
|
|
var ribbon_inner := PackedVector2Array([
|
|
Vector2(w * 0.394, 0.0), Vector2(w * 0.606, 0.0),
|
|
Vector2(w * 0.594, h * 0.093), Vector2(w * 0.406, h * 0.093),
|
|
])
|
|
draw_colored_polygon(ribbon_inner, ivory)
|
|
draw_polyline(ribbon_outer + PackedVector2Array([ribbon_outer[0]]), Color("#b99a59"), 2.0)
|
|
for split_x in [0.465, 0.535]:
|
|
draw_line(Vector2(w * split_x, 0.0), Vector2(w * split_x, h * 0.093), Color("#b59d6c", 0.62), 1.0)
|
|
_draw_pause_icon(Vector2(w * 0.428, h * 0.045), h * 0.025, navy_inner)
|
|
_draw_speed_icon(Vector2(w * 0.492, h * 0.045), h * 0.023, navy_inner)
|
|
_draw_magnifier(Vector2(w * 0.570, h * 0.042), h * 0.020, navy_inner)
|
|
|
|
# Settings is an invisible hit target in the approved corner. The master has
|
|
# no competing visible gear beside the enemy count.
|
|
|
|
|
|
func _draw_field_language() -> void:
|
|
var w := size.x
|
|
var h := size.y
|
|
var gold := Color("#e0bd69", 0.92)
|
|
var ivory := Color("#f5ead0", 0.94)
|
|
var path_color := _event_color()
|
|
|
|
# A narrow field-attached objective bracket keeps the painted world open.
|
|
var objective := PackedVector2Array([
|
|
Vector2(w * 0.018, h * 0.142), Vector2(w * 0.278, h * 0.142),
|
|
Vector2(w * 0.294, h * 0.157), Vector2(w * 0.282, h * 0.238),
|
|
Vector2(w * 0.018, h * 0.238),
|
|
])
|
|
# The approved frame uses a field-attached bracket, not an objective card.
|
|
# Leave the painted ruins unobscured; only the corner lines remain visible.
|
|
draw_line(objective[0], objective[1], Color(gold, 0.78), 1.5)
|
|
draw_line(objective[1], objective[2], Color(gold, 0.46), 1.0)
|
|
draw_line(objective[4], objective[3], Color(gold, 0.46), 1.0)
|
|
draw_line(objective[0], objective[4], Color(gold, 0.66), 1.5)
|
|
draw_line(objective[2], objective[3], Color(gold, 0.30), 1.0)
|
|
|
|
# Small role/status markers remain attached to the actors.
|
|
for index in range(ally_anchors.size()):
|
|
_draw_actor_state_marker(_to_px(ally_anchors[index]), Color("#6fc0e7"), index == ally_anchors.size() - 1)
|
|
for index in range(enemy_anchors.size()):
|
|
_draw_actor_state_marker(_to_px(enemy_anchors[index]), Color("#dc6662"), index == 0)
|
|
|
|
if event_kind.is_empty() or source_side.is_empty() or target_side.is_empty():
|
|
if live_state == "waiting_for_next_event":
|
|
var waiting_center := Vector2(w * 0.50, h * 0.345)
|
|
draw_arc(waiting_center, w * 0.014, 0.0, TAU, 24, Color(ivory, 0.78), 2.0)
|
|
_draw_diamond(waiting_center, w * 0.0045, gold)
|
|
elif event_kind == "opening":
|
|
var opening_center := Vector2(w * 0.50, h * 0.365)
|
|
draw_arc(opening_center, w * 0.020, 0.0, TAU, 28, Color(gold, 0.68), 2.0)
|
|
_draw_diamond(opening_center, w * 0.006, ivory)
|
|
return
|
|
|
|
var source := _to_px(source_anchor)
|
|
var target := _to_px(target_anchor)
|
|
var bend := -h * 0.095 if source.x < target.x else h * 0.075
|
|
var path := _quadratic_path(source, target, bend)
|
|
# M110 A5 (Owner-authorized bounded repair): dark twin underlay beneath the
|
|
# bright ladder so the rendered carrier immediately outside each bright
|
|
# stroke is the existing dark ink palette. Event hues, bright widths, shapes
|
|
# and non-color cues stay unchanged.
|
|
var underlay := Color("#101b2a", 0.94)
|
|
var underlay_deep := Color("#071015", 0.94)
|
|
for offset in [-4.0, 0.0, 4.0]:
|
|
var underlay_path := PackedVector2Array()
|
|
for point in path:
|
|
underlay_path.append(point + Vector2(0.0, offset))
|
|
draw_polyline(underlay_path, underlay_deep if offset == 0.0 else underlay, 6.0 if offset == 0.0 else 4.0)
|
|
for offset in [-4.0, 0.0, 4.0]:
|
|
var shifted := PackedVector2Array()
|
|
for point in path:
|
|
shifted.append(point + Vector2(0.0, offset))
|
|
draw_polyline(shifted, Color(path_color, 0.52 if offset != 0.0 else 0.98), 1.6 if offset != 0.0 else 3.0)
|
|
var direction := (path[-1] - path[-2]).normalized()
|
|
var perpendicular := Vector2(-direction.y, direction.x)
|
|
draw_colored_polygon(PackedVector2Array([
|
|
target,
|
|
target - direction * w * 0.018 + perpendicular * w * 0.008,
|
|
target - direction * w * 0.018 - perpendicular * w * 0.008,
|
|
]), path_color)
|
|
# A second small arrow in the corridor echoes the approved multi-stroke
|
|
# projection without introducing a separate event panel.
|
|
var mid_index := 11
|
|
var mid := path[mid_index]
|
|
var mid_direction := (path[mid_index + 1] - path[mid_index - 1]).normalized()
|
|
var mid_perpendicular := Vector2(-mid_direction.y, mid_direction.x)
|
|
draw_colored_polygon(PackedVector2Array([
|
|
mid,
|
|
mid - mid_direction * w * 0.012 + mid_perpendicular * w * 0.006,
|
|
mid - mid_direction * w * 0.012 - mid_perpendicular * w * 0.006,
|
|
]), Color(path_color, 0.82))
|
|
# The approved master uses a small cluster of parallel flight marks. Keep
|
|
# the extra strokes inside the action gap, without adding a diagnostic band.
|
|
var flight_mid := path[7]
|
|
var flight_direction := (path[8] - path[6]).normalized()
|
|
var flight_perpendicular := Vector2(-flight_direction.y, flight_direction.x)
|
|
for lane in [-1.0, 1.0]:
|
|
var lane_mid: Vector2 = flight_mid + flight_perpendicular * lane * w * 0.010
|
|
draw_line(lane_mid - flight_direction * w * 0.028, lane_mid + flight_direction * w * 0.028, Color(path_color, 0.62), 1.4)
|
|
draw_arc(source, w * 0.012, 0.0, TAU, 28, underlay, 5.5)
|
|
draw_arc(source, w * 0.012, 0.0, TAU, 28, Color(path_color, 0.92), 2.0)
|
|
draw_circle(source, w * 0.005, Color(path_color, 0.55))
|
|
draw_circle(target, w * 0.029, Color(path_color, 0.13))
|
|
draw_arc(target, w * 0.029, 0.0, TAU, 36, underlay, 5.5)
|
|
draw_arc(target, w * 0.029, 0.0, TAU, 36, Color(path_color, 0.92), 2.0)
|
|
|
|
# The urgent marker is tied to the target endpoint, never a central debug band.
|
|
var urgent := target - Vector2(0.0, h * 0.205)
|
|
draw_line(urgent + Vector2(0.0, h * 0.030), target - Vector2(0.0, h * 0.025), underlay, 5.0)
|
|
draw_line(urgent + Vector2(0.0, h * 0.030), target - Vector2(0.0, h * 0.025), Color(path_color, 0.45), 3.0)
|
|
var urgent_shape := PackedVector2Array([
|
|
urgent + Vector2(0.0, -h * 0.036), urgent + Vector2(w * 0.020, 0.0),
|
|
urgent + Vector2(0.0, h * 0.036), urgent + Vector2(-w * 0.020, 0.0),
|
|
])
|
|
draw_polyline(urgent_shape + PackedVector2Array([urgent_shape[0]]), underlay_deep, 4.0)
|
|
draw_colored_polygon(urgent_shape, Color(path_color, 0.94))
|
|
draw_polyline(urgent_shape + PackedVector2Array([urgent_shape[0]]), Color("#fff0d8"), 2.0)
|
|
draw_line(urgent - Vector2(0.0, h * 0.014), urgent + Vector2(0.0, h * 0.009), ivory, 3.0)
|
|
draw_circle(urgent + Vector2(0.0, h * 0.020), maxf(2.0, w * 0.0016), ivory)
|
|
|
|
|
|
func _draw_bottom_rail() -> void:
|
|
var w := size.x
|
|
var h := size.y
|
|
var top := h * 0.80
|
|
var gold := Color("#d0aa58", 0.96)
|
|
var rail := Color("#0e1723", 0.97)
|
|
var rail_inner := Color("#172434", 0.98)
|
|
var plate := Color("#1a2836", 0.84)
|
|
|
|
draw_rect(Rect2(0.0, top, w, h - top), rail)
|
|
draw_line(Vector2(0.0, top), Vector2(w, top), gold, 2.0)
|
|
draw_line(Vector2(0.0, top + h * 0.009), Vector2(w, top + h * 0.009), Color(gold, 0.28), 1.0)
|
|
draw_line(Vector2(0.0, h * 0.985), Vector2(w, h * 0.985), Color("#5b513c", 0.42), 1.0)
|
|
draw_rect(Rect2(w * 0.012, h * 0.835, w * 0.142, h * 0.132), rail_inner)
|
|
draw_style_box(_plate(Color("#131b25", 0.98), Color(gold, 0.78), 1, 3), Rect2(w * 0.018, h * 0.857, w * 0.125, h * 0.095))
|
|
_draw_hourglass(Vector2(w * 0.042, h * 0.905), h * 0.023, Color("#e6d5a8"))
|
|
|
|
var card_count := maxi(1, party_count)
|
|
var card_left := 0.170
|
|
var card_right := 0.885
|
|
var card_gap := clampf(0.012 - float(card_count) * 0.0008, 0.006, 0.012)
|
|
var card_width := (card_right - card_left - card_gap * float(card_count - 1)) / float(card_count)
|
|
for index in range(card_count):
|
|
var rect := Rect2(w * (card_left + index * (card_width + card_gap)), h * 0.812, w * card_width, h * 0.175)
|
|
var cut := clampf(rect.size.x * 0.055, 2.0, 7.0)
|
|
var card_shape := PackedVector2Array([
|
|
rect.position + Vector2(cut, 0.0), rect.position + Vector2(rect.size.x - cut, 0.0),
|
|
rect.position + Vector2(rect.size.x, cut), rect.position + Vector2(rect.size.x, rect.size.y - cut),
|
|
rect.position + Vector2(rect.size.x - cut, rect.size.y), rect.position + Vector2(cut, rect.size.y),
|
|
rect.position + Vector2(0.0, rect.size.y - cut), rect.position + Vector2(0.0, cut),
|
|
])
|
|
draw_colored_polygon(card_shape, plate)
|
|
draw_polyline(card_shape + PackedVector2Array([card_shape[0]]), Color(gold, 0.88), 1.2)
|
|
draw_polyline(PackedVector2Array([
|
|
rect.position + Vector2(cut + 2.0, 2.0),
|
|
rect.position + Vector2(rect.size.x - cut - 2.0, 2.0),
|
|
rect.position + Vector2(rect.size.x - 2.0, cut + 2.0),
|
|
rect.position + Vector2(rect.size.x - 2.0, rect.size.y - cut - 2.0),
|
|
rect.position + Vector2(rect.size.x - cut - 2.0, rect.size.y - 2.0),
|
|
rect.position + Vector2(cut + 2.0, rect.size.y - 2.0),
|
|
rect.position + Vector2(2.0, rect.size.y - cut - 2.0),
|
|
rect.position + Vector2(2.0, cut + 2.0),
|
|
rect.position + Vector2(cut + 2.0, 2.0),
|
|
]), Color("#f0d17d", 0.24), 1.0)
|
|
draw_line(rect.position + Vector2(rect.size.x * 0.06, rect.size.y * 0.71), rect.position + Vector2(rect.size.x * 0.94, rect.size.y * 0.71), Color(gold, 0.20), 1.0)
|
|
draw_rect(Rect2(rect.position + Vector2(rect.size.x * 0.055, rect.size.y * 0.745), Vector2(rect.size.x * 0.89, rect.size.y * 0.070)), Color("#071015", 0.92))
|
|
draw_rect(Rect2(rect.position + Vector2(rect.size.x * 0.055, rect.size.y * 0.862), Vector2(rect.size.x * 0.89, rect.size.y * 0.070)), Color("#071015", 0.92))
|
|
var badge_radius := clampf(rect.size.x * 0.105, h * 0.012, h * 0.028)
|
|
var badge := rect.position + Vector2(rect.size.x * 0.105, rect.size.y * 0.115)
|
|
_draw_diamond(badge, badge_radius, Color("#30291e"))
|
|
draw_arc(badge, badge_radius, 0.0, TAU, 4, gold, 1.5)
|
|
|
|
# The boundary command remains a compact rail glyph inside a generous hit area.
|
|
var command_center := Vector2(w * 0.943, h * 0.895)
|
|
draw_circle(command_center, h * 0.027, Color("#18283a", 0.84))
|
|
draw_arc(command_center, h * 0.027, 0.0, TAU, 28, Color(gold, 0.84), 1.5)
|
|
draw_line(command_center - Vector2(h * 0.010, 0.0), command_center + Vector2(h * 0.010, 0.0), Color("#e9dcb9"), 2.0)
|
|
draw_line(command_center + Vector2(h * 0.003, -h * 0.008), command_center + Vector2(h * 0.010, 0.0), Color("#e9dcb9"), 2.0)
|
|
draw_line(command_center + Vector2(h * 0.003, h * 0.008), command_center + Vector2(h * 0.010, 0.0), Color("#e9dcb9"), 2.0)
|
|
|
|
|
|
func _event_color() -> Color:
|
|
match event_kind:
|
|
"healing": return Color("#70c59b", 0.96)
|
|
"guard", "guarded_recovery": return Color("#73b8dc", 0.96)
|
|
"magic": return Color("#b58bd6", 0.96)
|
|
"knockout": return Color("#e16c67", 0.98)
|
|
"result": return Color("#dfbd68", 0.98)
|
|
return Color("#e2b75f", 0.96)
|
|
|
|
|
|
func _draw_actor_state_marker(center: Vector2, color: Color, emphasized: bool) -> void:
|
|
var radius := size.x * (0.0048 if not emphasized else 0.0062)
|
|
draw_circle(center, radius * 1.25, Color("#13202a", 0.62))
|
|
_draw_diamond(center, radius, color)
|
|
draw_arc(center, radius * 1.25, 0.0, TAU, 20, Color("#f4e6c3", 0.72), 1.0)
|
|
|
|
|
|
func _draw_crest(center: Vector2, radius: float, color: Color) -> void:
|
|
# Four tapered leaves read closer to the approved faction emblems than a
|
|
# generic gear/cross while remaining an engine-drawn, non-baked mark.
|
|
var leaf := func(angle: float) -> PackedVector2Array:
|
|
var direction := Vector2(cos(angle), sin(angle))
|
|
var side := Vector2(-direction.y, direction.x)
|
|
return PackedVector2Array([
|
|
center + direction * radius * 0.95,
|
|
center + direction * radius * 0.16 + side * radius * 0.34,
|
|
center - direction * radius * 0.08,
|
|
center + direction * radius * 0.16 - side * radius * 0.34,
|
|
])
|
|
for angle in [(-PI * 0.5), 0.0, (PI * 0.5), PI]:
|
|
draw_colored_polygon(leaf.call(angle), Color(color, 0.92))
|
|
draw_circle(center, radius * 0.16, Color("#29445c", 0.96))
|
|
draw_arc(center, radius * 0.68, 0.0, TAU, 16, Color(color, 0.78), 1.0)
|
|
|
|
|
|
func _draw_pause_icon(center: Vector2, radius: float, color: Color) -> void:
|
|
if playback_paused:
|
|
var triangle := PackedVector2Array([
|
|
center + Vector2(-radius * 0.42, -radius * 0.62),
|
|
center + Vector2(radius * 0.62, 0.0),
|
|
center + Vector2(-radius * 0.42, radius * 0.62),
|
|
])
|
|
draw_colored_polygon(triangle, color)
|
|
else:
|
|
draw_rect(Rect2(center - Vector2(radius * 0.48, radius * 0.62), Vector2(radius * 0.32, radius * 1.24)), color)
|
|
draw_rect(Rect2(center + Vector2(radius * 0.16, -radius * 0.62), Vector2(radius * 0.32, radius * 1.24)), color)
|
|
|
|
|
|
func _draw_speed_icon(center: Vector2, radius: float, color: Color) -> void:
|
|
var chevrons := 1 if playback_speed <= 1 else (2 if playback_speed <= 2 else 3)
|
|
for index in range(chevrons):
|
|
var offset := (float(index) - float(chevrons - 1) * 0.5) * radius * 0.72
|
|
var c := center + Vector2(offset, 0.0)
|
|
draw_colored_polygon(PackedVector2Array([
|
|
c + Vector2(-radius * 0.38, -radius * 0.62),
|
|
c + Vector2(radius * 0.42, 0.0),
|
|
c + Vector2(-radius * 0.38, radius * 0.62),
|
|
]), color)
|
|
|
|
|
|
func _draw_magnifier(center: Vector2, radius: float, color: Color) -> void:
|
|
draw_arc(center - Vector2(radius * 0.18, radius * 0.18), radius * 0.68, 0.0, TAU, 24, color, 3.0)
|
|
draw_line(center + Vector2(radius * 0.34, radius * 0.34), center + Vector2(radius * 0.95, radius * 0.95), color, 3.0)
|
|
|
|
|
|
func _draw_cog(center: Vector2, radius: float, color: Color, fill: Color) -> void:
|
|
draw_circle(center, radius * 1.12, Color(fill, 0.94))
|
|
for index in range(8):
|
|
var angle := TAU * float(index) / 8.0
|
|
var direction := Vector2(cos(angle), sin(angle))
|
|
draw_line(center + direction * radius * 0.82, center + direction * radius * 1.24, color, 2.0)
|
|
draw_arc(center, radius * 0.72, 0.0, TAU, 24, color, 2.0)
|
|
draw_circle(center, radius * 0.22, color)
|
|
|
|
|
|
func _draw_hourglass(center: Vector2, radius: float, color: Color) -> void:
|
|
var top_left := center + Vector2(-radius * 0.70, -radius)
|
|
var top_right := center + Vector2(radius * 0.70, -radius)
|
|
var bottom_left := center + Vector2(-radius * 0.70, radius)
|
|
var bottom_right := center + Vector2(radius * 0.70, radius)
|
|
draw_line(top_left, top_right, color, 2.0)
|
|
draw_line(bottom_left, bottom_right, color, 2.0)
|
|
draw_line(top_left, center + Vector2(radius * 0.18, 0.0), color, 2.0)
|
|
draw_line(top_right, center + Vector2(-radius * 0.18, 0.0), color, 2.0)
|
|
draw_line(center + Vector2(radius * 0.18, 0.0), bottom_left, color, 2.0)
|
|
draw_line(center + Vector2(-radius * 0.18, 0.0), bottom_right, color, 2.0)
|
|
draw_colored_polygon(PackedVector2Array([
|
|
center + Vector2(-radius * 0.16, radius * 0.12),
|
|
center + Vector2(radius * 0.16, radius * 0.12),
|
|
center + Vector2(radius * 0.30, radius * 0.58),
|
|
center + Vector2(-radius * 0.30, radius * 0.58),
|
|
]), Color(color, 0.72))
|
|
|
|
|
|
func _quadratic_path(source: Vector2, target: Vector2, bend: float) -> PackedVector2Array:
|
|
var points := PackedVector2Array()
|
|
var midpoint := (source + target) * 0.5 + Vector2(0.0, bend)
|
|
for index in range(21):
|
|
var t := float(index) / 20.0
|
|
var point := source * pow(1.0 - t, 2.0) + midpoint * 2.0 * (1.0 - t) * t + target * pow(t, 2.0)
|
|
points.append(point)
|
|
return points
|
|
|
|
|
|
func _to_px(point: Vector2) -> Vector2:
|
|
return Vector2(point.x * size.x, point.y * size.y)
|
|
|
|
|
|
func _draw_diamond(center: Vector2, radius: float, color: Color) -> void:
|
|
var points := PackedVector2Array([
|
|
center + Vector2(0.0, -radius),
|
|
center + Vector2(radius, 0.0),
|
|
center + Vector2(0.0, radius),
|
|
center + Vector2(-radius, 0.0),
|
|
])
|
|
draw_colored_polygon(points, color)
|
|
|
|
|
|
func _plate(background: Color, border: Color, border_width: int, radius: int) -> StyleBoxFlat:
|
|
var style := StyleBoxFlat.new()
|
|
style.bg_color = background
|
|
style.border_color = border
|
|
style.set_border_width_all(border_width)
|
|
style.set_corner_radius_all(radius)
|
|
return style
|
|
|
|
|
|
# --- M77-B event beats -------------------------------------------------------
|
|
# The approved field language (path, rings, urgent marker) stays; these add the
|
|
# readable beat: telegraph pulses, projectile travel, impact bursts, guard arc,
|
|
# healing motes, knockout mark. Sprites stay with the shell actor nodes (pose
|
|
# and fade are set there); the art owns the temporal VFX and its own clock, so
|
|
# no Tween lifecycle exists to leak.
|
|
|
|
const BEAT_DURATIONS := {
|
|
"opening": 1.2,
|
|
"telegraph": 1.2,
|
|
"guard": 0.7,
|
|
"ranged": 0.9,
|
|
"magic": 1.0,
|
|
"healing": 1.0,
|
|
"damage": 0.9,
|
|
"knockout": 1.4,
|
|
"guarded_recovery": 1.0,
|
|
}
|
|
const BEAT_IMPACT := {
|
|
"ranged": 0.40,
|
|
"magic": 0.45,
|
|
"damage": 0.32,
|
|
"knockout": 0.34,
|
|
"guard": 0.15,
|
|
"healing": 0.20,
|
|
}
|
|
|
|
var beat_kind := ""
|
|
var beat_key := ""
|
|
var beat_time := 0.0
|
|
var beat_duration := 0.0
|
|
var beat_amount := 0
|
|
var beat_source := Vector2.ZERO
|
|
var beat_target := Vector2.ZERO
|
|
var beat_played := 0
|
|
var beat_kinds_seen: Dictionary = {}
|
|
var reduce_motion := false
|
|
var reduce_flashes := false
|
|
var color_cues := true
|
|
var hit_pause_enabled := true
|
|
var _pause_consumed := true
|
|
|
|
|
|
func set_accessibility(presentation: Dictionary) -> void:
|
|
reduce_motion = bool(presentation.get("reduced_motion", false))
|
|
reduce_flashes = bool(presentation.get("reduced_flashes", false))
|
|
color_cues = bool(presentation.get("color_cues", true))
|
|
hit_pause_enabled = bool(presentation.get("hit_pause", true))
|
|
queue_redraw()
|
|
|
|
|
|
func set_beat(key: String, kind: String, source: Vector2, target: Vector2, amount := 0) -> void:
|
|
if key == beat_key and kind == beat_kind:
|
|
return
|
|
beat_key = key
|
|
beat_kind = kind
|
|
beat_source = source
|
|
beat_target = target
|
|
beat_amount = amount
|
|
beat_time = 0.0
|
|
beat_duration = float(BEAT_DURATIONS.get(kind, 0.9))
|
|
_pause_consumed = not hit_pause_enabled
|
|
beat_played += 1
|
|
beat_kinds_seen[kind] = int(beat_kinds_seen.get(kind, 0)) + 1
|
|
set_process(true)
|
|
queue_redraw()
|
|
|
|
|
|
func beat_progress() -> float:
|
|
if beat_duration <= 0.0:
|
|
return 1.0
|
|
return clampf(beat_time / beat_duration, 0.0, 1.0)
|
|
|
|
|
|
func _process(delta: float) -> void:
|
|
if beat_kind.is_empty():
|
|
set_process(false)
|
|
return
|
|
beat_time += delta
|
|
var impact := float(BEAT_IMPACT.get(beat_kind, 1.0))
|
|
if not _pause_consumed and beat_time >= beat_duration * impact:
|
|
# A short impact freeze; the flight already happened before it.
|
|
_pause_consumed = true
|
|
beat_time -= 0.10
|
|
queue_redraw()
|
|
if beat_time > beat_duration * 1.4:
|
|
set_process(false)
|
|
|
|
|
|
func _draw_beat() -> void:
|
|
if beat_kind.is_empty() or size.x <= 0.0:
|
|
return
|
|
var progress := beat_progress()
|
|
var source := _to_px(beat_source)
|
|
var target := _to_px(beat_target)
|
|
if not color_cues:
|
|
_draw_beat_monochrome(progress, source, target)
|
|
return
|
|
match beat_kind:
|
|
"opening":
|
|
_draw_opening_beat(progress)
|
|
"telegraph":
|
|
_draw_telegraph_beat(progress, target)
|
|
"guard":
|
|
_draw_guard_beat(progress, target)
|
|
"ranged":
|
|
_draw_projectile_beat(progress, source, target, Color("#f2d27a"))
|
|
"magic":
|
|
_draw_magic_beat(progress, source, target)
|
|
"healing":
|
|
_draw_healing_beat(progress, target)
|
|
"damage":
|
|
_draw_impact_beat(progress, target, Color("#ffd9a0"))
|
|
"knockout":
|
|
_draw_impact_beat(progress, target, Color("#e0695f"))
|
|
_draw_knockout_mark(progress, target)
|
|
"guarded_recovery":
|
|
_draw_recovery_beat(progress, target)
|
|
|
|
|
|
func _draw_beat_monochrome(progress: float, source: Vector2, target: Vector2) -> void:
|
|
# Colour-blind safe mode: identical shapes, one ink palette, no hue channel.
|
|
var ink := Color("#f0e2c0")
|
|
var alpha := 1.0 - progress
|
|
draw_arc(target, lerpf(size.x * 0.012, size.x * 0.032, progress), 0.0, TAU, 30, Color(ink, alpha * 0.85), 2.5)
|
|
if beat_kind in ["ranged", "magic", "damage", "knockout"]:
|
|
for index in 8:
|
|
var angle := TAU * float(index) / 8.0
|
|
var direction := Vector2(cos(angle), sin(angle))
|
|
draw_line(target + direction * size.x * 0.012, target + direction * size.x * lerpf(0.020, 0.038, progress), Color(ink, alpha * 0.7), 2.0)
|
|
if beat_kind in ["ranged", "magic"] and progress < float(BEAT_IMPACT.get(beat_kind, 0.4)):
|
|
draw_line(source, source.lerp(target, progress / maxf(0.001, float(BEAT_IMPACT.get(beat_kind, 0.4)))), Color(ink, 0.65), 2.0)
|
|
if beat_kind in ["healing", "guarded_recovery"]:
|
|
for index in 4:
|
|
var mote := target + Vector2((float(index) - 1.5) * size.x * 0.012, -progress * size.y * 0.055)
|
|
draw_circle(mote, maxf(1.6, size.x * 0.0022), Color(ink, alpha * 0.8))
|
|
|
|
|
|
func _draw_opening_beat(progress: float) -> void:
|
|
var center := Vector2(size.x * 0.50, size.y * 0.345)
|
|
var alpha := 0.35 + 0.45 * sin(progress * PI)
|
|
draw_arc(center, size.x * 0.022, 0.0, TAU, 30, Color("#efd694", alpha), 2.0)
|
|
draw_line(Vector2(size.x * 0.34, size.y * 0.56), Vector2(size.x * 0.66, size.y * 0.56), Color("#e8d39d", alpha * 0.5), 1.5)
|
|
|
|
|
|
func _draw_telegraph_beat(progress: float, target: Vector2) -> void:
|
|
var base := maxf(14.0, size.x * 0.014)
|
|
for index in 3:
|
|
var local := fmod(progress * 2.2 + float(index) * 0.33, 1.0)
|
|
var radius := lerpf(base, base * 5.0, local)
|
|
draw_arc(target, radius, 0.0, TAU, 40, Color("#e0575f", (1.0 - local) * 0.55), 2.5)
|
|
draw_circle(target, base * 0.55, Color("#e0575f", 0.30 * (1.0 - progress)))
|
|
|
|
|
|
func _draw_guard_beat(progress: float, target: Vector2) -> void:
|
|
var alpha := sin(progress * PI)
|
|
var center := target - Vector2(0.0, size.y * 0.045)
|
|
var radius := maxf(18.0, size.x * 0.018)
|
|
draw_arc(center, radius, PI, TAU, 26, Color("#84d7e6", 0.90 * alpha), 4.0)
|
|
draw_arc(center, radius * 0.78, PI, TAU, 22, Color("#d8f3f8", 0.55 * alpha), 1.5)
|
|
|
|
|
|
func _draw_projectile_beat(progress: float, source: Vector2, target: Vector2, tint: Color) -> void:
|
|
var impact := float(BEAT_IMPACT.get("ranged", 0.4))
|
|
if progress < impact:
|
|
var local := progress / impact
|
|
var position := source.lerp(target, local)
|
|
var direction := (target - source).normalized()
|
|
for index in 4:
|
|
var trail := position - direction * float(index) * size.x * 0.006
|
|
draw_circle(trail, maxf(1.5, size.x * 0.0022) * (1.0 - float(index) * 0.2), Color(tint, 0.55 - float(index) * 0.12))
|
|
draw_circle(position, maxf(3.0, size.x * 0.0045), tint)
|
|
return
|
|
_draw_impact_beat((progress - impact) / maxf(0.001, 1.0 - impact), target, tint)
|
|
|
|
|
|
func _draw_magic_beat(progress: float, source: Vector2, target: Vector2) -> void:
|
|
var impact := float(BEAT_IMPACT.get("magic", 0.45))
|
|
var violet := Color("#b98ce0")
|
|
if progress < impact:
|
|
var local := progress / impact
|
|
draw_arc(source, lerpf(size.x * 0.012, size.x * 0.030, local), 0.0, TAU, 30, Color(violet, 0.35 + 0.5 * local), 2.5)
|
|
draw_arc(source, lerpf(size.x * 0.006, size.x * 0.018, local), 0.0, TAU, 22, Color("#e8d6f6", 0.5 * local), 1.5)
|
|
return
|
|
var local := (progress - impact) / maxf(0.001, 1.0 - impact)
|
|
draw_line(source, target, Color(violet, 0.55 * (1.0 - local)), maxf(2.0, size.x * 0.004))
|
|
_draw_impact_beat(local, target, violet)
|
|
|
|
|
|
func _draw_healing_beat(progress: float, target: Vector2) -> void:
|
|
var green := Color("#95d9a0")
|
|
draw_arc(target, lerpf(size.x * 0.010, size.x * 0.030, progress), 0.0, TAU, 30, Color(green, (1.0 - progress) * 0.7), 2.0)
|
|
for index in 5:
|
|
var offset := (float(index) - 2.0) * size.x * 0.010
|
|
var rise := progress * size.y * 0.065
|
|
var mote := target + Vector2(offset, -size.y * 0.010 - rise)
|
|
draw_circle(mote, maxf(1.6, size.x * 0.0024), Color(green, (1.0 - progress) * 0.85))
|
|
|
|
|
|
func _draw_impact_beat(progress: float, target: Vector2, tint: Color) -> void:
|
|
if progress <= 0.0 or progress >= 1.0:
|
|
return
|
|
var fade := 1.0 - progress
|
|
if reduce_flashes:
|
|
draw_arc(target, lerpf(size.x * 0.012, size.x * 0.034, progress), 0.0, TAU, 30, Color(tint, fade * 0.85), 3.0)
|
|
else:
|
|
draw_circle(target, lerpf(size.x * 0.012, size.x * 0.026, progress), Color(tint, fade * 0.5))
|
|
for index in 8:
|
|
var angle := TAU * float(index) / 8.0
|
|
var direction := Vector2(cos(angle), sin(angle))
|
|
var inner := target + direction * size.x * 0.010
|
|
var outer := target + direction * size.x * lerpf(0.020, 0.040, progress)
|
|
draw_line(inner, outer, Color(tint, fade * 0.8), 2.0)
|
|
|
|
|
|
func _draw_knockout_mark(progress: float, target: Vector2) -> void:
|
|
var fade := clampf(1.0 - progress * 0.6, 0.0, 1.0)
|
|
draw_arc(target, size.x * 0.036, PI * 0.15, PI * 0.85, 24, Color("#8c3f47", fade), 3.0)
|
|
var glyph := target - Vector2(0.0, size.y * 0.075)
|
|
draw_line(glyph + Vector2(-size.x * 0.008, -size.x * 0.008), glyph + Vector2(size.x * 0.008, size.x * 0.008), Color("#f2c7c1", fade), 3.0)
|
|
draw_line(glyph + Vector2(size.x * 0.008, -size.x * 0.008), glyph + Vector2(-size.x * 0.008, size.x * 0.008), Color("#f2c7c1", fade), 3.0)
|
|
|
|
|
|
func _draw_recovery_beat(progress: float, target: Vector2) -> void:
|
|
var gold := Color("#f0cd83")
|
|
draw_arc(target, lerpf(size.x * 0.012, size.x * 0.032, progress), 0.0, TAU, 30, Color(gold, (1.0 - progress) * 0.75), 2.5)
|
|
for index in 4:
|
|
var offset := (float(index) - 1.5) * size.x * 0.012
|
|
var mote := target + Vector2(offset, -progress * size.y * 0.055)
|
|
draw_circle(mote, maxf(1.6, size.x * 0.0022), Color(gold, (1.0 - progress) * 0.8))
|