Initial project sync

This commit is contained in:
wxm
2026-07-06 22:55:14 -07:00
commit 8cca00d0da
1066 changed files with 58585 additions and 0 deletions
+139
View File
@@ -0,0 +1,139 @@
class_name Character
extends CharacterBody2D
const GRAVITY := 1200.0
@export var speed := 180.0
@export var jump_intensity := 304.056
@export var attack_duration := 0.4
@export var attack_lunge_duration := 0.18
@export var attack_lunge_speed := 220.0
@export var air_attack_duration := 0.45
@export var air_attack_lunge_duration := 0.22
@export var air_attack_lunge_speed := 260.0
@onready var visual: Node2D = $Visual
@onready var animation_player: AnimationPlayer = $AnimationPlayer
@onready var character_sprite: Sprite2D = $Visual/CharacterSprite
const PRESENTATION_IDLE := &"idle"
const PRESENTATION_WALK := &"walk"
const PRESENTATION_JUMP := &"jump"
const PRESENTATION_LAND := &"land"
const PRESENTATION_ATTACK := &"attack"
const PRESENTATION_AIR_ATTACK := &"air_attack"
var anim_map := {
PRESENTATION_IDLE: "idle",
PRESENTATION_WALK: "idle",
PRESENTATION_JUMP: "rising_slash",
PRESENTATION_LAND: "idle",
PRESENTATION_ATTACK: "atk_ground_1",
PRESENTATION_AIR_ATTACK: "atk_air",
}
var attack_direction := Vector2.LEFT
var attack_lunge_time_left := 0.0
var attack_time_left := 0.0
var heading := Vector2.LEFT
var height := 0.0
var height_speed := 0.0
var state := PRESENTATION_IDLE
func _physics_process(delta: float) -> void:
handle_input()
handle_air_time(delta)
handle_attack_time(delta)
handle_movement()
handle_animations()
set_sprite_height_position()
set_heading()
flip_sprites()
move_and_slide()
func _exit_tree() -> void:
if animation_player != null:
animation_player.stop()
func handle_input() -> void:
pass
func handle_air_time(delta: float) -> void:
if state != PRESENTATION_JUMP:
return
height += height_speed * delta
if height <= 0.0 and height_speed < 0.0:
height = 0.0
height_speed = 0.0
state = PRESENTATION_LAND
else:
height_speed -= GRAVITY * delta
func handle_attack_time(delta: float) -> void:
if state != PRESENTATION_ATTACK and state != PRESENTATION_AIR_ATTACK:
return
velocity.y = 0.0
attack_time_left -= delta
if attack_time_left <= 0.0:
state = PRESENTATION_IDLE
velocity = Vector2.ZERO
return
attack_lunge_time_left -= delta
if attack_lunge_time_left <= 0.0:
velocity.x = 0.0
func handle_movement() -> void:
if state == PRESENTATION_JUMP or state == PRESENTATION_ATTACK or state == PRESENTATION_AIR_ATTACK:
return
if state == PRESENTATION_LAND:
state = PRESENTATION_IDLE
elif absf(velocity.x) > 0.0:
state = PRESENTATION_WALK
else:
state = PRESENTATION_IDLE
func handle_animations() -> void:
var animation_name: String = anim_map.get(state, "idle")
if animation_player.has_animation(animation_name) and animation_player.current_animation != animation_name:
animation_player.play(animation_name)
func set_sprite_height_position() -> void:
if visual != null:
visual.position = Vector2.UP * height
func set_heading() -> void:
pass
func flip_sprites() -> void:
if visual == null:
return
var base_scale := absf(visual.scale.x)
visual.scale.x = base_scale if heading == Vector2.LEFT else -base_scale
func can_jump() -> bool:
return state == PRESENTATION_IDLE or state == PRESENTATION_WALK
func start_jump() -> void:
state = PRESENTATION_JUMP
height_speed = jump_intensity
func begin_attack_motion(duration: float, next_velocity: Vector2) -> void:
attack_lunge_time_left = maxf(0.0, duration)
velocity = next_velocity
func stop_attack_motion() -> void:
attack_lunge_time_left = 0.0
velocity = Vector2.ZERO
+1
View File
@@ -0,0 +1 @@
uid://bfrt3ewrqd07i
+890
View File
@@ -0,0 +1,890 @@
class_name Player
extends Character
const OneShotFxScript := preload("res://scenes/combat/one_shot_fx.gd")
signal combo_window_changed(slots: Array)
signal combo_window_cleared(reason: String)
signal charge_changed(current: float, maximum: float, ready: bool, active: bool)
signal energy_changed(current: int, maximum: int)
signal action_requested(action_id: String)
@export var combo_clear_display_time := 0.35
@export var max_energy := 100
@export var current_energy := 0
@export var base_move_distance := 100.0
@export var visual_ground_offset := 0.0
@export var action_animation_min_speed_scale := 0.35
@export var action_animation_max_speed_scale := 5.0
# Manual sheet animations. "fx" entries drive Visual/FxOverlay on a parallel timeline:
# frame 0 of every author overlay sheet is the embedded idle alignment reference and is
# never played; offsets below are computed by aligning that reference to the idle pose.
const PLAYER_ANIMATION_SPECS := {
&"atk_ground_3": {
"fps": 20.0,
"loop": false,
"segments": [
{"path": "res://assets/art/characters/player/03_basic_attack_iii/air_prep.png", "frames": 3, "hframes": 3, "vframes": 1},
{"path": "res://assets/art/characters/player/03_basic_attack_iii/air_swing.png", "frames": 3, "hframes": 3, "vframes": 1},
],
"fx": [
{"path": "res://assets/art/characters/player/03_basic_attack_iii/slash_overlay_fx.png", "hframes": 4, "vframes": 1, "first": 1, "last": 3, "fps": 20.0, "offset": Vector2(-147, -191), "start": 0.15},
],
},
&"dash_slash": {
"fps": 25.0,
"loop": false,
"segments": [
{"path": "res://assets/art/characters/player/04_dash_slash/dashattack.png", "frames": 12, "hframes": 6, "vframes": 2},
],
"fx": [
{"path": "res://assets/art/characters/player/04_dash_slash/dash_overlay_fx.png", "hframes": 4, "vframes": 3, "first": 1, "last": 11, "fps": 25.0, "offset": Vector2(-98, -128), "start": 0.0},
],
},
&"combo_finisher": {
"fps": 20.0,
"loop": false,
"segments": [
{"path": "res://assets/art/characters/player/05_combo_finisher/prep.png", "frames": 5, "hframes": 5, "vframes": 1},
{"path": "res://assets/art/characters/player/05_combo_finisher/air_swing.png", "frames": 6, "hframes": 6, "vframes": 1},
],
"fx": [
{"path": "res://assets/art/characters/player/05_combo_finisher/slash_overlay_fx.png", "hframes": 4, "vframes": 1, "first": 1, "last": 3, "fps": 20.0, "offset": Vector2(-147, -191), "start": 0.25},
],
},
&"ground_smash": {
"fps": 20.0,
"loop": false,
"segments": [
{"path": "res://assets/art/characters/player/06_ground_smash/prep.png", "frames": 8, "hframes": 4, "vframes": 2},
{"path": "res://assets/art/characters/player/06_ground_smash/swing.png", "frames": 6, "hframes": 3, "vframes": 2},
],
},
&"blade_rain_charge": {
"fps": 25.0,
"loop": false,
"segments": [
{"path": "res://assets/art/characters/player/08_sword_charge/air_cast_prep.png", "frames": 20, "hframes": 10, "vframes": 2},
],
},
&"blade_rain_cast": {
"fps": 20.0,
"loop": true,
"segments": [
{"path": "res://assets/art/characters/player/09_blade_rain/air_casting_loop.png", "frames": 2, "hframes": 2, "vframes": 1},
],
"fx": [
{"path": "res://assets/art/characters/player/08_sword_charge/sword_rain_prep_overlay_fx.png", "hframes": 10, "vframes": 2, "first": 17, "last": 19, "fps": 25.0, "offset": Vector2(-64, -305), "start": 0.0},
],
},
&"blade_wave_cast": {
"fps": 20.0,
"loop": false,
"segments": [
{"path": "res://assets/art/characters/player/11_blade_wave/cast.png", "frames": 4, "hframes": 4, "vframes": 1},
],
"fx": [
{"path": "res://assets/art/characters/player/11_blade_wave/cast_slash_overlay_fx.png", "hframes": 5, "vframes": 1, "first": 1, "last": 4, "fps": 20.0, "offset": Vector2(-115, -172), "start": 0.0},
],
},
&"atk_air": {
"fps": 25.0,
"loop": false,
"segments": [
{"path": "res://assets/art/characters/player/12_aerial_combo/prep.png", "frames": 2, "hframes": 2, "vframes": 1},
{"path": "res://assets/art/characters/player/12_aerial_combo/swing_start.png", "frames": 3, "hframes": 3, "vframes": 1},
{"path": "res://assets/art/characters/player/12_aerial_combo/falling_loop.png", "frames": 3, "hframes": 3, "vframes": 1, "fps": 20.0},
],
"fx": [
{"path": "res://assets/art/characters/player/12_aerial_combo/fx_2_swing_start_slash_overlay_fx.png", "hframes": 4, "vframes": 1, "first": 1, "last": 3, "fps": 25.0, "offset": Vector2(-107, -198), "start": 0.08},
{"path": "res://assets/art/characters/player/12_aerial_combo/fx_3_falling_slash_overlay_fx_hold.png", "hframes": 2, "vframes": 1, "first": 1, "last": 1, "fps": 20.0, "offset": Vector2(-107, -198), "start": 0.2, "hold": true},
],
},
&"air_slam_land": {
"fps": 20.0,
"loop": false,
"segments": [
{"path": "res://assets/art/characters/player/12_aerial_combo/land.png", "frames": 6, "hframes": 3, "vframes": 2},
],
"fx": [
{"path": "res://assets/art/characters/player/12_aerial_combo/fx_4_land_slash_overlay_fx.png", "hframes": 4, "vframes": 1, "first": 1, "last": 3, "fps": 20.0, "offset": Vector2(-71, -166), "start": 0.0},
],
},
&"plunge_start": {
"fps": 25.0,
"loop": false,
"segments": [
{"path": "res://assets/art/characters/player/13_plunging_strike/prep.png", "frames": 2, "hframes": 2, "vframes": 1},
{"path": "res://assets/art/characters/player/13_plunging_strike/swing_start.png", "frames": 3, "hframes": 3, "vframes": 1},
{"path": "res://assets/art/characters/player/13_plunging_strike/falling_loop.png", "frames": 3, "hframes": 3, "vframes": 1, "fps": 20.0},
],
"fx": [
{"path": "res://assets/art/characters/player/13_plunging_strike/fx_2_swing_start_slash_overlay_fx.png", "hframes": 4, "vframes": 1, "first": 1, "last": 3, "fps": 25.0, "offset": Vector2(-107, -198), "start": 0.08},
{"path": "res://assets/art/characters/player/13_plunging_strike/fx_3_falling_slash_overlay_fx_hold.png", "hframes": 2, "vframes": 1, "first": 1, "last": 1, "fps": 20.0, "offset": Vector2(-107, -198), "start": 0.2, "hold": true},
],
},
&"hit_stun": {
"fps": 14.0,
"loop": false,
"segments": [
{"path": "res://assets/art/characters/player/14_hit_stun/damaged.png", "frames": 3, "hframes": 3, "vframes": 1},
],
},
&"death": {
"fps": 12.0,
"loop": false,
"segments": [
{"path": "res://assets/art/characters/player/15_death/initial_death_stun_hold.png", "frames": 1, "hframes": 1, "vframes": 1, "fps": 2.0},
{"path": "res://assets/art/characters/player/15_death/fall_back.png", "frames": 10, "hframes": 5, "vframes": 2},
{"path": "res://assets/art/characters/player/15_death/he_s_finally_dead_hold.png", "frames": 1, "hframes": 1, "vframes": 1, "fps": 1.0},
],
},
}
@onready var state_machine: Node = $StateMachine
@onready var input_component: Node = $InputComponent
@onready var movement_motor: Node = $MovementMotor
@onready var combo_window: Node = $ComboWindow
@onready var action_controller: Node = $ActionController
@onready var motion_executor: Node = $MotionExecutor
@onready var burst_component: Node = $BurstComponent
@onready var charge_component: Node = $ChargeComponent
@onready var energy_component: Node = $EnergyComponent
@onready var effect_container: Node = $EffectContainer
@onready var fx_overlay: Sprite2D = $Visual/FxOverlay
var last_requested_action_id := ""
var current_action_animation := "idle"
var _manual_animation_segments: Array = []
var _manual_animation_segment := 0
var _manual_animation_frame := 0
var _manual_animation_elapsed := 0.0
var _manual_animation_frame_time := 0.05
var _manual_animation_fps := 20.0
var _manual_animation_speed_scale := 1.0
var _manual_animation_loop := false
var _manual_animation_active := false
var _manual_animation_time := 0.0
var _manual_animation_name: StringName = &""
var _base_sprite_offset := Vector2(-64.0, -128.0)
var _death_visible_bottom_anchor := 0.0
var _manual_fx: Array = []
var _hitstun_animation_started := false
var _death_animation_started := false
var _charge_body_animation_active := false
var _pending_air_slam_land := false
func _ready() -> void:
if character_sprite != null:
_base_sprite_offset = character_sprite.offset
_death_visible_bottom_anchor = _current_sprite_visible_bottom_local()
combo_window.clear_display_time = combo_clear_display_time
input_component.intent_created.connect(_on_input_intent_created)
action_controller.action_started.connect(_on_action_started)
action_controller.action_active_started.connect(_on_action_active_started)
action_controller.action_finished.connect(_on_action_finished)
action_controller.action_cancelled.connect(_on_action_cancelled)
combo_window.combo_updated.connect(_on_combo_updated)
combo_window.combo_cleared.connect(_on_combo_cleared)
charge_component.charge_changed.connect(_on_charge_component_changed)
energy_component.energy_changed.connect(_on_energy_component_changed)
energy_component.set_values(current_energy, max_energy)
func _exit_tree() -> void:
super._exit_tree()
func _process(delta: float) -> void:
charge_component.tick(delta, _is_action_phase(&"Charging"))
if charge_component.is_active():
_apply_charging_presentation()
elif _charge_body_animation_active:
_charge_body_animation_active = false
_stop_manual_animation()
_tick_manual_animation(delta)
func _apply_charging_presentation() -> void:
state = PRESENTATION_IDLE
attack_time_left = 0.0
stop_attack_motion()
var symbol := _active_charge_symbol()
var blade_rain_charge := symbol == &"A" or symbol == &"D"
charge_component.set_overlay_profile(&"blade_rain" if blade_rain_charge else &"wave")
if blade_rain_charge and not _charge_body_animation_active:
_charge_body_animation_active = true
if animation_player != null:
_reset_action_animation_speed()
animation_player.stop()
_start_manual_animation(&"blade_rain_charge")
func _active_charge_symbol() -> StringName:
var entry = action_controller.get("_active_charge_entry")
if entry is Dictionary:
return StringName(str((entry as Dictionary).get("symbol", &"")))
return &""
func _input(event: InputEvent) -> void:
if input_component.handle_input_event(event):
_mark_input_handled()
func _unhandled_input(event: InputEvent) -> void:
if input_component.handle_input_event(event):
_mark_input_handled()
func handle_input() -> void:
if charge_component.is_active():
velocity = Vector2.ZERO
return
movement_motor.handle_input()
func handle_air_time(delta: float) -> void:
movement_motor.handle_air_time(delta)
func handle_movement() -> void:
movement_motor.handle_movement()
func handle_animations() -> void:
var life_state := _life_state_name()
if life_state == &"Dead":
_play_death_animation_once()
return
_death_animation_started = false
if life_state == &"Hitstun":
_play_hitstun_animation_once()
return
if _hitstun_animation_started:
_hitstun_animation_started = false
_stop_manual_animation()
if _charge_body_animation_active and charge_component.is_active():
return
if _pending_air_slam_land:
if _grounded_now():
_pending_air_slam_land = false
_play_air_slam_land_animation()
else:
# Still falling: hold the falling pose until touchdown.
state = PRESENTATION_ATTACK
attack_time_left = maxf(attack_time_left, 0.1)
return
if current_action_animation == "air_slam_land" and _manual_animation_active:
return
super.handle_animations()
func _life_state_name() -> StringName:
if state_machine != null and state_machine.has_method("get_life_state"):
return state_machine.get_life_state()
return &"Alive"
func _play_air_slam_land_animation() -> void:
motion_executor.cancel()
stop_attack_motion()
current_action_animation = "air_slam_land"
anim_map[PRESENTATION_ATTACK] = "air_slam_land"
state = PRESENTATION_ATTACK
attack_time_left = _animation_length("air_slam_land") + 0.05
if animation_player != null:
_reset_action_animation_speed()
animation_player.stop()
_start_manual_animation(&"air_slam_land")
func _grounded_now() -> bool:
return height <= 0.0 and is_zero_approx(height_speed)
func _play_death_animation_once() -> void:
if _death_animation_started:
return
_death_animation_started = true
_pending_air_slam_land = false
motion_executor.cancel()
stop_attack_motion()
attack_time_left = 0.0
state = PRESENTATION_IDLE
if animation_player != null:
_reset_action_animation_speed()
animation_player.stop()
_start_manual_animation(&"death")
func _play_hitstun_animation_once() -> void:
if _hitstun_animation_started:
return
_hitstun_animation_started = true
_pending_air_slam_land = false
if animation_player != null:
_reset_action_animation_speed()
animation_player.stop()
_start_manual_animation(&"hit_stun")
func set_heading() -> void:
movement_motor.set_heading()
func set_sprite_height_position() -> void:
if visual != null:
visual.position = Vector2(0.0, visual_ground_offset) + Vector2.UP * height
func start_jump() -> void:
movement_motor.start_jump()
func apply_knockback(knockback: Vector2) -> void:
movement_motor.apply_knockback(knockback)
func submit_combo_input(symbol: String, forced_rating := "") -> String:
var data := _symbol_to_intent_data(symbol)
if data.is_empty():
return ""
var intent: RefCounted = load("res://scenes/components/input_intent.gd").create(data["symbol"], data["rhythm_action"], &"pressed", float(Time.get_ticks_msec()))
if not forced_rating.is_empty():
intent.judgement = _rating_result(StringName(forced_rating), 0.0)
action_controller.submit_intent(intent)
if data["symbol"] == &"A" or data["symbol"] == &"D":
var release_intent: RefCounted = load("res://scenes/components/input_intent.gd").create(data["symbol"], data["rhythm_action"], &"released", float(Time.get_ticks_msec()))
if not forced_rating.is_empty():
release_intent.judgement = _rating_result(StringName(forced_rating), 0.0)
action_controller.submit_intent(release_intent)
return last_requested_action_id
func get_combo_slots() -> Array[StringName]:
return combo_window.get_slots()
func projectile_spawn_position(action: Resource) -> Vector2:
var aim := projectile_direction(action)
var forward := -1.0 if aim.x < 0.0 else 1.0
return global_position + Vector2(forward * 30.0, -70.0)
func projectile_direction(_action: Resource) -> Vector2:
# Charge releases (zhan_bo) fire along the player's current facing, never
# auto-aimed at the nearest enemy: facing left releases left.
return Vector2.RIGHT if heading.x > 0.0 else Vector2.LEFT
func _symbol_to_intent_data(symbol: String) -> Dictionary:
match symbol:
"W":
return {"symbol": &"W", "rhythm_action": &"w"}
"A":
return {"symbol": &"A", "rhythm_action": &"a"}
"D":
return {"symbol": &"D", "rhythm_action": &"d"}
"S":
return {"symbol": &"S", "rhythm_action": &"s"}
"SP":
return {"symbol": &"SP", "rhythm_action": &"space"}
return {}
func _play_action_animation(animation_name: String, action: Resource = null) -> void:
current_action_animation = animation_name
anim_map[PRESENTATION_ATTACK] = animation_name
state = PRESENTATION_ATTACK
attack_time_left = _animation_length(animation_name)
if action != null:
attack_time_left = maxf(attack_time_left, float(action.get("action_beats")) * _beat_time() + 0.25)
var speed_scale := _action_animation_speed_scale(animation_name, action)
_charge_body_animation_active = false
_pending_air_slam_land = false
if action != null:
var direction := _displacement_direction(action)
if direction != Vector2.ZERO:
heading = direction
flip_sprites()
if animation_player != null and animation_player.has_animation(animation_name):
_stop_manual_animation()
if fx_overlay != null:
fx_overlay.visible = false
animation_player.stop()
animation_player.speed_scale = speed_scale
animation_player.play(animation_name)
elif has_player_animation(StringName(animation_name)):
if animation_player != null:
animation_player.stop()
_start_manual_animation(StringName(animation_name), speed_scale)
func has_player_animation(animation_name: StringName) -> bool:
if animation_player != null and animation_player.has_animation(str(animation_name)):
return true
return PLAYER_ANIMATION_SPECS.has(animation_name)
func _start_manual_animation(animation_name: StringName, speed_scale := 1.0) -> void:
var spec: Dictionary = PLAYER_ANIMATION_SPECS.get(animation_name, {})
_manual_animation_name = animation_name
_manual_animation_segments = spec.get("segments", [])
_manual_animation_segment = 0
_manual_animation_frame = 0
_manual_animation_elapsed = 0.0
_manual_animation_time = 0.0
_manual_animation_fps = maxf(1.0, float(spec.get("fps", 20.0)))
_manual_animation_speed_scale = maxf(0.01, speed_scale)
_manual_animation_frame_time = _segment_frame_time(_current_manual_segment())
_manual_animation_loop = bool(spec.get("loop", false))
_manual_fx = spec.get("fx", [])
if fx_overlay != null:
fx_overlay.visible = false
_manual_animation_active = not _manual_animation_segments.is_empty()
if _manual_animation_active:
_apply_manual_animation_frame()
_update_manual_fx()
func _stop_manual_animation() -> void:
_manual_animation_name = &""
_manual_animation_segments = []
_manual_animation_segment = 0
_manual_animation_frame = 0
_manual_animation_elapsed = 0.0
_manual_animation_time = 0.0
_manual_animation_speed_scale = 1.0
_manual_animation_active = false
if not _manual_fx.is_empty():
_manual_fx = []
if fx_overlay != null:
fx_overlay.visible = false
func _tick_manual_animation(delta: float) -> void:
if not _manual_animation_active:
return
var scaled_delta := delta * _manual_animation_speed_scale
_manual_animation_time += scaled_delta
_manual_animation_elapsed += scaled_delta
while _manual_animation_elapsed >= _manual_animation_frame_time and _manual_animation_active:
_manual_animation_elapsed -= _manual_animation_frame_time
_advance_manual_animation_frame()
_update_manual_fx()
func _advance_manual_animation_frame() -> void:
var segment := _current_manual_segment()
if segment.is_empty():
_stop_manual_animation()
return
_manual_animation_frame += 1
if _manual_animation_frame < int(segment.get("frames", 1)):
_apply_manual_animation_frame()
return
_manual_animation_segment += 1
_manual_animation_frame = 0
if _manual_animation_segment >= _manual_animation_segments.size():
if _manual_animation_loop:
_manual_animation_segment = 0
else:
_manual_animation_segment = _manual_animation_segments.size() - 1
_manual_animation_frame = maxi(0, int(_current_manual_segment().get("frames", 1)) - 1)
_apply_manual_animation_frame()
_manual_animation_active = false
if _manual_animation_name == &"death":
queue_free()
return
_manual_animation_frame_time = _segment_frame_time(_current_manual_segment())
_apply_manual_animation_frame()
func _segment_frame_time(segment: Dictionary) -> float:
var fps := _manual_animation_fps
if not segment.is_empty() and segment.has("fps"):
fps = maxf(1.0, float(segment.get("fps")))
return 1.0 / fps
func _update_manual_fx() -> void:
if _manual_fx.is_empty() or fx_overlay == null:
return
var active_fx: Dictionary = {}
for entry: Variant in _manual_fx:
if entry is Dictionary and _manual_animation_time >= float((entry as Dictionary).get("start", 0.0)):
active_fx = entry
if active_fx.is_empty():
fx_overlay.visible = false
return
var first := int(active_fx.get("first", 1))
var last := int(active_fx.get("last", first))
var fx_fps := maxf(1.0, float(active_fx.get("fps", 20.0)))
var local_time := _manual_animation_time - float(active_fx.get("start", 0.0))
var frame_index := first + int(local_time * fx_fps)
if frame_index > last:
if bool(active_fx.get("hold", false)):
frame_index = last
else:
fx_overlay.visible = false
return
var texture_path := str(active_fx.get("path", ""))
var texture: Texture2D = load(texture_path) if ResourceLoader.exists(texture_path) else null
if texture == null:
fx_overlay.visible = false
return
fx_overlay.texture = texture
fx_overlay.hframes = maxi(1, int(active_fx.get("hframes", 1)))
fx_overlay.vframes = maxi(1, int(active_fx.get("vframes", 1)))
fx_overlay.offset = active_fx.get("offset", Vector2.ZERO)
fx_overlay.frame = clampi(frame_index, 0, fx_overlay.hframes * fx_overlay.vframes - 1)
fx_overlay.visible = true
func _apply_manual_animation_frame() -> void:
var segment := _current_manual_segment()
if segment.is_empty() or character_sprite == null:
return
var texture := load(str(segment.get("path", ""))) as Texture2D
if texture == null:
return
character_sprite.texture = texture
character_sprite.hframes = maxi(1, int(segment.get("hframes", int(segment.get("frames", 1)))))
character_sprite.vframes = maxi(1, int(segment.get("vframes", 1)))
character_sprite.frame = clampi(_manual_animation_frame, 0, int(segment.get("frames", 1)) - 1)
if _manual_animation_name == &"death":
_align_sprite_visible_bottom_to(_death_visible_bottom_anchor)
func _current_manual_segment() -> Dictionary:
if _manual_animation_segment < 0 or _manual_animation_segment >= _manual_animation_segments.size():
return {}
var segment: Variant = _manual_animation_segments[_manual_animation_segment]
return segment if segment is Dictionary else {}
func _align_sprite_visible_bottom_to(anchor: float) -> void:
if character_sprite == null:
return
var bottom := _current_sprite_visible_bottom_pixel()
if bottom == INF:
return
character_sprite.offset.y = anchor - bottom
func _current_sprite_visible_bottom_local() -> float:
if character_sprite == null:
return INF
return _visible_frame_bottom_local(character_sprite.texture, character_sprite.hframes, character_sprite.vframes, character_sprite.frame, character_sprite.offset)
func _current_sprite_visible_bottom_pixel() -> float:
if character_sprite == null:
return INF
return _visible_frame_bottom_pixel(character_sprite.texture, character_sprite.hframes, character_sprite.vframes, character_sprite.frame)
func _visible_frame_bottom_local(texture: Texture2D, hframes: int, vframes: int, frame_index: int, sprite_offset: Vector2) -> float:
var bottom := _visible_frame_bottom_pixel(texture, hframes, vframes, frame_index)
if bottom == INF:
return INF
return sprite_offset.y + bottom
func _visible_frame_bottom_pixel(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 _displacement_direction(action: Resource) -> Vector2:
var tags = action.get("action_tags")
if tags is Array:
if (tags as Array).has(&"left"):
return Vector2.LEFT
if (tags as Array).has(&"right"):
return Vector2.RIGHT
var move_x := float(action.get("move_mult_x"))
if move_x == 0.0:
return Vector2.ZERO
if heading == Vector2.LEFT or heading == Vector2.RIGHT:
return heading
return Vector2.LEFT if move_x < 0.0 else Vector2.RIGHT
func _animation_length(animation_name: String) -> float:
if animation_player != null and animation_player.has_animation(animation_name):
return maxf(0.1, animation_player.get_animation(animation_name).length)
if PLAYER_ANIMATION_SPECS.has(StringName(animation_name)):
var spec: Dictionary = PLAYER_ANIMATION_SPECS[StringName(animation_name)]
var spec_fps := maxf(1.0, float(spec.get("fps", 20.0)))
var length := 0.0
for segment: Variant in spec.get("segments", []):
if segment is Dictionary:
var segment_fps := maxf(1.0, float((segment as Dictionary).get("fps", spec_fps)))
length += float((segment as Dictionary).get("frames", 1)) / segment_fps
return maxf(0.1, length)
return attack_duration
func _action_animation_speed_scale(animation_name: String, action: Resource) -> float:
if action == null or animation_name.is_empty():
return 1.0
var natural_length := _animation_length(animation_name)
var action_seconds := _action_visual_duration(action)
if natural_length <= 0.0 or action_seconds <= 0.0:
return 1.0
return clampf(natural_length / action_seconds, action_animation_min_speed_scale, action_animation_max_speed_scale)
func _action_visual_duration(action: Resource) -> float:
var startup_beats := float(action.get("startup_beats"))
var active_beats := float(action.get("active_beats"))
var recovery_beats := float(action.get("recovery_beats"))
if action_controller != null:
var snapshot_value = action_controller.get("_action_snapshot")
if snapshot_value is Dictionary:
var snapshot: Dictionary = snapshot_value
startup_beats = float(snapshot.get("startup_beats", startup_beats))
active_beats = float(snapshot.get("active_beats", active_beats))
recovery_beats = float(snapshot.get("recovery_beats", recovery_beats))
var duration := maxf(0.05, (startup_beats + active_beats + recovery_beats) * _beat_time())
if action_controller != null:
duration += maxf(0.0, float(action_controller.get("startup_stretch_seconds")))
return duration
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 _on_input_intent_created(intent) -> void:
action_controller.submit_intent(intent)
func _on_action_started(action: Resource, intent) -> void:
current_energy = energy_component.current
last_requested_action_id = str(action.get("id"))
action_requested.emit(last_requested_action_id)
var judgement := StringName(str(intent.judgement.get("label", "perfect"))) if intent != null else &"perfect"
var bus := _event_bus()
if bus.has_signal("skill_executed"):
bus.emit_signal("skill_executed", action, judgement)
_play_action_animation(str(action.get("animation")), action)
func _on_action_active_started(action: Resource, _intent) -> void:
current_energy = energy_component.current
_start_action_motion(action)
var animation_name := str(action.get("animation"))
if animation_name == "atk_air" or animation_name == "plunge_start":
_pending_air_slam_land = true
_spawn_world_fx(action)
func _spawn_world_fx(action: Resource) -> void:
# World FX live beside the player (ActorsContainer), addressed relatively so
# the player never reaches upward through the scene tree API surface.
var fx_parent := get_node_or_null(NodePath(".."))
if fx_parent == null:
return
var action_id := str(action.get("id"))
var forward := -1.0 if heading.x < 0.0 else 1.0
if action_id.begins_with("ground_smash"):
for index: int in range(3):
OneShotFxScript.spawn(fx_parent, global_position + Vector2(forward * (60.0 + 55.0 * index), -54.0), {
"path": "res://assets/art/characters/player/06_ground_smash/shockwave_tile.png",
"hframes": 6, "vframes": 2, "first": 0, "last": 11, "fps": 20.0,
"delay": 0.07 * index, "flip_h": forward < 0.0, "scale": 0.9,
})
elif action_id.begins_with("jian_yu"):
var level := 1
if action_id.ends_with("lv2"):
level = 2
elif action_id.ends_with("lv3"):
level = 3
var strike_count := 2 * (1 + level)
# Lv3 farthest drop = 40 + 240 = 280, matching the action's range so the
# visual footprint covers the hitbox edge-to-edge with no phantom strikes.
var spread := 120.0 + 40.0 * float(level)
var drop_height := BLADE_RAIN_DROP_FALL_SPEED * BLADE_RAIN_DROP_FALL_TIME
for index: int in range(strike_count):
var offset_x := forward * (40.0 + spread * float(index) / float(maxi(1, strike_count - 1)))
var stagger := 0.09 * index
var tile: Dictionary = BLADE_RAIN_TILE_SPECS[index % BLADE_RAIN_TILE_SPECS.size()]
var tile_scale := float(tile.get("scale", 1.0))
# Falling blade first; its flight ends exactly when the landing
# tile starts, whose own first frames carry the streak + impact.
OneShotFxScript.spawn(fx_parent, global_position + Vector2(offset_x, VISIBLE_GROUND_LINE_OFFSET - 50.0 - drop_height), {
"path": "res://assets/art/characters/player/09_blade_rain/raindrop_%d.png" % ((index % 4) + 1),
"hframes": 1, "vframes": 1, "first": 0, "last": 0, "fps": 20.0,
"delay": stagger, "duration": BLADE_RAIN_DROP_FALL_TIME,
"velocity": Vector2(0.0, BLADE_RAIN_DROP_FALL_SPEED),
"scale": tile_scale, "flip_h": forward < 0.0,
})
var tile_config := tile.duplicate()
tile_config["delay"] = stagger + BLADE_RAIN_DROP_FALL_TIME
tile_config["flip_h"] = forward < 0.0
# The tile art is bottom-anchored inside its cell: center it so the
# cell bottom (= visible lowest pixel) lands exactly on the world's
# visible ground line where the player's feet stand.
var tile_center_y := VISIBLE_GROUND_LINE_OFFSET - float(tile.get("cell_height", 360.0)) * tile_scale * 0.5
OneShotFxScript.spawn(fx_parent, global_position + Vector2(offset_x, tile_center_y), tile_config)
# The authored player sheets keep ~40px of transparent margin under the feet
# inside the 128px cell, so the visible feet baseline sits 40px above the actor
# origin (decisions.md: actors anchor at y=560, visible feet line verified at
# y=520). Ground-contact FX must anchor to that visible line, not the origin,
# or they render sunk into the floor.
const VISIBLE_GROUND_LINE_OFFSET := -40.0
const BLADE_RAIN_DROP_FALL_TIME := 0.24
const BLADE_RAIN_DROP_FALL_SPEED := 1900.0
const BLADE_RAIN_TILE_SPECS := [
{"path": "res://assets/art/characters/player/09_blade_rain/rain_lading_tile_1.png", "hframes": 6, "vframes": 2, "first": 0, "last": 11, "fps": 20.0, "scale": 1.0, "cell_height": 332.0},
{"path": "res://assets/art/characters/player/09_blade_rain/rain_lading_tile_2.png", "hframes": 9, "vframes": 1, "first": 0, "last": 8, "fps": 20.0, "scale": 1.0, "cell_height": 376.0},
{"path": "res://assets/art/characters/player/09_blade_rain/rain_lading_tile_3.png", "hframes": 5, "vframes": 2, "first": 0, "last": 9, "fps": 20.0, "scale": 1.0, "cell_height": 346.0},
{"path": "res://assets/art/characters/player/09_blade_rain/rain_lading_tile_4.png", "hframes": 5, "vframes": 2, "first": 0, "last": 9, "fps": 20.0, "scale": 1.0, "cell_height": 394.0},
]
func _on_action_finished(_action: Resource) -> void:
if _pending_air_slam_land:
# Action timed out mid-fall: keep dropping in the falling pose;
# handle_animations plays the land impact on touchdown.
motion_executor.cancel()
stop_attack_motion()
return
if current_action_animation == "air_slam_land" and _manual_animation_active:
motion_executor.cancel()
return
_set_idle_presentation()
func _on_action_cancelled(_action: Resource, _reason: StringName) -> void:
_pending_air_slam_land = false
motion_executor.cancel()
_set_idle_presentation()
func _start_action_motion(action: Resource) -> void:
var direction := _displacement_direction(action)
var move_y := float(action.get("move_mult_y"))
if move_y > 0.0:
height_speed = jump_intensity * move_y
height = maxf(height, 0.1)
state_machine.call("set_ground_state", &"Airborne")
elif move_y < 0.0 and height > 0.0:
height_speed = jump_intensity * move_y
if direction != Vector2.ZERO:
heading = direction
motion_executor.execute(action, direction, _beat_time(), _action_motion_speed(action))
attack_time_left = maxf(attack_time_left, motion_executor.duration)
begin_attack_motion(motion_executor.duration, motion_executor.velocity)
func _action_motion_speed(action: Resource) -> float:
# S6 conversion rule: move_mult_x of 1.0 means 100px of total displacement over the action.
# The motion only runs from Active start to action end, so the speed must be
# computed over that window or every action under-travels by the startup
# fraction (the 260px dash used to realize only ~195px and could stall
# inside the boss instead of piercing through).
var travel := absf(float(action.get("move_mult_x"))) * base_move_distance
if travel <= 0.0:
return 0.0
var motion_beats := float(action.get("action_beats")) - float(action.get("startup_beats"))
var duration := maxf(0.05, motion_beats * _beat_time())
return travel / duration
func _set_idle_presentation() -> void:
motion_executor.cancel()
stop_attack_motion()
attack_time_left = 0.0
state = PRESENTATION_IDLE
current_action_animation = "idle"
_stop_manual_animation()
if animation_player != null and animation_player.has_animation("idle"):
_reset_action_animation_speed()
animation_player.play("idle")
func _reset_action_animation_speed() -> void:
if animation_player != null:
animation_player.speed_scale = 1.0
_manual_animation_speed_scale = 1.0
func _on_combo_updated(slots: Array[StringName]) -> void:
combo_window_changed.emit(slots)
func _on_combo_cleared(reason: StringName) -> void:
combo_window_cleared.emit(str(reason))
func _on_charge_component_changed(current: float, maximum: float, ready: bool, active: bool) -> void:
charge_changed.emit(current, maximum, ready, active)
var bus := _event_bus()
if bus.has_signal("player_charge_changed"):
bus.emit_signal("player_charge_changed", current, maximum, ready, active)
func _on_energy_component_changed(current: int, maximum: int) -> void:
current_energy = current
max_energy = maximum
energy_changed.emit(current, maximum)
func _rating_result(label: StringName, offset_ms: float) -> Dictionary:
return {
"label": str(label),
"diff": offset_ms / 1000.0,
"abs_diff": absf(offset_ms / 1000.0),
}
func _mark_input_handled() -> void:
var viewport := get_viewport()
if viewport != null:
viewport.set_input_as_handled()
func _event_bus() -> Node:
var root := get_tree().root
var bus := root.get_node_or_null("EventBus")
if bus == null:
bus = load("res://autoload/event_bus.gd").new()
bus.name = "EventBus"
root.add_child(bus)
return bus
func _is_action_phase(phase_name: StringName) -> bool:
return state_machine != null and state_machine.get_action_phase() == phase_name
+1
View File
@@ -0,0 +1 @@
uid://dcpjibn5dex88
+532
View File
@@ -0,0 +1,532 @@
[gd_scene format=3]
[ext_resource type="Script" path="res://scenes/characters/player.gd" id="1_player_script"]
[ext_resource type="Texture2D" path="res://assets/art/characters/player/16_idle/idle_loop.png" id="2_idle"]
[ext_resource type="Texture2D" path="res://assets/art/characters/player/01_basic_attack_i/air_prep.png" id="3_atk1_prep"]
[ext_resource type="Texture2D" path="res://assets/art/characters/player/01_basic_attack_i/air_swing.png" id="4_atk1_swing"]
[ext_resource type="Texture2D" path="res://assets/art/characters/player/01_basic_attack_i/slash_overlay_fx.png" id="5_atk1_fx"]
[ext_resource type="Texture2D" path="res://assets/art/characters/player/02_basic_attack_ii/air_prep.png" id="6_atk2_prep"]
[ext_resource type="Texture2D" path="res://assets/art/characters/player/02_basic_attack_ii/air_swing.png" id="7_atk2_swing"]
[ext_resource type="Texture2D" path="res://assets/art/characters/player/02_basic_attack_ii/slash_overlay_fx.png" id="8_atk2_fx"]
[ext_resource type="Texture2D" path="res://assets/art/characters/player/07_rising_slash/prep.png" id="9_rise_prep"]
[ext_resource type="Texture2D" path="res://assets/art/characters/player/07_rising_slash/swing.png" id="10_rise_swing"]
[ext_resource type="Texture2D" path="res://assets/art/characters/player/07_rising_slash/slash_overlay_fx.png" id="11_rise_fx"]
[ext_resource type="Texture2D" path="res://assets/art/characters/player/17_turn_around/turn.png" id="12_turn"]
[ext_resource type="Texture2D" path="res://assets/art/characters/player/18_block/air_block_start.png" id="25_block_start"]
[ext_resource type="Script" path="res://scenes/components/state_machine.gd" id="13_state_machine"]
[ext_resource type="Script" path="res://scenes/components/input_component.gd" id="14_input_component"]
[ext_resource type="Script" path="res://scenes/components/movement_motor.gd" id="15_movement_motor"]
[ext_resource type="Script" path="res://scenes/components/combo_window.gd" id="16_combo_window"]
[ext_resource type="Script" path="res://scenes/combat/action_resolver.gd" id="17_action_resolver"]
[ext_resource type="Script" path="res://scenes/components/action_executor.gd" id="18_action_executor"]
[ext_resource type="Script" path="res://scenes/components/action_controller.gd" id="19_action_controller"]
[ext_resource type="Script" path="res://scenes/components/motion_executor.gd" id="20_motion_executor"]
[ext_resource type="Script" path="res://scenes/components/burst_component.gd" id="21_burst_component"]
[ext_resource type="Script" path="res://scenes/components/charge_component.gd" id="22_charge_component"]
[ext_resource type="Script" path="res://scenes/components/energy_component.gd" id="23_energy_component"]
[ext_resource type="Script" path="res://scenes/components/effect_container.gd" id="24_effect_container"]
[ext_resource type="Script" path="res://scenes/components/damage_emitter.gd" id="26_damage_emitter"]
[ext_resource type="Script" path="res://scenes/components/damage_receiver.gd" id="27_damage_receiver"]
[ext_resource type="Script" path="res://scenes/components/health_component.gd" id="28_health_component"]
[ext_resource type="Script" path="res://scenes/combat/contact_fx_spawner.gd" id="31_contact_fx"]
[ext_resource type="Script" path="res://scenes/components/streak_counter.gd" id="32_streak_counter"]
[ext_resource type="Script" path="res://scenes/components/attack_buff_component.gd" id="33_attack_buff"]
[ext_resource type="Script" path="res://scenes/components/frame_collision_driver.gd" id="34_frame_collision"]
[ext_resource type="Script" path="res://scenes/components/attack_buff_visual.gd" id="35_attack_buff_visual"]
[ext_resource type="Script" path="res://scenes/components/overhead_charge_segments.gd" id="36_overhead_charge_segments"]
[ext_resource type="Resource" path="res://resources/effects/effect_on_perfect_damage_reward.tres" id="29_perfect_reward"]
[ext_resource type="Resource" path="res://resources/effects/effect_on_landed_haste.tres" id="30_landed_haste"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_player"]
size = Vector2(18, 48)
[sub_resource type="RectangleShape2D" id="RectangleShape2D_hitbox"]
size = Vector2(48, 40)
[sub_resource type="RectangleShape2D" id="RectangleShape2D_hurtbox"]
size = Vector2(22, 52)
[sub_resource type="Animation" id="Animation_idle"]
resource_name = "idle"
length = 0.4
loop_mode = 1
step = 0.05
tracks/0/type = "value"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath("Visual/CharacterSprite:texture")
tracks/0/interp = 1
tracks/0/loop_wrap = true
tracks/0/keys = {"times": PackedFloat32Array(0), "transitions": PackedFloat32Array(1), "update": 1, "values": [ExtResource("2_idle")]}
tracks/1/type = "value"
tracks/1/imported = false
tracks/1/enabled = true
tracks/1/path = NodePath("Visual/CharacterSprite:hframes")
tracks/1/interp = 1
tracks/1/loop_wrap = true
tracks/1/keys = {"times": PackedFloat32Array(0), "transitions": PackedFloat32Array(1), "update": 1, "values": [4]}
tracks/2/type = "value"
tracks/2/imported = false
tracks/2/enabled = true
tracks/2/path = NodePath("Visual/CharacterSprite:vframes")
tracks/2/interp = 1
tracks/2/loop_wrap = true
tracks/2/keys = {"times": PackedFloat32Array(0), "transitions": PackedFloat32Array(1), "update": 1, "values": [2]}
tracks/3/type = "value"
tracks/3/imported = false
tracks/3/enabled = true
tracks/3/path = NodePath("Visual/CharacterSprite:frame")
tracks/3/interp = 1
tracks/3/loop_wrap = true
tracks/3/keys = {"times": PackedFloat32Array(0, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35), "transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1), "update": 1, "values": [0, 1, 2, 3, 4, 5, 6, 7]}
tracks/4/type = "value"
tracks/4/imported = false
tracks/4/enabled = true
tracks/4/path = NodePath("Visual/FxOverlay:visible")
tracks/4/interp = 1
tracks/4/loop_wrap = true
tracks/4/keys = {"times": PackedFloat32Array(0), "transitions": PackedFloat32Array(1), "update": 1, "values": [false]}
[sub_resource type="Animation" id="Animation_atk_ground_1"]
resource_name = "atk_ground_1"
length = 0.35
step = 0.05
tracks/0/type = "value"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath("Visual/CharacterSprite:texture")
tracks/0/interp = 1
tracks/0/loop_wrap = true
tracks/0/keys = {"times": PackedFloat32Array(0, 0.15), "transitions": PackedFloat32Array(1, 1), "update": 1, "values": [ExtResource("3_atk1_prep"), ExtResource("4_atk1_swing")]}
tracks/1/type = "value"
tracks/1/imported = false
tracks/1/enabled = true
tracks/1/path = NodePath("Visual/CharacterSprite:hframes")
tracks/1/interp = 1
tracks/1/loop_wrap = true
tracks/1/keys = {"times": PackedFloat32Array(0), "transitions": PackedFloat32Array(1), "update": 1, "values": [3]}
tracks/2/type = "value"
tracks/2/imported = false
tracks/2/enabled = true
tracks/2/path = NodePath("Visual/CharacterSprite:vframes")
tracks/2/interp = 1
tracks/2/loop_wrap = true
tracks/2/keys = {"times": PackedFloat32Array(0), "transitions": PackedFloat32Array(1), "update": 1, "values": [1]}
tracks/3/type = "value"
tracks/3/imported = false
tracks/3/enabled = true
tracks/3/path = NodePath("Visual/CharacterSprite:frame")
tracks/3/interp = 1
tracks/3/loop_wrap = true
tracks/3/keys = {"times": PackedFloat32Array(0, 0.05, 0.1, 0.15, 0.2, 0.25), "transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1), "update": 1, "values": [0, 1, 2, 0, 1, 2]}
tracks/4/type = "value"
tracks/4/imported = false
tracks/4/enabled = true
tracks/4/path = NodePath("Visual/FxOverlay:texture")
tracks/4/interp = 1
tracks/4/loop_wrap = true
tracks/4/keys = {"times": PackedFloat32Array(0.15), "transitions": PackedFloat32Array(1), "update": 1, "values": [ExtResource("5_atk1_fx")]}
tracks/5/type = "value"
tracks/5/imported = false
tracks/5/enabled = true
tracks/5/path = NodePath("Visual/FxOverlay:hframes")
tracks/5/interp = 1
tracks/5/loop_wrap = true
tracks/5/keys = {"times": PackedFloat32Array(0.15), "transitions": PackedFloat32Array(1), "update": 1, "values": [5]}
tracks/6/type = "value"
tracks/6/imported = false
tracks/6/enabled = true
tracks/6/path = NodePath("Visual/FxOverlay:vframes")
tracks/6/interp = 1
tracks/6/loop_wrap = true
tracks/6/keys = {"times": PackedFloat32Array(0.15), "transitions": PackedFloat32Array(1), "update": 1, "values": [1]}
tracks/7/type = "value"
tracks/7/imported = false
tracks/7/enabled = true
tracks/7/path = NodePath("Visual/FxOverlay:frame")
tracks/7/interp = 1
tracks/7/loop_wrap = true
tracks/7/keys = {"times": PackedFloat32Array(0.15, 0.2, 0.25, 0.3), "transitions": PackedFloat32Array(1, 1, 1, 1), "update": 1, "values": [1, 2, 3, 4]}
tracks/8/type = "value"
tracks/8/imported = false
tracks/8/enabled = true
tracks/8/path = NodePath("Visual/FxOverlay:visible")
tracks/8/interp = 1
tracks/8/loop_wrap = true
tracks/8/keys = {"times": PackedFloat32Array(0, 0.15, 0.35), "transitions": PackedFloat32Array(1, 1, 1), "update": 1, "values": [false, true, false]}
tracks/9/type = "value"
tracks/9/imported = false
tracks/9/enabled = true
tracks/9/path = NodePath("Visual/FxOverlay:offset")
tracks/9/interp = 1
tracks/9/loop_wrap = true
tracks/9/keys = {"times": PackedFloat32Array(0.15), "transitions": PackedFloat32Array(1), "update": 1, "values": [Vector2(-147, -191)]}
[sub_resource type="Animation" id="Animation_atk_ground_2"]
resource_name = "atk_ground_2"
length = 0.35
step = 0.05
tracks/0/type = "value"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath("Visual/CharacterSprite:texture")
tracks/0/interp = 1
tracks/0/loop_wrap = true
tracks/0/keys = {"times": PackedFloat32Array(0, 0.15), "transitions": PackedFloat32Array(1, 1), "update": 1, "values": [ExtResource("6_atk2_prep"), ExtResource("7_atk2_swing")]}
tracks/1/type = "value"
tracks/1/imported = false
tracks/1/enabled = true
tracks/1/path = NodePath("Visual/CharacterSprite:hframes")
tracks/1/interp = 1
tracks/1/loop_wrap = true
tracks/1/keys = {"times": PackedFloat32Array(0), "transitions": PackedFloat32Array(1), "update": 1, "values": [3]}
tracks/2/type = "value"
tracks/2/imported = false
tracks/2/enabled = true
tracks/2/path = NodePath("Visual/CharacterSprite:vframes")
tracks/2/interp = 1
tracks/2/loop_wrap = true
tracks/2/keys = {"times": PackedFloat32Array(0), "transitions": PackedFloat32Array(1), "update": 1, "values": [1]}
tracks/3/type = "value"
tracks/3/imported = false
tracks/3/enabled = true
tracks/3/path = NodePath("Visual/CharacterSprite:frame")
tracks/3/interp = 1
tracks/3/loop_wrap = true
tracks/3/keys = {"times": PackedFloat32Array(0, 0.05, 0.1, 0.15, 0.2, 0.25), "transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1), "update": 1, "values": [0, 1, 2, 0, 1, 2]}
tracks/4/type = "value"
tracks/4/imported = false
tracks/4/enabled = true
tracks/4/path = NodePath("Visual/FxOverlay:texture")
tracks/4/interp = 1
tracks/4/loop_wrap = true
tracks/4/keys = {"times": PackedFloat32Array(0.15), "transitions": PackedFloat32Array(1), "update": 1, "values": [ExtResource("8_atk2_fx")]}
tracks/5/type = "value"
tracks/5/imported = false
tracks/5/enabled = true
tracks/5/path = NodePath("Visual/FxOverlay:hframes")
tracks/5/interp = 1
tracks/5/loop_wrap = true
tracks/5/keys = {"times": PackedFloat32Array(0.15), "transitions": PackedFloat32Array(1), "update": 1, "values": [5]}
tracks/6/type = "value"
tracks/6/imported = false
tracks/6/enabled = true
tracks/6/path = NodePath("Visual/FxOverlay:vframes")
tracks/6/interp = 1
tracks/6/loop_wrap = true
tracks/6/keys = {"times": PackedFloat32Array(0.15), "transitions": PackedFloat32Array(1), "update": 1, "values": [1]}
tracks/7/type = "value"
tracks/7/imported = false
tracks/7/enabled = true
tracks/7/path = NodePath("Visual/FxOverlay:frame")
tracks/7/interp = 1
tracks/7/loop_wrap = true
tracks/7/keys = {"times": PackedFloat32Array(0.15, 0.2, 0.25, 0.3), "transitions": PackedFloat32Array(1, 1, 1, 1), "update": 1, "values": [1, 2, 3, 4]}
tracks/8/type = "value"
tracks/8/imported = false
tracks/8/enabled = true
tracks/8/path = NodePath("Visual/FxOverlay:visible")
tracks/8/interp = 1
tracks/8/loop_wrap = true
tracks/8/keys = {"times": PackedFloat32Array(0, 0.15, 0.35), "transitions": PackedFloat32Array(1, 1, 1), "update": 1, "values": [false, true, false]}
tracks/9/type = "value"
tracks/9/imported = false
tracks/9/enabled = true
tracks/9/path = NodePath("Visual/FxOverlay:offset")
tracks/9/interp = 1
tracks/9/loop_wrap = true
tracks/9/keys = {"times": PackedFloat32Array(0.15), "transitions": PackedFloat32Array(1), "update": 1, "values": [Vector2(-147, -191)]}
[sub_resource type="Animation" id="Animation_rising_slash"]
resource_name = "rising_slash"
length = 0.42
step = 0.03
tracks/0/type = "value"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath("Visual/CharacterSprite:texture")
tracks/0/interp = 1
tracks/0/loop_wrap = true
tracks/0/keys = {"times": PackedFloat32Array(0, 0.09), "transitions": PackedFloat32Array(1, 1), "update": 1, "values": [ExtResource("9_rise_prep"), ExtResource("10_rise_swing")]}
tracks/1/type = "value"
tracks/1/imported = false
tracks/1/enabled = true
tracks/1/path = NodePath("Visual/CharacterSprite:hframes")
tracks/1/interp = 1
tracks/1/loop_wrap = true
tracks/1/keys = {"times": PackedFloat32Array(0, 0.09), "transitions": PackedFloat32Array(1, 1), "update": 1, "values": [3, 5]}
tracks/2/type = "value"
tracks/2/imported = false
tracks/2/enabled = true
tracks/2/path = NodePath("Visual/CharacterSprite:vframes")
tracks/2/interp = 1
tracks/2/loop_wrap = true
tracks/2/keys = {"times": PackedFloat32Array(0, 0.09), "transitions": PackedFloat32Array(1, 1), "update": 1, "values": [1, 2]}
tracks/3/type = "value"
tracks/3/imported = false
tracks/3/enabled = true
tracks/3/path = NodePath("Visual/CharacterSprite:frame")
tracks/3/interp = 1
tracks/3/loop_wrap = true
tracks/3/keys = {"times": PackedFloat32Array(0, 0.03, 0.06, 0.09, 0.12, 0.15, 0.18, 0.21, 0.24, 0.27, 0.3, 0.33, 0.36), "transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1), "update": 1, "values": [0, 1, 2, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]}
tracks/4/type = "value"
tracks/4/imported = false
tracks/4/enabled = true
tracks/4/path = NodePath("Visual/FxOverlay:texture")
tracks/4/interp = 1
tracks/4/loop_wrap = true
tracks/4/keys = {"times": PackedFloat32Array(0.09), "transitions": PackedFloat32Array(1), "update": 1, "values": [ExtResource("11_rise_fx")]}
tracks/5/type = "value"
tracks/5/imported = false
tracks/5/enabled = true
tracks/5/path = NodePath("Visual/FxOverlay:hframes")
tracks/5/interp = 1
tracks/5/loop_wrap = true
tracks/5/keys = {"times": PackedFloat32Array(0.09), "transitions": PackedFloat32Array(1), "update": 1, "values": [5]}
tracks/6/type = "value"
tracks/6/imported = false
tracks/6/enabled = true
tracks/6/path = NodePath("Visual/FxOverlay:vframes")
tracks/6/interp = 1
tracks/6/loop_wrap = true
tracks/6/keys = {"times": PackedFloat32Array(0.09), "transitions": PackedFloat32Array(1), "update": 1, "values": [2]}
tracks/7/type = "value"
tracks/7/imported = false
tracks/7/enabled = true
tracks/7/path = NodePath("Visual/FxOverlay:frame")
tracks/7/interp = 1
tracks/7/loop_wrap = true
tracks/7/keys = {"times": PackedFloat32Array(0.09, 0.12, 0.15, 0.18, 0.21, 0.24, 0.27, 0.3, 0.33), "transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1), "update": 1, "values": [1, 2, 3, 4, 5, 6, 7, 8, 9]}
tracks/8/type = "value"
tracks/8/imported = false
tracks/8/enabled = true
tracks/8/path = NodePath("Visual/FxOverlay:visible")
tracks/8/interp = 1
tracks/8/loop_wrap = true
tracks/8/keys = {"times": PackedFloat32Array(0, 0.09, 0.42), "transitions": PackedFloat32Array(1, 1, 1), "update": 1, "values": [false, true, false]}
tracks/9/type = "value"
tracks/9/imported = false
tracks/9/enabled = true
tracks/9/path = NodePath("Visual/FxOverlay:offset")
tracks/9/interp = 1
tracks/9/loop_wrap = true
tracks/9/keys = {"times": PackedFloat32Array(0.09), "transitions": PackedFloat32Array(1), "update": 1, "values": [Vector2(-111, -156)]}
[sub_resource type="Animation" id="Animation_block_start"]
resource_name = "block_start"
length = 0.25
step = 0.05
tracks/0/type = "value"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath("Visual/CharacterSprite:texture")
tracks/0/interp = 1
tracks/0/loop_wrap = true
tracks/0/keys = {"times": PackedFloat32Array(0), "transitions": PackedFloat32Array(1), "update": 1, "values": [ExtResource("25_block_start")]}
tracks/1/type = "value"
tracks/1/imported = false
tracks/1/enabled = true
tracks/1/path = NodePath("Visual/CharacterSprite:hframes")
tracks/1/interp = 1
tracks/1/loop_wrap = true
tracks/1/keys = {"times": PackedFloat32Array(0), "transitions": PackedFloat32Array(1), "update": 1, "values": [5]}
tracks/2/type = "value"
tracks/2/imported = false
tracks/2/enabled = true
tracks/2/path = NodePath("Visual/CharacterSprite:vframes")
tracks/2/interp = 1
tracks/2/loop_wrap = true
tracks/2/keys = {"times": PackedFloat32Array(0), "transitions": PackedFloat32Array(1), "update": 1, "values": [1]}
tracks/3/type = "value"
tracks/3/imported = false
tracks/3/enabled = true
tracks/3/path = NodePath("Visual/CharacterSprite:frame")
tracks/3/interp = 1
tracks/3/loop_wrap = true
tracks/3/keys = {"times": PackedFloat32Array(0, 0.05, 0.1, 0.15, 0.2), "transitions": PackedFloat32Array(1, 1, 1, 1, 1), "update": 1, "values": [0, 1, 2, 3, 4]}
tracks/4/type = "value"
tracks/4/imported = false
tracks/4/enabled = true
tracks/4/path = NodePath("Visual/FxOverlay:visible")
tracks/4/interp = 1
tracks/4/loop_wrap = true
tracks/4/keys = {"times": PackedFloat32Array(0), "transitions": PackedFloat32Array(1), "update": 1, "values": [false]}
[sub_resource type="Animation" id="Animation_turn"]
resource_name = "turn"
length = 0.15
step = 0.025
tracks/0/type = "value"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath("Visual/CharacterSprite:texture")
tracks/0/interp = 1
tracks/0/loop_wrap = true
tracks/0/keys = {"times": PackedFloat32Array(0), "transitions": PackedFloat32Array(1), "update": 1, "values": [ExtResource("12_turn")]}
tracks/1/type = "value"
tracks/1/imported = false
tracks/1/enabled = true
tracks/1/path = NodePath("Visual/CharacterSprite:hframes")
tracks/1/interp = 1
tracks/1/loop_wrap = true
tracks/1/keys = {"times": PackedFloat32Array(0), "transitions": PackedFloat32Array(1), "update": 1, "values": [3]}
tracks/2/type = "value"
tracks/2/imported = false
tracks/2/enabled = true
tracks/2/path = NodePath("Visual/CharacterSprite:vframes")
tracks/2/interp = 1
tracks/2/loop_wrap = true
tracks/2/keys = {"times": PackedFloat32Array(0), "transitions": PackedFloat32Array(1), "update": 1, "values": [2]}
tracks/3/type = "value"
tracks/3/imported = false
tracks/3/enabled = true
tracks/3/path = NodePath("Visual/CharacterSprite:frame")
tracks/3/interp = 1
tracks/3/loop_wrap = true
tracks/3/keys = {"times": PackedFloat32Array(0, 0.025, 0.05, 0.075, 0.1, 0.125), "transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1), "update": 1, "values": [0, 1, 2, 3, 4, 5]}
tracks/4/type = "value"
tracks/4/imported = false
tracks/4/enabled = true
tracks/4/path = NodePath("Visual/FxOverlay:visible")
tracks/4/interp = 1
tracks/4/loop_wrap = true
tracks/4/keys = {"times": PackedFloat32Array(0), "transitions": PackedFloat32Array(1), "update": 1, "values": [false]}
[sub_resource type="AnimationLibrary" id="AnimationLibrary_player"]
_data = {
&"atk_ground_1": SubResource("Animation_atk_ground_1"),
&"atk_ground_2": SubResource("Animation_atk_ground_2"),
&"block_start": SubResource("Animation_block_start"),
&"idle": SubResource("Animation_idle"),
&"rising_slash": SubResource("Animation_rising_slash"),
&"turn": SubResource("Animation_turn")
}
[node name="Player" type="CharacterBody2D"]
collision_layer = 32
collision_mask = 65
floor_snap_length = 0.0
safe_margin = 0.001
script = ExtResource("1_player_script")
[node name="Visual" type="Node2D" parent="."]
[node name="CharacterSprite" type="Sprite2D" parent="Visual"]
texture_filter = 2
texture = ExtResource("2_idle")
centered = false
offset = Vector2(-64, -128)
hframes = 4
vframes = 2
[node name="FxOverlay" type="Sprite2D" parent="Visual"]
visible = false
z_index = 2
texture_filter = 2
centered = false
[node name="AttackBuffVisual" type="Node2D" parent="Visual"]
z_index = 1
script = ExtResource("35_attack_buff_visual")
[node name="FxAnimationPlayer" type="AnimationPlayer" parent="Visual"]
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
position = Vector2(0, -72)
shape = SubResource("RectangleShape2D_player")
[node name="DamageEmitter" type="Area2D" parent="."]
collision_layer = 8
collision_mask = 4
monitoring = false
script = ExtResource("26_damage_emitter")
damage = 100
[node name="CollisionShape2D" type="CollisionShape2D" parent="DamageEmitter"]
position = Vector2(0, -72)
shape = SubResource("RectangleShape2D_hitbox")
[node name="DamageReceiver" type="Area2D" parent="."]
collision_layer = 2
collision_mask = 16
script = ExtResource("27_damage_receiver")
[node name="CollisionShape2D" type="CollisionShape2D" parent="DamageReceiver"]
position = Vector2(0, -72)
shape = SubResource("RectangleShape2D_hurtbox")
[node name="OverheadChargeSegments" type="Node2D" parent="."]
visible = false
position = Vector2(0, -18)
z_index = 96
script = ExtResource("36_overhead_charge_segments")
segment_count = 3
segment_size = Vector2(42, 12)
[node name="AnimationPlayer" type="AnimationPlayer" parent="."]
libraries/ = SubResource("AnimationLibrary_player")
autoplay = &"idle"
[node name="ContactFxSpawner" type="Node" parent="."]
script = ExtResource("31_contact_fx")
[node name="StateMachine" type="Node" parent="."]
script = ExtResource("13_state_machine")
[node name="InputComponent" type="Node" parent="."]
script = ExtResource("14_input_component")
[node name="MovementMotor" type="Node" parent="."]
script = ExtResource("15_movement_motor")
[node name="ComboWindow" type="Node" parent="."]
script = ExtResource("16_combo_window")
[node name="ActionResolver" type="Node" parent="."]
script = ExtResource("17_action_resolver")
[node name="ActionExecutor" type="Node" parent="."]
script = ExtResource("18_action_executor")
energy_component_path = NodePath("../EnergyComponent")
damage_emitter_path = NodePath("../DamageEmitter")
[node name="ActionController" type="Node" parent="."]
script = ExtResource("19_action_controller")
combo_window_path = NodePath("../ComboWindow")
action_resolver_path = NodePath("../ActionResolver")
action_executor_path = NodePath("../ActionExecutor")
state_machine_path = NodePath("../StateMachine")
burst_component_path = NodePath("../BurstComponent")
[node name="MotionExecutor" type="Node" parent="."]
script = ExtResource("20_motion_executor")
[node name="BurstComponent" type="Node" parent="."]
script = ExtResource("21_burst_component")
[node name="EffectContainer" type="Node" parent="."]
script = ExtResource("24_effect_container")
initial_effects = Array[Resource]([ExtResource("29_perfect_reward"), ExtResource("30_landed_haste")])
[node name="HealthComponent" type="Node" parent="."]
script = ExtResource("28_health_component")
maximum = 1000
current = 1000
hitstun_seconds = 0.4
[node name="ChargeComponent" type="Node" parent="."]
script = ExtResource("22_charge_component")
animation_player_path = NodePath("../AnimationPlayer")
effect_sprite_path = NodePath("../Visual/FxOverlay")
[node name="EnergyComponent" type="Node" parent="."]
script = ExtResource("23_energy_component")
[node name="StreakCounter" type="Node" parent="."]
script = ExtResource("32_streak_counter")
[node name="AttackBuffComponent" type="Node" parent="."]
script = ExtResource("33_attack_buff")
[node name="FrameCollisionDriver" type="Node" parent="."]
script = ExtResource("34_frame_collision")