Files
aetherbound-guild/runtime/campaign/fg2_graybox_layer.gd
T

547 lines
24 KiB
GDScript

class_name ABGFG2GrayboxLayer
extends Control
const AssetRegistry = preload("res://campaign/fg2_asset_registry.gd")
const SKY := Color("101723")
const FAR := Color("253346")
const MID := Color("3d5360")
const GROUND := Color("172429")
const GLASS := Color("7ad2ce")
const ALLY := Color("6ea6d8")
const ALLY_LIGHT := Color("b8d9ec")
const ENEMY := Color("c36354")
const ENEMY_ALT := Color("7da05d")
const INK := Color("111820")
const WHITE := Color("e7edf0")
const GOLD := Color("e4bf67")
var layer_kind := ""
var screen := "market"
var view: Dictionary = {}
var recent_events: Array[Dictionary] = []
var presentation: Dictionary = {}
var pulse := 0.0
var reduced_motion := false
var assets = AssetRegistry.new()
func configure(kind: String) -> void:
layer_kind = kind
mouse_filter = Control.MOUSE_FILTER_IGNORE
clip_contents = false
texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
set_meta("production_media_loaded", assets.is_valid())
func sync(next_screen: String, next_view: Dictionary, events: Array[Dictionary], next_pulse: float, reduced: bool, next_presentation := {}) -> void:
screen = next_screen
view = next_view
recent_events = events.duplicate(true)
presentation = Dictionary(next_presentation).duplicate(true)
pulse = next_pulse
reduced_motion = reduced
queue_redraw()
func _draw() -> void:
match layer_kind:
"background":
_draw_background()
"midground":
_draw_midground()
"actors":
_draw_actors()
"foreground":
_draw_foreground()
"vfx":
_draw_vfx()
func _draw_background() -> void:
var region_id := String(view.get("region_id", ""))
var texture := assets.environment_texture("background", Vector2i(size), region_id)
set_meta("environment_region_id", region_id if texture != null else "fallback")
if texture != null:
draw_texture_rect(texture, Rect2(Vector2.ZERO, size), false)
return
draw_rect(Rect2(Vector2.ZERO, size), SKY)
var horizon := size.y * 0.53
draw_colored_polygon(PackedVector2Array([
Vector2(0, horizon),
Vector2(size.x * 0.10, size.y * 0.27),
Vector2(size.x * 0.22, horizon),
Vector2(size.x * 0.37, size.y * 0.20),
Vector2(size.x * 0.52, horizon),
Vector2(size.x * 0.70, size.y * 0.25),
Vector2(size.x * 0.84, horizon),
Vector2(size.x, size.y * 0.31),
Vector2(size.x, size.y),
Vector2(0, size.y),
]), FAR)
for index in range(9):
var x := int(size.x * 0.04) + index * int(size.x * 0.12)
var height := 6 + (index % 3) * 4
draw_colored_polygon(PackedVector2Array([
Vector2(x, horizon),
Vector2(x + 3, horizon - height),
Vector2(x + 6, horizon),
]), GLASS.darkened(0.38))
func _draw_midground() -> void:
var lane_y := int(size.y * 0.69)
var texture := assets.environment_texture("midground", Vector2i(size), String(view.get("region_id", "")))
if texture != null:
draw_texture_rect(texture, Rect2(Vector2.ZERO, size), false)
else:
draw_colored_polygon(PackedVector2Array([
Vector2(0, lane_y - 5),
Vector2(size.x, lane_y - 12),
Vector2(size.x, size.y),
Vector2(0, size.y),
]), GROUND)
draw_line(Vector2(0, lane_y + 7), Vector2(size.x, lane_y - 1), MID, 4.0)
if screen == "market":
for index in range(3):
var route_x := int(size.x * (0.57 + 0.15 * index))
var risk := index
draw_rect(Rect2(route_x - 7, lane_y - 25 - risk * 3, 14, 25 + risk * 3), MID.lightened(0.08 * index))
draw_rect(Rect2(route_x - 4, lane_y - 22 - risk * 3, 8, 8), GLASS if risk == 0 else GOLD if risk == 1 else ENEMY)
func _draw_actors() -> void:
var fallen_ghosts := 0
set_meta("authored_actor_ids", [])
if screen == "battle":
var battle: Dictionary = view.get("battle", {})
var allies: Array = battle.get("allies", [])
var enemies: Array = battle.get("enemies", [])
var current_ids: Dictionary = {}
for index in range(allies.size()):
current_ids[String(allies[index].get("actor_id", ""))] = true
_draw_actor(_battle_position(index, allies.size(), false), allies[index], false, index)
for index in range(enemies.size()):
current_ids[String(enemies[index].get("actor_id", ""))] = true
_draw_actor(_battle_position(index, enemies.size(), true), enemies[index], true, index)
for actor_id in presentation.get("actor_cues", {}):
if current_ids.has(actor_id):
continue
var cue: Dictionary = presentation.actor_cues[actor_id]
if String(cue.get("side", "")).is_empty():
continue
var enemy := String(cue.get("side", "")) == "enemy"
var slot_index := int(cue.get("slot_index", 0))
var ghost := {"actor_id": actor_id, "identity_id": String(cue.get("identity_id", "")), "hp": 1, "max_hp": 1, "guard": 0}
_draw_actor(_battle_position(slot_index, 1, enemy), ghost, enemy, slot_index, false, true)
if String(cue.get("motion", "")) == "fallen":
fallen_ghosts += 1
else:
var company: Array = view.get("company", [])
var deployed_index := 0
var reserve_index := 0
for actor in company:
if bool(actor.get("deployed", false)):
_draw_actor(Vector2(47 + deployed_index * 44, size.y * 0.63), actor, false, deployed_index)
deployed_index += 1
else:
_draw_actor(Vector2(32 + reserve_index * 25, size.y * 0.66), actor, false, reserve_index, true)
reserve_index += 1
set_meta("fallen_ghost_count", fallen_ghosts)
func _draw_actor(at: Vector2, actor: Dictionary, enemy: bool, index: int, reserve := false, ghost := false) -> void:
var hp := int(actor.get("hp", 1))
var max_hp := maxi(1, int(actor.get("max_hp", 1)))
var alive := hp > 0 or ghost
var body := ENEMY_ALT if enemy and index % 2 == 1 else ENEMY if enemy else ALLY
if not alive:
body = body.darkened(0.55)
var bob := 0
if not reduced_motion and alive:
bob = int(round(sin(pulse * 2.0 + float(index)) * 1.0))
at.y += bob
if reserve:
body = body.darkened(0.25)
var identity_id := String(actor.get("identity_id", actor.get("profession_id", "")))
var actor_id := String(actor.get("actor_id", actor.get("recruit_id", "")))
var cue: Dictionary = presentation.get("actor_cues", {}).get(actor_id, {})
var motion := String(cue.get("motion", ""))
var progress := float(cue.get("progress", 0.0))
at += _motion_offset(motion, progress, enemy)
var authored := assets.actor_frame(identity_id, int(cue.get("frame_index", _identity_frame(identity_id))))
if authored != null:
var authored_ids: Array = get_meta("authored_actor_ids", [])
if identity_id not in authored_ids:
authored_ids.append(identity_id)
set_meta("authored_actor_ids", authored_ids)
var play_size := assets.actor_play_size(identity_id)
var tint := Color(1, 1, 1, 0.45 if reserve else 1.0)
if motion == "hit" and not bool(presentation.get("reduced_flashes", false)):
tint = Color(1.0, 0.72, 0.66, tint.a)
elif motion == "guard":
tint = Color(1.0, 0.94, 0.68, tint.a)
elif motion == "fallen":
tint.a *= clampf(1.0 - progress, 0.18, 1.0)
if not alive:
tint = Color(0.32, 0.32, 0.36, 0.72)
draw_texture_rect(authored, Rect2(at - Vector2(play_size.x * 0.5, play_size.y), play_size), false, tint)
else:
draw_rect(Rect2(at.x - 7, at.y - 17, 14, 16), body)
draw_rect(Rect2(at.x - 5, at.y - 23, 10, 7), ALLY_LIGHT if not enemy else WHITE.darkened(0.18))
draw_rect(Rect2(at.x - 9, at.y - 13, 3, 9), body.lightened(0.12))
draw_rect(Rect2(at.x + 6, at.y - 13, 3, 9), body.darkened(0.18))
draw_rect(Rect2(at.x - 6, at.y - 1, 4, 6), INK)
draw_rect(Rect2(at.x + 2, at.y - 1, 4, 6), INK)
var bar_width := 22.0
if not ghost:
draw_rect(Rect2(at.x - bar_width * 0.5, at.y - 35, bar_width, 3), INK)
draw_rect(Rect2(at.x - bar_width * 0.5 + 1, at.y - 34, (bar_width - 2) * clampf(float(hp) / float(max_hp), 0.0, 1.0), 1), GLASS if not enemy else ENEMY.lightened(0.20))
if not ghost and int(actor.get("guard", 0)) > 0:
draw_arc(Vector2(at.x, at.y - 11), 12, -2.2, 0.6, 12, GOLD, 2.0)
func _draw_foreground() -> void:
var lane_y := int(size.y * 0.69)
var texture := assets.environment_texture("foreground", Vector2i(size), String(view.get("region_id", "")))
if texture != null:
draw_texture_rect(texture, Rect2(Vector2.ZERO, size), false)
else:
draw_rect(Rect2(0, lane_y + 26, size.x, size.y - lane_y - 26), INK.darkened(0.12))
if screen == "battle":
var center_x := int(size.x * 0.51)
draw_line(Vector2(center_x, lane_y - 28), Vector2(center_x, lane_y + 20), GLASS.darkened(0.15), 1.0)
func _draw_vfx() -> void:
set_meta("active_vfx_count", 0)
set_meta("result_vfx_visible", false)
set_meta("recovery_vfx_visible", false)
set_meta("fg3_disclosure_vfx_count", 0)
set_meta("fg4_world_vfx_count", 0)
set_meta("fg4_spark_slot_count", 0)
set_meta("fg4_promoted_spark_count", 0)
set_meta("fg4_pressure_visible", false)
set_meta("fg4_construct_visible", false)
set_meta("fg4_archive_clause_visible", false)
set_meta("fg4_copy_source_visible", false)
set_meta("fg4_redaction_source_visible", false)
set_meta("fg4_omitted_target_visible", false)
set_meta("fg4_replay_order_visible", false)
set_meta("fg4_replay_slot_count", 0)
set_meta("fg4_index_pointer_visible", false)
var battle: Dictionary = view.get("battle", {})
var allies: Array = battle.get("allies", [])
var enemies: Array = battle.get("enemies", [])
_draw_fg3_disclosures(allies, enemies)
_draw_fg4_mechanics(battle, enemies)
for transient in presentation.get("transients", []):
var vfx := String(transient.get("vfx", ""))
if screen != "battle" and vfx != "recovery":
continue
set_meta("active_vfx_count", int(get_meta("active_vfx_count", 0)) + 1)
var progress := float(transient.get("progress", 0.0))
var fade := clampf(1.0 - progress, 0.0, 1.0)
if vfx == "result":
set_meta("result_vfx_visible", true)
draw_line(Vector2(size.x * 0.36, size.y * 0.46), Vector2(size.x * 0.64, size.y * 0.46), GOLD * Color(1, 1, 1, fade), 2.0)
continue
var target_id := String(transient.get("target_id", ""))
if target_id.is_empty():
target_id = String(transient.get("source_id", ""))
var target := _market_actor_position(target_id) if screen != "battle" else _presentation_actor_position(target_id, allies, enemies)
var source := _actor_position(String(transient.get("source_id", "")), allies, enemies)
if target == Vector2.ZERO:
continue
var flash := bool(transient.get("flash", false))
match vfx:
"arrival":
draw_line(target + Vector2(0, -34), target + Vector2(0, 2), GLASS * Color(1, 1, 1, fade), 2.0)
"impact":
draw_line(target + Vector2(-8, -23), target + Vector2(8, -7), ENEMY.lightened(0.30) * Color(1, 1, 1, fade), 2.0)
if flash:
draw_line(target + Vector2(8, -23), target + Vector2(-8, -7), WHITE * Color(1, 1, 1, fade), 1.0)
"heal":
draw_arc(target + Vector2(0, -14), 8 + progress * 8, 0, TAU, 20, GLASS * Color(1, 1, 1, fade), 2.0)
"guard":
draw_arc(target + Vector2(0, -14), 13 + progress * 3, -2.3, 0.7, 20, GOLD * Color(1, 1, 1, fade), 2.0)
"hinder":
draw_line(source + Vector2(0, -14), target + Vector2(0, -14), GLASS.darkened(0.2) * Color(1, 1, 1, fade), 2.0)
"fallen":
draw_line(target + Vector2(-14, -3), target + Vector2(14, -3), ENEMY * Color(1, 1, 1, fade), 3.0)
"recovery":
set_meta("recovery_vfx_visible", true)
draw_arc(target + Vector2(0, -14), 10 + progress * 7, 0, TAU, 20, GLASS * Color(1, 1, 1, fade), 2.0)
draw_line(target + Vector2(0, -30), target + Vector2(0, -4), GOLD * Color(1, 1, 1, fade), 2.0)
func _draw_fg3_disclosures(allies: Array, enemies: Array) -> void:
if screen != "battle" or allies.is_empty():
return
for event in recent_events:
var kind := String(event.get("kind", ""))
var detail: Dictionary = event.get("detail", {})
if kind in ["pulse_linked", "linked_positions_disclosed", "third_pulse_link"]:
var indices: Array = detail.get("indices", [])
if indices.size() < 2:
continue
var points: PackedVector2Array = []
for index_value in indices:
var actor_index := clampi(int(index_value) - 1, 0, allies.size() - 1)
points.append(_battle_position(actor_index, allies.size(), false) + Vector2(0, -28))
for point_index in range(points.size() - 1):
draw_line(points[point_index], points[point_index + 1], GOLD if kind == "third_pulse_link" else GLASS, 2.0)
for point in points:
draw_arc(point, 5, 0, TAU, 16, GOLD if kind == "third_pulse_link" else GLASS, 1.5)
_increment_fg3_vfx()
elif kind == "safe_index_forecast":
var current_index := clampi(int(detail.get("current", 1)) - 1, 0, allies.size() - 1)
var next_index := clampi(int(detail.get("next", 1)) - 1, 0, allies.size() - 1)
var current_point := _battle_position(current_index, allies.size(), false)
var next_point := _battle_position(next_index, allies.size(), false)
draw_line(current_point + Vector2(0, -38), current_point + Vector2(0, 3), GLASS, 2.0)
draw_line(current_point + Vector2(0, -34), next_point + Vector2(0, -34), GOLD, 1.5)
_increment_fg3_vfx()
elif kind == "egg_positions_disclosed":
for index_value in detail.get("positions", []):
var actor_index := clampi(int(index_value) - 1, 0, allies.size() - 1)
var point := _battle_position(actor_index, allies.size(), false)
draw_circle(point + Vector2(0, 2), 4, ENEMY.lightened(0.25))
_increment_fg3_vfx()
elif kind in ["plan_response", "pulse_family_interrupted", "linked_actions_staggered", "eggs_cleared_on_index", "index_shifted_to_area"] and not enemies.is_empty():
var start := _battle_position(0, allies.size(), false) + Vector2(0, -18)
var finish := _battle_position(0, enemies.size(), true) + Vector2(0, -18)
draw_line(start, finish, GOLD, 2.0)
_increment_fg3_vfx()
func _increment_fg3_vfx() -> void:
set_meta("fg3_disclosure_vfx_count", int(get_meta("fg3_disclosure_vfx_count", 0)) + 1)
func _draw_fg4_mechanics(battle: Dictionary, enemies: Array) -> void:
if screen != "battle":
return
var mechanics: Dictionary = battle.get("mechanics", {})
var kind := String(mechanics.get("kind", ""))
if String(view.get("region_id", "")) == "RG-04" and kind == "quillroot_archive":
_draw_quillroot_archive(mechanics, enemies)
elif String(view.get("region_id", "")) == "RG-03" and kind in ["valve_heat", "furnace_pressure"]:
_draw_fg4_valve(mechanics)
elif String(view.get("region_id", "")) == "RG-03" and kind == "spark_repeat":
_draw_fg4_sparks(mechanics, enemies)
func _draw_quillroot_archive(mechanics: Dictionary, enemies: Array) -> void:
var lane_y := size.y * 0.69
var archive_origin := Vector2(size.x * 0.53, lane_y - 50)
if bool(mechanics.get("clause_disclosed", false)):
set_meta("fg4_archive_clause_visible", true)
draw_rect(Rect2(archive_origin.x - 17, archive_origin.y - 7, 34, 18), Color("26363a"))
draw_rect(Rect2(archive_origin.x - 14, archive_origin.y - 4, 28, 12), Color("ead7a1"))
var clause_kind := String(mechanics.get("clause_kind", ""))
if clause_kind == "repeat":
draw_arc(archive_origin + Vector2(0, 2), 7, -2.7, 2.7, 18, Color("783f46"), 2.0)
draw_line(archive_origin + Vector2(-8, -1), archive_origin + Vector2(-4, -5), Color("783f46"), 2.0)
elif clause_kind == "alternate":
draw_line(archive_origin + Vector2(-8, 5), archive_origin + Vector2(0, -2), Color("783f46"), 2.0)
draw_line(archive_origin + Vector2(0, -2), archive_origin + Vector2(8, 5), Color("783f46"), 2.0)
else:
draw_rect(Rect2(archive_origin.x - 6, archive_origin.y - 1, 12, 8), Color("783f46"), false, 2.0)
draw_arc(archive_origin + Vector2(0, -1), 4, PI, TAU, 12, Color("783f46"), 2.0)
if bool(mechanics.get("copy_disclosed", false)):
set_meta("fg4_copy_source_visible", true)
var copy_source := archive_origin + Vector2(30, 2)
var copy_target := copy_source + Vector2(34, 0)
draw_circle(copy_source, 6, Color("86b58a"))
draw_circle(copy_target, 6, Color("d98b67"))
draw_line(copy_source + Vector2(7, 0), copy_target - Vector2(7, 0), GOLD, 2.0)
draw_line(copy_target - Vector2(10, -3), copy_target - Vector2(6, 0), GOLD, 2.0)
draw_line(copy_target - Vector2(10, 3), copy_target - Vector2(6, 0), GOLD, 2.0)
if bool(mechanics.get("redaction_source_visible", false)):
set_meta("fg4_redaction_source_visible", true)
var redaction_source := archive_origin + Vector2(-37, 26)
draw_rect(Rect2(redaction_source.x - 11, redaction_source.y - 5, 22, 10), Color("c4d49a"))
draw_line(redaction_source + Vector2(-12, 8), redaction_source + Vector2(12, 8), Color("b65a54"), 2.0)
if bool(mechanics.get("omitted_target_visible", false)):
set_meta("fg4_omitted_target_visible", true)
var omitted := archive_origin + Vector2(-5, 26)
draw_rect(Rect2(omitted.x - 10, omitted.y - 5, 20, 10), Color("ead7a1"), false, 2.0)
draw_line(omitted + Vector2(-9, 6), omitted + Vector2(9, -6), Color("b65a54"), 2.0)
if bool(mechanics.get("replay_order_visible", false)):
set_meta("fg4_replay_order_visible", true)
var replay_order: Array = mechanics.get("replay_order", [])
var replay_count := clampi(replay_order.size(), 1, 6)
set_meta("fg4_replay_slot_count", replay_count)
var replay_start := archive_origin + Vector2(27, 25)
for index in range(replay_count):
var point := replay_start + Vector2(index * 10, 0)
draw_circle(point, 3.0, Color("86b58a") if index % 2 == 0 else Color("d98b67"))
if index > 0:
draw_line(point - Vector2(7, 0), point - Vector2(3, 0), GOLD, 1.0)
if bool(mechanics.get("pointer_visible", false)) and not enemies.is_empty():
set_meta("fg4_index_pointer_visible", true)
var boss_point := _battle_position(0, enemies.size(), true) + Vector2(0, -40)
var pointer_angle := fmod(pulse * 2.5, TAU) if not reduced_motion else -PI * 0.5
draw_arc(boss_point, 18, 0, TAU, 24, Color("c4d49a"), 2.0)
draw_line(boss_point, boss_point + Vector2(cos(pointer_angle), sin(pointer_angle)) * 14.0, Color("b65a54"), 2.0)
if _has_recent_event([
"archive_clause_disclosed", "copy_source_disclosed", "archive_clause_obeyed",
"archive_clause_broken", "redaction_source_disclosed", "target_clause_omitted",
"replay_order_disclosed", "index_pointer_disclosed", "action_count_steered",
"index_pointer_interrupted", "safe_rule_seeded", "replay_order_changed",
"dangerous_replay_interrupted",
]):
draw_arc(archive_origin, 24 + fmod(pulse * 14.0, 7.0), 0, TAU, 24, Color("86b58a", 0.78), 1.5)
_increment_fg4_vfx()
func _draw_fg4_valve(mechanics: Dictionary) -> void:
if not bool(mechanics.get("valve_disclosed", false)):
return
var lane_y := size.y * 0.69
var center := Vector2(size.x * 0.55, lane_y - 4)
set_meta("fg4_pressure_visible", true)
draw_line(center + Vector2(0, -36), center + Vector2(0, 5), Color("6f5840"), 3.0)
draw_arc(center + Vector2(0, -20), 11, 0, TAU, 20, GOLD, 2.0)
for spoke in range(4):
var angle := float(spoke) * PI * 0.5
draw_line(center + Vector2(0, -20), center + Vector2(cos(angle), sin(angle)) * 9.0 + Vector2(0, -20), GOLD.darkened(0.12), 1.5)
var rupture_tick := int(mechanics.get("rupture_tick", mechanics.get("valve", {}).get("resolve_tick", 0)))
var heat_ratio := clampf(float(rupture_tick) / 80.0, 0.1, 1.0)
draw_rect(Rect2(center.x - 17, center.y - 44, 34, 4), INK)
draw_rect(Rect2(center.x - 16, center.y - 43, 32 * heat_ratio, 2), ENEMY.lightened(0.18))
if bool(mechanics.get("construct_visible", false)):
set_meta("fg4_construct_visible", true)
var construct := center + Vector2(32, 0)
var removed := bool(mechanics.get("construct_removed", false))
draw_rect(Rect2(construct.x - 8, construct.y - 19, 16, 18), Color("5b4936", 0.35 if removed else 1.0))
draw_line(construct + Vector2(-6, -21), construct + Vector2(6, -21), GOLD * Color(1, 1, 1, 0.35 if removed else 1.0), 2.0)
if bool(mechanics.get("delayed_heat_visible", false)):
draw_arc(center + Vector2(-30, -21), 8 + sin(pulse * 5.0) * 1.5, PI, TAU, 14, ENEMY.lightened(0.25), 2.0)
if bool(mechanics.get("side_swap_visible", false)):
draw_line(center + Vector2(-26, -31), center + Vector2(26, -31), GLASS, 1.5)
draw_line(center + Vector2(21, -34), center + Vector2(26, -31), GLASS, 1.5)
draw_line(center + Vector2(21, -28), center + Vector2(26, -31), GLASS, 1.5)
if _has_recent_event(["pressure_valve_disclosed", "weak_side_vented", "strong_side_burst", "construct_state_changed", "delayed_heat_forecast"]):
draw_arc(center + Vector2(0, -20), 14 + fmod(pulse * 18.0, 8.0), 0, TAU, 24, GOLD * Color(1, 1, 1, 0.75), 1.5)
_increment_fg4_vfx()
func _draw_fg4_sparks(mechanics: Dictionary, enemies: Array) -> void:
if not bool(mechanics.get("repeat_disclosed", false)):
return
var spark_texture := assets.prop_frame("spark")
var spark_size := assets.prop_play_size("spark")
var positions: Array[Vector2] = []
var boss_mode := String(battle_identity(enemies)) == "BO-R3-02"
if boss_mode:
var center := _battle_position(0, maxi(1, enemies.size()), true) + Vector2(0, -34)
for index in range(8):
var angle := -PI * 0.5 + TAU * float(index) / 8.0
positions.append(center + Vector2(cos(angle) * 43.0, sin(angle) * 28.0))
else:
var start := Vector2(size.x * 0.62, size.y * 0.41)
for index in range(8):
positions.append(start + Vector2((index % 4) * 20, (index / 4) * 20))
var next_index := clampi(int(mechanics.get("next_spark_index", 1)) - 1, 0, 7)
var promoted := bool(mechanics.get("promoted", false))
var removed := bool(mechanics.get("removed", false))
for index in range(8):
var point := positions[index]
var selected := index == next_index
var scale := 1.30 if selected and promoted else 1.0
var alpha := 0.25 if selected and removed else 1.0
if spark_texture != null:
var draw_size := spark_size * scale
draw_texture_rect(spark_texture, Rect2(point - draw_size * 0.5, draw_size), false, Color(1, 1, 1, alpha))
else:
draw_circle(point, 4.0 * scale, ENEMY.lightened(0.25) * Color(1, 1, 1, alpha))
if selected:
draw_arc(point, 7.0 * scale, 0, TAU, 16, GOLD * Color(1, 1, 1, alpha), 1.5)
set_meta("fg4_spark_slot_count", positions.size())
set_meta("fg4_promoted_spark_count", 1 if promoted else 0)
var threshold := maxi(2, int(mechanics.get("repeat_threshold", 3)))
var counter_origin := Vector2(size.x * 0.74, size.y * 0.28)
for index in range(threshold):
var fill := promoted or (not bool(mechanics.get("reset", false)) and index < threshold - 1)
draw_rect(Rect2(counter_origin.x + index * 9, counter_origin.y, 7, 4), GOLD if fill else INK)
if _has_recent_event(["repeat_counter_disclosed", "next_spark_disclosed", "spark_promoted", "spark_removed", "repeat_counter_reset"]):
draw_arc(positions[next_index], 10 + fmod(pulse * 16.0, 6.0), 0, TAU, 20, GLASS if removed else GOLD, 1.5)
_increment_fg4_vfx()
func battle_identity(enemies: Array) -> String:
return String(enemies[0].get("identity_id", "")) if not enemies.is_empty() else ""
func _has_recent_event(kinds: Array[String]) -> bool:
for event in recent_events:
if String(event.get("kind", "")) in kinds:
return true
return false
func _increment_fg4_vfx() -> void:
set_meta("fg4_world_vfx_count", int(get_meta("fg4_world_vfx_count", 0)) + 1)
func _battle_position(index: int, count: int, enemy: bool) -> Vector2:
var lane_y := size.y * 0.69
if enemy:
return Vector2(size.x * 0.66 + index * 34, lane_y + (index % 2) * 5)
return Vector2(size.x * 0.39 - index * 38, lane_y + (index % 2) * 4)
func _identity_frame(identity_id: String) -> int:
if identity_id == "EN-R1-08":
return 1
return 0
func _motion_offset(motion: String, progress: float, enemy: bool) -> Vector2:
var direction := -1.0 if enemy else 1.0
match motion:
"entry":
return Vector2(direction * (1.0 - progress) * 18.0, 0)
"attack":
return Vector2(direction * sin(progress * PI) * 6.0, 0)
"hit":
return Vector2(-direction * sin(progress * PI) * 4.0, 0)
"fallen":
return Vector2(0, progress * 8.0)
return Vector2.ZERO
func _actor_position(actor_id: String, allies: Array, enemies: Array) -> Vector2:
for index in range(allies.size()):
if String(allies[index].get("actor_id", "")) == actor_id:
return _battle_position(index, allies.size(), false)
for index in range(enemies.size()):
if String(enemies[index].get("actor_id", "")) == actor_id:
return _battle_position(index, enemies.size(), true)
return Vector2.ZERO
func _presentation_actor_position(actor_id: String, allies: Array, enemies: Array) -> Vector2:
var current := _actor_position(actor_id, allies, enemies)
if current != Vector2.ZERO:
return current
var cue: Dictionary = presentation.get("actor_cues", {}).get(actor_id, {})
if cue.is_empty():
return Vector2.ZERO
return _battle_position(int(cue.get("slot_index", 0)), 1, String(cue.get("side", "")) == "enemy")
func _market_actor_position(actor_id: String) -> Vector2:
var deployed_index := 0
var reserve_index := 0
for actor in view.get("company", []):
if String(actor.get("recruit_id", "")) == actor_id:
return Vector2(47 + deployed_index * 44, size.y * 0.63) if bool(actor.get("deployed", false)) else Vector2(32 + reserve_index * 25, size.y * 0.66)
if bool(actor.get("deployed", false)):
deployed_index += 1
else:
reserve_index += 1
return Vector2.ZERO