diff --git a/README.md b/README.md index 20cfe2d..54928c1 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,9 @@ Product first, then implementation: 5. [AI handoff](docs/AI_HANDOFF.md) — current state, known issues, next tasks 6. [Contributor rules](AGENTS.md) and [GitHub workflow](docs/GITHUB_WORKFLOW.md) -Art assets are governed by [docs/ART_SPEC.md](docs/ART_SPEC.md). All sprites are placeholders, marked with a magenta border. +The playable actors, projectile, combat effects, and matching HUD fields use the +production Art V2 pack. The legacy placeholder set remains as source material +for modules not yet integrated; see +[docs/ART_V2_INTEGRATION_REPORT.md](docs/ART_V2_INTEGRATION_REPORT.md). This repository is a greybox prototype, not a finished or publicly supported game. diff --git a/actors/boss/boss.tscn b/actors/boss/boss.tscn index 4eaeb2a..170c628 100644 --- a/actors/boss/boss.tscn +++ b/actors/boss/boss.tscn @@ -1,18 +1,20 @@ -[gd_scene load_steps=8 format=3 uid="uid://bnnbossgate001"] +[gd_scene load_steps=10 format=3 uid="uid://bnnbossgate001"] [ext_resource type="Script" path="res://actors/boss/boss_actor.gd" id="1_boss"] -[ext_resource type="SpriteFrames" path="res://actors/boss/boss_frames.tres" id="2_frames"] +[ext_resource type="SpriteFrames" path="res://assets_v2/godot/gate_warden_frames.tres" id="2_frames"] [ext_resource type="Script" path="res://core/hurtbox.gd" id="3_hurtbox"] [ext_resource type="Script" path="res://core/hitbox.gd" id="4_hitbox"] +[ext_resource type="Script" path="res://visuals/death_effect.gd" id="5_death_effect"] +[ext_resource type="Texture2D" path="res://assets_v2/effects/death_dissolve.png" id="6_death_texture"] [sub_resource type="RectangleShape2D" id="RectangleShape2D_body"] -size = Vector2(30, 52) +size = Vector2(54, 104) [sub_resource type="RectangleShape2D" id="RectangleShape2D_hurt"] -size = Vector2(38, 56) +size = Vector2(70, 120) [sub_resource type="RectangleShape2D" id="RectangleShape2D_hit"] -size = Vector2(46, 40) +size = Vector2(110, 70) [node name="Boss" type="CharacterBody2D"] collision_layer = 4 @@ -20,13 +22,14 @@ collision_mask = 1 script = ExtResource("1_boss") [node name="Sprite" type="AnimatedSprite2D" parent="."] -position = Vector2(0, -48) sprite_frames = ExtResource("2_frames") -animation = &"idle" -autoplay = "idle" +animation = &"gate_warden_idle" +autoplay = "gate_warden_idle" +centered = false +offset = Vector2(-96, -176) [node name="Body" type="CollisionShape2D" parent="."] -position = Vector2(0, -26) +position = Vector2(0, -52) shape = SubResource("RectangleShape2D_body") [node name="Hurtbox" type="Area2D" parent="."] @@ -35,7 +38,7 @@ collision_mask = 0 script = ExtResource("3_hurtbox") [node name="Shape" type="CollisionShape2D" parent="Hurtbox"] -position = Vector2(0, -28) +position = Vector2(0, -60) shape = SubResource("RectangleShape2D_hurt") [node name="AttackHitbox" type="Area2D" parent="."] @@ -44,5 +47,16 @@ collision_mask = 8 script = ExtResource("4_hitbox") [node name="Shape" type="CollisionShape2D" parent="AttackHitbox"] -position = Vector2(34, -26) +position = Vector2(66, -58) shape = SubResource("RectangleShape2D_hit") + +[node name="DeathEffect" type="Sprite2D" parent="."] +position = Vector2(0, -88) +scale = Vector2(2, 2) +texture = ExtResource("6_death_texture") +hframes = 8 +visible = false +z_index = 3 +modulate = Color(1, 1, 1, 0.72) +script = ExtResource("5_death_effect") +playback_fps = 8.0 diff --git a/actors/boss/boss_actor.gd b/actors/boss/boss_actor.gd index ba211ef..234d2f1 100644 --- a/actors/boss/boss_actor.gd +++ b/actors/boss/boss_actor.gd @@ -19,13 +19,103 @@ extends EnemyBase signal defeated(boss: BossActor) +const ACTIVE_FIRST_FRAME := 4 +const ACTIVE_LAST_FRAME := 5 + +var _death_source_id: StringName = &"unknown" +var _boss_death_published: bool = false + + +func _ready() -> void: + super._ready() + sprite.animation_finished.connect(_on_animation_finished) + sprite.frame_changed.connect(_sync_attack_window) + func start_encounter() -> void: EventBus.boss_started.emit( EventBus.context({"actor_id": actor_id(), "max_hp": max_hp()}) ) func _publish_death(source_id: StringName) -> void: + # EnemyBase owns idempotent death. Completion waits for the authored + # collapse so the victory screen cannot cover its first frame. + _death_source_id = source_id + + +func _enter_state(next: int) -> void: + match next: + State.IDLE: + sprite.play(&"gate_warden_idle") + State.CHASE: + sprite.play(&"gate_warden_walk") + State.WINDUP: + attack_hitbox.set_active(false) + sprite.modulate = Color.WHITE + sprite.play(&"gate_warden_attack_1") + sprite.frame = 0 + State.ATTACK: + _sync_attack_window() + State.RECOVER: + attack_hitbox.set_active(false) + State.HURT: + attack_hitbox.set_active(false) + sprite.play(&"gate_warden_hurt") + sprite.frame = 0 + State.DEAD: + attack_hitbox.set_active(false) + sprite.play(&"gate_warden_death") + sprite.frame = 0 + + +func _exit_state(_previous: int) -> void: + attack_hitbox.set_active(false) + + +func _update_state(delta: float) -> void: + match _state: + State.IDLE: + _decelerate(delta) + if _target_in_range(config.detection_range): + change_state(State.CHASE) + State.CHASE: + _chase(delta) + State.WINDUP: + _decelerate(delta) + _face_target() + if sprite.frame >= ACTIVE_FIRST_FRAME: + change_state(State.ATTACK) + State.ATTACK: + _decelerate(delta) + if sprite.frame > ACTIVE_LAST_FRAME: + change_state(State.RECOVER) + State.RECOVER: + _decelerate(delta) + if not sprite.is_playing(): + _cooldown = config.attack_cooldown + change_state(State.IDLE) + State.HURT: + _decelerate(delta, 400.0) + if not sprite.is_playing(): + change_state(State.CHASE if _target_in_range(config.detection_range) else State.IDLE) + State.DEAD: + pass + + +func _sync_attack_window() -> void: + var active := ( + not _is_dead + and sprite.animation == &"gate_warden_attack_1" + and sprite.frame >= ACTIVE_FIRST_FRAME + and sprite.frame <= ACTIVE_LAST_FRAME + ) + attack_hitbox.set_active(active) + + +func _on_animation_finished() -> void: + if _state != State.DEAD or _boss_death_published: + return + _boss_death_published = true EventBus.boss_died.emit( - EventBus.context({"actor_id": actor_id(), "source_id": source_id}) + EventBus.context({"actor_id": actor_id(), "source_id": _death_source_id}) ) defeated.emit(self) diff --git a/actors/enemies/ghost_archer.gd b/actors/enemies/ghost_archer.gd index 62d335a..f06ea5d 100644 --- a/actors/enemies/ghost_archer.gd +++ b/actors/enemies/ghost_archer.gd @@ -10,6 +10,7 @@ signal arrow_fired(arrow: GhostArrow) var _patrol_origin_x: float var _patrol_direction: int = 1 +var _arrow_released: bool = false func _ready() -> void: @@ -25,10 +26,28 @@ func initialize(enemy_config: EnemyConfig, target: Node2D) -> void: func _enter_state(next: int) -> void: - super._enter_state(next) - if next == State.ATTACK: - attack_hitbox.set_active(false) - _shoot() + attack_hitbox.set_active(false) + match next: + State.IDLE: + sprite.play(&"ghost_archer_idle") + State.CHASE: + sprite.play(&"ghost_archer_retreat") + State.WINDUP: + sprite.modulate = Color.WHITE + sprite.play(&"ghost_archer_aim") + sprite.frame = 0 + State.ATTACK: + _arrow_released = false + sprite.play(&"ghost_archer_shoot") + sprite.frame = 0 + State.RECOVER: + pass + State.HURT: + sprite.play(&"ghost_archer_hurt") + sprite.frame = 0 + State.DEAD: + sprite.play(&"ghost_archer_death") + sprite.frame = 0 func _update_state(delta: float) -> void: @@ -42,11 +61,17 @@ func _update_state(delta: float) -> void: State.WINDUP: _decelerate(delta) _face_target() - if _state_elapsed >= config.attack_windup: + # frame_map.json: the bow is fully drawn on aim frame 5. + if sprite.frame >= 5 or not sprite.is_playing(): change_state(State.ATTACK) State.ATTACK: _decelerate(delta) - if _state_elapsed >= config.attack_active: + _face_target() + # frame_map.json: release exactly on shoot frame 1. + if not _arrow_released and sprite.frame >= 1: + _arrow_released = true + _shoot() + if not sprite.is_playing(): change_state(State.RECOVER) State.RECOVER: _decelerate(delta) diff --git a/actors/enemies/ghost_archer.tscn b/actors/enemies/ghost_archer.tscn index 3783d97..049bab5 100644 --- a/actors/enemies/ghost_archer.tscn +++ b/actors/enemies/ghost_archer.tscn @@ -1,9 +1,20 @@ -[gd_scene load_steps=5 format=3] +[gd_scene load_steps=10 format=3] [ext_resource type="PackedScene" path="res://actors/enemies/enemy_base.tscn" id="1_base"] [ext_resource type="Script" path="res://actors/enemies/ghost_archer.gd" id="2_script"] -[ext_resource type="SpriteFrames" path="res://actors/enemies/ghost_archer_frames.tres" id="3_frames"] +[ext_resource type="SpriteFrames" path="res://assets_v2/godot/ghost_archer_frames.tres" id="3_frames"] [ext_resource type="Resource" path="res://data/actors/ghost_archer_tactics.tres" id="4_tactics"] +[ext_resource type="Script" path="res://visuals/death_effect.gd" id="5_death_effect"] +[ext_resource type="Texture2D" path="res://assets_v2/effects/death_dissolve.png" id="6_death_texture"] + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_v2_body"] +size = Vector2(20, 48) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_v2_hurt"] +size = Vector2(26, 56) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_v2_hit"] +size = Vector2(1, 1) [node name="GhostArcher" instance=ExtResource("1_base")] script = ExtResource("2_script") @@ -11,8 +22,30 @@ archer_config = ExtResource("4_tactics") [node name="Sprite" parent="." index="0"] sprite_frames = ExtResource("3_frames") -animation = &"idle" -autoplay = "idle" +animation = &"ghost_archer_idle" +autoplay = "ghost_archer_idle" +position = Vector2(0, 0) +centered = false +offset = Vector2(-48, -88) + +[node name="Body" parent="." index="1"] +position = Vector2(0, -24) +shape = SubResource("RectangleShape2D_v2_body") + +[node name="Shape" parent="Hurtbox" index="0"] +position = Vector2(0, -28) +shape = SubResource("RectangleShape2D_v2_hurt") [node name="Shape" parent="AttackHitbox" index="0"] disabled = true +shape = SubResource("RectangleShape2D_v2_hit") + +[node name="DeathEffect" type="Sprite2D" parent="." index="4"] +position = Vector2(0, -44) +texture = ExtResource("6_death_texture") +hframes = 8 +visible = false +z_index = 3 +modulate = Color(1, 1, 1, 0.72) +script = ExtResource("5_death_effect") +playback_fps = 8.0 diff --git a/actors/enemies/ghost_arrow.gd b/actors/enemies/ghost_arrow.gd index 3289177..3da6f0c 100644 --- a/actors/enemies/ghost_arrow.gd +++ b/actors/enemies/ghost_arrow.gd @@ -19,6 +19,10 @@ var _configured: bool = false func _ready() -> void: area_entered.connect(_on_area_entered) body_entered.connect(_on_body_entered) + var visual := get_node_or_null(^"Visual") as Sprite2D + var fallback := get_node_or_null(^"FallbackVisual") as CanvasItem + if fallback != null: + fallback.visible = visual == null or visual.texture == null func configure( @@ -54,6 +58,11 @@ func has_impacted() -> bool: return _has_impacted +func uses_v2_texture() -> bool: + var visual := get_node_or_null(^"Visual") as Sprite2D + return visual != null and visual.texture != null + + func _on_area_entered(area: Area2D) -> void: if _has_impacted or not (area is Hurtbox): return diff --git a/actors/enemies/ghost_arrow.tscn b/actors/enemies/ghost_arrow.tscn index 833ff09..1439eab 100644 --- a/actors/enemies/ghost_arrow.tscn +++ b/actors/enemies/ghost_arrow.tscn @@ -1,6 +1,7 @@ -[gd_scene load_steps=3 format=3] +[gd_scene load_steps=4 format=3] [ext_resource type="Script" path="res://actors/enemies/ghost_arrow.gd" id="1_arrow"] +[ext_resource type="Texture2D" path="res://assets_v2/projectiles/ghost_fire_arrow.png" id="2_arrow_texture"] [sub_resource type="CapsuleShape2D" id="CapsuleShape2D_arrow"] radius = 2.0 @@ -11,9 +12,13 @@ collision_layer = 64 collision_mask = 9 script = ExtResource("1_arrow") +[node name="Visual" type="Sprite2D" parent="."] +texture = ExtResource("2_arrow_texture") + [node name="FallbackVisual" type="Polygon2D" parent="."] polygon = PackedVector2Array(-7, -1, 4, -1, 4, -3, 8, 0, 4, 3, 4, 1, -7, 1) color = Color(0.31, 0.88, 0.65, 0.9) +visible = false [node name="Shape" type="CollisionShape2D" parent="."] rotation = 1.5708 diff --git a/actors/enemies/ghost_melee.tscn b/actors/enemies/ghost_melee.tscn index 6dd41d2..92f3a25 100644 --- a/actors/enemies/ghost_melee.tscn +++ b/actors/enemies/ghost_melee.tscn @@ -1,11 +1,49 @@ -[gd_scene load_steps=3 format=3 uid="uid://bnnghostmelee01"] +[gd_scene load_steps=8 format=3 uid="uid://bnnghostmelee01"] [ext_resource type="PackedScene" path="res://actors/enemies/enemy_base.tscn" id="1_base"] -[ext_resource type="SpriteFrames" path="res://actors/enemies/ghost_melee_frames.tres" id="2_frames"] +[ext_resource type="SpriteFrames" path="res://assets_v2/godot/melee_ghost_frames.tres" id="2_frames"] +[ext_resource type="Script" path="res://actors/enemies/melee_ghost.gd" id="3_script"] +[ext_resource type="Script" path="res://visuals/death_effect.gd" id="4_death_effect"] +[ext_resource type="Texture2D" path="res://assets_v2/effects/death_dissolve.png" id="5_death_texture"] + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_v2_body"] +size = Vector2(22, 48) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_v2_hurt"] +size = Vector2(28, 56) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_v2_hit"] +size = Vector2(46, 30) [node name="GhostMelee" instance=ExtResource("1_base")] +script = ExtResource("3_script") [node name="Sprite" parent="." index="0"] sprite_frames = ExtResource("2_frames") -animation = &"idle" -autoplay = "idle" +animation = &"melee_ghost_idle" +autoplay = "melee_ghost_idle" +position = Vector2(0, 0) +centered = false +offset = Vector2(-48, -90) + +[node name="Body" parent="." index="1"] +position = Vector2(0, -24) +shape = SubResource("RectangleShape2D_v2_body") + +[node name="Shape" parent="Hurtbox" index="0"] +position = Vector2(0, -28) +shape = SubResource("RectangleShape2D_v2_hurt") + +[node name="Shape" parent="AttackHitbox" index="0"] +position = Vector2(30, -36) +shape = SubResource("RectangleShape2D_v2_hit") + +[node name="DeathEffect" type="Sprite2D" parent="." index="4"] +position = Vector2(0, -45) +texture = ExtResource("5_death_texture") +hframes = 8 +visible = false +z_index = 3 +modulate = Color(1, 1, 1, 0.72) +script = ExtResource("4_death_effect") +playback_fps = 8.0 diff --git a/actors/enemies/melee_ghost.gd b/actors/enemies/melee_ghost.gd new file mode 100644 index 0000000..b337aa8 --- /dev/null +++ b/actors/enemies/melee_ghost.gd @@ -0,0 +1,81 @@ +class_name MeleeGhost +extends EnemyBase +## V2 visual specialization of EnemyBase. The existing AI states and damage +## path remain intact; authored animation frames only gate the existing hitbox. + +const ACTIVE_FIRST_FRAME := 4 +const ACTIVE_LAST_FRAME := 5 + + +func _ready() -> void: + super._ready() + sprite.frame_changed.connect(_sync_attack_window) + + +func _enter_state(next: int) -> void: + match next: + State.IDLE: + _play(&"melee_ghost_idle") + State.CHASE: + _play(&"melee_ghost_walk") + State.WINDUP: + attack_hitbox.set_active(false) + sprite.modulate = Color.WHITE + sprite.play(&"melee_ghost_attack") + sprite.frame = 0 + State.ATTACK: + _sync_attack_window() + State.RECOVER: + attack_hitbox.set_active(false) + State.HURT: + attack_hitbox.set_active(false) + sprite.play(&"melee_ghost_hurt") + sprite.frame = 0 + State.DEAD: + attack_hitbox.set_active(false) + sprite.play(&"melee_ghost_death") + sprite.frame = 0 + + +func _exit_state(_previous: int) -> void: + attack_hitbox.set_active(false) + + +func _update_state(delta: float) -> void: + match _state: + State.IDLE: + _decelerate(delta) + if _target_in_range(config.detection_range): + change_state(State.CHASE) + State.CHASE: + _chase(delta) + State.WINDUP: + _decelerate(delta) + _face_target() + if sprite.frame >= ACTIVE_FIRST_FRAME: + change_state(State.ATTACK) + State.ATTACK: + _decelerate(delta) + if sprite.frame > ACTIVE_LAST_FRAME: + change_state(State.RECOVER) + State.RECOVER: + _decelerate(delta) + if not sprite.is_playing(): + _cooldown = config.attack_cooldown + change_state(State.IDLE) + State.HURT: + _decelerate(delta, 400.0) + if not sprite.is_playing(): + change_state(State.CHASE if _target_in_range(config.detection_range) else State.IDLE) + State.DEAD: + pass + + +func _sync_attack_window() -> void: + var active := ( + not _is_dead + and sprite.animation == &"melee_ghost_attack" + and sprite.frame >= ACTIVE_FIRST_FRAME + and sprite.frame <= ACTIVE_LAST_FRAME + ) + attack_hitbox.set_active(active) diff --git a/actors/player/player.tscn b/actors/player/player.tscn index db804e7..5284592 100644 --- a/actors/player/player.tscn +++ b/actors/player/player.tscn @@ -1,15 +1,19 @@ [gd_scene load_steps=8 format=3 uid="uid://bnnplayer0001"] [ext_resource type="Script" path="res://actors/player/player.gd" id="1_player"] -[ext_resource type="SpriteFrames" path="res://actors/player/player_frames.tres" id="2_frames"] +[ext_resource type="SpriteFrames" path="res://assets_v2/godot/player_frames.tres" id="2_frames"] [ext_resource type="Script" path="res://core/hurtbox.gd" id="3_hurtbox"] [ext_resource type="Script" path="res://core/hitbox.gd" id="4_hitbox"] +[ext_resource type="Script" path="res://visuals/player_slash_visual.gd" id="5_slash"] +[ext_resource type="Texture2D" path="res://assets_v2/effects/blade_slash.png" id="6_slash_texture"] +[ext_resource type="Script" path="res://visuals/death_effect.gd" id="7_death_effect"] +[ext_resource type="Texture2D" path="res://assets_v2/effects/death_dissolve.png" id="8_death_texture"] [sub_resource type="RectangleShape2D" id="RectangleShape2D_body"] -size = Vector2(14, 26) +size = Vector2(18, 44) [sub_resource type="RectangleShape2D" id="RectangleShape2D_hurt"] -size = Vector2(16, 28) +size = Vector2(24, 52) [sub_resource type="RectangleShape2D" id="RectangleShape2D_hit"] size = Vector2(24, 20) @@ -20,13 +24,33 @@ collision_mask = 1 script = ExtResource("1_player") [node name="Sprite" type="AnimatedSprite2D" parent="."] -position = Vector2(0, -24) sprite_frames = ExtResource("2_frames") -animation = &"idle" -autoplay = "idle" +animation = &"player_idle" +autoplay = "player_idle" +centered = false +offset = Vector2(-48, -88) + +[node name="SlashVisual" type="Sprite2D" parent="."] +position = Vector2(18, -48) +texture = ExtResource("6_slash_texture") +hframes = 6 +visible = false +z_index = 2 +script = ExtResource("5_slash") +playback_fps = 18.0 + +[node name="DeathEffect" type="Sprite2D" parent="."] +position = Vector2(0, -44) +texture = ExtResource("8_death_texture") +hframes = 8 +visible = false +z_index = 3 +modulate = Color(1, 1, 1, 0.72) +script = ExtResource("7_death_effect") +playback_fps = 8.0 [node name="Body" type="CollisionShape2D" parent="."] -position = Vector2(0, -13) +position = Vector2(0, -22) shape = SubResource("RectangleShape2D_body") [node name="Hurtbox" type="Area2D" parent="."] @@ -35,7 +59,7 @@ collision_mask = 0 script = ExtResource("3_hurtbox") [node name="Shape" type="CollisionShape2D" parent="Hurtbox"] -position = Vector2(0, -14) +position = Vector2(0, -26) shape = SubResource("RectangleShape2D_hurt") [node name="AttackHitbox" type="Area2D" parent="."] @@ -44,10 +68,10 @@ collision_mask = 16 script = ExtResource("4_hitbox") [node name="Shape" type="CollisionShape2D" parent="AttackHitbox"] -position = Vector2(18, -14) +position = Vector2(28, -38) shape = SubResource("RectangleShape2D_hit") [node name="Camera" type="Camera2D" parent="."] -position = Vector2(0, -24) +position = Vector2(0, -44) position_smoothing_enabled = true position_smoothing_speed = 6.0 diff --git a/actors/player/states/airborne_state.gd b/actors/player/states/airborne_state.gd index 49617ad..7ba0e43 100644 --- a/actors/player/states/airborne_state.gd +++ b/actors/player/states/airborne_state.gd @@ -7,11 +7,12 @@ func id() -> StringName: return AIRBORNE func enter() -> void: - player.play(&"jump") + player.play(&"player_jump") func physics_update(delta: float) -> StringName: player.apply_gravity(delta) player.apply_horizontal_input(player.balance().player_air_control) + player.play(&"player_jump" if player.velocity.y < 0.0 else &"player_fall") if wants_attack(): return ATTACKING diff --git a/actors/player/states/attacking_state.gd b/actors/player/states/attacking_state.gd index 7246e86..3a613c6 100644 --- a/actors/player/states/attacking_state.gd +++ b/actors/player/states/attacking_state.gd @@ -1,27 +1,25 @@ class_name AttackingState extends PlayerState -## Light attack: wind-up, active window, recovery. Timings and the stamina cost -## (0 by design — see research report 6.1) come from BalanceConfig. -## -## The hitbox is only live during the active window, so the swing's reach in -## time is data, not an animation coincidence. +## V2 light attack. The authored frame map is authoritative: +## startup 0–3, active 4–5, recovery 6–7. -enum Phase { WINDUP, ACTIVE, RECOVERY } - -var _phase: int = Phase.WINDUP -var _elapsed: float = 0.0 +const ACTIVE_FIRST_FRAME := 4 +const ACTIVE_LAST_FRAME := 5 func id() -> StringName: return ATTACKING func enter() -> void: - _phase = Phase.WINDUP - _elapsed = 0.0 - player.play(&"attack") + player.attack_hitbox.set_active(false) + if not player.sprite.frame_changed.is_connected(_sync_attack_window): + player.sprite.frame_changed.connect(_sync_attack_window) + player.play(&"player_light_attack_1") player.sprite.frame = 0 player.spend_stamina(player.balance().light_attack_stamina_cost) func exit() -> void: + if player.sprite.frame_changed.is_connected(_sync_attack_window): + player.sprite.frame_changed.disconnect(_sync_attack_window) player.attack_hitbox.set_active(false) func physics_update(delta: float) -> StringName: @@ -29,21 +27,16 @@ func physics_update(delta: float) -> StringName: # Committed: the swing does not steer, but momentum is bled off so the # player does not slide through the whole animation. player.decelerate(delta) - _elapsed += delta - - var balance := player.balance() - match _phase: - Phase.WINDUP: - if _elapsed >= balance.light_attack_windup: - _phase = Phase.ACTIVE - _elapsed = 0.0 - player.attack_hitbox.set_active(true) - Phase.ACTIVE: - if _elapsed >= balance.light_attack_active: - _phase = Phase.RECOVERY - _elapsed = 0.0 - player.attack_hitbox.set_active(false) - Phase.RECOVERY: - if _elapsed >= balance.light_attack_recovery: - return GROUNDED if player.is_on_floor() else AIRBORNE + _sync_attack_window() + if player.animation_finished(): + return GROUNDED if player.is_on_floor() else AIRBORNE return &"" + + +func _sync_attack_window() -> void: + var active := ( + player.sprite.animation == &"player_light_attack_1" + and player.sprite.frame >= ACTIVE_FIRST_FRAME + and player.sprite.frame <= ACTIVE_LAST_FRAME + ) + player.attack_hitbox.set_active(active) diff --git a/actors/player/states/dead_state.gd b/actors/player/states/dead_state.gd index e076696..d08b2b0 100644 --- a/actors/player/states/dead_state.gd +++ b/actors/player/states/dead_state.gd @@ -9,7 +9,7 @@ func enter() -> void: player.input_enabled = false player.velocity = Vector2.ZERO player.attack_hitbox.set_active(false) - player.play(&"death") + player.play(&"player_death") player.sprite.frame = 0 func physics_update(delta: float) -> StringName: diff --git a/actors/player/states/grounded_state.gd b/actors/player/states/grounded_state.gd index 2a194f4..7683955 100644 --- a/actors/player/states/grounded_state.gd +++ b/actors/player/states/grounded_state.gd @@ -6,7 +6,7 @@ func id() -> StringName: return GROUNDED func enter() -> void: - player.play(&"idle") + player.play(&"player_idle") func physics_update(delta: float) -> StringName: player.apply_gravity(delta) @@ -20,5 +20,5 @@ func physics_update(delta: float) -> StringName: if not player.is_on_floor(): return AIRBORNE - player.play(&"run" if player.input_direction() != 0 else &"idle") + player.play(&"player_run" if player.input_direction() != 0 else &"player_idle") return &"" diff --git a/actors/player/states/hurt_state.gd b/actors/player/states/hurt_state.gd index 8cdbf7d..8066ecc 100644 --- a/actors/player/states/hurt_state.gd +++ b/actors/player/states/hurt_state.gd @@ -10,7 +10,7 @@ func id() -> StringName: func enter() -> void: _remaining = player.balance().player_hurt_duration - player.play(&"hurt") + player.play(&"player_hurt") player.sprite.frame = 0 func physics_update(delta: float) -> StringName: diff --git a/assets_v2/characters/gate_warden/gate_warden_attack_1.png.import b/assets_v2/characters/gate_warden/gate_warden_attack_1.png.import new file mode 100644 index 0000000..5c3405a --- /dev/null +++ b/assets_v2/characters/gate_warden/gate_warden_attack_1.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://j6rbty577upi" +path="res://.godot/imported/gate_warden_attack_1.png-55d2c6fc2166d02dc8182be7cb21274a.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets_v2/characters/gate_warden/gate_warden_attack_1.png" +dest_files=["res://.godot/imported/gate_warden_attack_1.png-55d2c6fc2166d02dc8182be7cb21274a.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/assets_v2/characters/gate_warden/gate_warden_attack_2.png.import b/assets_v2/characters/gate_warden/gate_warden_attack_2.png.import new file mode 100644 index 0000000..4d44e65 --- /dev/null +++ b/assets_v2/characters/gate_warden/gate_warden_attack_2.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dqdm7po2ghik8" +path="res://.godot/imported/gate_warden_attack_2.png-7679e14f6429408917e92e48a9e4324f.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets_v2/characters/gate_warden/gate_warden_attack_2.png" +dest_files=["res://.godot/imported/gate_warden_attack_2.png-7679e14f6429408917e92e48a9e4324f.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/assets_v2/characters/gate_warden/gate_warden_death.png.import b/assets_v2/characters/gate_warden/gate_warden_death.png.import new file mode 100644 index 0000000..c03281f --- /dev/null +++ b/assets_v2/characters/gate_warden/gate_warden_death.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c8441m0s2q81m" +path="res://.godot/imported/gate_warden_death.png-fba36d5833078e9ab2907a8988ab22eb.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets_v2/characters/gate_warden/gate_warden_death.png" +dest_files=["res://.godot/imported/gate_warden_death.png-fba36d5833078e9ab2907a8988ab22eb.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/assets_v2/characters/gate_warden/gate_warden_hurt.png.import b/assets_v2/characters/gate_warden/gate_warden_hurt.png.import new file mode 100644 index 0000000..8dbc9c5 --- /dev/null +++ b/assets_v2/characters/gate_warden/gate_warden_hurt.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b1kd54q3hp82d" +path="res://.godot/imported/gate_warden_hurt.png-543808f87028b14f2a25f4ff2f9e6354.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets_v2/characters/gate_warden/gate_warden_hurt.png" +dest_files=["res://.godot/imported/gate_warden_hurt.png-543808f87028b14f2a25f4ff2f9e6354.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/assets_v2/characters/gate_warden/gate_warden_idle.png.import b/assets_v2/characters/gate_warden/gate_warden_idle.png.import new file mode 100644 index 0000000..cb20c0c --- /dev/null +++ b/assets_v2/characters/gate_warden/gate_warden_idle.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dx25xv64b07w1" +path="res://.godot/imported/gate_warden_idle.png-3025077d03ca4e33bd39dbef99b3840f.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets_v2/characters/gate_warden/gate_warden_idle.png" +dest_files=["res://.godot/imported/gate_warden_idle.png-3025077d03ca4e33bd39dbef99b3840f.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/assets_v2/characters/gate_warden/gate_warden_walk.png.import b/assets_v2/characters/gate_warden/gate_warden_walk.png.import new file mode 100644 index 0000000..c410e18 --- /dev/null +++ b/assets_v2/characters/gate_warden/gate_warden_walk.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://d3cpg18mbw5ly" +path="res://.godot/imported/gate_warden_walk.png-28bfd73c2fa4dcd80e41958860202947.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets_v2/characters/gate_warden/gate_warden_walk.png" +dest_files=["res://.godot/imported/gate_warden_walk.png-28bfd73c2fa4dcd80e41958860202947.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/assets_v2/characters/ghost_archer/ghost_archer_aim.png.import b/assets_v2/characters/ghost_archer/ghost_archer_aim.png.import new file mode 100644 index 0000000..9a69daf --- /dev/null +++ b/assets_v2/characters/ghost_archer/ghost_archer_aim.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cmst5hbssnlno" +path="res://.godot/imported/ghost_archer_aim.png-d2d528df8799bd0b83e71aa4c0f36ea2.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets_v2/characters/ghost_archer/ghost_archer_aim.png" +dest_files=["res://.godot/imported/ghost_archer_aim.png-d2d528df8799bd0b83e71aa4c0f36ea2.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/assets_v2/characters/ghost_archer/ghost_archer_death.png.import b/assets_v2/characters/ghost_archer/ghost_archer_death.png.import new file mode 100644 index 0000000..5c1ee79 --- /dev/null +++ b/assets_v2/characters/ghost_archer/ghost_archer_death.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://utefdtrhsnxn" +path="res://.godot/imported/ghost_archer_death.png-4b527082d473b378704eef605e449f8f.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets_v2/characters/ghost_archer/ghost_archer_death.png" +dest_files=["res://.godot/imported/ghost_archer_death.png-4b527082d473b378704eef605e449f8f.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/assets_v2/characters/ghost_archer/ghost_archer_hurt.png.import b/assets_v2/characters/ghost_archer/ghost_archer_hurt.png.import new file mode 100644 index 0000000..9602030 --- /dev/null +++ b/assets_v2/characters/ghost_archer/ghost_archer_hurt.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bg6n2ink626oe" +path="res://.godot/imported/ghost_archer_hurt.png-966e0ef3067991128e1e0faf3d14e49b.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets_v2/characters/ghost_archer/ghost_archer_hurt.png" +dest_files=["res://.godot/imported/ghost_archer_hurt.png-966e0ef3067991128e1e0faf3d14e49b.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/assets_v2/characters/ghost_archer/ghost_archer_idle.png.import b/assets_v2/characters/ghost_archer/ghost_archer_idle.png.import new file mode 100644 index 0000000..5ab45fc --- /dev/null +++ b/assets_v2/characters/ghost_archer/ghost_archer_idle.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ufhdunh7tx0g" +path="res://.godot/imported/ghost_archer_idle.png-f80c504667d65e8a9049ab2d2e0f13ce.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets_v2/characters/ghost_archer/ghost_archer_idle.png" +dest_files=["res://.godot/imported/ghost_archer_idle.png-f80c504667d65e8a9049ab2d2e0f13ce.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/assets_v2/characters/ghost_archer/ghost_archer_retreat.png.import b/assets_v2/characters/ghost_archer/ghost_archer_retreat.png.import new file mode 100644 index 0000000..d9cca5c --- /dev/null +++ b/assets_v2/characters/ghost_archer/ghost_archer_retreat.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b8cdv28hxq057" +path="res://.godot/imported/ghost_archer_retreat.png-c1593dc723ceaa23c691361b5fff2a75.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets_v2/characters/ghost_archer/ghost_archer_retreat.png" +dest_files=["res://.godot/imported/ghost_archer_retreat.png-c1593dc723ceaa23c691361b5fff2a75.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/assets_v2/characters/ghost_archer/ghost_archer_shoot.png.import b/assets_v2/characters/ghost_archer/ghost_archer_shoot.png.import new file mode 100644 index 0000000..fc41cd4 --- /dev/null +++ b/assets_v2/characters/ghost_archer/ghost_archer_shoot.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bxaee112ws615" +path="res://.godot/imported/ghost_archer_shoot.png-6fbcb7deb382192364effd26ff78fc3c.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets_v2/characters/ghost_archer/ghost_archer_shoot.png" +dest_files=["res://.godot/imported/ghost_archer_shoot.png-6fbcb7deb382192364effd26ff78fc3c.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/assets_v2/characters/melee_ghost/melee_ghost_attack.png.import b/assets_v2/characters/melee_ghost/melee_ghost_attack.png.import new file mode 100644 index 0000000..b6cf12f --- /dev/null +++ b/assets_v2/characters/melee_ghost/melee_ghost_attack.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cljpnpfd4s2vm" +path="res://.godot/imported/melee_ghost_attack.png-4bb00c0167bce8093773d8260e3c2285.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets_v2/characters/melee_ghost/melee_ghost_attack.png" +dest_files=["res://.godot/imported/melee_ghost_attack.png-4bb00c0167bce8093773d8260e3c2285.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/assets_v2/characters/melee_ghost/melee_ghost_death.png.import b/assets_v2/characters/melee_ghost/melee_ghost_death.png.import new file mode 100644 index 0000000..4645a6b --- /dev/null +++ b/assets_v2/characters/melee_ghost/melee_ghost_death.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b31o0wt7r06e5" +path="res://.godot/imported/melee_ghost_death.png-87bf7b6f79744fc31ef1890ff456b948.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets_v2/characters/melee_ghost/melee_ghost_death.png" +dest_files=["res://.godot/imported/melee_ghost_death.png-87bf7b6f79744fc31ef1890ff456b948.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/assets_v2/characters/melee_ghost/melee_ghost_hurt.png.import b/assets_v2/characters/melee_ghost/melee_ghost_hurt.png.import new file mode 100644 index 0000000..97be28b --- /dev/null +++ b/assets_v2/characters/melee_ghost/melee_ghost_hurt.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b4tj0t5tdxqa5" +path="res://.godot/imported/melee_ghost_hurt.png-d0650d60650818199caed31e3c50d4e2.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets_v2/characters/melee_ghost/melee_ghost_hurt.png" +dest_files=["res://.godot/imported/melee_ghost_hurt.png-d0650d60650818199caed31e3c50d4e2.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/assets_v2/characters/melee_ghost/melee_ghost_idle.png.import b/assets_v2/characters/melee_ghost/melee_ghost_idle.png.import new file mode 100644 index 0000000..71ec021 --- /dev/null +++ b/assets_v2/characters/melee_ghost/melee_ghost_idle.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://birc1yirpasb8" +path="res://.godot/imported/melee_ghost_idle.png-dc160e6e8b313fdf23483a6486b07d63.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets_v2/characters/melee_ghost/melee_ghost_idle.png" +dest_files=["res://.godot/imported/melee_ghost_idle.png-dc160e6e8b313fdf23483a6486b07d63.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/assets_v2/characters/melee_ghost/melee_ghost_walk.png.import b/assets_v2/characters/melee_ghost/melee_ghost_walk.png.import new file mode 100644 index 0000000..6096855 --- /dev/null +++ b/assets_v2/characters/melee_ghost/melee_ghost_walk.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dkmjswuuwxtb0" +path="res://.godot/imported/melee_ghost_walk.png-276d25bac0480ef7dcf2ea756e8d694a.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets_v2/characters/melee_ghost/melee_ghost_walk.png" +dest_files=["res://.godot/imported/melee_ghost_walk.png-276d25bac0480ef7dcf2ea756e8d694a.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/assets_v2/characters/player/player_death.png.import b/assets_v2/characters/player/player_death.png.import new file mode 100644 index 0000000..162c37a --- /dev/null +++ b/assets_v2/characters/player/player_death.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dt4026i4msp1y" +path="res://.godot/imported/player_death.png-5addb46f63130cb5ee1c23398de3b76f.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets_v2/characters/player/player_death.png" +dest_files=["res://.godot/imported/player_death.png-5addb46f63130cb5ee1c23398de3b76f.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/assets_v2/characters/player/player_fall.png.import b/assets_v2/characters/player/player_fall.png.import new file mode 100644 index 0000000..2de7def --- /dev/null +++ b/assets_v2/characters/player/player_fall.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://degl70y52h452" +path="res://.godot/imported/player_fall.png-ca7b6848ca99a61b8a9f92ed678b9120.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets_v2/characters/player/player_fall.png" +dest_files=["res://.godot/imported/player_fall.png-ca7b6848ca99a61b8a9f92ed678b9120.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/assets_v2/characters/player/player_heavy_attack.png.import b/assets_v2/characters/player/player_heavy_attack.png.import new file mode 100644 index 0000000..d06e761 --- /dev/null +++ b/assets_v2/characters/player/player_heavy_attack.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dijfvtkagpo05" +path="res://.godot/imported/player_heavy_attack.png-65dc856cc0a2e5893ed67236886c8926.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets_v2/characters/player/player_heavy_attack.png" +dest_files=["res://.godot/imported/player_heavy_attack.png-65dc856cc0a2e5893ed67236886c8926.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/assets_v2/characters/player/player_hurt.png.import b/assets_v2/characters/player/player_hurt.png.import new file mode 100644 index 0000000..03cdd06 --- /dev/null +++ b/assets_v2/characters/player/player_hurt.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://74eot40vnbfb" +path="res://.godot/imported/player_hurt.png-6e20f3fc7495cc01fb3bf558ab884d83.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets_v2/characters/player/player_hurt.png" +dest_files=["res://.godot/imported/player_hurt.png-6e20f3fc7495cc01fb3bf558ab884d83.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/assets_v2/characters/player/player_idle.png.import b/assets_v2/characters/player/player_idle.png.import new file mode 100644 index 0000000..8db47d6 --- /dev/null +++ b/assets_v2/characters/player/player_idle.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dno2lakm1q7nn" +path="res://.godot/imported/player_idle.png-f3a900ae7b18aaae603e94e0f90994ca.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets_v2/characters/player/player_idle.png" +dest_files=["res://.godot/imported/player_idle.png-f3a900ae7b18aaae603e94e0f90994ca.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/assets_v2/characters/player/player_jump.png.import b/assets_v2/characters/player/player_jump.png.import new file mode 100644 index 0000000..6220f1f --- /dev/null +++ b/assets_v2/characters/player/player_jump.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://uagbrjo32t7g" +path="res://.godot/imported/player_jump.png-7e099fa489dc2c2088fff924fbdd469d.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets_v2/characters/player/player_jump.png" +dest_files=["res://.godot/imported/player_jump.png-7e099fa489dc2c2088fff924fbdd469d.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/assets_v2/characters/player/player_light_attack_1.png.import b/assets_v2/characters/player/player_light_attack_1.png.import new file mode 100644 index 0000000..68a53e4 --- /dev/null +++ b/assets_v2/characters/player/player_light_attack_1.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dhivsiv1jct5c" +path="res://.godot/imported/player_light_attack_1.png-7c23cbc793659ad75f714443c7c51390.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets_v2/characters/player/player_light_attack_1.png" +dest_files=["res://.godot/imported/player_light_attack_1.png-7c23cbc793659ad75f714443c7c51390.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/assets_v2/characters/player/player_light_attack_2.png.import b/assets_v2/characters/player/player_light_attack_2.png.import new file mode 100644 index 0000000..e247ba7 --- /dev/null +++ b/assets_v2/characters/player/player_light_attack_2.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bcxjtg8emol8h" +path="res://.godot/imported/player_light_attack_2.png-94e3a934a8524f9da8aeecc1f819f35e.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets_v2/characters/player/player_light_attack_2.png" +dest_files=["res://.godot/imported/player_light_attack_2.png-94e3a934a8524f9da8aeecc1f819f35e.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/assets_v2/characters/player/player_run.png.import b/assets_v2/characters/player/player_run.png.import new file mode 100644 index 0000000..5ae7f9f --- /dev/null +++ b/assets_v2/characters/player/player_run.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dd2c7ln422rmx" +path="res://.godot/imported/player_run.png-c433a47e7243b632ae312a431c5e09db.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets_v2/characters/player/player_run.png" +dest_files=["res://.godot/imported/player_run.png-c433a47e7243b632ae312a431c5e09db.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/assets_v2/effects/blade_slash.png.import b/assets_v2/effects/blade_slash.png.import new file mode 100644 index 0000000..70b24e9 --- /dev/null +++ b/assets_v2/effects/blade_slash.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://gdroiq70dtrq" +path="res://.godot/imported/blade_slash.png-3741e87deef95279eb3b52be03bf1db3.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets_v2/effects/blade_slash.png" +dest_files=["res://.godot/imported/blade_slash.png-3741e87deef95279eb3b52be03bf1db3.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/assets_v2/effects/death_dissolve.png.import b/assets_v2/effects/death_dissolve.png.import new file mode 100644 index 0000000..f96a2f9 --- /dev/null +++ b/assets_v2/effects/death_dissolve.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b2euomx6hqvmr" +path="res://.godot/imported/death_dissolve.png-da26d00dcede2c530dad81da82ae2857.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets_v2/effects/death_dissolve.png" +dest_files=["res://.godot/imported/death_dissolve.png-da26d00dcede2c530dad81da82ae2857.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/assets_v2/effects/hit.png.import b/assets_v2/effects/hit.png.import new file mode 100644 index 0000000..03354e8 --- /dev/null +++ b/assets_v2/effects/hit.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bk4ibkefdygf" +path="res://.godot/imported/hit.png-9d767548c80c50bf28c1dc0c91212aa8.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets_v2/effects/hit.png" +dest_files=["res://.godot/imported/hit.png-9d767548c80c50bf28c1dc0c91212aa8.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/assets_v2/icons/icon_ghostfire.png.import b/assets_v2/icons/icon_ghostfire.png.import new file mode 100644 index 0000000..5feb75b --- /dev/null +++ b/assets_v2/icons/icon_ghostfire.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bvhxkblt8ioay" +path="res://.godot/imported/icon_ghostfire.png-702e2140ea0f25e29c07fc3292edab97.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets_v2/icons/icon_ghostfire.png" +dest_files=["res://.godot/imported/icon_ghostfire.png-702e2140ea0f25e29c07fc3292edab97.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/assets_v2/icons/icon_obsession.png.import b/assets_v2/icons/icon_obsession.png.import new file mode 100644 index 0000000..6092c3a --- /dev/null +++ b/assets_v2/icons/icon_obsession.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://eie1xexltsff" +path="res://.godot/imported/icon_obsession.png-033b7bd6686c8ec3615b954972ca2f8c.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets_v2/icons/icon_obsession.png" +dest_files=["res://.godot/imported/icon_obsession.png-033b7bd6686c8ec3615b954972ca2f8c.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/assets_v2/icons/icon_qi.png.import b/assets_v2/icons/icon_qi.png.import new file mode 100644 index 0000000..c8ab168 --- /dev/null +++ b/assets_v2/icons/icon_qi.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bt7876vq0rbpo" +path="res://.godot/imported/icon_qi.png-4aeddc91b381f3565842788346310541.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets_v2/icons/icon_qi.png" +dest_files=["res://.godot/imported/icon_qi.png-4aeddc91b381f3565842788346310541.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/assets_v2/icons/icon_songdao.png.import b/assets_v2/icons/icon_songdao.png.import new file mode 100644 index 0000000..ceeb0c3 --- /dev/null +++ b/assets_v2/icons/icon_songdao.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ckh78856gf7vy" +path="res://.godot/imported/icon_songdao.png-983974d374eae7380e182341491aaae3.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets_v2/icons/icon_songdao.png" +dest_files=["res://.godot/imported/icon_songdao.png-983974d374eae7380e182341491aaae3.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/assets_v2/icons/icon_talisman.png.import b/assets_v2/icons/icon_talisman.png.import new file mode 100644 index 0000000..210a013 --- /dev/null +++ b/assets_v2/icons/icon_talisman.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dxtrimjtoer1b" +path="res://.godot/imported/icon_talisman.png-eb0392b04f496bb6266bf2daf30e51e8.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets_v2/icons/icon_talisman.png" +dest_files=["res://.godot/imported/icon_talisman.png-eb0392b04f496bb6266bf2daf30e51e8.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/assets_v2/icons/icon_yang.png.import b/assets_v2/icons/icon_yang.png.import new file mode 100644 index 0000000..59e34dd --- /dev/null +++ b/assets_v2/icons/icon_yang.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c0h1wuutul0ou" +path="res://.godot/imported/icon_yang.png-aec7b01804e56686be9b78f012623620.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets_v2/icons/icon_yang.png" +dest_files=["res://.godot/imported/icon_yang.png-aec7b01804e56686be9b78f012623620.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/assets_v2/projectiles/ghost_fire_arrow.png.import b/assets_v2/projectiles/ghost_fire_arrow.png.import new file mode 100644 index 0000000..ca6063c --- /dev/null +++ b/assets_v2/projectiles/ghost_fire_arrow.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dn4xo57ducjhg" +path="res://.godot/imported/ghost_fire_arrow.png-70dea4f205556bd0adda7db065ce6d59.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets_v2/projectiles/ghost_fire_arrow.png" +dest_files=["res://.godot/imported/ghost_fire_arrow.png-70dea4f205556bd0adda7db065ce6d59.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/assets_v2/review/integration_archer_aim.png b/assets_v2/review/integration_archer_aim.png new file mode 100644 index 0000000..8c0afdc Binary files /dev/null and b/assets_v2/review/integration_archer_aim.png differ diff --git a/assets_v2/review/integration_archer_arrow.png b/assets_v2/review/integration_archer_arrow.png new file mode 100644 index 0000000..c94f1ff Binary files /dev/null and b/assets_v2/review/integration_archer_arrow.png differ diff --git a/assets_v2/review/integration_boss_attack.png b/assets_v2/review/integration_boss_attack.png new file mode 100644 index 0000000..b767eed Binary files /dev/null and b/assets_v2/review/integration_boss_attack.png differ diff --git a/assets_v2/review/integration_boss_death.png b/assets_v2/review/integration_boss_death.png new file mode 100644 index 0000000..b034ea0 Binary files /dev/null and b/assets_v2/review/integration_boss_death.png differ diff --git a/assets_v2/review/integration_idle.png b/assets_v2/review/integration_idle.png new file mode 100644 index 0000000..ef22a69 Binary files /dev/null and b/assets_v2/review/integration_idle.png differ diff --git a/assets_v2/review/integration_jump.png b/assets_v2/review/integration_jump.png new file mode 100644 index 0000000..8a15147 Binary files /dev/null and b/assets_v2/review/integration_jump.png differ diff --git a/assets_v2/review/integration_player_attack_hitbox.png b/assets_v2/review/integration_player_attack_hitbox.png new file mode 100644 index 0000000..fcc8a95 Binary files /dev/null and b/assets_v2/review/integration_player_attack_hitbox.png differ diff --git a/assets_v2/review/integration_run.png b/assets_v2/review/integration_run.png new file mode 100644 index 0000000..b3c846e Binary files /dev/null and b/assets_v2/review/integration_run.png differ diff --git a/data/actors/boss_gate_guardian.tres b/data/actors/boss_gate_guardian.tres index c068267..c7b809f 100644 --- a/data/actors/boss_gate_guardian.tres +++ b/data/actors/boss_gate_guardian.tres @@ -16,10 +16,10 @@ attack_range = 78.0 preferred_gap = 48.0 attack_damage = 22.0 attack_skill_multiplier = 1.0 -attack_windup = 0.55 -attack_active = 0.18 -attack_recovery = 0.75 +attack_windup = 0.445 +attack_active = 0.222 +attack_recovery = 0.445 attack_cooldown = 0.6 -hurt_duration = 0.14 -death_duration = 1.4 +hurt_duration = 0.5 +death_duration = 2.05 telegraph_tint = Color(1.9, 1, 1.4, 1) diff --git a/data/actors/ghost_archer.tres b/data/actors/ghost_archer.tres index f2246e8..05f009c 100644 --- a/data/actors/ghost_archer.tres +++ b/data/actors/ghost_archer.tres @@ -16,10 +16,10 @@ attack_range = 220.0 preferred_gap = 150.0 attack_damage = 8.0 attack_skill_multiplier = 1.0 -attack_windup = 0.55 -attack_active = 0.12 -attack_recovery = 0.38 +attack_windup = 0.75 +attack_active = 0.286 +attack_recovery = 0.05 attack_cooldown = 0.85 -hurt_duration = 0.18 -death_duration = 0.5 +hurt_duration = 0.4 +death_duration = 1.05 telegraph_tint = Color(1.15, 1.75, 1.45, 1) diff --git a/data/actors/ghost_archer_tactics.tres b/data/actors/ghost_archer_tactics.tres index 5db18f8..bab6344 100644 --- a/data/actors/ghost_archer_tactics.tres +++ b/data/actors/ghost_archer_tactics.tres @@ -12,4 +12,4 @@ retreat_distance = 92.0 projectile_scene = ExtResource("2_projectile") projectile_speed = 190.0 projectile_lifetime = 2.4 -projectile_spawn_offset = Vector2(18, -22) +projectile_spawn_offset = Vector2(24, -48) diff --git a/data/actors/ghost_melee.tres b/data/actors/ghost_melee.tres index 7e45dde..69f5ec1 100644 --- a/data/actors/ghost_melee.tres +++ b/data/actors/ghost_melee.tres @@ -17,9 +17,9 @@ preferred_gap = 34.0 attack_damage = 9.0 attack_skill_multiplier = 1.0 attack_windup = 0.4 -attack_active = 0.15 -attack_recovery = 0.6 +attack_active = 0.2 +attack_recovery = 0.2 attack_cooldown = 0.35 -hurt_duration = 0.18 -death_duration = 0.5 +hurt_duration = 0.4 +death_duration = 1.05 telegraph_tint = Color(1.7, 1.1, 1, 1) diff --git a/docs/AI_HANDOFF.md b/docs/AI_HANDOFF.md index 7ad5698..d647705 100644 --- a/docs/AI_HANDOFF.md +++ b/docs/AI_HANDOFF.md @@ -1,166 +1,100 @@ # AI Handoff -- **Current branch:** `codex/x02-ghost-archer` -- **Current phase:** X02 Ghost Archer implemented on the M1 foundation; PR #4 open -- **Next owner:** Claude and Game Director for X02 review +- **Current branch:** `codex/m15-art-v2-integration` +- **Current phase:** M1.5 Art V2 integrated; draft PR #6 open for review +- **Next owner:** Claude and Game Director for visual/gameplay review - **Last updated:** 2026-08-02 (Australia/Melbourne) ---- +## Playable status -## M1 playable status +The complete M1 loop remains playable: arena wave → sacrifice → Gate Warden → +victory/defeat → restart. Normal gameplay now renders the production V2 player, +melee ghost, Ghost Archer, Ghost Arrow, Gate Warden, combat effects, and matching +HUD icons. Debug collision shapes remain off by default and available on F8. -**Playable end to end.** Launch → arena → move, jump, attack → clear a wave of -three ghost soldiers → preview and accept one sacrifice → become measurably -sharper and structurally weaker → fight the prototype Boss → victory or death → -restart. +## M1.5 integration -Verified by the automated run-loop case and by capturing frames under a virtual -display during development. Engine: Godot 4.3 stable. - -## Implemented systems - -| System | State | +| Area | Integrated mapping | | --- | --- | -| `RunState` | Authoritative; private fields, named commands, snapshot/restore/clone/hash | -| `CombatResolver` | Single damage path, bucket order frozen, breakdown reported | -| `IntegrityService` | Five-component structural formula; momentary state excluded | -| `SacrificeService` | Shared preview/apply transaction with whole-state rollback | -| `EventBus` | 11 signals with a uniform correlation envelope | -| `RNGService` | Per-subsystem streams derived from the run seed | -| `BalanceConfig` | Every tunable number, in `data/balance_config.tres` | -| Player | Move, jump, light attack, hit reaction, death, restart, camera follow, 5-state machine | -| `EnemyBase` | Detect, approach, telegraphed wind-up, attack, recovery, hurt, idempotent death | -| Reference enemy | 鬼卒 Ghost Soldier at X01 timings | -| Ghost Archer (X02) | Patrol, detect, maintain range, retreat, aim, shoot, cooldown, hurt and death | -| Ghost Arrow (X02) | Fixed trajectory, one hit, owner exclusion, impact/timeout cleanup, CombatResolver path | -| `BossActor` | Extends `EnemyBase`; idle, chase, one telegraphed melee attack, health bar, victory | -| Arena | One fixed 1024×360 space, collision, spawns, parallax, camera bounds | -| UI | HUD, sacrifice preview/confirm, death and victory screens, restart | -| Debug | Panel plus 9 commands, all routed through `RunCoordinator` | -| Tests + CI | 52 tests / 205 assertions; `godot-tests` workflow | +| Player | idle, run, jump, fall, light attack 1, hurt, death | +| Melee ghost | idle, walk, attack, hurt, death | +| Ghost Archer | idle, retreat, aim, shoot, hurt, death | +| Ghost Arrow | supplied 32×32 projectile texture; hidden emergency fallback | +| Gate Warden | idle, walk, attack 1, hurt, death | +| Effects | blade slash, confirmed hit, post-animation death dissolve | +| HUD | HP, stamina, attack, integrity/Boss, imbalance, sacrifice icons | + +Animation frames are authoritative only for presentation and for gating the +existing attack flow. Player and melee active frames are 4–5; Gate Warden active +frames are 4–5; Ghost Archer releases exactly once on shoot frame 1 after aim +reaches full draw at frame 5. Damage calculation remains entirely in +`CombatResolver`. Player defeat UI and Boss victory wait for their respective +death strips to complete. + +See `docs/ART_V2_INTEGRATION_REPORT.md` for the full asset inventory, dimensions, +frame rates, pivots, foot positions, timing windows, fallbacks, and unused art. + +## Architecture compliance + +No frozen framework interface changed. In particular, this work leaves +`RunState`, `CombatResolver`, `DamageContext`, `DamageResult`, +`IntegrityService`, `SacrificeService`, `EventBus`, `RNGService`, +`RunCoordinator`, `EnemyBase`, the player controller/state-machine foundation, +and the Hitbox/Hurtbox pipeline unchanged. The V2 adapters live in concrete +actor/state scripts and scenes. + +## Notable files + +### Added + +- `actors/enemies/melee_ghost.gd` +- `visuals/player_slash_visual.gd` +- `visuals/death_effect.gd` +- `visuals/one_shot_sprite.gd` +- `visuals/combat_effects.gd` +- `tests/cases/test_art_v2.gd` +- `docs/ART_V2_INTEGRATION_REPORT.md` + +### Updated + +- Existing player, melee ghost, Ghost Archer/Arrow, and Boss scenes/scripts +- Concrete player state scripts for animation selection and hitbox frame gating +- Existing actor configuration resources for authored strip durations +- Main composition, HUD, sacrifice panel, debug panel, and result screen +- Actor, Ghost Archer, and end-to-end run-loop tests + +## Tests + +- **Local:** 66 tests, 421 assertions, 0 failures +- **Engine:** Godot 4.7.1 stable headless locally; CI remains pinned to 4.3 +- **Commands:** + - `godot --headless --import` + - `godot --headless --path . res://tests/test_runner.tscn` + +Coverage added for V2 resource/scene loading, all required player mappings, +player/melee/Boss active frames, Archer release timing, hurt/death overrides, +Boss victory deferral, no post-death release, and debug-overlay defaults. + +Visual evidence is stored in `assets_v2/review/integration_*.png`: idle, run, +jump, player active-frame hitbox, Archer aim/arrow, and Boss attack/death. Godot +rendered the live main scene at 640×360. The environment's interactive macOS +control service and MP4 conversion were unavailable, so no preview video was +added. + +## Known limitations and next work + +1. Only actions already present in gameplay are mapped. Player light attack 2, + heavy attack, and Gate Warden attack 2 remain unused. +2. Corpse Beast art remains reserved for X03. +3. The Ghost Arrow polygon remains hidden as an emergency load-failure fallback. +4. Combat has no audio; audio integration remains outside M1.5 scope. +5. X04 now has a second supplied attack strip, but still depends on the Boss + `PhaseController` / `AttackScheduler` foundation and an approved move design. ## Frozen interfaces -See `docs/INTERFACES.md` section 1 for signatures and guarantees. +See `docs/INTERFACES.md` section 1. Changes require an ADR and Claude review. `RunState` · `DamageContext` / `DamageResult` / `CombatResolver` · -`IntegrityService` · `SacrificeDefinition` · `SacrificeService` · -the Damageable shape (`actor_id` / `armour` / `current_hp` / `receive_damage`) · +`IntegrityService` · `SacrificeDefinition` · `SacrificeService` · Damageable · `EnemyBase` · `EventBus` · `RNGService` · `RunCoordinator` · `Hitbox` / `Hurtbox` - -Changing any of them requires a `docs/DECISIONS.md` proposal and Claude review. - -## Files Codex must not modify - -``` -core/run_state.gd core/combat_resolver.gd -core/damage_context.gd core/damage_result.gd -core/soft_caps.gd core/derived_stats.gd -core/event_bus.gd core/rng_service.gd -core/game_data.gd core/hitbox.gd core/hurtbox.gd -systems/integrity_service.gd systems/sacrifice_service.gd -systems/sacrifice_definition.gd systems/sacrifice_result.gd -systems/run_coordinator.gd -actors/enemies/enemy_base.gd actors/enemies/enemy_base.tscn -actors/enemies/enemy_config.gd -actors/player/player.gd actors/player/player_state_machine.gd -actors/player/states/player_state.gd -scenes/main.gd project.godot -.github/workflows/repository-validation.yml -``` - -Also unchanged without their owner: product requirements (Game Director) and -`docs/art/CONCEPT_*.md` (Art Director). - -## Available Codex tasks - -All interfaces these depend on are now frozen. See `docs/TASKS/README.md`. - -| ID | Task | Depends on | Notes | -| --- | --- | --- | --- | -| X01 | Melee ghost, full behaviour | `EnemyBase` | The reference enemy is a starting point, not the finished X01 | -| X02 | Ghost Archer | `EnemyBase` | **Done on `codex/x02-ghost-archer`; pending review** | -| X03 | Charger ghost | `EnemyBase` | Art present: `corpse_beast_*` at 64×48 — needs its own collider sizes | -| X04 | Boss attack pack | `BossActor` | Blocked on art or a ruling — see gaps below | -| X05 | Sacrifice selection UI | `SacrificeService` | Three A/B/C slots; preview through the service, confirm through the coordinator | -| X06 | Twelve sacrifice definitions | `SacrificeDefinition` | Data only. S4/S5 cards need at least one rule-type cost | -| X07 | Debug and telemetry panel | `EventBus`, `RunCoordinator` | Event timeline and JSON/CSV export; the current panel is the baseline | -| X08 | Test expansion | harness | Stamina/dodge and same-death cases once those systems exist | - -Not yet unblocked, because Claude has not built the framework they extend: the -three-slot generator, `StaminaService`, `SameDeathController`, the world -director, and the Boss `PhaseController`. - -## Known issues - -1. **`assets/effects/*.png` are single-frame.** All four are 48×48 while - `assets/effects/metadata.md` specifies 2–4 frames (96×48 to 192×48). No VFX - are used in M1. WorkBuddy needs to re-export the strips. -2. **The Boss has one attack.** `assets/boss/` ships one attack sheet, so the - charge and the ground slam from A14/X04 are absent (ADR-010). The fight is a - readable pattern with one answer — thin on purpose, not by oversight. -3. **`docs/PRD.md` and `docs/PROTOTYPE_CONTRACT.md` are still stubs.** M1 was - derived from the Development Pack and research report under ADR-001. Land the - real documents before Codex starts, so X01–X08 have something to be reviewed - against. -4. **Stamina has no consumer** (ADR-011). The bar sits full during normal play. -5. **Imbalance is tracked but inert.** Nothing reads it yet; the world director - is the consumer. -6. **No telemetry persistence.** Events are emitted with a correlation envelope - but nothing writes them to disk (X07). -7. **No audio.** Out of scope for M1. -8. **Placeholder art everywhere.** Magenta border = placeholder, per - `docs/ART_SPEC.md` section 5. -9. **The Ghost Arrow has no supplied sprite.** X02 uses a small procedural - polygon fallback in `ghost_arrow.tscn`; replace only the `FallbackVisual` - when approved projectile art arrives. Collision and behaviour are final. - -## Test status - -- **Last successful run:** 2026-08-02 — 59 tests, 229 assertions, 0 failures, - Godot 4.7.1 stable headless locally. Existing CI remains pinned to Godot 4.3. -- **Command:** `godot --headless --import` then - `godot --headless --path . res://tests/test_runner.tscn` -- **CI:** `repository-validation` (unchanged) and `godot-tests` (new). Both - should be required checks on `develop`. - -## Asset integration gaps - -| Asset group | Status | -| --- | --- | -| `assets/player/` | All 6 sheets used; dimensions match `docs/ART_SPEC.md` exactly | -| `assets/enemy/ghost_melee_*` | All 5 sheets used | -| `assets/enemy/ghost_archer_*` | All 5 sheets used by X02 | -| `assets/enemy/corpse_beast_*` | Verified, unused — X03 | -| `assets/boss/` | idle, run, attack, hurt, death used; only one attack sheet exists | -| `assets/tiles/` | floor, stone_brick, brazier, tombstone used; `tile_wall`, `tile_wood_bridge`, `tile_ground_spike` unused (no platforms or hazards in M1) | -| `assets/background/` | bg_deep, bg_tree, bg_broken_flag, bg_chains all used in three parallax layers | -| `assets/ui/` | ui_health_bar, ui_stamina_bar used; `ui_frame`, `ui_minimap_frame` unused (no inventory, no minimap) | -| `assets/icons/ui/` | icon_hp, icon_boss, icon_sacrifice used; the other 8 unused | -| `assets/icons/weapons/` | All 6 unused — no weapon system in M1 | -| `assets/effects/` | **Unusable as animations** — see known issue 1 | - -Nothing was stretched, scaled by a non-integer factor, or resized. Regenerate -`SpriteFrames` with `python3 tools/generate_sprite_frames.py` after replacing a -sheet; it aborts if the new dimensions disagree with the spec. - -## X02 handoff - -### Files added - -- `actors/enemies/ghost_archer.gd`, `.tscn`, `_config.gd`, and `_frames.tres` -- `actors/enemies/ghost_arrow.gd` and `.tscn` -- `data/actors/ghost_archer.tres` and `ghost_archer_tactics.tres` -- `tests/cases/test_ghost_archer.gd` - -### Integration and validation - -- `scenes/main.tscn` injects the Ghost Archer through the existing - `RunCoordinator.enemy_scene` / `enemy_config` extension seam. The wave and F4 - debug command therefore use the same coordinator-owned spawn path. -- Added tests for patrol/detection, retreat spacing, aim/shoot/cooldown, one-hit - projectile resolution, armour integration, owner exclusion, timeout, damage, - death, and no post-death behaviour. -- Commands run: SpriteFrames generator, headless import, full test scene, and a - 180-frame headless M1 main-scene smoke run. -- No frozen interface or protected framework file changed. diff --git a/docs/ART_V2_INTEGRATION_REPORT.md b/docs/ART_V2_INTEGRATION_REPORT.md new file mode 100644 index 0000000..81b579a --- /dev/null +++ b/docs/ART_V2_INTEGRATION_REPORT.md @@ -0,0 +1,105 @@ +# Art V2 Integration Report + +**Branch:** `codex/m15-art-v2-integration` + +**Audit date:** 2026-08-02 + +**Target viewport:** 640×360 + +## Verification method + +Every integrated PNG was opened programmatically and checked for RGBA +transparency, exact sheet dimensions, whole-frame divisibility, and the frame +count declared by its adjacent `frame_map.json` / `metadata.md`. Godot then +imported every resource and instantiated the critical scenes in the headless +test suite. Source PNGs were not resized or rewritten. + +The supplied `assets_v2/godot/*_frames.tres` resources are used directly. +Project-wide nearest-neighbour filtering and disabled mipmaps remain in force; +sprites use integer scale and explicit foot-aligned offsets independent of +their collision shapes. + +## Integrated inventory + +| Asset | Declared frames | Actual frames | Integrated | Fallback | Notes | +| --- | ---: | ---: | --- | --- | --- | +| Player idle | 8 × 96×96 | 8 (768×96) | Yes | None | 8 FPS, loop, pivot (48,66), foot y=88 | +| Player run | 8 × 96×96 | 8 (768×96) | Yes | None | 12 FPS, loop | +| Player jump | 3 × 96×96 | 3 (288×96) | Yes | None | 10 FPS, non-looping; rising state | +| Player fall | 3 × 96×96 | 3 (288×96) | Yes | None | 10 FPS, non-looping; falling state | +| Player light attack 1 | 8 × 96×96 | 8 (768×96) | Yes | None | 12 FPS; startup 0–3, active 4–5, recovery 6–7 | +| Player hurt | 4 × 96×96 | 4 (384×96) | Yes | None | 10 FPS, non-looping | +| Player death | 10 × 96×96 | 10 (960×96) | Yes | None | 8 FPS, non-looping; result overlay waits | +| Melee ghost idle | 6 × 96×96 | 6 (576×96) | Yes | None | 8 FPS, loop, pivot (48,70), foot y=90 | +| Melee ghost walk | 8 × 96×96 | 8 (768×96) | Yes | None | 8 FPS, loop | +| Melee ghost attack | 8 × 96×96 | 8 (768×96) | Yes | None | 10 FPS; startup 0–3, active 4–5, recovery 6–7 | +| Melee ghost hurt | 4 × 96×96 | 4 (384×96) | Yes | None | 10 FPS, non-looping | +| Melee ghost death | 8 × 96×96 | 8 (768×96) | Yes | None | 8 FPS, non-looping; removal waits | +| Ghost Archer idle | 6 × 96×96 | 6 (576×96) | Yes | None | 8 FPS, loop, pivot (48,64), foot y=88 | +| Ghost Archer retreat | 8 × 96×96 | 8 (768×96) | Yes | None | 10 FPS, loop; patrol/retreat movement | +| Ghost Archer aim | 6 × 96×96 | 6 (576×96) | Yes | None | 8 FPS, full draw at frame 5 | +| Ghost Archer shoot | 4 × 96×96 | 4 (384×96) | Yes | None | 14 FPS; projectile release frame 1, recovery 2–3 | +| Ghost Archer hurt | 4 × 96×96 | 4 (384×96) | Yes | None | 10 FPS, non-looping | +| Ghost Archer death | 8 × 96×96 | 8 (768×96) | Yes | None | 8 FPS, non-looping; no post-death release | +| Gate Warden idle | 8 × 192×192 | 8 (1536×192) | Yes | None | 6 FPS, loop, pivot (96,128), foot y=176 | +| Gate Warden walk | 8 × 192×192 | 8 (1536×192) | Yes | None | 8 FPS, loop | +| Gate Warden attack 1 | 10 × 192×192 | 10 (1920×192) | Yes | None | 9 FPS; startup 0–3, active 4–5, recovery 6–9 | +| Gate Warden hurt | 4 × 192×192 | 4 (768×192) | Yes | None | 8 FPS, non-looping | +| Gate Warden death | 12 × 192×192 | 12 (2304×192) | Yes | None | 6 FPS, non-looping; victory waits | +| Ghost Fire Arrow | 1 × 32×32 | 1 (32×32) | Yes | Hidden polygon | Emergency fallback only if texture load fails | +| Blade slash | 6 × 128×128 | 6 (768×128) | Yes | None | Read-only visual event on player active frame | +| Confirmed-hit effect | 4 × 96×96 | 4 (384×96) | Yes | None | Spawned by EventBus observer only after positive final damage | +| Death dissolve | 8 × 96×96 | 8 (768×96) | Yes | None | Secondary effect; never replaces death animation | +| Yang / HP icon | 1 × 64×64 | 1 | Yes | None | Existing HP HUD field | +| Qi / stamina icon | 1 × 64×64 | 1 | Yes | None | Existing stamina HUD field | +| Songdao / attack icon | 1 × 64×64 | 1 | Yes | None | Existing attack readout | +| Obsession / integrity icon | 1 × 64×64 | 1 | Yes | None | Integrity and Boss structural readouts | +| Ghostfire / imbalance icon | 1 × 64×64 | 1 | Yes | None | Existing imbalance field | +| Talisman / sacrifice icon | 1 × 64×64 | 1 | Yes | None | Existing sacrifice panel | + +## Gameplay mapping and timing + +The existing player state machine and `EnemyBase` state model remain the source +of gameplay truth. Concrete actor scripts translate those states to V2 +animation names. `AnimatedSprite2D.frame_changed` only gates the existing +`Hitbox`; damage still flows through `Hitbox` → `Hurtbox` → `CombatResolver`. + +- Player: grounded idle/run, rising jump, falling fall, light attack 1, hurt, + death. Facing flips the sprite, slash, and existing attack hitbox together. +- Melee ghost: idle, walk, attack, hurt, death. Damage is enabled on frames 4–5. +- Ghost Archer: idle, retreat, aim, shoot, hurt, death. Aim reaches frame 5 + before shoot begins; its arrow is emitted once on shoot frame 1. +- Gate Warden: idle, walk, attack 1, hurt, death. Damage is enabled on frames + 4–5, and victory is emitted after the death strip completes. + +## Mismatches, fallbacks, and remaining art + +No integrated sheet has an invalid dimension, missing frame, transparency +failure, or metadata mismatch. The Ghost Arrow retains its old polygon as a +hidden emergency fallback, but normal gameplay always uses the supplied V2 +texture. + +Unused art was deliberately left out when gameplay has no matching action: +player light attack 2 and heavy attack, Gate Warden attack 2, the Corpse Beast +pack, Ghost Fire effect, and unmatched inventory/weapon icons. Integrating +those belongs with the corresponding gameplay modules, not this visual pass. + +The following frozen/protected architecture files were not changed: +`RunState`, `CombatResolver`, `DamageContext`, `DamageResult`, +`IntegrityService`, `SacrificeService`, `EventBus`, `RNGService`, +`RunCoordinator`, `EnemyBase`, `player.gd`, `player_state_machine.gd`, and +`player_state.gd`. + +## Visual validation evidence + +Godot 4.7.1 rendered the real `main.tscn` at 640×360 while an automation driver +exercised idle, run, jump, player attack with F8 collision display, F4 Archer +spawn/aim/arrow release, and F5 Boss attack/death. The driver was removed after +capture; the resulting screenshots are under `assets_v2/review/integration_*.png`. + +The screenshots confirm V2 actors/projectile/effects are visible, foot baselines +remain on the arena floor, the active player hitbox overlaps the visible slash, +and disabling the overlay leaves no primitive actor rendering. The full M1 loop, +sacrifice application, death deferral, and victory/restart were also exercised +by the automated run-loop suite. Interactive macOS control and MP4 conversion +were unavailable, so screenshots are supplied instead of a video. diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 876fa15..5e01fa9 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -213,3 +213,25 @@ Append decisions in this format; do not rewrite accepted history. correct rather than broken. A future StaminaService can wrap `Player.spend_stamina` without changing its callers. - **Interfaces affected:** `RunState` stamina commands, `Player.spend_stamina`. + +## ADR-012: V2 authored frames gate the existing attack flow + +- **Status:** Accepted +- **Date:** 2026-08-02 +- **Owner:** Codex +- **Context:** Art V2 frame maps declare visible startup, active, recovery, and + projectile-release frames. The M1 actors previously used coarse timers, which + could deal damage before or after the visible strike. The frozen combat and + actor foundations must remain authoritative. +- **Decision:** Concrete actor/state adapters map existing gameplay states to + V2 animations and use `AnimatedSprite2D.frame_changed` only to gate the + existing `Hitbox` or emit the already-defined projectile command. Damage + remains `Hitbox` → `Hurtbox` → `CombatResolver`. Player/melee active frames + are 4–5, Gate Warden active frames are 4–5, and Ghost Archer release is shoot + frame 1 after aim reaches frame 5. Boss victory and player defeat presentation + wait for the death animation to complete. +- **Consequences:** Visible and effective strikes agree without a second combat + system. Concrete animations now define attack phase duration; a missing strip + must use the existing configured timer as a documented fallback. Only actions + already supported by gameplay are mapped. +- **Interfaces affected:** none. No frozen or protected framework file changed. diff --git a/docs/TASKS/README.md b/docs/TASKS/README.md index 61982cc..afdcb5e 100644 --- a/docs/TASKS/README.md +++ b/docs/TASKS/README.md @@ -2,6 +2,16 @@ Update status and PR only when work actually begins. +## Milestone M1.5 — Art V2 integration + +| ID | Task | Owner | Status | Branch | Dependency | PR | +| --- | --- | --- | --- | --- | --- | --- | +| M15 | V2 character, enemy, Boss, projectile, effect, and HUD integration | Codex | In review | `codex/m15-art-v2-integration` | M1 + X02 | #6 | + +M1.5 maps only existing gameplay actions. It does not add player attacks, Boss +phases, or a parallel combat/animation framework. See +`docs/ART_V2_INTEGRATION_REPORT.md` for the complete integration matrix. + ## Milestone M1 — playable vertical slice foundation All Claude tasks are delivered on `claude/core-framework` in one PR to @@ -30,15 +40,16 @@ files in section 3. | X01 | Melee ghost — full behaviour | Codex | Ready | `codex/x01-melee-ghost` | `EnemyBase` frozen | — | | X02 | Ghost Archer — projectile, spacing, reposition | Codex | Done | `codex/x02-ghost-archer` | `EnemyBase` frozen | #4 | | X03 | Charger ghost — telegraphed line charge, wall stagger | Codex | Ready | `codex/x03-charger-ghost` | `EnemyBase` frozen | — | -| X04 | Boss attack pack — three moves | Codex | Blocked on art | `codex/x04-boss-attacks` | ADR-010 | — | +| X04 | Boss attack pack — three moves | Codex | Blocked on framework/design | `codex/x04-boss-attacks` | `PhaseController`, `AttackScheduler` | — | | X05 | Sacrifice selection UI — A/B/C slots | Codex | Ready | `codex/x05-sacrifice-ui` | `SacrificeService` frozen | — | | X06 | Twelve sacrifice definitions | Codex | Ready | `codex/x06-sacrifice-data` | `SacrificeDefinition` frozen | — | | X07 | Debug and telemetry panel — timeline, JSON/CSV export | Codex | Ready | `codex/x07-debug-panel` | `EventBus` frozen | — | | X08 | Automated test expansion | Codex | Partly blocked | `codex/x08-test-expansion` | Stamina and same-death systems | — | -**X04 is blocked** because `assets/boss/` ships one attack sheet. It needs new -art from WorkBuddy, or a Game Director ruling that reusing the run cycle for a -charge is acceptable. +**X04 art is now available** in the V2 Gate Warden pack, including attack 2. +The task remains blocked until Claude supplies the Boss `PhaseController` / +`AttackScheduler` foundation and the Game Director approves the move set. M1.5 +uses attack 1 only because that is the only existing gameplay action. **X08 is partly blocked**: the combat, integrity, sacrifice, actor, RNG and run-loop cases exist. The stamina/dodge and same-death cases the Development diff --git a/docs/TESTING.md b/docs/TESTING.md index 7f4a03d..a2652f1 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -46,7 +46,9 @@ tests/ ├── framework/test_case.gd assertions, physics stepping, config helper └── cases/ ├── test_actors.gd EnemyBase and BossActor + ├── test_art_v2.gd V2 loading, state mapping, frame timing, debug visibility ├── test_combat_resolver.gd damage pipeline + ├── test_ghost_archer.gd ranged AI, release timing, projectile and death ├── test_integrity.gd structural integrity and snapshots ├── test_rng.gd determinism and stream isolation ├── test_run_loop.gd end-to-end run plus scene smoke checks @@ -81,7 +83,14 @@ lock-budget gating; and every shipped card passes `validate()`. **Enemy and Boss** — the Damageable shape, damage through the resolver, death firing exactly once across four routes (two lethal hits, a hit after death, and two direct `die()` calls), a corpse taking no damage and running no behaviour, -target acquisition, and that `BossActor` extends `EnemyBase`. +target acquisition, that `BossActor` extends `EnemyBase`, authored melee/Boss +active-frame gating, and victory waiting for the Boss death strip. + +**Art V2 and Ghost Archer** — required resources and critical scenes load, +player state-to-animation mappings, player startup/active/recovery gating, +hurt/death overrides, melee and Boss active frames, Archer patrol/range/aim/ +release/cooldown/death, Ghost Arrow owner exclusion/one-hit/timeout behaviour, +and the debug overlay being off by default while remaining toggleable. **RNG** — same seed replays, different seeds diverge, draining one stream does not shift another, stream names change the derived seed, and integer draws stay @@ -92,6 +101,8 @@ connection from simulated input, wave clear, sacrifice offer and confirm, boss spawn, victory, restart, and player death — plus every debug command and a smoke check that all ten critical scenes load and instantiate. +Current baseline after M1.5: **66 tests / 421 assertions**. + ## What is not covered Honest gaps, so nobody reads a green suite as more than it is: diff --git a/scenes/main.tscn b/scenes/main.tscn index 6da8a80..2ed3ee2 100644 --- a/scenes/main.tscn +++ b/scenes/main.tscn @@ -1,18 +1,20 @@ -[gd_scene load_steps=13 format=3 uid="uid://bnnmain0000001"] +[gd_scene load_steps=15 format=3 uid="uid://bnnmain0000001"] [ext_resource type="Script" path="res://scenes/main.gd" id="1_main"] [ext_resource type="PackedScene" path="res://scenes/arena.tscn" id="2_arena"] [ext_resource type="Script" path="res://systems/run_coordinator.gd" id="3_coordinator"] [ext_resource type="PackedScene" path="res://actors/player/player.tscn" id="4_player"] -[ext_resource type="PackedScene" path="res://actors/enemies/ghost_archer.tscn" id="5_enemy"] +[ext_resource type="PackedScene" path="res://actors/enemies/ghost_melee.tscn" id="5_enemy"] [ext_resource type="PackedScene" path="res://actors/boss/boss.tscn" id="6_boss"] -[ext_resource type="Resource" path="res://data/actors/ghost_archer.tres" id="7_enemy_config"] +[ext_resource type="Resource" path="res://data/actors/ghost_melee.tres" id="7_enemy_config"] [ext_resource type="Resource" path="res://data/actors/boss_gate_guardian.tres" id="8_boss_config"] [ext_resource type="PackedScene" path="res://ui/hud.tscn" id="9_hud"] [ext_resource type="PackedScene" path="res://ui/sacrifice_panel.tscn" id="10_sacrifice"] [ext_resource type="PackedScene" path="res://ui/result_screen.tscn" id="11_result"] [ext_resource type="PackedScene" path="res://ui/debug_panel.tscn" id="12_debug"] [ext_resource type="Script" path="res://ui/debug_shape_overlay.gd" id="13_shapes"] +[ext_resource type="Script" path="res://visuals/combat_effects.gd" id="14_effects"] +[ext_resource type="Texture2D" path="res://assets_v2/effects/hit.png" id="15_hit"] [node name="Main" type="Node2D"] script = ExtResource("1_main") @@ -21,6 +23,12 @@ script = ExtResource("1_main") [node name="Actors" type="Node2D" parent="."] +[node name="CombatEffects" type="Node2D" parent="."] +script = ExtResource("14_effects") +hit_texture = ExtResource("15_hit") +hit_frames = 4 +hit_fps = 16.0 + [node name="RunCoordinator" type="Node" parent="."] script = ExtResource("3_coordinator") player_scene = ExtResource("4_player") diff --git a/tests/cases/test_actors.gd b/tests/cases/test_actors.gd index d40227f..7730d01 100644 --- a/tests/cases/test_actors.gd +++ b/tests/cases/test_actors.gd @@ -105,6 +105,8 @@ func test_boss_death_publishes_its_own_events_once() -> void: boss.receive_damage(_blow(boss, 9999.0)) boss.die(&"test") + assert_equal(defeated[0], 0, "victory waits while the collapse animation plays") + await step_physics(125) EventBus.boss_died.disconnect(handler) assert_equal(defeated[0], 1, "defeated fired once") diff --git a/tests/cases/test_art_v2.gd b/tests/cases/test_art_v2.gd new file mode 100644 index 0000000..1114604 --- /dev/null +++ b/tests/cases/test_art_v2.gd @@ -0,0 +1,182 @@ +extends TestCase +## M1.5 Art V2 resource, state mapping and authored-frame timing coverage. + +const MAIN_SCENE := "res://scenes/main.tscn" +const PLAYER_SCENE := "res://actors/player/player.tscn" +const MELEE_SCENE := "res://actors/enemies/ghost_melee.tscn" +const ARCHER_SCENE := "res://actors/enemies/ghost_archer.tscn" +const ARROW_SCENE := "res://actors/enemies/ghost_arrow.tscn" +const BOSS_SCENE := "res://actors/boss/boss.tscn" + +var _main: Main +var _spawned: Array[Node] = [] + + +func after_each() -> void: + Input.action_release(&"move_left") + Input.action_release(&"move_right") + Input.action_release(&"jump") + Input.action_release(&"light_attack") + if _main != null and is_instance_valid(_main): + _main.queue_free() + for node in _spawned: + if is_instance_valid(node): + node.queue_free() + _main = null + _spawned.clear() + + +func _launch() -> RunCoordinator: + _main = (load(MAIN_SCENE) as PackedScene).instantiate() + tree.root.add_child(_main) + await step_physics(3) + return _main.coordinator() + + +func _spawn_enemy(scene_path: String, config_path: String, target: Node2D) -> EnemyBase: + var enemy := (load(scene_path) as PackedScene).instantiate() as EnemyBase + tree.root.add_child(enemy) + _spawned.append(enemy) + var enemy_config := (load(config_path) as EnemyConfig).duplicate(true) as EnemyConfig + enemy_config.gravity = 0.0 + enemy.initialize(enemy_config, target) + return enemy + + +func test_all_integrated_v2_resources_load_with_declared_animations() -> void: + var player_frames := load("res://assets_v2/godot/player_frames.tres") as SpriteFrames + var melee_frames := load("res://assets_v2/godot/melee_ghost_frames.tres") as SpriteFrames + var archer_frames := load("res://assets_v2/godot/ghost_archer_frames.tres") as SpriteFrames + var boss_frames := load("res://assets_v2/godot/gate_warden_frames.tres") as SpriteFrames + assert_not_null(player_frames, "player SpriteFrames load") + assert_not_null(melee_frames, "melee ghost SpriteFrames load") + assert_not_null(archer_frames, "Ghost Archer SpriteFrames load") + assert_not_null(boss_frames, "Gate Warden SpriteFrames load") + assert_equal(player_frames.get_frame_count(&"player_light_attack_1"), 8, "player attack has 8 frames") + assert_equal(melee_frames.get_frame_count(&"melee_ghost_attack"), 8, "melee attack has 8 frames") + assert_equal(archer_frames.get_frame_count(&"ghost_archer_shoot"), 4, "archer shoot has 4 frames") + assert_equal(boss_frames.get_frame_count(&"gate_warden_attack_1"), 10, "Boss attack has 10 frames") + assert_not_null(load("res://assets_v2/projectiles/ghost_fire_arrow.png"), "Ghost Arrow texture imports") + assert_not_null(load("res://assets_v2/effects/hit.png"), "hit effect imports") + assert_not_null(load("res://assets_v2/effects/blade_slash.png"), "slash effect imports") + + +func test_actor_scenes_use_v2_frames_and_stable_foot_offsets() -> void: + var player := (load(PLAYER_SCENE) as PackedScene).instantiate() as Player + var melee := (load(MELEE_SCENE) as PackedScene).instantiate() as MeleeGhost + var archer := (load(ARCHER_SCENE) as PackedScene).instantiate() as GhostArcher + var boss := (load(BOSS_SCENE) as PackedScene).instantiate() as BossActor + for actor in [player, melee, archer, boss]: + tree.root.add_child(actor) + _spawned.append(actor) + assert_equal(player.sprite.offset, Vector2(-48, -88), "player feet use V2 baseline") + assert_equal(melee.sprite.offset, Vector2(-48, -90), "melee feet use V2 baseline") + assert_equal(archer.sprite.offset, Vector2(-48, -88), "archer feet use V2 baseline") + assert_equal(boss.sprite.offset, Vector2(-96, -176), "Boss feet use V2 baseline") + var arrow := (load(ARROW_SCENE) as PackedScene).instantiate() as GhostArrow + tree.root.add_child(arrow) + _spawned.append(arrow) + assert_true(arrow.uses_v2_texture(), "normal projectile rendering uses the supplied V2 texture") + assert_false(arrow.get_node("FallbackVisual").visible, "procedural fallback is hidden") + + +func test_player_maps_idle_run_jump_fall_and_attack() -> void: + var coordinator := await _launch() + var player := coordinator.player() + assert_equal(player.sprite.animation, &"player_idle", "grounded rest selects idle") + + Input.action_press(&"move_right") + await step_physics(3) + assert_equal(player.sprite.animation, &"player_run", "movement selects run") + assert_false(player.sprite.flip_h, "right-facing run is not flipped") + Input.action_release(&"move_right") + Input.action_press(&"move_left") + await step_physics(2) + assert_true(player.sprite.flip_h, "left-facing run flips the sprite") + Input.action_release(&"move_left") + + Input.action_press(&"jump") + await step_physics(2) + Input.action_release(&"jump") + assert_equal(player.sprite.animation, &"player_jump", "rising selects jump") + player.velocity.y = 40.0 + await step_physics(2) + assert_equal(player.sprite.animation, &"player_fall", "descending selects fall") + + Input.action_press(&"light_attack") + await step_physics(2) + Input.action_release(&"light_attack") + assert_equal(player.sprite.animation, &"player_light_attack_1", "current light action selects attack 1") + + +func test_player_hitbox_matches_only_authored_active_frames() -> void: + var coordinator := await _launch() + var player := coordinator.player() + Input.action_press(&"light_attack") + await step_physics(2) + Input.action_release(&"light_attack") + var saw_startup := false + var saw_active := false + var saw_recovery := false + for _frame in range(45): + await step_physics(1) + if player.sprite.animation != &"player_light_attack_1": + continue + var authored_active := player.sprite.frame >= 4 and player.sprite.frame <= 5 + assert_equal(player.attack_hitbox.is_active(), authored_active, "hitbox follows attack frame %d" % player.sprite.frame) + saw_startup = saw_startup or player.sprite.frame < 4 + saw_active = saw_active or authored_active + saw_recovery = saw_recovery or player.sprite.frame > 5 + assert_true(saw_startup, "startup frames were observed") + assert_true(saw_active, "active frames were observed") + assert_true(saw_recovery, "recovery frames were observed") + assert_false(player.attack_hitbox.is_active(), "hitbox closes after the animation") + + +func test_player_hurt_and_death_override_locomotion() -> void: + var coordinator := await _launch() + var player := coordinator.player() + coordinator.debug_damage(20.0) + await step_physics(1) + assert_equal(player.sprite.animation, &"player_hurt", "hurt overrides locomotion") + await step_physics(35) + coordinator.debug_damage(10000.0) + await step_physics(1) + assert_equal(player.sprite.animation, &"player_death", "lethal damage selects death") + + +func test_melee_and_boss_hitboxes_follow_authored_frames() -> void: + var target := Node2D.new() + tree.root.add_child(target) + _spawned.append(target) + target.global_position = Vector2(40.0, 0.0) + var melee := _spawn_enemy(MELEE_SCENE, "res://data/actors/ghost_melee.tres", target) + var saw_melee_active := false + for _i in range(60): + await step_physics(1) + if melee.sprite.animation == &"melee_ghost_attack": + var expected := melee.sprite.frame >= 4 and melee.sprite.frame <= 5 + assert_equal(melee.attack_hitbox.is_active(), expected, "melee hitbox follows frame map") + saw_melee_active = saw_melee_active or expected + assert_true(saw_melee_active, "melee active frames occurred") + + target.global_position = Vector2(60.0, 0.0) + var boss := _spawn_enemy(BOSS_SCENE, "res://data/actors/boss_gate_guardian.tres", target) + var saw_boss_active := false + for _i in range(70): + await step_physics(1) + if boss.sprite.animation == &"gate_warden_attack_1": + var expected := boss.sprite.frame >= 4 and boss.sprite.frame <= 5 + assert_equal(boss.attack_hitbox.is_active(), expected, "Boss hitbox follows frame map") + saw_boss_active = saw_boss_active or expected + assert_true(saw_boss_active, "Boss active frames occurred") + + +func test_debug_collision_overlay_is_off_by_default_and_still_toggles() -> void: + await _launch() + var overlay := _main.get_node("DebugRoot/DebugShapes") as DebugShapeOverlay + assert_false(overlay.is_enabled(), "debug shapes start disabled") + assert_false(overlay.visible, "no collision geometry appears in normal play") + overlay.toggle() + assert_true(overlay.is_enabled(), "existing debug toggle still enables shapes") + assert_true(overlay.visible, "enabled overlay becomes visible") diff --git a/tests/cases/test_ghost_archer.gd b/tests/cases/test_ghost_archer.gd index 73b5cb3..0699b2a 100644 --- a/tests/cases/test_ghost_archer.gd +++ b/tests/cases/test_ghost_archer.gd @@ -120,12 +120,21 @@ func test_archer_aims_shoots_and_respects_cooldown() -> void: var target := _target(Vector2(150.0, 0.0)) var archer := _spawn_archer(target) var shots := [0] - archer.arrow_fired.connect(func(_arrow: GhostArrow) -> void: shots[0] += 1) - - await step_physics(3) - assert_equal(archer.state(), EnemyBase.State.WINDUP, "preferred range begins the readable aim") - await step_physics(36) + var release_frames := [] + archer.arrow_fired.connect(func(_arrow: GhostArrow) -> void: + shots[0] += 1 + release_frames.append(archer.sprite.frame) + ) + + var saw_aim := false + for _i in range(20): + await step_physics(1) + saw_aim = saw_aim or archer.state() == EnemyBase.State.WINDUP + assert_true(saw_aim, "preferred range begins the readable aim") + await step_physics(45) assert_equal(shots[0], 1, "one arrow is released after the wind-up") + if not release_frames.is_empty(): + assert_equal(release_frames[0], 1, "the arrow releases on authored shoot frame 1") await step_physics(55) assert_equal(shots[0], 1, "recovery and cooldown prevent an immediate second shot") diff --git a/tests/cases/test_run_loop.gd b/tests/cases/test_run_loop.gd index e73b04b..a7de0ed 100644 --- a/tests/cases/test_run_loop.gd +++ b/tests/cases/test_run_loop.gd @@ -110,7 +110,7 @@ func test_full_loop_reaches_victory_and_restarts() -> void: assert_greater(state.imbalance(), float(before["imbalance"]), "imbalance rose") coordinator.boss().die(&"test") - await step_physics(2) + await step_physics(125) assert_equal(coordinator.phase(), RunCoordinator.PHASE_RESULT, "the run ends") assert_equal(coordinator.state().run_status(), RunState.STATUS_VICTORY, "as a victory") @@ -160,12 +160,19 @@ func test_debug_commands_route_through_the_coordinator() -> void: var enemies_before := coordinator.live_enemies().size() coordinator.debug_spawn_reference_enemy() assert_equal( - coordinator.live_enemies().size(), enemies_before + 1, "debug spawn added an enemy" + coordinator.live_enemies().size(), enemies_before + 1, "reference spawn added the wave enemy" ) assert_true( - coordinator.live_enemies()[-1] is GhostArcher, - "the injected debug enemy is the Ghost Archer" + coordinator.live_enemies()[-1] is MeleeGhost, + "the coordinator reference spawn uses the injected wave composition" + ) + + var debug_panel: DebugPanel = _main.get_node("DebugRoot/DebugPanel") + debug_panel.spawn_ghost_archer_for_debug() + assert_equal( + coordinator.live_enemies().size(), enemies_before + 2, "F4 debug spawn added an enemy" ) + assert_true(coordinator.live_enemies()[-1] is GhostArcher, "F4 spawns the Ghost Archer") var digest := state.snapshot_hash() var preview := coordinator.debug_preview_sacrifice() diff --git a/ui/debug_panel.gd b/ui/debug_panel.gd index 1490e9f..2eef6fa 100644 --- a/ui/debug_panel.gd +++ b/ui/debug_panel.gd @@ -12,6 +12,8 @@ extends CanvasLayer const HEAL_AMOUNT := 25.0 const DAMAGE_AMOUNT := 15.0 +const GHOST_ARCHER_SCENE := preload("res://actors/enemies/ghost_archer.tscn") +const GHOST_ARCHER_CONFIG := preload("res://data/actors/ghost_archer.tres") @onready var _panel: Panel = $Root/Panel @onready var _readout: Label = $Root/Panel/Readout @@ -43,7 +45,7 @@ func _unhandled_input(event: InputEvent) -> void: _coordinator.debug_damage(DAMAGE_AMOUNT) _note("damaged %.0f" % DAMAGE_AMOUNT) elif event.is_action_pressed(&"debug_spawn_enemy"): - _coordinator.debug_spawn_reference_enemy() + spawn_ghost_archer_for_debug() _note("spawned Ghost Archer") elif event.is_action_pressed(&"debug_start_boss"): _coordinator.begin_boss() @@ -110,3 +112,16 @@ func _describe(result: SacrificeResult, verb: String) -> String: func _note(message: String) -> void: _last_message = message print("[debug] ", message) + + +func spawn_ghost_archer_for_debug() -> void: + # Keep the existing coordinator-owned spawn/list/death path. Its scene and + # config are injected public composition properties, so the debug command can + # select X02 without reaching into private coordinator state. + var previous_scene := _coordinator.enemy_scene + var previous_config := _coordinator.enemy_config + _coordinator.enemy_scene = GHOST_ARCHER_SCENE + _coordinator.enemy_config = GHOST_ARCHER_CONFIG + _coordinator.debug_spawn_reference_enemy() + _coordinator.enemy_scene = previous_scene + _coordinator.enemy_config = previous_config diff --git a/ui/hud.tscn b/ui/hud.tscn index c4b68a0..37906a5 100644 --- a/ui/hud.tscn +++ b/ui/hud.tscn @@ -1,10 +1,13 @@ -[gd_scene load_steps=6 format=3 uid="uid://bnnhud0000001"] +[gd_scene load_steps=9 format=3 uid="uid://bnnhud0000001"] [ext_resource type="Script" path="res://ui/hud.gd" id="1_hud"] [ext_resource type="Texture2D" path="res://assets/ui/ui_health_bar.png" id="2_health_bar"] [ext_resource type="Texture2D" path="res://assets/ui/ui_stamina_bar.png" id="3_stamina_bar"] -[ext_resource type="Texture2D" path="res://assets/icons/ui/icon_hp.png" id="4_icon_hp"] -[ext_resource type="Texture2D" path="res://assets/icons/ui/icon_boss.png" id="5_icon_boss"] +[ext_resource type="Texture2D" path="res://assets_v2/icons/icon_yang.png" id="4_icon_hp"] +[ext_resource type="Texture2D" path="res://assets_v2/icons/icon_obsession.png" id="5_icon_boss"] +[ext_resource type="Texture2D" path="res://assets_v2/icons/icon_qi.png" id="6_icon_qi"] +[ext_resource type="Texture2D" path="res://assets_v2/icons/icon_songdao.png" id="7_icon_attack"] +[ext_resource type="Texture2D" path="res://assets_v2/icons/icon_ghostfire.png" id="8_icon_imbalance"] [node name="Hud" type="CanvasLayer"] script = ExtResource("1_hud") @@ -14,11 +17,20 @@ offset_right = 240.0 offset_bottom = 120.0 [node name="Icon" type="TextureRect" parent="Player"] -offset_left = 4.0 -offset_top = 4.0 -offset_right = 36.0 -offset_bottom = 36.0 +offset_left = 6.0 +offset_top = 6.0 +offset_right = 22.0 +offset_bottom = 22.0 texture = ExtResource("4_icon_hp") +expand_mode = 1 + +[node name="StaminaIcon" type="TextureRect" parent="Player"] +offset_left = 6.0 +offset_top = 24.0 +offset_right = 22.0 +offset_bottom = 40.0 +texture = ExtResource("6_icon_qi") +expand_mode = 1 [node name="HealthBar" type="TextureRect" parent="Player"] offset_left = 40.0 @@ -49,7 +61,7 @@ offset_bottom = 15.0 color = Color(0.165, 0.541, 0.29, 1) [node name="Stats" type="Label" parent="Player"] -offset_left = 6.0 +offset_left = 24.0 offset_top = 44.0 offset_right = 236.0 offset_bottom = 96.0 @@ -59,6 +71,30 @@ text = "ATK EHP Integrity" +[node name="AttackIcon" type="TextureRect" parent="Player"] +offset_left = 6.0 +offset_top = 45.0 +offset_right = 20.0 +offset_bottom = 59.0 +texture = ExtResource("7_icon_attack") +expand_mode = 1 + +[node name="IntegrityIcon" type="TextureRect" parent="Player"] +offset_left = 6.0 +offset_top = 61.0 +offset_right = 20.0 +offset_bottom = 75.0 +texture = ExtResource("5_icon_boss") +expand_mode = 1 + +[node name="ImbalanceIcon" type="TextureRect" parent="Player"] +offset_left = 6.0 +offset_top = 77.0 +offset_right = 20.0 +offset_bottom = 91.0 +texture = ExtResource("8_icon_imbalance") +expand_mode = 1 + [node name="Boss" type="Control" parent="."] offset_left = 220.0 offset_right = 480.0 @@ -70,6 +106,7 @@ offset_top = 4.0 offset_right = 36.0 offset_bottom = 36.0 texture = ExtResource("5_icon_boss") +expand_mode = 1 [node name="BossBar" type="TextureRect" parent="Boss"] offset_left = 40.0 diff --git a/ui/result_screen.gd b/ui/result_screen.gd index d71ef2c..3d459a9 100644 --- a/ui/result_screen.gd +++ b/ui/result_screen.gd @@ -22,6 +22,14 @@ func _ready() -> void: func show_outcome(outcome: StringName) -> void: var state := _coordinator.state() var victory := outcome == RunState.STATUS_VICTORY + if not victory: + var player := _coordinator.player() + if ( + player != null + and player.sprite.animation == &"player_death" + and player.sprite.is_playing() + ): + await player.sprite.animation_finished # Not 同归: the same-death mechanic is not implemented in M1, so a win here # is an ordinary victory and the screen must not claim otherwise. _title.text = "VICTORY" if victory else "DEATH" diff --git a/ui/sacrifice_panel.tscn b/ui/sacrifice_panel.tscn index 71c551d..1a45ce0 100644 --- a/ui/sacrifice_panel.tscn +++ b/ui/sacrifice_panel.tscn @@ -1,7 +1,7 @@ [gd_scene load_steps=3 format=3 uid="uid://bnnsacpanel001"] [ext_resource type="Script" path="res://ui/sacrifice_panel.gd" id="1_panel"] -[ext_resource type="Texture2D" path="res://assets/icons/ui/icon_sacrifice.png" id="2_icon"] +[ext_resource type="Texture2D" path="res://assets_v2/icons/icon_talisman.png" id="2_icon"] [node name="SacrificePanel" type="CanvasLayer"] layer = 5 @@ -28,6 +28,7 @@ offset_top = 8.0 offset_right = 40.0 offset_bottom = 40.0 texture = ExtResource("2_icon") +expand_mode = 1 [node name="Title" type="Label" parent="Root/Panel"] offset_left = 46.0 diff --git a/visuals/combat_effects.gd b/visuals/combat_effects.gd new file mode 100644 index 0000000..8acd2fc --- /dev/null +++ b/visuals/combat_effects.gd @@ -0,0 +1,54 @@ +class_name CombatEffects +extends Node2D +## EventBus observer for confirmed damage feedback. No calculation or gameplay +## mutation occurs here; the existing CombatResolver result is already final. + +@export var hit_texture: Texture2D +@export var hit_frames: int = 4 +@export var hit_fps: float = 16.0 + + +func _ready() -> void: + z_index = 20 + EventBus.hit_dealt.connect(_on_hit) + EventBus.hit_taken.connect(_on_hit) + + +func _on_hit(payload: Dictionary) -> void: + if float(payload.get("final_damage", 0.0)) <= 0.0 or hit_texture == null: + return + var target := _find_actor(StringName(payload.get("target_id", &"unknown"))) + if target == null: + return + var effect := OneShotSprite.new() + effect.configure(hit_texture, hit_frames, hit_fps) + add_child(effect) + effect.global_position = target.global_position + Vector2(0.0, -32.0) + + +func _find_actor(target_id: StringName) -> Node2D: + var candidates: Array[Node2D] = [] + _collect_actors(get_tree().current_scene, target_id, candidates) + if candidates.is_empty(): + return null + if candidates.size() == 1: + return candidates[0] + var players: Array[Node2D] = [] + _collect_actors(get_tree().current_scene, &"player", players) + var player: Node2D = players[0] if not players.is_empty() else null + if player == null: + return candidates[0] + var closest := candidates[0] + for candidate in candidates: + if candidate.global_position.distance_squared_to(player.global_position) < closest.global_position.distance_squared_to(player.global_position): + closest = candidate + return closest + + +func _collect_actors(node: Node, target_id: StringName, output: Array[Node2D]) -> void: + if node == null: + return + if node is Node2D and node.has_method("actor_id") and node.call("actor_id") == target_id: + output.append(node as Node2D) + for child in node.get_children(): + _collect_actors(child, target_id, output) diff --git a/visuals/death_effect.gd b/visuals/death_effect.gd new file mode 100644 index 0000000..a11d15d --- /dev/null +++ b/visuals/death_effect.gd @@ -0,0 +1,28 @@ +class_name DeathEffect +extends Sprite2D +## Optional overlay that accompanies, but never replaces, an actor's authored +## death animation. The actor owns death timing and removal. + +@export var playback_fps: float = 8.0 + +var _playing: bool = false +var _finished: bool = false +var _elapsed: float = 0.0 + + +func _process(delta: float) -> void: + var actor := get_parent() + if actor == null or not actor.has_method("is_dead"): + return + if not _playing and not _finished and bool(actor.call("is_dead")): + _playing = true + visible = true + frame = 0 + if not _playing: + return + _elapsed += delta + frame = mini(int(_elapsed * playback_fps), hframes - 1) + if _elapsed >= float(hframes) / playback_fps: + _playing = false + _finished = true + visible = false diff --git a/visuals/one_shot_sprite.gd b/visuals/one_shot_sprite.gd new file mode 100644 index 0000000..e1db9d6 --- /dev/null +++ b/visuals/one_shot_sprite.gd @@ -0,0 +1,20 @@ +class_name OneShotSprite +extends Sprite2D +## Small self-cleaning frame-strip player for combat feedback only. + +var _fps: float = 1.0 +var _elapsed: float = 0.0 + + +func configure(source: Texture2D, frame_count: int, fps: float) -> void: + texture = source + hframes = frame_count + _fps = fps + frame = 0 + + +func _process(delta: float) -> void: + _elapsed += delta + frame = mini(int(_elapsed * _fps), hframes - 1) + if _elapsed >= float(hframes) / _fps: + queue_free() diff --git a/visuals/player_slash_visual.gd b/visuals/player_slash_visual.gd new file mode 100644 index 0000000..ee6c90e --- /dev/null +++ b/visuals/player_slash_visual.gd @@ -0,0 +1,34 @@ +class_name PlayerSlashVisual +extends Sprite2D +## Read-only visual follower for the existing player light attack. It never +## applies damage; the existing Hitbox and CombatResolver remain authoritative. + +@export var playback_fps: float = 18.0 + +var _playing: bool = false +var _elapsed: float = 0.0 + + +func _process(delta: float) -> void: + var player := get_parent() as Player + if player == null: + visible = false + return + flip_h = player.facing() < 0 + position.x = absf(position.x) * player.facing() + if ( + not _playing + and player.sprite.animation == &"player_light_attack_1" + and player.sprite.frame == 4 + ): + _playing = true + _elapsed = 0.0 + frame = 0 + visible = true + if not _playing: + return + _elapsed += delta + frame = mini(int(_elapsed * playback_fps), hframes - 1) + if _elapsed >= float(hframes) / playback_fps: + _playing = false + visible = false