547 lines
24 KiB
GDScript
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
|