Initial project sync
This commit is contained in:
@@ -0,0 +1,665 @@
|
||||
class_name Minion
|
||||
extends Character
|
||||
|
||||
const DEFAULT_HEADING := Vector2.LEFT
|
||||
const DEFAULT_ANIMATION_FPS := 12.0
|
||||
const VISIBLE_FEET_ANCHOR_Y := -1.0
|
||||
const PHASE_SWAP_MOVEMENT_HOLD_FRAMES := 2
|
||||
|
||||
const FORM_SPECS := {
|
||||
&"jin_zhan_1": {
|
||||
"native_facing": Vector2.LEFT,
|
||||
"idle": {"path": "res://assets/art/characters/minions/jin_zhan_1/idle.png", "frames": 6, "fps": 8.0, "loop": true},
|
||||
"death": {"path": "res://assets/art/characters/minions/jin_zhan_1/death.png", "frames": 6, "fps": 10.0, "loop": false},
|
||||
"dash": {"path": "res://assets/art/characters/minions/jin_zhan_1/dash.png", "frames": 6, "fps": 10.0, "loop": true},
|
||||
"attacks": [
|
||||
{"name": &"jin_zhan_1_attack", "path": "res://assets/art/characters/minions/jin_zhan_1/attack.png", "frames": 6, "fps": 12.0, "loop": false},
|
||||
],
|
||||
"actions": [&"minion_jin_zhan_1_attack"],
|
||||
"sprite_offset": Vector2(-28.0, -48.0),
|
||||
"projectile_height": 42.0,
|
||||
},
|
||||
&"jin_zhan_3": {
|
||||
"native_facing": Vector2.RIGHT,
|
||||
"idle": {"path": "res://assets/art/characters/minions/jin_zhan_3/idle.png", "frames": 7, "fps": 8.0, "loop": true},
|
||||
"death": {"path": "res://assets/art/characters/minions/jin_zhan_3/death.png", "frames": 12, "fps": 12.0, "loop": false},
|
||||
"dash": {"path": "res://assets/art/characters/minions/jin_zhan_3/dash.png", "frames": 5, "fps": 10.0, "loop": true},
|
||||
"attacks": [
|
||||
{"name": &"jin_zhan_3_attack_1", "path": "res://assets/art/characters/minions/jin_zhan_3/attack_1.png", "frames": 6, "fps": 12.0, "loop": false},
|
||||
{"name": &"jin_zhan_3_attack_2", "path": "res://assets/art/characters/minions/jin_zhan_3/attack_2.png", "frames": 5, "fps": 12.0, "loop": false},
|
||||
{"name": &"jin_zhan_3_attack_3", "path": "res://assets/art/characters/minions/jin_zhan_3/attack_3.png", "frames": 6, "fps": 12.0, "loop": false},
|
||||
],
|
||||
"actions": [&"minion_jin_zhan_3_attack_1", &"minion_jin_zhan_3_attack_2", &"minion_jin_zhan_3_attack_3"],
|
||||
"sprite_offset": Vector2(-47.0, -62.0),
|
||||
"projectile_height": 44.0,
|
||||
},
|
||||
&"yuan_cheng_1": {
|
||||
# 素材原生朝右(眼睛与挥击弧线都在右侧);朝左时再做镜像。
|
||||
"native_facing": Vector2.RIGHT,
|
||||
"idle": {"path": "res://assets/art/characters/minions/yuan_cheng_1/idle.png", "frames": 8, "fps": 8.0, "loop": true},
|
||||
"death": {"path": "res://assets/art/characters/minions/yuan_cheng_1/death.png", "frames": 7, "fps": 10.0, "loop": false},
|
||||
"dash": {"path": "res://assets/art/characters/minions/yuan_cheng_1/dash.png", "frames": 6, "fps": 10.0, "loop": true},
|
||||
"attacks": [
|
||||
{"name": &"yuan_cheng_1_attack_1", "path": "res://assets/art/characters/minions/yuan_cheng_1/attack_1.png", "frames": 6, "fps": 12.0, "loop": false},
|
||||
{"name": &"yuan_cheng_1_attack_2", "path": "res://assets/art/characters/minions/yuan_cheng_1/attack_2.png", "frames": 5, "fps": 12.0, "loop": false},
|
||||
],
|
||||
"actions": [&"minion_yuan_cheng_1_attack_1", &"minion_yuan_cheng_1_attack_2"],
|
||||
"sprite_offset": Vector2(-39.0, -48.0),
|
||||
# 可见脚底在 actor 原点上方约 40px,出手点再高约 46px(甩击释放高度),
|
||||
# 两个远程形态共用同一条弹道,切相位不跳变。
|
||||
"projectile_height": 86.0,
|
||||
},
|
||||
&"yuan_cheng_2": {
|
||||
"native_facing": Vector2.RIGHT,
|
||||
"visual_scale": 1.12,
|
||||
"idle": {"path": "res://assets/art/characters/minions/yuan_cheng_2/idle.png", "frames": 10, "fps": 8.0, "loop": true},
|
||||
"death": {"path": "res://assets/art/characters/minions/yuan_cheng_2/death.png", "frames": 18, "fps": 14.0, "loop": false},
|
||||
"dash": {"path": "res://assets/art/characters/minions/yuan_cheng_2/dash.png", "frames": 3, "fps": 10.0, "loop": true},
|
||||
"attacks": [
|
||||
{"name": &"yuan_cheng_2_attack", "path": "res://assets/art/characters/minions/yuan_cheng_2/attack.png", "frames": 13, "fps": 14.0, "loop": false},
|
||||
],
|
||||
"actions": [&"minion_yuan_cheng_2_attack"],
|
||||
"sprite_offset": Vector2(-74.0, -97.0),
|
||||
# 对齐法杖顶端法球中心(脚底上方约 49px),并与小女孩共用同一条弹道。
|
||||
"projectile_height": 86.0,
|
||||
},
|
||||
}
|
||||
|
||||
@export var max_health := 650
|
||||
@export var current_health := 650
|
||||
@export var past_form_id: StringName = &"jin_zhan_3"
|
||||
@export var future_form_id: StringName = &"jin_zhan_1"
|
||||
@export var strength_profile: StringName = &"past_strong"
|
||||
@export var time_phase_profile: Resource
|
||||
@export var visual_scale := 2.0
|
||||
@export var visual_ground_offset := -38.0
|
||||
@export var projectile_spawn_forward := 44.0
|
||||
@export var minion_lunge_speed := 100.0
|
||||
@export var approach_speed := 120.0
|
||||
|
||||
## Set by MinionBehavior each physics frame: signed walk intent whose magnitude
|
||||
## scales approach_speed (patrol strolls at ~0.45, a ranged retreat hops at ~1.7).
|
||||
var approach_direction := 0.0
|
||||
|
||||
@onready var state_machine: Node = get_node_or_null("StateMachine")
|
||||
@onready var action_controller: Node = get_node_or_null("ActionController")
|
||||
@onready var motion_executor: Node = get_node_or_null("MotionExecutor")
|
||||
@onready var movement_motor: Node = get_node_or_null("MovementMotor")
|
||||
@onready var health_component: Node = get_node_or_null("HealthComponent")
|
||||
@onready var damage_receiver: Node = get_node_or_null("DamageReceiver")
|
||||
@onready var strong_buff_visual: Node = get_node_or_null("Visual/StrongBuffVisual")
|
||||
@onready var overhead_health_bar: Node2D = get_node_or_null("OverheadHealthBar") as Node2D
|
||||
|
||||
var form_id: StringName = &""
|
||||
var _pending_form_id: StringName = &""
|
||||
var _current_action_animation: StringName = &""
|
||||
var _frame_delta := 0.0
|
||||
var _dead_state_entered := false
|
||||
var _death_animation_started := false
|
||||
var _phase_swap_hold_frames := 0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
heading = DEFAULT_HEADING
|
||||
anim_map = {
|
||||
PRESENTATION_IDLE: "idle",
|
||||
PRESENTATION_WALK: "idle",
|
||||
PRESENTATION_JUMP: "idle",
|
||||
PRESENTATION_LAND: "idle",
|
||||
PRESENTATION_ATTACK: "idle",
|
||||
PRESENTATION_AIR_ATTACK: "idle",
|
||||
}
|
||||
_install_minion_animations()
|
||||
_connect_once(animation_player, &"animation_finished", Callable(self, "_on_animation_finished"))
|
||||
_wire_action_controller()
|
||||
_wire_damage_receiver()
|
||||
_connect_time_phase_bus()
|
||||
_apply_time_phase_profile()
|
||||
if health_component != null and health_component.has_method("set_values"):
|
||||
var health_multiplier := _difficulty_health_multiplier()
|
||||
health_component.call("set_values", int(round(current_health * health_multiplier)), int(round(max_health * health_multiplier)))
|
||||
apply_time_phase(_current_time_phase())
|
||||
state = PRESENTATION_IDLE
|
||||
_play_idle()
|
||||
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
_frame_delta = delta
|
||||
super._physics_process(delta)
|
||||
|
||||
|
||||
func handle_input() -> void:
|
||||
pass
|
||||
|
||||
|
||||
## 击退滑行(趔趄)期间不得起手新动作(EnemyActionDriver 钩子,boss 同款):
|
||||
## 否则动作突进会立刻接管 velocity,把击退位移吃掉。
|
||||
func can_start_enemy_action(_action_id: StringName) -> bool:
|
||||
if movement_motor != null and movement_motor.has_method("has_knockback_stray") and bool(movement_motor.call("has_knockback_stray")):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func handle_air_time(delta: float) -> void:
|
||||
# 委托 MovementMotor(boss 同款):击退残速的摩擦衰减、假高度轴积分、
|
||||
# 落地复位 Grounded 全在里面。旧版只在 JUMP 态积分,导致垂直击退后
|
||||
# ground_state 永久卡 Airborne,小怪从此不能出招(allowed_ground_states)。
|
||||
if movement_motor != null and movement_motor.has_method("handle_air_time"):
|
||||
movement_motor.call("handle_air_time", delta)
|
||||
elif state == PRESENTATION_JUMP:
|
||||
super.handle_air_time(delta)
|
||||
|
||||
|
||||
func handle_movement() -> void:
|
||||
if motion_executor != null and bool(motion_executor.get("active")):
|
||||
velocity = motion_executor.call("tick", _frame_delta)
|
||||
return
|
||||
if state == PRESENTATION_ATTACK or state == PRESENTATION_AIR_ATTACK:
|
||||
return
|
||||
if _phase_swap_hold_frames > 0:
|
||||
_phase_swap_hold_frames -= 1
|
||||
approach_direction = 0.0
|
||||
velocity.x = 0.0
|
||||
state = PRESENTATION_IDLE
|
||||
return
|
||||
# 击退残速期:滑行交给 MovementMotor 的摩擦衰减,AI 不接管 velocity.x
|
||||
# (否则击退初速活不过一帧,小怪击退位移恒为 0)。必须放在相位切换
|
||||
# hold 之后——换相位定身优先,hold 清零速度后 stray 标志随衰减自清。
|
||||
if movement_motor != null and movement_motor.has_method("has_knockback_stray") and bool(movement_motor.call("has_knockback_stray")):
|
||||
state = PRESENTATION_IDLE
|
||||
return
|
||||
if absf(approach_direction) > 0.05 and _life_state() != &"Dead":
|
||||
# 上限 4.0 给受击脱离的跳跃速度(escape_speed_scale 3.2)留出空间。
|
||||
velocity.x = clampf(approach_direction, -4.0, 4.0) * approach_speed
|
||||
state = PRESENTATION_WALK
|
||||
return
|
||||
velocity.x = 0.0
|
||||
state = PRESENTATION_IDLE
|
||||
|
||||
|
||||
func handle_animations() -> void:
|
||||
var life_state := _life_state()
|
||||
if life_state == &"Dead":
|
||||
_enter_dead_state()
|
||||
_play_death_animation_once()
|
||||
_queue_free_if_death_animation_finished()
|
||||
return
|
||||
_dead_state_entered = false
|
||||
_death_animation_started = false
|
||||
if (state == PRESENTATION_ATTACK or state == PRESENTATION_AIR_ATTACK) and not _current_action_animation.is_empty():
|
||||
_play_animation(_current_action_animation)
|
||||
return
|
||||
if state == PRESENTATION_WALK:
|
||||
_play_animation(_dash_animation_name(form_id))
|
||||
return
|
||||
_play_idle()
|
||||
|
||||
|
||||
func set_heading() -> void:
|
||||
pass
|
||||
|
||||
|
||||
func set_sprite_height_position() -> void:
|
||||
if visual != null:
|
||||
visual.position = Vector2(0.0, visual_ground_offset) + Vector2.UP * height
|
||||
_refresh_overhead_health_bar_position()
|
||||
|
||||
|
||||
func flip_sprites() -> void:
|
||||
_apply_form_visual_scale()
|
||||
|
||||
|
||||
func apply_time_phase(time_phase: StringName) -> void:
|
||||
_refresh_strong_buff_visual(time_phase)
|
||||
var next_form := future_form_id if time_phase == &"future" else past_form_id
|
||||
if next_form == form_id:
|
||||
_pending_form_id = &""
|
||||
if state != PRESENTATION_ATTACK and state != PRESENTATION_AIR_ATTACK and _life_state() != &"Dead":
|
||||
_apply_sprite_offset()
|
||||
_play_idle(true)
|
||||
return
|
||||
if state == PRESENTATION_ATTACK or state == PRESENTATION_AIR_ATTACK:
|
||||
_pending_form_id = next_form
|
||||
return
|
||||
_hold_phase_swap_position()
|
||||
form_id = next_form
|
||||
_pending_form_id = &""
|
||||
_apply_sprite_offset()
|
||||
if _life_state() != &"Dead":
|
||||
_play_idle(true)
|
||||
|
||||
|
||||
func current_action_ids() -> Array[StringName]:
|
||||
var result: Array[StringName] = []
|
||||
var actions: Array = _form_spec().get("actions", [])
|
||||
for action_id: Variant in actions:
|
||||
result.append(StringName(str(action_id)))
|
||||
return result
|
||||
|
||||
|
||||
func current_attack_interval_beats() -> int:
|
||||
return 2 if is_strong_in_current_time_phase() else 4
|
||||
|
||||
|
||||
func is_strong_in_current_time_phase() -> bool:
|
||||
return _is_strong_in_time_phase(_current_time_phase())
|
||||
|
||||
|
||||
func _is_strong_in_time_phase(time_phase: StringName) -> bool:
|
||||
return (strength_profile == &"past_strong" and time_phase == &"past") or (strength_profile == &"future_strong" and time_phase == &"future")
|
||||
|
||||
|
||||
func look_at_target(target: Node2D) -> void:
|
||||
if target == null:
|
||||
return
|
||||
heading = Vector2.LEFT if target.global_position.x < global_position.x else Vector2.RIGHT
|
||||
|
||||
|
||||
func projectile_spawn_position(_action: Resource) -> Vector2:
|
||||
return global_position + Vector2(_heading_sign() * projectile_spawn_forward, -_projectile_height())
|
||||
|
||||
|
||||
func projectile_direction(_action: Resource) -> Vector2:
|
||||
return heading.normalized() if heading != Vector2.ZERO else DEFAULT_HEADING
|
||||
|
||||
|
||||
func projectile_requests_for_action(action: Resource) -> Array[Dictionary]:
|
||||
return [{
|
||||
"spawn_position": projectile_spawn_position(action),
|
||||
"direction": projectile_direction(action),
|
||||
}]
|
||||
|
||||
|
||||
func _install_minion_animations() -> void:
|
||||
if animation_player == null:
|
||||
return
|
||||
var library: AnimationLibrary
|
||||
if animation_player.has_animation_library(""):
|
||||
library = animation_player.get_animation_library("")
|
||||
else:
|
||||
library = AnimationLibrary.new()
|
||||
animation_player.add_animation_library("", library)
|
||||
for next_form: StringName in FORM_SPECS.keys():
|
||||
var spec: Dictionary = FORM_SPECS[next_form]
|
||||
var sprite_offset: Vector2 = spec.get("sprite_offset", Vector2(-32.0, -64.0))
|
||||
_add_sheet_animation(library, _idle_animation_name(next_form), spec.get("idle", {}), VISIBLE_FEET_ANCHOR_Y, sprite_offset)
|
||||
_add_sheet_animation(library, _death_animation_name(next_form), spec.get("death", {}), VISIBLE_FEET_ANCHOR_Y, sprite_offset)
|
||||
_add_sheet_animation(library, _dash_animation_name(next_form), spec.get("dash", {}), VISIBLE_FEET_ANCHOR_Y, sprite_offset)
|
||||
for attack: Variant in spec.get("attacks", []):
|
||||
if attack is Dictionary:
|
||||
_add_sheet_animation(library, StringName(str((attack as Dictionary).get("name", &""))), attack, VISIBLE_FEET_ANCHOR_Y, sprite_offset)
|
||||
|
||||
|
||||
func _add_sheet_animation(library: AnimationLibrary, animation_name: StringName, spec: Dictionary, visible_bottom_anchor := INF, sprite_offset := Vector2.ZERO) -> void:
|
||||
if animation_name.is_empty() or spec.is_empty() or library.has_animation(animation_name):
|
||||
return
|
||||
var texture := load(str(spec.get("path", ""))) as Texture2D
|
||||
if texture == null:
|
||||
return
|
||||
var frame_count: int = int(spec.get("frames", 1))
|
||||
var hframes: int = int(spec.get("hframes", frame_count))
|
||||
library.add_animation(animation_name, _make_sheet_animation(texture, frame_count, hframes, float(spec.get("fps", DEFAULT_ANIMATION_FPS)), bool(spec.get("loop", false)), visible_bottom_anchor, sprite_offset))
|
||||
|
||||
|
||||
func _make_sheet_animation(texture: Texture2D, frame_count: int, hframes: int, fps: float, loop: bool, visible_bottom_anchor := INF, sprite_offset := Vector2.ZERO) -> Animation:
|
||||
var safe_frame_count := maxi(1, frame_count)
|
||||
var safe_hframes := maxi(safe_frame_count, hframes)
|
||||
var safe_fps := maxf(1.0, fps)
|
||||
var frame_time := 1.0 / safe_fps
|
||||
var animation := Animation.new()
|
||||
animation.length = maxf(frame_time, float(safe_frame_count) * frame_time)
|
||||
animation.step = frame_time
|
||||
animation.loop_mode = Animation.LOOP_LINEAR if loop else Animation.LOOP_NONE
|
||||
var texture_track := animation.add_track(Animation.TYPE_VALUE)
|
||||
animation.track_set_path(texture_track, NodePath("Visual/CharacterSprite:texture"))
|
||||
animation.value_track_set_update_mode(texture_track, Animation.UPDATE_DISCRETE)
|
||||
animation.track_insert_key(texture_track, 0.0, texture)
|
||||
var hframes_track := animation.add_track(Animation.TYPE_VALUE)
|
||||
animation.track_set_path(hframes_track, NodePath("Visual/CharacterSprite:hframes"))
|
||||
animation.value_track_set_update_mode(hframes_track, Animation.UPDATE_DISCRETE)
|
||||
animation.track_insert_key(hframes_track, 0.0, safe_hframes)
|
||||
var vframes_track := animation.add_track(Animation.TYPE_VALUE)
|
||||
animation.track_set_path(vframes_track, NodePath("Visual/CharacterSprite:vframes"))
|
||||
animation.value_track_set_update_mode(vframes_track, Animation.UPDATE_DISCRETE)
|
||||
animation.track_insert_key(vframes_track, 0.0, 1)
|
||||
var frame_track := animation.add_track(Animation.TYPE_VALUE)
|
||||
animation.track_set_path(frame_track, NodePath("Visual/CharacterSprite:frame"))
|
||||
animation.value_track_set_update_mode(frame_track, Animation.UPDATE_DISCRETE)
|
||||
for frame_index: int in range(safe_frame_count):
|
||||
animation.track_insert_key(frame_track, float(frame_index) * frame_time, frame_index)
|
||||
if visible_bottom_anchor < INF:
|
||||
var offset_track := animation.add_track(Animation.TYPE_VALUE)
|
||||
animation.track_set_path(offset_track, NodePath("Visual/CharacterSprite:offset"))
|
||||
animation.value_track_set_update_mode(offset_track, Animation.UPDATE_DISCRETE)
|
||||
for frame_index: int in range(safe_frame_count):
|
||||
var offset := sprite_offset
|
||||
var bottom := _visible_frame_bottom_in_texture(texture, safe_hframes, 1, frame_index)
|
||||
if bottom != INF:
|
||||
offset.y = visible_bottom_anchor - bottom
|
||||
animation.track_insert_key(offset_track, float(frame_index) * frame_time, offset)
|
||||
return animation
|
||||
|
||||
|
||||
func _wire_action_controller() -> void:
|
||||
if action_controller == null:
|
||||
return
|
||||
_connect_once(action_controller, &"action_started", Callable(self, "_on_action_started"))
|
||||
_connect_once(action_controller, &"action_active_started", Callable(self, "_on_action_active_started"))
|
||||
_connect_once(action_controller, &"action_finished", Callable(self, "_on_action_finished"))
|
||||
_connect_once(action_controller, &"action_cancelled", Callable(self, "_on_action_cancelled"))
|
||||
|
||||
|
||||
func _wire_damage_receiver() -> void:
|
||||
if damage_receiver != null:
|
||||
_connect_once(damage_receiver, &"damage_received", Callable(self, "_on_damage_received"))
|
||||
|
||||
|
||||
func _connect_once(source: Object, signal_name: StringName, callback: Callable) -> void:
|
||||
if not source.is_connected(signal_name, callback):
|
||||
source.connect(signal_name, callback)
|
||||
|
||||
|
||||
func _connect_time_phase_bus() -> void:
|
||||
var bus := _event_bus_or_null()
|
||||
if bus != null and bus.has_signal("time_phase_changed") and not bus.is_connected("time_phase_changed", _on_time_phase_changed):
|
||||
bus.connect("time_phase_changed", _on_time_phase_changed)
|
||||
|
||||
|
||||
func _apply_time_phase_profile() -> void:
|
||||
var adapter := get_node_or_null("TimePhaseAdapter")
|
||||
if adapter == null:
|
||||
return
|
||||
adapter.set("profile", time_phase_profile)
|
||||
if adapter.has_method("apply_time_phase"):
|
||||
adapter.call("apply_time_phase", _current_time_phase())
|
||||
|
||||
|
||||
func _on_time_phase_changed(_previous: StringName, current: StringName, _reason: StringName) -> void:
|
||||
apply_time_phase(current)
|
||||
|
||||
|
||||
func _on_action_started(action: Resource, _intent) -> void:
|
||||
if _life_state() == &"Dead":
|
||||
return
|
||||
_face_target_if_available()
|
||||
_current_action_animation = StringName(str(action.get("animation")))
|
||||
state = PRESENTATION_ATTACK
|
||||
attack_time_left = _action_duration_seconds(action) + 0.05
|
||||
attack_lunge_time_left = attack_time_left
|
||||
_align_action_animation_speed(action)
|
||||
if animation_player != null:
|
||||
animation_player.stop()
|
||||
_play_animation(_current_action_animation)
|
||||
|
||||
|
||||
func _on_action_active_started(action: Resource, _intent) -> void:
|
||||
if _life_state() == &"Dead" or motion_executor == null:
|
||||
return
|
||||
if absf(float(action.get("move_mult_x"))) <= 0.0 and absf(float(action.get("move_mult_y"))) <= 0.0:
|
||||
return
|
||||
var speed_scale := maxf(1.0, absf(float(action.get("move_mult_x"))))
|
||||
motion_executor.call("execute", action, heading, _beat_time(), minion_lunge_speed * speed_scale)
|
||||
|
||||
|
||||
func _on_action_finished(_action: Resource) -> void:
|
||||
_finish_action_presentation()
|
||||
|
||||
|
||||
func _on_action_cancelled(_action: Resource, _reason: StringName) -> void:
|
||||
_finish_action_presentation()
|
||||
|
||||
|
||||
func _finish_action_presentation() -> void:
|
||||
_current_action_animation = &""
|
||||
_reset_animation_speed()
|
||||
if motion_executor != null and motion_executor.has_method("cancel"):
|
||||
motion_executor.call("cancel")
|
||||
if _life_state() == &"Dead":
|
||||
velocity = Vector2.ZERO
|
||||
return
|
||||
state = PRESENTATION_IDLE
|
||||
velocity = Vector2.ZERO
|
||||
if not _pending_form_id.is_empty():
|
||||
apply_time_phase(_current_time_phase())
|
||||
_play_idle()
|
||||
|
||||
|
||||
func _on_damage_received(_amount: int, _hit_type: StringName, _from: Vector2) -> void:
|
||||
if _life_state() == &"Dead":
|
||||
return
|
||||
_play_idle()
|
||||
|
||||
|
||||
func _enter_dead_state() -> void:
|
||||
if _dead_state_entered:
|
||||
return
|
||||
_dead_state_entered = true
|
||||
_current_action_animation = &""
|
||||
attack_time_left = 0.0
|
||||
attack_lunge_time_left = 0.0
|
||||
velocity = Vector2.ZERO
|
||||
_reset_animation_speed()
|
||||
if action_controller != null and action_controller.has_method("cancel_current") and int(action_controller.get("phase")) != 0:
|
||||
action_controller.call("cancel_current", &"death")
|
||||
if motion_executor != null and motion_executor.has_method("cancel"):
|
||||
motion_executor.call("cancel")
|
||||
var damage_emitter := get_node_or_null("DamageEmitter")
|
||||
if damage_emitter != null and damage_emitter.has_method("clear_hit"):
|
||||
damage_emitter.call("clear_hit")
|
||||
var receiver := get_node_or_null("DamageReceiver") as Area2D
|
||||
if receiver != null:
|
||||
receiver.monitoring = false
|
||||
receiver.monitorable = false
|
||||
var behavior := get_node_or_null("MinionBehavior")
|
||||
if behavior != null:
|
||||
behavior.set("enabled", false)
|
||||
_refresh_strong_buff_visual(_current_time_phase())
|
||||
|
||||
|
||||
func _play_death_animation_once() -> void:
|
||||
if _death_animation_started:
|
||||
return
|
||||
_death_animation_started = true
|
||||
_play_animation(_death_animation_name(form_id))
|
||||
|
||||
|
||||
func _on_animation_finished(animation_name: StringName) -> void:
|
||||
if _death_animation_started and animation_name == _death_animation_name(form_id):
|
||||
queue_free()
|
||||
|
||||
|
||||
func _queue_free_if_death_animation_finished() -> void:
|
||||
if not _death_animation_started or is_queued_for_deletion():
|
||||
return
|
||||
if animation_player == null:
|
||||
queue_free()
|
||||
return
|
||||
if not animation_player.is_playing():
|
||||
queue_free()
|
||||
|
||||
|
||||
func _play_idle(force_refresh := false) -> void:
|
||||
_play_animation(_idle_animation_name(form_id), force_refresh)
|
||||
|
||||
|
||||
func _play_animation(animation_name: StringName, force_refresh := false) -> void:
|
||||
if animation_player == null or animation_name.is_empty():
|
||||
return
|
||||
if not animation_player.has_animation(animation_name):
|
||||
return
|
||||
if force_refresh or animation_player.current_animation != String(animation_name):
|
||||
animation_player.play(animation_name)
|
||||
if force_refresh:
|
||||
animation_player.seek(0.0, true)
|
||||
|
||||
|
||||
func _align_action_animation_speed(action: Resource) -> void:
|
||||
if animation_player == null or action == null or _current_action_animation.is_empty():
|
||||
return
|
||||
if not animation_player.has_animation(_current_action_animation):
|
||||
return
|
||||
var natural_length := animation_player.get_animation(_current_action_animation).length
|
||||
var action_seconds := _action_duration_seconds(action)
|
||||
if natural_length <= 0.0 or action_seconds <= 0.0:
|
||||
return
|
||||
animation_player.speed_scale = clampf(natural_length / action_seconds, 0.25, 3.0)
|
||||
|
||||
|
||||
func _reset_animation_speed() -> void:
|
||||
if animation_player != null:
|
||||
animation_player.speed_scale = 1.0
|
||||
|
||||
|
||||
func _action_duration_seconds(action: Resource) -> float:
|
||||
if action == null:
|
||||
return attack_duration
|
||||
return maxf(0.05, float(action.get("action_beats")) * _beat_time())
|
||||
|
||||
|
||||
func _beat_time() -> float:
|
||||
var rhythm := get_tree().root.get_node_or_null("RhythmManager") if is_inside_tree() else null
|
||||
if rhythm != null:
|
||||
return float(rhythm.get("beat_time"))
|
||||
return 0.5
|
||||
|
||||
|
||||
func _face_target_if_available() -> void:
|
||||
var target := _target_or_null()
|
||||
if target != null:
|
||||
look_at_target(target)
|
||||
|
||||
|
||||
func _target_or_null() -> Node2D:
|
||||
var behavior := get_node_or_null("MinionBehavior")
|
||||
if behavior != null:
|
||||
var behavior_target = behavior.get("target")
|
||||
if behavior_target is Node2D and is_instance_valid(behavior_target):
|
||||
return behavior_target
|
||||
var container := get_parent()
|
||||
if container != null:
|
||||
var sibling := container.get_node_or_null("Player") as Node2D
|
||||
if sibling != null:
|
||||
return sibling
|
||||
return null
|
||||
|
||||
|
||||
func _form_spec() -> Dictionary:
|
||||
return FORM_SPECS.get(form_id, FORM_SPECS.get(past_form_id, {}))
|
||||
|
||||
|
||||
func _form_visual_scale() -> float:
|
||||
return float(_form_spec().get("visual_scale", visual_scale))
|
||||
|
||||
|
||||
func _native_facing() -> Vector2:
|
||||
return _form_spec().get("native_facing", Vector2.RIGHT)
|
||||
|
||||
|
||||
func _apply_sprite_offset() -> void:
|
||||
if character_sprite != null:
|
||||
var sprite_offset: Vector2 = _form_spec().get("sprite_offset", Vector2(-32.0, -64.0))
|
||||
var idle_spec: Dictionary = _form_spec().get("idle", {})
|
||||
var texture := load(str(idle_spec.get("path", ""))) as Texture2D
|
||||
var hframes := int(idle_spec.get("hframes", idle_spec.get("frames", 1)))
|
||||
var bottom := _visible_frame_bottom_in_texture(texture, maxi(1, hframes), 1, 0)
|
||||
sprite_offset.y = VISIBLE_FEET_ANCHOR_Y - bottom if bottom < INF else sprite_offset.y
|
||||
character_sprite.offset = sprite_offset
|
||||
_apply_form_visual_scale()
|
||||
_refresh_overhead_health_bar_position()
|
||||
|
||||
|
||||
func _apply_form_visual_scale() -> void:
|
||||
if visual == null:
|
||||
return
|
||||
var scale_value := _form_visual_scale()
|
||||
visual.scale.x = scale_value if heading == _native_facing() else -scale_value
|
||||
visual.scale.y = scale_value
|
||||
|
||||
|
||||
func _hold_phase_swap_position() -> void:
|
||||
_phase_swap_hold_frames = PHASE_SWAP_MOVEMENT_HOLD_FRAMES
|
||||
approach_direction = 0.0
|
||||
velocity.x = 0.0
|
||||
|
||||
|
||||
func _refresh_overhead_health_bar_position() -> void:
|
||||
if overhead_health_bar == null or visual == null or character_sprite == null:
|
||||
return
|
||||
overhead_health_bar.position = Vector2(0.0, visual.position.y + character_sprite.offset.y * absf(visual.scale.y) - 10.0)
|
||||
|
||||
|
||||
func _visible_frame_bottom_in_texture(texture: Texture2D, hframes: int, vframes: int, frame_index: int) -> float:
|
||||
if texture == null:
|
||||
return INF
|
||||
var image := texture.get_image()
|
||||
if image == null or image.is_empty():
|
||||
return INF
|
||||
var safe_hframes := maxi(1, hframes)
|
||||
var safe_vframes := maxi(1, vframes)
|
||||
var frame_width := image.get_width() / safe_hframes
|
||||
var frame_height := image.get_height() / safe_vframes
|
||||
var safe_frame := clampi(frame_index, 0, safe_hframes * safe_vframes - 1)
|
||||
var column := safe_frame % safe_hframes
|
||||
var row := int(safe_frame / safe_hframes)
|
||||
var bottom := -1
|
||||
for y: int in range(frame_height):
|
||||
for x: int in range(frame_width):
|
||||
var pixel := image.get_pixel(column * frame_width + x, row * frame_height + y)
|
||||
if pixel.a > 0.01:
|
||||
bottom = y
|
||||
if bottom < 0:
|
||||
return INF
|
||||
return float(bottom)
|
||||
|
||||
|
||||
func _projectile_height() -> float:
|
||||
return float(_form_spec().get("projectile_height", 42.0))
|
||||
|
||||
|
||||
func _idle_animation_name(next_form: StringName) -> StringName:
|
||||
return StringName("%s_idle" % str(next_form))
|
||||
|
||||
|
||||
func _death_animation_name(next_form: StringName) -> StringName:
|
||||
return StringName("%s_death" % str(next_form))
|
||||
|
||||
|
||||
func _dash_animation_name(next_form: StringName) -> StringName:
|
||||
return StringName("%s_dash" % str(next_form))
|
||||
|
||||
|
||||
func _life_state() -> StringName:
|
||||
if state_machine != null and state_machine.has_method("build_context"):
|
||||
return StringName(str(state_machine.call("build_context").get("life_state", &"Alive")))
|
||||
return &"Alive"
|
||||
|
||||
|
||||
func _heading_sign() -> float:
|
||||
return -1.0 if heading.x < 0.0 else 1.0
|
||||
|
||||
|
||||
func _current_time_phase() -> StringName:
|
||||
var manager := get_tree().root.get_node_or_null("TimePhaseManager") if is_inside_tree() else null
|
||||
if manager != null:
|
||||
return StringName(str(manager.get("current_time_phase")))
|
||||
return &"past"
|
||||
|
||||
|
||||
func _difficulty_health_multiplier() -> float:
|
||||
var settings := get_tree().root.get_node_or_null("GameSettings") if is_inside_tree() else null
|
||||
if settings != null and settings.has_method("minion_health_multiplier"):
|
||||
return maxf(0.01, float(settings.call("minion_health_multiplier")))
|
||||
return 1.0
|
||||
|
||||
|
||||
func _refresh_strong_buff_visual(time_phase: StringName = &"") -> void:
|
||||
if strong_buff_visual == null or not strong_buff_visual.has_method("set_active"):
|
||||
return
|
||||
var phase := _current_time_phase() if time_phase.is_empty() else time_phase
|
||||
strong_buff_visual.call("set_active", _is_strong_in_time_phase(phase) and _life_state() != &"Dead")
|
||||
|
||||
|
||||
func _event_bus_or_null() -> Node:
|
||||
if not is_inside_tree():
|
||||
return null
|
||||
return get_tree().root.get_node_or_null("EventBus")
|
||||
Reference in New Issue
Block a user