346 lines
13 KiB
GDScript
346 lines
13 KiB
GDScript
class_name P101MotionPresenter
|
|
extends RefCounted
|
|
|
|
const MAX_EFFECTS := 8
|
|
const ACTION_DURATION := 0.66
|
|
const HIT_DURATION := 0.46
|
|
const PULL_DURATION := 0.58
|
|
const TIDE_DURATION := 0.72
|
|
const WAVE_DURATION := 0.70
|
|
const EFFECT_DURATION := 0.78
|
|
const HIT_STOP_DURATION := 0.07
|
|
|
|
var actor_order: Array = []
|
|
var current_safe_index := 0
|
|
var initial_safe_index := 0
|
|
var visible_enemy_ids: Array[String] = []
|
|
var actor_actions: Dictionary = {}
|
|
var enemy_actions: Dictionary = {}
|
|
var fallen_ids: Dictionary = {}
|
|
var effects: Array[Dictionary] = []
|
|
var pull_from_slots: Dictionary = {}
|
|
var pull_started := -100.0
|
|
var tide_offset_from := 0.0
|
|
var tide_offset_to := 0.0
|
|
var tide_started := -100.0
|
|
var healing_denied_target := ""
|
|
var healing_denial_source := ""
|
|
var hindrance_target := ""
|
|
var mud_index := -1
|
|
var terminal_outcome := ""
|
|
var hit_stop_until := -100.0
|
|
var trauma_started := -100.0
|
|
var trauma_strength := 0.0
|
|
|
|
|
|
func reset(initial_order: Array, safe_index: int) -> void:
|
|
actor_order = initial_order.duplicate()
|
|
current_safe_index = safe_index
|
|
initial_safe_index = safe_index
|
|
visible_enemy_ids.clear()
|
|
actor_actions.clear()
|
|
enemy_actions.clear()
|
|
fallen_ids.clear()
|
|
effects.clear()
|
|
pull_from_slots.clear()
|
|
pull_started = -100.0
|
|
tide_offset_from = 0.0
|
|
tide_offset_to = 0.0
|
|
tide_started = -100.0
|
|
healing_denied_target = ""
|
|
healing_denial_source = ""
|
|
hindrance_target = ""
|
|
mud_index = -1
|
|
terminal_outcome = ""
|
|
hit_stop_until = -100.0
|
|
trauma_started = -100.0
|
|
trauma_strength = 0.0
|
|
|
|
|
|
func consume(event: Dictionary, now: float) -> Array:
|
|
var kind := String(event.get("kind", ""))
|
|
_add_effect(event, now)
|
|
match kind:
|
|
"wave_entered":
|
|
for enemy_id in event.get("enemy_ids", []):
|
|
var value := String(enemy_id)
|
|
if value not in visible_enemy_ids:
|
|
visible_enemy_ids.append(value)
|
|
enemy_actions[value] = {"kind": "wave_enter", "started": now, "duration": WAVE_DURATION}
|
|
"healing_denial_attached":
|
|
healing_denied_target = String(event.get("target_id", ""))
|
|
healing_denial_source = String(event.get("enemy_id", ""))
|
|
_set_enemy_action(healing_denial_source, "telegraph", now, ACTION_DURATION)
|
|
"healing_converted_to_enemy_guard", "healing_applied":
|
|
_set_actor_action(String(event.get("source_id", "")), "heal", now, ACTION_DURATION)
|
|
_set_actor_action(String(event.get("target_id", "")), "blocked" if kind == "healing_converted_to_enemy_guard" else "healed", now, HIT_DURATION)
|
|
"timed_hindrance_applied":
|
|
hindrance_target = String(event.get("target_id", ""))
|
|
_set_enemy_action(String(event.get("enemy_id", "")), "telegraph", now, ACTION_DURATION)
|
|
"hindrance_expired":
|
|
hindrance_target = ""
|
|
"pull_resisted":
|
|
_set_actor_action(String(event.get("target_id", "")), "resist", now, HIT_DURATION)
|
|
_set_enemy_action(String(event.get("enemy_id", "")), "attack", now, ACTION_DURATION)
|
|
_add_trauma(now, 0.22)
|
|
"pull_committed":
|
|
pull_from_slots = _slot_map(actor_order)
|
|
actor_order = Array(event.get("order_after", actor_order)).duplicate()
|
|
pull_started = now
|
|
_set_actor_action(String(event.get("target_id", "")), "pulled", now, PULL_DURATION)
|
|
_set_enemy_action(String(event.get("enemy_id", "")), "attack", now, ACTION_DURATION)
|
|
_add_trauma(now, 0.36)
|
|
"indexed_bank_marked":
|
|
mud_index = int(event.get("position_index", -1))
|
|
"indexed_mud_created":
|
|
mud_index = int(event.get("crossed_index", -1))
|
|
_set_actor_action(String(event.get("target_id", "")), "hindered", now, HIT_DURATION)
|
|
"tide_shift_warning":
|
|
_add_trauma(now, 0.12)
|
|
"tide_followed":
|
|
tide_offset_from = tide_offset_to
|
|
var next_safe := int(event.get("safe_index", current_safe_index))
|
|
tide_offset_to += float(next_safe - current_safe_index) * 38.0
|
|
current_safe_index = next_safe
|
|
tide_started = now
|
|
_add_trauma(now, 0.20)
|
|
"tide_shifted_while_holding":
|
|
current_safe_index = int(event.get("safe_index", current_safe_index))
|
|
for actor_id in event.get("wet_targets", []):
|
|
_set_actor_action(String(actor_id), "wet", now, HIT_DURATION)
|
|
_add_trauma(now, 0.52)
|
|
"allied_basic":
|
|
var source_id := String(event.get("actor_id", ""))
|
|
_set_actor_action(source_id, "attack", now, ACTION_DURATION)
|
|
var enemy_id := front_visible_enemy()
|
|
_set_enemy_action(enemy_id, "hit", now + 0.22, HIT_DURATION)
|
|
"enemy_basic_damage":
|
|
_set_enemy_action(front_visible_enemy(), "attack", now, ACTION_DURATION)
|
|
_set_actor_action(String(event.get("target_id", "")), "hit", now + 0.20, HIT_DURATION)
|
|
"authoritative_damage":
|
|
_set_actor_action(String(event.get("target_id", "")), "hit", now, HIT_DURATION)
|
|
hit_stop_until = maxf(hit_stop_until, now + HIT_STOP_DURATION)
|
|
_add_trauma(now, 0.44)
|
|
"recruit_fallen":
|
|
var recruit_id := String(event.get("recruit_id", ""))
|
|
fallen_ids[recruit_id] = now
|
|
actor_actions.erase(recruit_id)
|
|
_add_trauma(now, 0.68)
|
|
"battle_terminal":
|
|
terminal_outcome = String(event.get("outcome", ""))
|
|
healing_denied_target = ""
|
|
healing_denial_source = ""
|
|
hindrance_target = ""
|
|
mud_index = -1
|
|
if terminal_outcome == "victory":
|
|
for enemy_id in visible_enemy_ids:
|
|
enemy_actions[enemy_id] = {"kind": "death", "started": now, "duration": 0.9}
|
|
_add_trauma(now, 0.30 if terminal_outcome == "victory" else 0.58)
|
|
prune(now)
|
|
return actor_order.duplicate()
|
|
|
|
|
|
func actor_visual(actor_id: String, current_slot: int, slot_step: float, now: float, reduced_motion: bool, low_power: bool) -> Dictionary:
|
|
var offset := Vector2.ZERO
|
|
var scale := Vector2.ONE
|
|
var rotation := 0.0
|
|
var alpha := 1.0
|
|
var state := "idle"
|
|
var animation_time := now
|
|
if not low_power:
|
|
offset.y += sin(now * 4.4 + float(abs(actor_id.hash()) % 9)) * (0.8 if reduced_motion else 2.0)
|
|
if now - pull_started < PULL_DURATION and pull_from_slots.has(actor_id):
|
|
var from_slot := int(pull_from_slots[actor_id])
|
|
var progress := _smoothstep(clampf((now - pull_started) / PULL_DURATION, 0.0, 1.0))
|
|
offset.x += lerpf(float(from_slot - current_slot) * slot_step, 0.0, progress)
|
|
offset.y -= sin(progress * PI) * (3.0 if reduced_motion else 14.0)
|
|
var tide_progress := clampf((now - tide_started) / TIDE_DURATION, 0.0, 1.0)
|
|
var tide_offset := lerpf(tide_offset_from, tide_offset_to, _smoothstep(tide_progress))
|
|
offset.x += tide_offset * (0.35 if reduced_motion else 1.0)
|
|
if fallen_ids.has(actor_id):
|
|
var fallen_progress := clampf((now - float(fallen_ids[actor_id])) / 0.62, 0.0, 1.0)
|
|
rotation = lerpf(0.0, -1.18, _smoothstep(fallen_progress)) if not reduced_motion else 0.0
|
|
offset.y += lerpf(0.0, 22.0, _smoothstep(fallen_progress))
|
|
alpha = lerpf(1.0, 0.24, fallen_progress)
|
|
state = "fallen"
|
|
return _visual(offset, scale, rotation, alpha, state, fallen_progress)
|
|
var action: Dictionary = actor_actions.get(actor_id, {})
|
|
if not action.is_empty():
|
|
var started := float(action.started)
|
|
var duration := float(action.duration)
|
|
var progress := clampf((now - started) / duration, 0.0, 1.0)
|
|
if now >= started and progress < 1.0:
|
|
var kind := String(action.kind)
|
|
animation_time = maxf(0.0, now - started)
|
|
match kind:
|
|
"attack":
|
|
state = "attack"
|
|
offset.x += _contact_curve(progress, 4.0, 24.0, reduced_motion)
|
|
rotation = sin(progress * PI) * (0.0 if reduced_motion else 0.08)
|
|
"heal", "healed":
|
|
state = "heal"
|
|
scale = Vector2.ONE * (1.0 + sin(progress * PI) * (0.03 if reduced_motion else 0.10))
|
|
offset.y -= sin(progress * PI) * (2.0 if reduced_motion else 8.0)
|
|
"hit", "blocked", "wet", "hindered":
|
|
state = "hit"
|
|
offset.x -= sin(progress * PI) * (3.0 if reduced_motion else 11.0)
|
|
offset.y += sin(progress * PI * 3.0) * (1.0 if reduced_motion else 3.0)
|
|
scale = Vector2(1.0 + 0.06 * sin(progress * PI), 1.0 - 0.05 * sin(progress * PI))
|
|
"resist":
|
|
state = "guard"
|
|
offset.x -= sin(progress * PI) * (2.0 if reduced_motion else 7.0)
|
|
"pulled":
|
|
state = "hit"
|
|
if terminal_outcome == "victory" and state == "idle":
|
|
state = "victory"
|
|
scale = Vector2.ONE * 1.04
|
|
if not low_power:
|
|
offset.y -= abs(sin(now * 3.4 + float(abs(actor_id.hash()) % 7))) * (1.0 if reduced_motion else 5.0)
|
|
return _visual(offset, scale, rotation, alpha, state, animation_time)
|
|
|
|
|
|
func enemy_visual(enemy_id: String, now: float, reduced_motion: bool, low_power: bool) -> Dictionary:
|
|
if enemy_id not in visible_enemy_ids:
|
|
return _visual(Vector2(90, 0), Vector2.ONE, 0.0, 0.0, "idle", now)
|
|
var offset := Vector2.ZERO
|
|
var scale := Vector2.ONE
|
|
var alpha := 1.0
|
|
var state := "idle"
|
|
var animation_time := now
|
|
if not low_power:
|
|
offset.y += sin(now * 4.0 + float(abs(enemy_id.hash()) % 11)) * (0.8 if reduced_motion else 2.2)
|
|
var action: Dictionary = enemy_actions.get(enemy_id, {})
|
|
if not action.is_empty():
|
|
var started := float(action.started)
|
|
var duration := float(action.duration)
|
|
var progress := clampf((now - started) / duration, 0.0, 1.0)
|
|
if now >= started and (progress < 1.0 or String(action.kind) == "death"):
|
|
animation_time = maxf(0.0, now - started)
|
|
match String(action.kind):
|
|
"wave_enter":
|
|
state = "advance"
|
|
offset.x += lerpf(90.0 if not reduced_motion else 16.0, 0.0, _smoothstep(progress))
|
|
alpha = clampf(progress * 1.8, 0.0, 1.0)
|
|
"telegraph":
|
|
state = "telegraph"
|
|
scale = Vector2.ONE * (1.0 + sin(progress * PI) * (0.03 if reduced_motion else 0.09))
|
|
"attack":
|
|
state = "attack"
|
|
offset.x -= _contact_curve(progress, 4.0, 22.0, reduced_motion)
|
|
"hit":
|
|
state = "hit"
|
|
offset.x += sin(progress * PI) * (3.0 if reduced_motion else 10.0)
|
|
"death":
|
|
state = "death"
|
|
alpha = lerpf(1.0, 0.16, _smoothstep(progress))
|
|
offset.y += lerpf(0.0, 18.0, _smoothstep(progress))
|
|
return _visual(offset, scale, 0.0, alpha, state, animation_time)
|
|
|
|
|
|
func front_visible_enemy() -> String:
|
|
return "" if visible_enemy_ids.is_empty() else visible_enemy_ids[0]
|
|
|
|
|
|
func active_effects(now: float) -> Array[Dictionary]:
|
|
prune(now)
|
|
return effects.duplicate(true)
|
|
|
|
|
|
func playback_held(now: float, reduced_motion: bool) -> bool:
|
|
return not reduced_motion and now < hit_stop_until
|
|
|
|
|
|
func camera_offset(now: float, reduced_motion: bool, low_power: bool) -> Vector2:
|
|
if low_power or reduced_motion or trauma_strength <= 0.0:
|
|
return Vector2.ZERO
|
|
var age := now - trauma_started
|
|
if age < 0.0 or age >= 0.42:
|
|
return Vector2.ZERO
|
|
var strength := trauma_strength * pow(1.0 - age / 0.42, 2.0)
|
|
return Vector2(sin(age * 71.0) * 7.0 * strength, sin(age * 53.0) * 4.0 * strength)
|
|
|
|
|
|
func transient_resting(now: float) -> bool:
|
|
prune(now)
|
|
if not effects.is_empty() or now < hit_stop_until:
|
|
return false
|
|
for action in actor_actions.values():
|
|
if now < float(action.started) + float(action.duration):
|
|
return false
|
|
for action in enemy_actions.values():
|
|
if String(action.kind) != "death" and now < float(action.started) + float(action.duration):
|
|
return false
|
|
return true
|
|
|
|
|
|
func prune(now: float) -> void:
|
|
effects = effects.filter(func(effect: Dictionary): return now - float(effect.started) < float(effect.duration))
|
|
for actor_id in actor_actions.keys():
|
|
var action: Dictionary = actor_actions[actor_id]
|
|
if now >= float(action.started) + float(action.duration):
|
|
actor_actions.erase(actor_id)
|
|
for enemy_id in enemy_actions.keys():
|
|
var action: Dictionary = enemy_actions[enemy_id]
|
|
if String(action.kind) != "death" and now >= float(action.started) + float(action.duration):
|
|
enemy_actions.erase(enemy_id)
|
|
|
|
|
|
func _set_actor_action(actor_id: String, kind: String, now: float, duration: float) -> void:
|
|
if actor_id.is_empty() or fallen_ids.has(actor_id):
|
|
return
|
|
actor_actions[actor_id] = {"kind": kind, "started": now, "duration": duration}
|
|
|
|
|
|
func _set_enemy_action(enemy_id: String, kind: String, now: float, duration: float) -> void:
|
|
if enemy_id.is_empty():
|
|
return
|
|
enemy_actions[enemy_id] = {"kind": kind, "started": now, "duration": duration}
|
|
|
|
|
|
func _add_effect(event: Dictionary, now: float) -> void:
|
|
var kind := String(event.get("kind", ""))
|
|
if kind in ["battle_started", "wave_entered", "allied_basic", "enemy_basic_damage", "healing_denial_attached", "healing_converted_to_enemy_guard", "healing_applied", "pull_committed", "pull_resisted", "indexed_mud_created", "tide_shift_warning", "tide_followed", "tide_shifted_while_holding", "authoritative_damage", "recruit_fallen", "battle_terminal"]:
|
|
var effect := event.duplicate(true)
|
|
effect["started"] = now
|
|
effect["duration"] = 1.05 if kind in ["tide_shift_warning", "battle_terminal"] else EFFECT_DURATION
|
|
effects.append(effect)
|
|
while effects.size() > MAX_EFFECTS:
|
|
effects.pop_front()
|
|
|
|
|
|
func _add_trauma(now: float, amount: float) -> void:
|
|
trauma_started = now
|
|
trauma_strength = clampf(maxf(trauma_strength * 0.45, amount), 0.0, 1.0)
|
|
|
|
|
|
func _slot_map(order: Array) -> Dictionary:
|
|
var result := {}
|
|
for index in range(order.size()):
|
|
result[String(order[index])] = index
|
|
return result
|
|
|
|
|
|
func _contact_curve(progress: float, anticipation: float, contact: float, reduced_motion: bool) -> float:
|
|
var scale := 0.30 if reduced_motion else 1.0
|
|
if progress < 0.18:
|
|
return lerpf(0.0, -anticipation * scale, _smoothstep(progress / 0.18))
|
|
if progress < 0.56:
|
|
return lerpf(-anticipation * scale, contact * scale, _smoothstep((progress - 0.18) / 0.38))
|
|
return lerpf(contact * scale, 0.0, _smoothstep((progress - 0.56) / 0.44))
|
|
|
|
|
|
func _smoothstep(value: float) -> float:
|
|
var bounded := clampf(value, 0.0, 1.0)
|
|
return bounded * bounded * (3.0 - 2.0 * bounded)
|
|
|
|
|
|
func _visual(offset: Vector2, scale: Vector2, rotation: float, alpha: float, state: String, animation_time: float) -> Dictionary:
|
|
return {
|
|
"offset": offset,
|
|
"scale": scale,
|
|
"rotation": rotation,
|
|
"alpha": alpha,
|
|
"state": state,
|
|
"animation_time": animation_time,
|
|
}
|