From bbf9255f38ec431472daa4505303eff7ef553778 Mon Sep 17 00:00:00 2001 From: Lee <3263402823@qq.com> Date: Mon, 15 Jun 2026 19:16:49 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=A7=E5=B9=85=E5=A2=9E=E5=BC=BA=E9=AA=91?= =?UTF-8?q?=E5=A3=AB=EF=BC=9B=E5=A2=9E=E5=8A=A0=E5=BC=B9=E5=B9=95=E8=B4=B4?= =?UTF-8?q?=E5=9B=BE=EF=BC=9B=E9=87=8D=E5=81=9A=E5=8F=8C=E5=AD=90=EF=BC=9B?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E6=A8=A1=E5=BC=8F=E5=8A=A0=E5=85=A5=E6=8A=80?= =?UTF-8?q?=E8=83=BD=E5=8D=87=E7=BA=A7=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- actors/bosses/town/emperor_boss.gd | 17 + actors/bosses/town/ranger_boss.gd | 17 + actors/bosses/town/twin_princes_boss.gd | 657 +++++++++++------- actors/enemy/town_enemy.gd | 76 ++ assets/effects/pickups/experience.webp | Bin 0 -> 610 bytes assets/effects/pickups/experience.webp.import | 40 ++ assets/effects/pickups/gold.webp | Bin 0 -> 418 bytes assets/effects/pickups/gold.webp.import | 40 ++ .../effects/projectiles/apprentice_orb.webp | Bin 0 -> 848 bytes .../projectiles/apprentice_orb.webp.import | 40 ++ .../effects/projectiles/arcanist_missile.webp | Bin 0 -> 1168 bytes .../projectiles/arcanist_missile.webp.import | 40 ++ assets/effects/projectiles/arrow.webp | Bin 0 -> 436 bytes assets/effects/projectiles/arrow.webp.import | 40 ++ assets/effects/projectiles/basic_bullet.webp | Bin 0 -> 744 bytes .../projectiles/basic_bullet.webp.import | 40 ++ .../effects/projectiles/boss_bullet_red.webp | Bin 0 -> 354 bytes .../projectiles/boss_bullet_red.webp.import | 40 ++ .../projectiles/boss_bullet_yellow.webp | Bin 0 -> 360 bytes .../boss_bullet_yellow.webp.import | 40 ++ .../effects/projectiles/player_staff_orb.webp | Bin 0 -> 1332 bytes .../projectiles/player_staff_orb.webp.import | 40 ++ assets/effects/projectiles/staff_orb.webp | Bin 0 -> 1512 bytes .../effects/projectiles/staff_orb.webp.import | 40 ++ assets/effects/vfx/laser_body.webp | Bin 0 -> 420 bytes assets/effects/vfx/laser_body.webp.import | 40 ++ assets/effects/vfx/laser_core.webp | Bin 0 -> 1284 bytes assets/effects/vfx/laser_core.webp.import | 40 ++ assets/effects/vfx/magic_circle.webp | Bin 0 -> 4822 bytes assets/effects/vfx/magic_circle.webp.import | 40 ++ characters/knight/knight.gd | 88 ++- characters/knight/states/buff_state.gd | 3 +- characters/knight/states/charge_state.gd | 3 +- characters/knight/states/guard_state.gd | 3 +- characters/knight/states/hit_state.gd | 3 +- characters/knight/states/skill_state.gd | 3 +- effects/projectiles/arcane_bolt.gd | 71 +- effects/projectiles/arcane_bolt.tscn | 10 +- effects/projectiles/enemy_bolt.gd | 80 ++- effects/projectiles/enemy_bolt.tscn | 10 +- effects/projectiles/piercing_arrow.gd | 45 +- effects/projectiles/piercing_arrow.tscn | 9 +- effects/projectiles/royal_bolt.gd | 56 +- effects/projectiles/royal_bolt.tscn | 8 +- systems/pickups/run_pickup.gd | 21 +- tools/character_debug_world.gd | 51 ++ ui/battle_status.gd | 13 + ui/debug_panel.gd | 129 +++- world.gd | 9 +- 49 files changed, 1486 insertions(+), 416 deletions(-) create mode 100644 assets/effects/pickups/experience.webp create mode 100644 assets/effects/pickups/experience.webp.import create mode 100644 assets/effects/pickups/gold.webp create mode 100644 assets/effects/pickups/gold.webp.import create mode 100644 assets/effects/projectiles/apprentice_orb.webp create mode 100644 assets/effects/projectiles/apprentice_orb.webp.import create mode 100644 assets/effects/projectiles/arcanist_missile.webp create mode 100644 assets/effects/projectiles/arcanist_missile.webp.import create mode 100644 assets/effects/projectiles/arrow.webp create mode 100644 assets/effects/projectiles/arrow.webp.import create mode 100644 assets/effects/projectiles/basic_bullet.webp create mode 100644 assets/effects/projectiles/basic_bullet.webp.import create mode 100644 assets/effects/projectiles/boss_bullet_red.webp create mode 100644 assets/effects/projectiles/boss_bullet_red.webp.import create mode 100644 assets/effects/projectiles/boss_bullet_yellow.webp create mode 100644 assets/effects/projectiles/boss_bullet_yellow.webp.import create mode 100644 assets/effects/projectiles/player_staff_orb.webp create mode 100644 assets/effects/projectiles/player_staff_orb.webp.import create mode 100644 assets/effects/projectiles/staff_orb.webp create mode 100644 assets/effects/projectiles/staff_orb.webp.import create mode 100644 assets/effects/vfx/laser_body.webp create mode 100644 assets/effects/vfx/laser_body.webp.import create mode 100644 assets/effects/vfx/laser_core.webp create mode 100644 assets/effects/vfx/laser_core.webp.import create mode 100644 assets/effects/vfx/magic_circle.webp create mode 100644 assets/effects/vfx/magic_circle.webp.import diff --git a/actors/bosses/town/emperor_boss.gd b/actors/bosses/town/emperor_boss.gd index 3043b35..ecc365c 100644 --- a/actors/bosses/town/emperor_boss.gd +++ b/actors/bosses/town/emperor_boss.gd @@ -8,6 +8,7 @@ const MELEE_UTILS := preload("res://combat/melee_utils.gd") const TEXTURE_LOADER := preload("res://combat/runtime_texture_loader.gd") const EMPEROR_BODY_TEXTURE_PATH := "res://actors/bosses/textures/emperor_boss.png" const EMPEROR_WEAPON_TEXTURE_PATH := "res://art/final_materials/weapons/boss_weapon_emperor_sword.png" +const MELEE_EFFECT_TEXTURE_PATH := "res://assets/effects/vfx/magic_circle.webp" @export var max_hp: float = 5000.0 @export var defense_value: float = 260.0 @@ -54,6 +55,7 @@ var shadow_reposition_point: Vector2 = Vector2.ZERO var body_sprite: Sprite2D = null var weapon_sprite: Sprite2D = null var weapon_angle_offset: float = 0.0 +var melee_texture: Texture2D = null var visual_last_position: Vector2 = Vector2.ZERO var visual_bob_time: float = 0.0 @@ -64,6 +66,7 @@ func _ready() -> void: health_component.died.connect(_on_died) hp = max_hp visual_last_position = global_position + melee_texture = TEXTURE_LOADER.load_texture(MELEE_EFFECT_TEXTURE_PATH) _setup_body_visual() _setup_weapon_visual() burst_ring.visible = false @@ -309,6 +312,20 @@ func _hit_target_in_arc(radius: float, damage: float, arc_degrees: float) -> voi }) func _spawn_slash_effect(radius: float, color: Color) -> void: + if melee_texture != null: + var texture_slash := Sprite2D.new() + texture_slash.texture = melee_texture + texture_slash.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST + texture_slash.centered = true + texture_slash.modulate = color + texture_slash.position = line_direction * (radius * 0.42) + texture_slash.rotation = line_direction.angle() + texture_slash.scale = Vector2(0.24, 0.10) + effects_layer.add_child(texture_slash) + var texture_tween := texture_slash.create_tween() + texture_tween.tween_property(texture_slash, "scale", Vector2(0.34, 0.16), 0.14) + texture_tween.parallel().tween_property(texture_slash, "modulate:a", 0.0, 0.14) + texture_tween.finished.connect(texture_slash.queue_free) var slash := Line2D.new() slash.width = 12.0 slash.default_color = color diff --git a/actors/bosses/town/ranger_boss.gd b/actors/bosses/town/ranger_boss.gd index dceef0e..3801cf4 100644 --- a/actors/bosses/town/ranger_boss.gd +++ b/actors/bosses/town/ranger_boss.gd @@ -8,6 +8,7 @@ const MELEE_UTILS := preload("res://combat/melee_utils.gd") const TEXTURE_LOADER := preload("res://combat/runtime_texture_loader.gd") const RANGER_BODY_TEXTURE_PATH := "res://actors/bosses/textures/ranger_boss.png" const RANGER_WEAPON_TEXTURE_PATH := "res://art/final_materials/weapons/boss_weapon_ranger_blade.png" +const MELEE_EFFECT_TEXTURE_PATH := "res://assets/effects/vfx/magic_circle.webp" @export var max_hp: float = 3600.0 @export var defense_value: float = 190.0 @@ -71,6 +72,7 @@ var slow_factor: float = 1.0 var body_sprite: Sprite2D = null var weapon_sprite: Sprite2D = null var weapon_angle_offset: float = 0.0 +var melee_texture: Texture2D = null var visual_last_position: Vector2 = Vector2.ZERO var visual_bob_time: float = 0.0 @@ -82,6 +84,7 @@ func _ready() -> void: health_component.died.connect(_on_died) hp = max_hp visual_last_position = global_position + melee_texture = TEXTURE_LOADER.load_texture(MELEE_EFFECT_TEXTURE_PATH) _setup_body_visual() _setup_weapon_visual() aim_ring.visible = false @@ -412,6 +415,20 @@ func _spawn_piercing_arrow(direction: Vector2, damage: float) -> void: arrow.setup(self, direction, damage, 0.1) func _spawn_slash_effect(radius: float, color: Color) -> void: + if melee_texture != null: + var texture_slash := Sprite2D.new() + texture_slash.texture = melee_texture + texture_slash.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST + texture_slash.centered = true + texture_slash.modulate = color + texture_slash.position = line_direction * (radius * 0.42) + texture_slash.rotation = line_direction.angle() + texture_slash.scale = Vector2(0.22, 0.10) + effects_layer.add_child(texture_slash) + var texture_tween := texture_slash.create_tween() + texture_tween.tween_property(texture_slash, "scale", Vector2(0.32, 0.16), 0.12) + texture_tween.parallel().tween_property(texture_slash, "modulate:a", 0.0, 0.12) + texture_tween.finished.connect(texture_slash.queue_free) var slash := Line2D.new() slash.width = 12.0 slash.antialiased = true diff --git a/actors/bosses/town/twin_princes_boss.gd b/actors/bosses/town/twin_princes_boss.gd index 577de29..0567124 100644 --- a/actors/bosses/town/twin_princes_boss.gd +++ b/actors/bosses/town/twin_princes_boss.gd @@ -5,22 +5,50 @@ signal defeated const DAMAGE_NUMBER_SCENE := preload("res://effects/damage_number.tscn") const ROYAL_BOLT_SCENE := preload("res://effects/projectiles/royal_bolt.tscn") const TEXTURE_LOADER := preload("res://combat/runtime_texture_loader.gd") +const MELEE_UTILS := preload("res://combat/melee_utils.gd") const TWIN_PHASE1_BODY_TEXTURE_PATH := "res://actors/bosses/textures/twin_first_boss.png" const TWIN_PHASE2_BODY_TEXTURE_PATH := "res://actors/bosses/textures/twin_second_boss.png" const TWIN_PHASE1_WEAPON_TEXTURE_PATH := "res://art/final_materials/weapons/boss_weapon_twin_first_sword.png" const TWIN_PHASE2_WEAPON_TEXTURE_PATH := "res://art/final_materials/weapons/boss_weapon_twin_second_sword.png" - -@export var phase1_hp: float = 4000.0 -@export var phase2_hp: float = 4000.0 +const MELEE_EFFECT_TEXTURE_PATH := "res://assets/effects/vfx/magic_circle.webp" +const BARRAGE_WAVE_TIMINGS := [0.18, 0.42, 0.68, 0.96] +const BARRAGE_WAVE_OFFSETS := [ + [-10.0, 0.0, 10.0], + [-18.0, -9.0, 0.0, 9.0, 18.0], + [-28.0, -18.0, -8.0, 0.0, 8.0, 18.0, 28.0], + [-36.0, -28.0, -20.0, -12.0, -4.0, 4.0, 12.0, 20.0, 28.0, 36.0] +] + +@export var max_hp_value: float = 5000.0 @export var defense_value: float = 220.0 -@export var move_speed: float = 170.0 -@export var teleport_slash_damage: float = 30.0 -@export var teleport_charge_duration: float = 4.0 -@export var teleport_shock_damage: float = 30.0 -@export var teleport_shock_radius: float = 116.0 -@export var spear_charge_damage: float = 50.0 +@export var move_speed: float = 235.0 +@export var basic_attack_damage: float = 50.0 +@export var basic_attack_interval: float = 2.0 +@export var basic_attack_recover: float = 1.0 +@export var basic_attack_range: float = 90.0 +@export var teleport_interval: float = 5.0 +@export var teleport_charge_duration: float = 1.5 +@export var teleport_landing_radius: float = 132.0 +@export var teleport_landing_damage: float = 50.0 +@export var line_slash_damage: float = 72.0 +@export var line_slash_length: float = 392.0 +@export var line_slash_width: float = 84.0 +@export var line_slash_charge_duration: float = 1.0 @export var barrage_damage: float = 18.0 -@export_range(0.1, 0.9, 0.05) var desperation_threshold_ratio: float = 0.35 +@export var barrage_interval: float = 5.0 +@export var barrage_recover: float = 0.8 +@export var dash_charge_duration: float = 1.5 +@export var dash_duration: float = 0.68 +@export var dash_speed: float = 980.0 +@export var dash_hit_width: float = 44.0 +@export var dash_damage: float = 20.0 +@export var dash_player_root_duration: float = 3.0 +@export var dash_hit_lockout_duration: float = 5.0 +@export var dash_miss_stun_duration: float = 4.0 +@export var dash_miss_damage_multiplier: float = 1.5 +@export var dash_miss_burst_damage: float = 24.0 +@export var dash_miss_burst_radius: float = 120.0 +@export var dash_miss_teleport_delay: float = 1.0 @onready var body: Polygon2D = $Body @onready var spear: Polygon2D = $Spear @@ -33,37 +61,43 @@ const TWIN_PHASE2_WEAPON_TEXTURE_PATH := "res://art/final_materials/weapons/boss var target: Node2D = null var hp: float = 0.0 -var max_hp: float = 0.0 var current_phase: int = 1 var state: StringName = &"intro" var state_time: float = 0.0 +var recover_duration: float = 0.0 var action_committed: bool = false var invulnerable: bool = false -var teleport_cooldown: float = 0.8 -var spear_cooldown: float = 3.0 -var barrage_cooldown: float = 6.0 +var teleport_cooldown: float = 1.2 +var barrage_cooldown: float = 2.4 +var attack_cooldown: float = 0.8 +var phase_threshold_hp: float = 2500.0 +var normal_attack_counter: int = 0 +var barrage_counter: int = 0 +var barrage_wave_index: int = 0 +var pending_post_stun_teleport: bool = false +var dash_connected: bool = false +var line_direction: Vector2 = Vector2.RIGHT var teleport_target_position: Vector2 = Vector2.ZERO -var charge_direction: Vector2 = Vector2.RIGHT -var charge_start_position: Vector2 = Vector2.ZERO -var silenced_time_remaining: float = 0.0 -var root_time_remaining: float = 0.0 -var slow_time_remaining: float = 0.0 -var slow_factor: float = 1.0 -var desperation_active: bool = false +var dash_direction: Vector2 = Vector2.RIGHT var body_sprite: Sprite2D = null var spear_sprite: Sprite2D = null -var spear_angle_offset: float = 0.0 +var melee_texture: Texture2D = null var visual_last_position: Vector2 = Vector2.ZERO var visual_bob_time: float = 0.0 +var silenced_time_remaining: float = 0.0 +var root_time_remaining: float = 0.0 +var slow_time_remaining: float = 0.0 +var slow_factor: float = 1.0 func _ready() -> void: add_to_group("damageable") - max_hp = phase1_hp - hp = max_hp - health_component.setup(max_hp, defense_value) + hp = max_hp_value + phase_threshold_hp = max_hp_value * 0.5 + health_component.setup(max_hp_value, defense_value) health_component.damaged.connect(_on_damaged) health_component.died.connect(_on_died) visual_last_position = global_position + melee_texture = TEXTURE_LOADER.load_texture(MELEE_EFFECT_TEXTURE_PATH) _setup_body_visual() _setup_weapon_visual() _refresh_phase_visuals() @@ -76,77 +110,40 @@ func bind_player(player: Node2D) -> void: target = player func get_status_title() -> String: - return _locale_text("Grand Prince", "大王子", "大王子") if current_phase == 1 else _locale_text("Saint Prince Yage", "圣裔王子雅格", "聖裔王子雅格") + return "Twin Princes" func get_status_text() -> String: - return _locale_text("Phase %d%s\nHP %d / %d\nState: %s", "阶段 %d%s\n生命 %d / %d\n状态:%s", "階段 %d%s\n生命 %d / %d\n狀態:%s") % [ + return "Phase %d\nHP %d / %d\nState: %s" % [ current_phase, - _locale_text(" DESPERATE", " 殊死", " 殊死") if desperation_active else "", int(round(hp)), - int(round(max_hp)), - _localized_state_name(String(state)) + int(round(max_hp_value)), + String(state) ] -func _current_locale() -> String: - var ui_settings := get_node_or_null("/root/UISettings") - if ui_settings != null and ui_settings.has_method("get_locale"): - return String(ui_settings.get_locale()) - return "en" - -func _locale_text(en_text: String, zh_hans_text: String, zh_hant_text: String) -> String: - match _current_locale(): - "zh_Hant": - return zh_hant_text - "zh_Hans": - return zh_hans_text - _: - return en_text - -func _localized_state_name(state_name: String) -> String: - match state_name: - "intro": - return _locale_text("Intro", "登场", "登場") - "idle": - return _locale_text("Idle", "待机", "待機") - "teleport_mark": - return _locale_text("Teleport Mark", "跃迁标记", "躍遷標記") - "teleport_slash": - return _locale_text("Teleport Slash", "跃迁斩", "躍遷斬") - "spear_charge": - return _locale_text("Spear Charge", "枪阵突袭", "槍陣突襲") - "barrage_cast": - return _locale_text("Royal Barrage", "王室弹幕", "王室彈幕") - "phase_change": - return _locale_text("Phase Change", "转阶段", "轉階段") - "recover": - return _locale_text("Recover", "恢复", "恢復") - "dead": - return _locale_text("Defeated", "倒下", "倒下") - _: - return state_name.capitalize() - func _physics_process(delta: float) -> void: if state == &"dead": return if target == null or not is_instance_valid(target): _find_target() _update_status_timers(delta) + attack_cooldown = maxf(attack_cooldown - delta, 0.0) teleport_cooldown = maxf(teleport_cooldown - delta, 0.0) - spear_cooldown = maxf(spear_cooldown - delta, 0.0) barrage_cooldown = maxf(barrage_cooldown - delta, 0.0) state_time += delta - _update_desperation_state() _update_state(delta) _update_visuals() func receive_hit(payload: Dictionary) -> void: if invulnerable or state == &"dead": return - var result: Dictionary = health_component.receive_hit(payload) + var hit_payload := payload.duplicate() + if state == &"self_stunned": + hit_payload["damage"] = float(hit_payload.get("damage", 0.0)) * dash_miss_damage_multiplier + var result: Dictionary = health_component.receive_hit(hit_payload) var final_damage := float(result.get("damage", 0.0)) if final_damage > 0.0: apply_control_effects(payload) - _spawn_damage_number(final_damage, bool(result.get("is_critical", false))) + _spawn_damage_number(float(result.get("total_damage", final_damage)), bool(result.get("is_critical", false))) func apply_control_effects(payload: Dictionary) -> void: if payload.has("silence_duration"): @@ -158,8 +155,11 @@ func apply_control_effects(payload: Dictionary) -> void: slow_factor = minf(slow_factor, float(payload.get("slow_multiplier", 1.0))) func _find_target() -> void: - var players := get_tree().get_nodes_in_group("player") - target = players[0] if not players.is_empty() else null + for candidate in get_tree().get_nodes_in_group("player"): + if candidate is Node2D and is_instance_valid(candidate): + target = candidate as Node2D + return + target = null func _update_status_timers(delta: float) -> void: silenced_time_remaining = maxf(silenced_time_remaining - delta, 0.0) @@ -172,168 +172,259 @@ func _update_status_timers(delta: float) -> void: func _update_state(delta: float) -> void: match state: &"intro": - _process_intro() + if state_time >= 0.8: + state = &"idle" + state_time = 0.0 &"idle": _process_idle(delta) + &"basic_attack": + _process_basic_attack() &"teleport_mark": _process_teleport_mark() - &"teleport_slash": - _process_teleport_slash() - &"spear_charge": - _process_spear_charge(delta) + &"teleport_attack": + _process_teleport_attack() + &"line_slash_charge": + _process_line_slash_charge() + &"line_slash": + _process_line_slash() &"barrage_cast": _process_barrage_cast() + &"dash_charge": + _process_dash_charge() + &"dash_attack": + _process_dash_attack(delta) + &"self_stunned": + _process_self_stunned() &"recover": _process_recover() &"phase_change": _process_phase_change() -func _process_intro() -> void: - if state_time >= 1.0: - _start_teleport_attack() - func _process_idle(delta: float) -> void: if target == null or not is_instance_valid(target): return var to_target := target.global_position - global_position var distance := to_target.length() - if distance > 0.0: - spear.rotation = _weapon_guard_rotation(to_target, -42.0) - if current_phase == 2 and _can_use_skills() and barrage_cooldown <= 0.0 and distance >= (120.0 if desperation_active else 150.0): + if to_target.length_squared() > 0.0001: + line_direction = to_target.normalized() + if normal_attack_counter >= 4 and _can_use_skills(): + _start_line_slash() + return + if current_phase == 2 and barrage_counter >= 3 and _can_use_skills(): + _start_dash_charge() + return + if current_phase == 2 and barrage_cooldown <= 0.0 and _can_use_skills(): _start_barrage() return - if _can_use_skills() and teleport_cooldown <= 0.0: + if teleport_cooldown <= 0.0 and _can_use_skills(): _start_teleport_attack() return - if _can_use_skills() and spear_cooldown <= 0.0 and distance >= 110.0: - _start_spear_charge() + if distance <= basic_attack_range and attack_cooldown <= 0.0: + _start_basic_attack() return - if root_time_remaining <= 0.0 and distance > 80.0: - var direction := to_target.normalized() - var orbit := Vector2(-direction.y, direction.x) * (0.35 if current_phase == 1 else -0.35) - global_position += (direction + orbit).normalized() * move_speed * slow_factor * delta + if root_time_remaining > 0.0: + return + var preferred_distance := basic_attack_range * (1.1 if current_phase == 1 else 1.26) + if distance > preferred_distance: + global_position += line_direction * move_speed * slow_factor * delta + else: + var orbit := Vector2(-line_direction.y, line_direction.x) + if orbit.length_squared() > 0.0001: + var orbit_sign := 1.0 if int(Time.get_ticks_msec() / 260) % 2 == 0 else -1.0 + global_position += orbit.normalized() * move_speed * 0.16 * orbit_sign * slow_factor * delta + +func _start_basic_attack() -> void: + state = &"basic_attack" + state_time = 0.0 + action_committed = false + attack_cooldown = basic_attack_interval + _show_intent_text("Slash", Color(1.0, 0.84, 0.62, 1.0), global_position, 0.8) + +func _process_basic_attack() -> void: + if not action_committed and state_time >= 0.24: + action_committed = true + _hit_target_in_arc(basic_attack_range, 110.0, basic_attack_damage) + _spawn_melee_slash_effect(basic_attack_range, Color(1.0, 0.84, 0.62, 0.92)) + normal_attack_counter += 1 + if state_time >= basic_attack_recover: + _enter_recover(0.18) func _start_teleport_attack() -> void: state = &"teleport_mark" state_time = 0.0 action_committed = false - teleport_cooldown = teleport_charge_duration + (5.0 if current_phase == 1 else (2.45 if desperation_active else 3.2)) - if target != null and is_instance_valid(target): - var side := Vector2(52.0, 0.0) - side.x *= -1.0 if int(Time.get_ticks_msec() / 100) % 2 == 0 else 1.0 - teleport_target_position = target.global_position + side - else: - teleport_target_position = global_position + teleport_cooldown = teleport_interval + teleport_target_position = target.global_position if target != null and is_instance_valid(target) else global_position teleport_marker.visible = true teleport_marker.global_position = teleport_target_position teleport_marker.rotation = 0.0 - _show_intent_text("Blink Slash", Color(1.0, 0.82, 0.68, 1.0), teleport_target_position, 0.86) Sfx.play_event(&"boss_twin_teleport", global_position) + _show_intent_text("Teleport Slash", Color(1.0, 0.78, 0.62, 1.0), teleport_target_position, 0.86) func _process_teleport_mark() -> void: - teleport_marker.rotation += 0.18 - if state_time >= teleport_charge_duration: - state = &"teleport_slash" - state_time = 0.0 - action_committed = false - global_position = teleport_target_position - teleport_marker.visible = false - _spawn_teleport_shockwave(teleport_target_position, teleport_shock_radius) - _hit_teleport_landing_targets() + teleport_marker.rotation += 0.14 + if state_time < teleport_charge_duration: + return + state = &"teleport_attack" + state_time = 0.0 + action_committed = false + global_position = teleport_target_position + teleport_marker.visible = false + if target != null and is_instance_valid(target): + var to_target := target.global_position - global_position + if to_target.length_squared() > 0.0001: + line_direction = to_target.normalized() -func _process_teleport_slash() -> void: +func _process_teleport_attack() -> void: if not action_committed and state_time >= 0.08: action_committed = true - if state_time >= 0.28: - _enter_recover(0.4 if current_phase == 1 else (0.16 if desperation_active else 0.24)) - -func _start_spear_charge() -> void: - state = &"spear_charge" + _hit_targets_in_radius(global_position, teleport_landing_radius, teleport_landing_damage) + _spawn_radius_burst(teleport_landing_radius, Color(1.0, 0.78, 0.58, 0.86)) + _spawn_melee_slash_effect(basic_attack_range + 14.0, Color(1.0, 0.72, 0.56, 0.94)) + normal_attack_counter += 1 + if state_time >= 0.38: + _enter_recover(0.18) + +func _start_line_slash() -> void: + state = &"line_slash_charge" state_time = 0.0 action_committed = false - spear_cooldown = 4.8 if desperation_active else 6.0 - charge_start_position = global_position - charge_direction = (target.global_position - global_position).normalized() if target != null else Vector2.RIGHT - if charge_direction == Vector2.ZERO: - charge_direction = Vector2.RIGHT + normal_attack_counter = 0 + if target != null and is_instance_valid(target): + var to_target := target.global_position - global_position + if to_target.length_squared() > 0.0001: + line_direction = to_target.normalized() charge_line.visible = true charge_line.global_position = global_position - charge_line.rotation = charge_direction.angle() - _show_intent_text("Spear Charge", Color(1.0, 0.76, 0.58, 1.0), global_position, 0.88) + charge_line.rotation = line_direction.angle() + charge_line.points = PackedVector2Array([Vector2.ZERO, Vector2(line_slash_length, 0.0)]) Sfx.play_event(&"boss_twin_charge", global_position) + _show_intent_text("Heavy Cleave", Color(1.0, 0.74, 0.54, 1.0), global_position, 0.9) -func _process_spear_charge(delta: float) -> void: - if state_time < 1.0: - charge_line.visible = true - charge_line.global_position = global_position +func _process_line_slash_charge() -> void: + charge_line.visible = true + charge_line.global_position = global_position + charge_line.rotation = line_direction.angle() + if state_time < line_slash_charge_duration: return - charge_line.visible = false - if state_time < 1.35: - global_position += charge_direction * 480.0 * slow_factor * delta - if not action_committed: - action_committed = true - _hit_target_in_line(spear_charge_damage, 220.0, 38.0) - else: + state = &"line_slash" + state_time = 0.0 + action_committed = false + +func _process_line_slash() -> void: + if not action_committed: + action_committed = true charge_line.visible = false - _enter_recover(2.0) + _hit_target_in_line(line_slash_damage, line_slash_length, line_slash_width) + _spawn_line_impact(line_slash_length, line_slash_width) + if state_time >= 0.36: + _enter_recover(0.34) func _start_barrage() -> void: state = &"barrage_cast" state_time = 0.0 action_committed = false - barrage_cooldown = 5.8 if desperation_active else 8.0 - _show_intent_text("Royal Barrage", Color(1.0, 0.86, 0.62, 1.0), global_position, 0.9) + barrage_wave_index = 0 + barrage_cooldown = barrage_interval + _show_intent_text("Barrage", Color(1.0, 0.84, 0.58, 1.0), global_position, 0.82) Sfx.play_event(&"boss_twin_barrage", global_position) func _process_barrage_cast() -> void: - if not action_committed and state_time >= 0.35: + while barrage_wave_index < BARRAGE_WAVE_TIMINGS.size() and state_time >= float(BARRAGE_WAVE_TIMINGS[barrage_wave_index]): action_committed = true - _fire_barrage() - if state_time >= 0.82: - _enter_recover(0.42 if desperation_active else 0.7) + _fire_barrage_wave(barrage_wave_index) + barrage_wave_index += 1 + if barrage_wave_index >= BARRAGE_WAVE_TIMINGS.size() and state_time >= barrage_recover: + barrage_counter += 1 + _enter_recover(0.16) + +func _start_dash_charge() -> void: + state = &"dash_charge" + state_time = 0.0 + action_committed = false + barrage_counter = 0 + charge_line.visible = true + charge_line.global_position = global_position + charge_line.points = PackedVector2Array([Vector2.ZERO, Vector2(240.0, 0.0)]) + _show_intent_text("Twin Lunge", Color(1.0, 0.7, 0.52, 1.0), global_position, 0.92) + Sfx.play_event(&"boss_twin_charge", global_position, 1.0) + +func _process_dash_charge() -> void: + if target != null and is_instance_valid(target): + var to_target := target.global_position - global_position + if to_target.length_squared() > 0.0001: + line_direction = to_target.normalized() + charge_line.visible = true + charge_line.global_position = global_position + charge_line.rotation = line_direction.angle() + if state_time < dash_charge_duration: + return + dash_direction = line_direction if line_direction.length_squared() > 0.0001 else Vector2.RIGHT + dash_connected = false + charge_line.visible = false + state = &"dash_attack" + state_time = 0.0 + action_committed = false + +func _process_dash_attack(delta: float) -> void: + var start_position := global_position + global_position += dash_direction * dash_speed * slow_factor * delta + if not dash_connected and _player_intersects_segment(start_position, global_position, dash_hit_width): + dash_connected = true + _apply_dash_hit() + _enter_recover(dash_hit_lockout_duration) + return + if state_time >= dash_duration: + _start_self_stun() + +func _start_self_stun() -> void: + state = &"self_stunned" + state_time = 0.0 + action_committed = false + pending_post_stun_teleport = true + _show_intent_text("Staggered", Color(1.0, 0.66, 0.58, 1.0), global_position, 0.82) + +func _process_self_stunned() -> void: + if state_time < dash_miss_stun_duration: + return + if not action_committed: + action_committed = true + _spawn_radius_burst(dash_miss_burst_radius, Color(1.0, 0.72, 0.54, 0.88)) + _hit_target_in_radius(dash_miss_burst_radius, dash_miss_burst_damage) + state_time = 0.0 + return + if state_time >= dash_miss_teleport_delay: + pending_post_stun_teleport = false + _start_teleport_attack() func _process_recover() -> void: - if state_time >= 0.7: + if state_time >= maxf(recover_duration, 0.1): state = &"idle" state_time = 0.0 func _enter_recover(duration: float) -> void: state = &"recover" state_time = 0.0 - if duration > 0.0: - barrage_cooldown = maxf(barrage_cooldown, duration) + recover_duration = duration charge_line.visible = false teleport_marker.visible = false func _process_phase_change() -> void: phase_ring.visible = true phase_ring.rotation += 0.12 - if state_time >= 1.2: + if state_time >= 1.0: invulnerable = false phase_ring.visible = false state = &"idle" state_time = 0.0 + teleport_cooldown = 1.2 + barrage_cooldown = 1.5 + attack_cooldown = 0.8 -func _fire_barrage() -> void: - if target == null or not is_instance_valid(target): - return - var base_direction := (target.global_position - global_position).normalized() - var first_wave := [-18.0, -9.0, 0.0, 9.0, 18.0] - if desperation_active: - first_wave = [-24.0, -16.0, -8.0, 0.0, 8.0, 16.0, 24.0] - _spawn_barrage_wave(base_direction, first_wave, barrage_damage) - if desperation_active: - var timer := get_tree().create_timer(0.18) - timer.timeout.connect(func() -> void: - if is_instance_valid(self) and target != null and is_instance_valid(target): - var second_direction := (target.global_position - global_position).normalized() - _spawn_barrage_wave(second_direction, [-12.0, 0.0, 12.0], barrage_damage * 0.8) - ) - -func _hit_target_in_radius(radius: float, damage: float) -> void: +func _hit_target_in_arc(radius: float, arc_degrees: float, damage: float) -> void: if target == null or not is_instance_valid(target): return - if global_position.distance_to(target.global_position) > radius: + if not MELEE_UTILS.is_point_in_arc(global_position, line_direction, target.global_position, radius, arc_degrees): return target.receive_hit({ "source": self, @@ -345,7 +436,7 @@ func _hit_target_in_line(damage: float, length: float, width: float) -> void: if target == null or not is_instance_valid(target): return var start_position := global_position - var end_position := global_position + charge_direction * length + var end_position := global_position + line_direction * length if _distance_to_segment(target.global_position, start_position, end_position) > width: return target.receive_hit({ @@ -354,23 +445,46 @@ func _hit_target_in_line(damage: float, length: float, width: float) -> void: "crit_rate": 0.0 }) -func _hit_teleport_landing_targets() -> void: +func _hit_target_in_radius(radius: float, damage: float) -> void: + _hit_targets_in_radius(global_position, radius, damage) + +func _hit_targets_in_radius(center: Vector2, radius: float, damage: float) -> void: + for candidate in get_tree().get_nodes_in_group("damageable"): + if candidate == self or not (candidate is Node2D): + continue + if not candidate.has_method("receive_hit"): + continue + var node_2d: Node2D = candidate + if node_2d.global_position.distance_to(center) > radius: + continue + candidate.receive_hit({ + "source": self, + "damage": damage, + "crit_rate": 0.0 + }) + +func _player_intersects_segment(start_position: Vector2, end_position: Vector2, width: float) -> bool: + if target == null or not is_instance_valid(target): + return false + return _distance_to_segment(target.global_position, start_position, end_position) <= width + +func _apply_dash_hit() -> void: if target == null or not is_instance_valid(target): return - var distance := global_position.distance_to(target.global_position) - var damage := 0.0 - if distance <= teleport_shock_radius: - damage += teleport_shock_damage - if distance <= 84.0: - damage += teleport_slash_damage - if damage <= 0.0: - return - action_committed = true - target.receive_hit({ - "source": self, - "damage": damage, - "crit_rate": 0.0 - }) + if target.has_node("HealthComponent"): + var target_health: Node = target.get_node("HealthComponent") + if target_health != null: + if target_health.has_method("clear_shield"): + target_health.clear_shield() + target_health.set("hp", 1.0) + target.set("hp", 1.0) + if target.has_signal("hp_changed"): + target.hp_changed.emit(1.0, float(target.get("max_hp"))) + if target.has_method("apply_control_effects"): + target.apply_control_effects({ + "root_duration": dash_player_root_duration, + "silence_duration": dash_player_root_duration + }) func _distance_to_segment(point: Vector2, start_position: Vector2, end_position: Vector2) -> float: var segment := end_position - start_position @@ -384,18 +498,25 @@ func _distance_to_segment(point: Vector2, start_position: Vector2, end_position: func _can_use_skills() -> bool: return silenced_time_remaining <= 0.0 -func _update_desperation_state() -> void: - if desperation_active or current_phase != 2 or hp > max_hp * desperation_threshold_ratio: +func _fire_barrage_wave(wave_index: int) -> void: + if target == null or not is_instance_valid(target): return - desperation_active = true - move_speed *= 1.08 - teleport_cooldown = minf(teleport_cooldown, 1.0) - spear_cooldown = minf(spear_cooldown, 1.5) - barrage_cooldown = minf(barrage_cooldown, 1.6) - phase_ring.visible = true - phase_ring.rotation = 0.0 - _show_intent_text("Desperate", Color(1.0, 0.66, 0.50, 1.0), global_position, 0.94) - Sfx.play_event(&"boss_twin_barrage", global_position, 2.0) + var base_direction := (target.global_position - projectile_spawner.global_position).normalized() + if base_direction == Vector2.ZERO: + base_direction = Vector2.RIGHT + var wave_offsets: Array = BARRAGE_WAVE_OFFSETS[min(wave_index, BARRAGE_WAVE_OFFSETS.size() - 1)] + _spawn_barrage_wave(base_direction, wave_offsets, barrage_damage * (1.0 + 0.08 * float(wave_index))) + +func _spawn_barrage_wave(base_direction: Vector2, angles: Array, damage: float) -> void: + var payload := { + "slow_duration": 0.9, + "slow_multiplier": 0.78 + } + for angle_offset in angles: + var bolt := ROYAL_BOLT_SCENE.instantiate() + bolt.global_position = projectile_spawner.global_position + get_tree().current_scene.add_child(bolt) + bolt.setup(self, base_direction.rotated(deg_to_rad(float(angle_offset))), damage, payload) func _setup_body_visual() -> void: body_sprite = Sprite2D.new() @@ -405,12 +526,6 @@ func _setup_body_visual() -> void: body.add_child(body_sprite) body.color = Color(1.0, 1.0, 1.0, 0.0) -func _set_body_tint(color: Color) -> void: - if body_sprite != null: - body_sprite.self_modulate = color - else: - body.color = color - func _setup_weapon_visual() -> void: spear_sprite = Sprite2D.new() spear_sprite.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST @@ -427,33 +542,30 @@ func _refresh_phase_visuals() -> void: spear_sprite.texture = TEXTURE_LOADER.load_texture(TWIN_PHASE1_WEAPON_TEXTURE_PATH if current_phase == 1 else TWIN_PHASE2_WEAPON_TEXTURE_PATH) func _update_visuals() -> void: - var base_color := Color(0.84, 0.82, 0.88, 1.0) if current_phase == 1 else Color(0.95, 0.76, 0.58, 1.0) - if silenced_time_remaining > 0.0: - base_color = Color(0.72, 0.64, 0.92, 1.0) - elif desperation_active: - base_color = Color(1.0, 0.62, 0.46, 1.0) + var base_color := Color(0.84, 0.82, 0.88, 1.0) if current_phase == 1 else Color(0.96, 0.76, 0.58, 1.0) + if state == &"self_stunned": + base_color = Color(1.0, 0.62, 0.62, 1.0) + elif silenced_time_remaining > 0.0: + base_color = Color(0.74, 0.66, 0.92, 1.0) _set_body_tint(base_color) - phase_ring.default_color = Color(1.0, 0.64, 0.42, 0.9) if desperation_active else (Color(1.0, 0.82, 0.56, 0.85) if current_phase == 2 else Color(0.82, 0.86, 1.0, 0.8)) - var aim_direction := charge_direction if charge_direction != Vector2.ZERO else Vector2.RIGHT + var aim_direction := line_direction if line_direction.length_squared() > 0.0001 else Vector2.RIGHT if target != null and is_instance_valid(target): var to_target := target.global_position - global_position - if to_target != Vector2.ZERO: + if to_target.length_squared() > 0.0001 and state != &"dash_attack": aim_direction = to_target.normalized() spear.position = aim_direction * 20.0 + Vector2(0.0, 4.0) - spear.rotation = _weapon_guard_rotation(aim_direction, -42.0) + spear_angle_offset + spear.rotation = _weapon_guard_rotation(aim_direction, -42.0) _apply_prince_body_motion(aim_direction) var pulse := 0.82 + 0.18 * sin(Time.get_ticks_msec() * 0.01) if teleport_marker.visible: teleport_marker.scale = Vector2.ONE * (0.92 + 0.08 * pulse) teleport_marker.width = 3.0 + 0.8 * pulse - teleport_marker.modulate = Color(1.0, 1.0, 1.0, 0.76 + 0.14 * pulse) if charge_line.visible: - charge_line.width = 3.6 + 1.0 * pulse - charge_line.modulate = Color(1.0, 1.0, 1.0, 0.74 + 0.16 * pulse) + charge_line.width = 5.0 + 1.2 * pulse + charge_line.modulate = Color(1.0, 1.0, 1.0, 0.74 + 0.12 * pulse) if phase_ring.visible: phase_ring.scale = Vector2.ONE * (0.94 + 0.08 * pulse) phase_ring.width = 3.4 + 1.0 * pulse - phase_ring.modulate = Color(1.0, 1.0, 1.0, 0.74 + 0.16 * pulse) visual_last_position = global_position func _weapon_guard_rotation(direction: Vector2, guard_degrees: float) -> float: @@ -464,13 +576,19 @@ func _weapon_guard_rotation(direction: Vector2, guard_degrees: float) -> float: func _apply_prince_body_motion(aim_direction: Vector2) -> void: var movement := global_position - visual_last_position var motion_ratio := clampf(movement.length() / maxf(move_speed * get_physics_process_delta_time(), 1.0), 0.0, 1.0) - visual_bob_time += 0.09 + motion_ratio * 0.13 + visual_bob_time += 0.08 + motion_ratio * 0.14 var facing := -1.0 if aim_direction.x < -0.05 else 1.0 if body_sprite != null: body_sprite.flip_h = facing < 0.0 - body.position = Vector2(0.0, sin(visual_bob_time) * (1.0 + motion_ratio * 2.6)) + body.position = Vector2(0.0, sin(visual_bob_time) * (1.0 + motion_ratio * 2.5)) body.rotation = sin(visual_bob_time * 0.72) * (0.018 + motion_ratio * 0.04) * facing +func _set_body_tint(color: Color) -> void: + if body_sprite != null: + body_sprite.self_modulate = color + else: + body.color = color + func _spawn_damage_number(amount: float, is_critical: bool) -> void: var damage_number := DAMAGE_NUMBER_SCENE.instantiate() damage_number.position = Vector2(0.0, -44.0) @@ -479,24 +597,83 @@ func _spawn_damage_number(amount: float, is_critical: bool) -> void: func _show_intent_text(label_text: String, color_value: Color, world_position: Vector2, scale_value: float = 0.84) -> void: var popup := DAMAGE_NUMBER_SCENE.instantiate() - popup.position = to_local(world_position) + Vector2(-42.0, -56.0) + popup.position = to_local(world_position) + Vector2(-40.0, -56.0) if popup.has_method("setup_text"): popup.setup_text(label_text, color_value, scale_value) effects_layer.add_child(popup) -func _spawn_teleport_shockwave(center: Vector2, radius: float) -> void: +func _spawn_melee_slash_effect(radius: float, color: Color) -> void: + if effects_layer == null: + return + if melee_texture != null: + var texture_slash := Sprite2D.new() + texture_slash.texture = melee_texture + texture_slash.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST + texture_slash.centered = true + texture_slash.modulate = color + texture_slash.position = line_direction * (radius * 0.42) + texture_slash.rotation = line_direction.angle() + texture_slash.scale = Vector2(0.22, 0.1) + effects_layer.add_child(texture_slash) + var texture_tween := texture_slash.create_tween() + texture_tween.tween_property(texture_slash, "scale", Vector2(0.32, 0.16), 0.14) + texture_tween.parallel().tween_property(texture_slash, "modulate:a", 0.0, 0.14) + texture_tween.finished.connect(texture_slash.queue_free) + var slash := Line2D.new() + slash.width = 12.0 + slash.default_color = color + slash.position = line_direction * 14.0 + slash.rotation = line_direction.angle() + slash.points = PackedVector2Array([ + Vector2(-8.0, -22.0), + Vector2(radius * 0.3, -radius * 0.16), + Vector2(radius * 0.78, 0.0), + Vector2(radius * 0.3, radius * 0.16), + Vector2(-8.0, 22.0) + ]) + effects_layer.add_child(slash) + var tween := create_tween() + tween.tween_property(slash, "modulate:a", 0.0, 0.16) + tween.parallel().tween_property(slash, "scale", Vector2.ONE * 1.1, 0.16) + tween.finished.connect(slash.queue_free) + +func _spawn_line_impact(length: float, width: float) -> void: + var slash := Polygon2D.new() + slash.color = Color(1.0, 0.78, 0.54, 0.9) + slash.position = line_direction * (length * 0.18) + slash.rotation = line_direction.angle() + slash.polygon = PackedVector2Array([ + Vector2(-10.0, -width * 0.5), + Vector2(length, -width * 0.5), + Vector2(length, width * 0.5), + Vector2(-10.0, width * 0.5) + ]) + effects_layer.add_child(slash) + var tween := create_tween() + tween.tween_property(slash, "modulate:a", 0.0, 0.18) + tween.parallel().tween_property(slash, "scale", Vector2(1.08, 1.04), 0.18) + tween.finished.connect(slash.queue_free) + +func _spawn_radius_burst(radius: float, color: Color) -> void: var ring := Line2D.new() - ring.width = 5.0 + ring.width = 6.0 ring.closed = true - ring.default_color = Color(1.0, 0.74, 0.48, 0.9) - ring.points = _build_ring_points(radius, 20) - ring.global_position = center + ring.default_color = color + ring.points = _build_ring_points(radius, 24) + ring.global_position = global_position get_tree().current_scene.add_child(ring) var tween := create_tween() - tween.tween_property(ring, "scale", Vector2.ONE * 1.16, 0.18) + tween.tween_property(ring, "scale", Vector2.ONE * 1.18, 0.18) tween.parallel().tween_property(ring, "modulate:a", 0.0, 0.18) tween.finished.connect(ring.queue_free) +func _build_ring_points(radius: float, steps: int = 16) -> PackedVector2Array: + var points := PackedVector2Array() + for index in range(steps): + var angle := TAU * float(index) / float(steps) + points.append(Vector2.RIGHT.rotated(angle) * radius) + return points + func _on_damaged(_amount: float, remaining_hp: float, _source: Node) -> void: hp = remaining_hp _set_body_tint(Color(1.0, 0.8, 0.8, 1.0)) @@ -505,23 +682,21 @@ func _on_damaged(_amount: float, remaining_hp: float, _source: Node) -> void: if is_instance_valid(self) and state != &"dead": _update_visuals() ) - -func _on_died() -> void: - if current_phase == 1: - _show_intent_text("Phase Two", Color(1.0, 0.88, 0.62, 1.0), global_position, 0.94) + if current_phase == 1 and hp > 0.0 and hp <= phase_threshold_hp: current_phase = 2 - desperation_active = false - max_hp = phase2_hp - hp = max_hp - _refresh_phase_visuals() - invulnerable = true - teleport_cooldown = 1.0 - spear_cooldown = 2.4 - barrage_cooldown = 3.2 state = &"phase_change" state_time = 0.0 - health_component.setup(max_hp, defense_value) - return + action_committed = false + invulnerable = true + normal_attack_counter = 0 + barrage_counter = 0 + teleport_marker.visible = false + charge_line.visible = false + phase_ring.visible = true + _refresh_phase_visuals() + _show_intent_text("Phase Two", Color(1.0, 0.88, 0.62, 1.0), global_position, 0.94) + +func _on_died() -> void: state = &"dead" invulnerable = true teleport_marker.visible = false @@ -534,21 +709,3 @@ func _on_died() -> void: defeated.emit() queue_free() ) - -func _build_ring_points(radius: float, steps: int = 16) -> PackedVector2Array: - var points := PackedVector2Array() - for index in range(steps): - var angle := TAU * float(index) / float(steps) - points.append(Vector2.RIGHT.rotated(angle) * radius) - return points - -func _spawn_barrage_wave(base_direction: Vector2, angles: Array, damage: float) -> void: - var payload := { - "slow_duration": 0.9 if not desperation_active else 1.15, - "slow_multiplier": 0.78 if not desperation_active else 0.72 - } - for angle_offset in angles: - var bolt := ROYAL_BOLT_SCENE.instantiate() - bolt.global_position = projectile_spawner.global_position - get_tree().current_scene.add_child(bolt) - bolt.setup(self, base_direction.rotated(deg_to_rad(float(angle_offset))), damage, payload) diff --git a/actors/enemy/town_enemy.gd b/actors/enemy/town_enemy.gd index 92c1d82..6f210d7 100644 --- a/actors/enemy/town_enemy.gd +++ b/actors/enemy/town_enemy.gd @@ -4,6 +4,10 @@ signal defeated const DAMAGE_NUMBER_SCENE := preload("res://effects/damage_number.tscn") const ENEMY_BOLT_SCENE := preload("res://effects/projectiles/enemy_bolt.tscn") +const TEXTURE_LOADER := preload("res://combat/runtime_texture_loader.gd") +const LASER_BODY_TEXTURE_PATH := "res://assets/effects/vfx/laser_body.webp" +const LASER_CORE_TEXTURE_PATH := "res://assets/effects/vfx/laser_core.webp" +const SLASH_TEXTURE_PATH := "res://assets/effects/vfx/magic_circle.webp" const ENEMY_WEAPON_TEXTURE_PATHS := [ [ "res://art/final_materials/weapons/enemy_sword_guard_sword_normal.png", @@ -107,6 +111,9 @@ var base_weapon_position: Vector2 = Vector2.ZERO var base_projectile_spawner_position: Vector2 = Vector2.ZERO var visual_bob_time: float = 0.0 var death_sequence_started: bool = false +var laser_effect_root: Node2D = null +var laser_body_sprite: Sprite2D = null +var laser_core_sprite: Sprite2D = null func _ready() -> void: movement_rng.randomize() @@ -125,6 +132,7 @@ func _ready() -> void: hp = max_hp telegraph_ring.visible = false telegraph_line.visible = false + _setup_effect_sprites() _find_target() _update_visuals() @@ -545,6 +553,8 @@ func _enter_recover(duration: float) -> void: recover_duration = duration velocity = Vector2.ZERO telegraph_ring.visible = false + telegraph_line.visible = false + _set_laser_visual_visible(false) func _process_recover() -> void: velocity = Vector2.ZERO @@ -558,6 +568,16 @@ func _can_use_skills() -> bool: func _spawn_melee_attack_effect(radius: float, color: Color, spread: float, forward_offset: float, heavy: bool = false) -> void: if effects_layer == null: return + var slash_sprite := Sprite2D.new() + slash_sprite.texture = TEXTURE_LOADER.load_texture(SLASH_TEXTURE_PATH) + slash_sprite.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST + slash_sprite.centered = true + slash_sprite.modulate = color + slash_sprite.position = line_direction * (forward_offset + radius * 0.42) + slash_sprite.rotation = line_direction.angle() + slash_sprite.scale = Vector2(0.24 if heavy else 0.18, 0.10 if heavy else 0.08) + effects_layer.add_child(slash_sprite) + var slash := Line2D.new() slash.antialiased = false slash.width = 12.0 if heavy else 8.0 @@ -603,6 +623,11 @@ func _spawn_melee_attack_effect(radius: float, color: Color, spread: float, forw spark_tween.parallel().tween_property(spark, "modulate:a", 0.0, 0.12) spark_tween.finished.connect(spark.queue_free) + var slash_sprite_tween := slash_sprite.create_tween() + slash_sprite_tween.tween_property(slash_sprite, "scale", slash_sprite.scale * Vector2(1.3, 1.6), 0.14) + slash_sprite_tween.parallel().tween_property(slash_sprite, "modulate:a", 0.0, 0.14) + slash_sprite_tween.finished.connect(slash_sprite.queue_free) + func _show_intent_text(label_text: String, color_value: Color, scale_value: float = 0.8) -> void: if effects_layer == null: return @@ -643,6 +668,7 @@ func _update_laser_telegraph(length: float = ARCANIST_LASER_LENGTH) -> void: telegraph_line.global_position = global_position telegraph_line.rotation = line_direction.angle() telegraph_line.points = PackedVector2Array([Vector2.ZERO, Vector2(visible_length, 0.0)]) + _update_laser_visuals(visible_length) func _laser_blocked_length(length: float) -> float: var world := get_world_2d() @@ -845,6 +871,7 @@ func _on_died() -> void: set_physics_process(false) telegraph_ring.visible = false telegraph_line.visible = false + _set_laser_visual_visible(false) if has_node("CollisionShape2D"): var collision_shape := $CollisionShape2D as CollisionShape2D if collision_shape != null: @@ -875,6 +902,55 @@ func _on_died() -> void: queue_free() ) +func _setup_effect_sprites() -> void: + if effects_layer == null: + return + laser_effect_root = Node2D.new() + laser_effect_root.name = "LaserEffectRoot" + laser_effect_root.visible = false + effects_layer.add_child(laser_effect_root) + laser_body_sprite = Sprite2D.new() + laser_body_sprite.texture = TEXTURE_LOADER.load_texture(LASER_BODY_TEXTURE_PATH) + laser_body_sprite.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST + laser_body_sprite.centered = false + laser_body_sprite.visible = false + laser_body_sprite.modulate = Color(1.0, 1.0, 1.0, 0.92) + laser_effect_root.add_child(laser_body_sprite) + laser_core_sprite = Sprite2D.new() + laser_core_sprite.texture = TEXTURE_LOADER.load_texture(LASER_CORE_TEXTURE_PATH) + laser_core_sprite.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST + laser_core_sprite.centered = true + laser_core_sprite.visible = false + laser_effect_root.add_child(laser_core_sprite) + +func _update_laser_visuals(length: float) -> void: + if laser_body_sprite == null or laser_core_sprite == null: + return + var show_laser := telegraph_line.visible and length > 0.0 + _set_laser_visual_visible(show_laser) + if not show_laser: + return + var texture_size := laser_body_sprite.texture.get_size() if laser_body_sprite.texture != null else Vector2(84.0, 24.0) + var body_scale_x := maxf(length / maxf(texture_size.x, 1.0), 0.01) + var body_scale_y := (1.08 if state == &"skill_laser" else 0.8) * (1.08 if elite else 1.0) + laser_body_sprite.position = Vector2.ZERO + laser_body_sprite.rotation = 0.0 + laser_body_sprite.scale = Vector2(body_scale_x, body_scale_y) + laser_body_sprite.modulate = Color(1.0, 0.8, 0.62, 0.92) if state == &"skill_laser" else Color(1.0, 0.9, 0.78, 0.62) + laser_core_sprite.position = Vector2(0.0, 0.0) + laser_core_sprite.scale = Vector2.ONE * (0.78 if state == &"skill_laser" else 0.62) * (1.12 if elite else 1.0) + laser_core_sprite.modulate = Color(1.0, 0.88, 0.6, 0.94) if state == &"skill_laser" else Color(1.0, 0.94, 0.8, 0.72) + laser_effect_root.position = Vector2.ZERO + laser_effect_root.rotation = line_direction.angle() + +func _set_laser_visual_visible(visible_value: bool) -> void: + if laser_effect_root != null: + laser_effect_root.visible = visible_value + if laser_body_sprite != null: + laser_body_sprite.visible = visible_value + if laser_core_sprite != null: + laser_core_sprite.visible = visible_value + func _spawn_death_burst() -> void: if effects_layer == null: return diff --git a/assets/effects/pickups/experience.webp b/assets/effects/pickups/experience.webp new file mode 100644 index 0000000000000000000000000000000000000000..d2f2fdb3926024d5cf6e3806f0eaa6575847fe0a GIT binary patch literal 610 zcmV-o0-gO*Nk&Fm0ssJ4MM6+kP&il$0000G0000T0012T06|PpNRa>l00D4S+jbMt zTvP*Shz}$@kPsmcz9R-5fq$k6=&#L+fQV4rwhbfg>3Jec#$f`OogR38L?u;S`!AuZ zD-HFwpI;uwR%__vlNftNH1H?o^plgL38EN{g9hWHanN9FG=;I}qH)N<;Hef<$6+KP&go>0RRB72mqY{DjonH z06uLfkwv5;p_}@w06+%Bwg66)K^#(;2G|R+t>gdOIj1jO`aqg?*+1T*O)x_q=UZ{xp0GzKpt_}@kbpP*^m0aAzkP94c!VfuVKphSP?f1$(wn%irAHxKK@|EE9y zW}4kLMjp19&z_NphIt_Pc%xYiM=D<<)#++fvRb;csa#~fq8$F{qMV=T4&K)qdx3HL z60h#-zX+5iI*?v|S0&H>R2Dy@mw&Kdt>_A(n#VPBPGgvB5MP$F^9!B*t%VsOT!gZU zr2ISONc^4#nd<`cIKyA;HD}Vk6)Z2lqFOKk<4*BvDTA~8Fh99-F*iq+BpIYVC{ wb`$EzwR8n*?(wvwDfE8iaqJ&GeJ#}mWc~(#!d0fCh`a(%M(3t-dxxX|0N^bju>b%7 literal 0 HcmV?d00001 diff --git a/assets/effects/pickups/experience.webp.import b/assets/effects/pickups/experience.webp.import new file mode 100644 index 0000000..ed87c08 --- /dev/null +++ b/assets/effects/pickups/experience.webp.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bwsjw8ih2y6x0" +path="res://.godot/imported/experience.webp-ab1d9feaed0534f8f19daf8698b94cfa.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/effects/pickups/experience.webp" +dest_files=["res://.godot/imported/experience.webp-ab1d9feaed0534f8f19daf8698b94cfa.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=1 diff --git a/assets/effects/pickups/gold.webp b/assets/effects/pickups/gold.webp new file mode 100644 index 0000000000000000000000000000000000000000..ac4ed9d81e1755cf3a89d0ee0c2e175a5bc5cf64 GIT binary patch literal 418 zcmV;T0bTx5Nk&GR0RRA3MM6+kP&il$0000G0000K000^Q06|PpNNWH900Cee+jf$N z(}Q~tSy6syBl1%P?yzDR5k%8At(;u1z-}hyqawrZLoCaS5=JM8C6Nn+e$gu$P7I`Y z1O#3!=YyAHTX*Kj)tE_Xh8qSFV*VcrK~LVv5=IYxysG7I^kIYm09H^q zAP4~f0PqF?odGHp02=^4Z7`HaBqE|AC<>^64T)?3tS^xIPY{OSgaGsa=}{sv3-R_E zDqVyBpqelfQnco6^fS850092`?|odFwf>h*%R7mDxZC~eBR|N?IgG?eH0GK=O`3Ok zPTbSSmDt2ZI)PXp?MK+FC?uzjf5SVp{SUAKLj(T*{N=_!qrnaeiC&@?J`R4KH&`pS6Kk+@MyK{00DqwYunNY zDMx;xg&5Sa6$o--UdnYh!(Ds~mC#vS|5EFdBnxNW4!%XoZ`pezFeFycj#M;I`a zWgv{EX%=!=kR$Xnw*quw?J{qg21;`IS*K26ywl=Z=HYL=CLv3w(IVVa^ChK(lGN4;U#GaCA%nvAjx0{~1O@9U$1#R`iBgOR>}oP+>i z%*|b%wm%()_UYvuhcQCfEsPM)3ybb73Op~0(y}|tk|>fvra@*3nT^OiJ2LM8fp>|} zJ4xo=D5L^xG5W8Ri{`Au#5xEHh0r?QAkCB=*^?$x) zM|HO(%N4-B{QCyr5fuWpGGXYY4@du4CySj6Z+{bhe6teIi+{hOH@zRE3QW+*98~Al zQh?W&hQ2-8Jkf{1=|~t@fl!Xthq>#J|Gb{Uym=nxqTvH8l7G3!`5J@QtJ7zE<^M&^ z|Na|(vQ__qNrCZEShY+y^a~Nl_7(BYydK{Be^63rV+pf;{bYp?%e6XC)oVnyoqct_yT zfFT7YEyYv7^9x-6GmKCEbVOeUh0Ld#w>H@Fjxi>ypOpgaoT~VEIm5+JR;?`z?Ng;i zf%C!NT-^}YKxvu=66#iEB0PzX-W@4%6B9{Li9xM?oL>USL a%MJS|6K*DxVA!)3k>UUT?GO*bumAvLmy%on literal 0 HcmV?d00001 diff --git a/assets/effects/projectiles/apprentice_orb.webp.import b/assets/effects/projectiles/apprentice_orb.webp.import new file mode 100644 index 0000000..755ea6b --- /dev/null +++ b/assets/effects/projectiles/apprentice_orb.webp.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://djie3lyfe3tda" +path="res://.godot/imported/apprentice_orb.webp-93af867cbf120263f905a5943f4eb37a.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/effects/projectiles/apprentice_orb.webp" +dest_files=["res://.godot/imported/apprentice_orb.webp-93af867cbf120263f905a5943f4eb37a.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=1 diff --git a/assets/effects/projectiles/arcanist_missile.webp b/assets/effects/projectiles/arcanist_missile.webp new file mode 100644 index 0000000000000000000000000000000000000000..7946b709485fb10d5b5a83e84b500bef53792e2c GIT binary patch literal 1168 zcmV;B1aJFNNk&G91ONb6MM6+kP&il$0000G0000Z001KZ06|PpNDKl101cpQZF|~C zdOz86uz8r7d3Lj#E1%u0nVA{f!LlrU3)`vmTtP%k0K?hJt6AQbv^96oGBV=qRYM3M zDuA3w{TZ4YigR#S1kkQkyFUZM1%yBti!+AkM_+QEjSfE>)MfU)cjdHsdGRfQVEauL zPKh>^HlK8Q5f*^V>E);&KlN*m0oPs%^EKN87rC_gxK)=Xl79cuPu>SCA5pX5B9Thj zxjT@`n2oFs&%|h~iNT3dCoiL1Y)JJ(fZ!{xL4Pc2l>Fhm5C44CoSJz|M4TYcRFOiU@QxrL`CR@%w+DV ztNyRt+di@QsF_nw}(U+amg zrqA_V6_@<&Yu$gB7Xx7-%Um*tQX7k5wP1P8BJEDwzq7sPVXm&+3{+7^F#FT`-~0K} zO-(f*K%lB~H<1#(iBeDy8dM84Gw!`{^u*^s^V2>)99P#^g)s@s4Pr9&q@qX&1ja-3 zy$|ay_8Zc)KMh37jr$pQHQN=?E%^0$wd#pCfma0w!~ZWntlS0aj2r zAXWkZ0B{ZfodGH&03-lDZ7h&Qq#~i06_`K<#I^uhjmepZ$^Q#~AUY(r!@9XR{OJDE z@&m93;jcpekRPhPpg&Zcm@t;t9 zU~|3EP*v2iaR1mY<7tba&Ze&AWy_SHcd7s%ZPG_#zL-ickEN#BE9~ zLY)|ZmNDM?z|td^24wLk1#w=s z@b&nNb*pL5%Vmq?H4|RrpQXr1wB0=CI_uzCn=l$g+`YD!qw}(FOFFzq=r?Kq>kI$> i$lO|LPH9WsEQL*bT3$JQir2Knt3FGC=f@Jp7VA^$Zo0^BekzP}{Z*BTY`5as^m2pX7KWpXa%8agYQl>oZ2k z=L$2=^AHND0}J!#CJbfOe@yOswv&C#G>#;Y1d0)wLqGXC09H^qASM9-05AstodGH{ z01N;=Z7`BXq$44rFc=5`fDMUd0Ko14?YwiK9Y76j|M9%uve$T55yf+F{|<*GiaqX9 zY*cf2ahw1E{{3`l_u-j{hodS?5q5uXDKrzRA*^3)LBk$=m%c2Nc$hYq%caXd_Kjay z-R%`O`jVI6d%<`E`x!I%Jc$vLKY_P*MzlUEuTm;)Kx1(L zc=d0pm7kM{>NH~ga|-HQ-MLI9q20^RF%~=6#D74I1oidV2VNHKN3avnz<-uP+b4M+ zZ!j?tr$I)muS}u~TK@O!J^cb#44?VBWsNdZnf7(W>o3B`mKTzKC#v#~pI-^eCXmrk e_gy66b7FM-d3{lT;$!<4lvPD}2&;S7MRKOJg literal 0 HcmV?d00001 diff --git a/assets/effects/projectiles/arrow.webp.import b/assets/effects/projectiles/arrow.webp.import new file mode 100644 index 0000000..07b6129 --- /dev/null +++ b/assets/effects/projectiles/arrow.webp.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cdx3stshq8bu3" +path="res://.godot/imported/arrow.webp-9242dd6011b1d83a693f34863960dad3.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/effects/projectiles/arrow.webp" +dest_files=["res://.godot/imported/arrow.webp-9242dd6011b1d83a693f34863960dad3.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=1 diff --git a/assets/effects/projectiles/basic_bullet.webp b/assets/effects/projectiles/basic_bullet.webp new file mode 100644 index 0000000000000000000000000000000000000000..3ada7b270789948b99f1fc92533a91410352a884 GIT binary patch literal 744 zcmVa|~;Z06P{y7TtIiB?UMuAe#jXyyV#m&sw+U%(){;0RTkMKyVbr z5*qlcqyT`T2mqQS7)}tKiW&fzFfYrTU`3R4fBRqQfc9NffFzM7lN5_pUH4`%u!7MH z1O-?oC=#onV{ZnMH!Gls90{PgqpXNJkGxrOZ&?6@FvdLX9ykr_;Emb2#IqETjHrP% zOU^Y0>DXHphl+g`K$Wp3dGN@2b;6L?igku&5P;bWrmPyhinJxqCdq?GTej_T0GbNA z?8Qu(-2=i5LlG2Uq=-OpvaAt|q$-kR&9Y@0{N7EHj8xHs$>7B^006=O2m>HqH*+Ld zv}}PeAb((tv8-cBk!4v{6%2^xKr=EdQWc7#DV75N%90MUA}O-0NB|fBR8Ih)N#Iig z09H^qAQAxp0B{HZodGH;01*H_Z7`EYq$44rFcnw;fDMUk0I4TXq1s?Nj{iW!ANv*h zSMUGO<<~cg$oviEOP-(`B32>u z>*%;Y`4xzG)BsTs^nWbw%_};|{;YZAAY0hcR2b*6K@V zw<>kX{R4P{W&certa)zTg9*6He%dJQl^{JN*uQd{#@xA3t(*U&Yf2~72McCrtECSZ z#fDG7w95aA1{jeD^9l{A*9DC3)JL6=Au>T!cmeg?Llx)!fB4X@Km66Dz^Q6LU+W(R a-;*09A;Cg){xEpk{&z(-Kk`xhN5B9#gg^lR literal 0 HcmV?d00001 diff --git a/assets/effects/projectiles/basic_bullet.webp.import b/assets/effects/projectiles/basic_bullet.webp.import new file mode 100644 index 0000000..7fadbea --- /dev/null +++ b/assets/effects/projectiles/basic_bullet.webp.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dxjpvyc4cvgsl" +path="res://.godot/imported/basic_bullet.webp-1ec75e1ff79dfea252f5918b811c08a1.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/effects/projectiles/basic_bullet.webp" +dest_files=["res://.godot/imported/basic_bullet.webp-1ec75e1ff79dfea252f5918b811c08a1.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=1 diff --git a/assets/effects/projectiles/boss_bullet_red.webp b/assets/effects/projectiles/boss_bullet_red.webp new file mode 100644 index 0000000000000000000000000000000000000000..7fe3f17f0c818f21e49fe28b8654ee1c7024c471 GIT binary patch literal 354 zcmV-o0iFI*Nk&Fm0RRA3MM6+kP&il$0000G0000f000pH06|PpNM`^500CeW+g6)5 z2xt;;hXW)$Pyhx{hJv8k3vPUN}QFK5VNXkG`1Va_OMq%lIw$}F)I->t- z+qPj`lHK}3v^gN}UX)#eS}R=-5vn+Ce|`L#?fUo~;Wtf=M$SuuAdzu!1y)cvAkqK; z05ApsodGH;01^N`Z7PyRq#_}qFdC=;fDMUc0JF2&em9^YfB5(Dnu9UY&|R( z`IF};7eHVD{^&{=&ERs&uUmKJ0q34jngWFT4eTdBzlS)(%T6)$k*+V7km){DL;w0a zp`-q-^W!b~r_Pl>=_l{8E{|K0YK0Hq5nb~*MnJks!ccDHQNVXspC09Whof~F`?iq< zImDZc>huQ=upa_trc6K#qy(&ibQBG4qI(-A-B-|82WWRvi@;|usKJG#$l3^!b7m2$vZE literal 0 HcmV?d00001 diff --git a/assets/effects/projectiles/boss_bullet_yellow.webp.import b/assets/effects/projectiles/boss_bullet_yellow.webp.import new file mode 100644 index 0000000..0be5dac --- /dev/null +++ b/assets/effects/projectiles/boss_bullet_yellow.webp.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c7rali64y1xjd" +path="res://.godot/imported/boss_bullet_yellow.webp-55cbf61618aeda6aca6abb5cf5fe1df5.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/effects/projectiles/boss_bullet_yellow.webp" +dest_files=["res://.godot/imported/boss_bullet_yellow.webp-55cbf61618aeda6aca6abb5cf5fe1df5.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=1 diff --git a/assets/effects/projectiles/player_staff_orb.webp b/assets/effects/projectiles/player_staff_orb.webp new file mode 100644 index 0000000000000000000000000000000000000000..00394e40704f82d14a15e8327f837deaee5e1808 GIT binary patch literal 1332 zcmV-41U3<dNAouL_roLSrmx?CI>_`8PhRKDyq7TAghW;stQg-ly!{*suHs? z7kNzgI0e!zmQAg9nGkQCsiW*3biqzT9oIkyK8 zsp2b>V%Cv!M0-#u5dcF2M3vaXs0s&gW&%iP4-6s$FvHY2qmIO)GDK^fv4Ej7joDOp zl12Osjj1vFsxd~K1&gFBw(@hEXWd2iq*xUG@zI^;O5!X{ZOj`~rf90l%}oy;H8kHp z-_ZUEjX70jPFYeabUeRd?A6o^Aw!wc|ChTDZFh6 zU4!hC=bemJCKU`rq$}=HMe)}Dsh1@MY)0? zxKB}VEVeSSBP0seiHOd#PWZYxKc92~{zN2~@(b7xp_xk0Kgu%kXCCT|;%m(QiR5qo zT#MxV%=qH9ce~O)!Cz8%tC6RKjc<7MH&__PPDOvEkkYaT>?+h+%+PIl=03SVP=3_Ek-;(mTYcC& zv;%mh9k!pY8H=h3nXY1MGZB9~{}%SZ{+!AzEAFw%wUqwphVn^5klATZ^?kskW_ zMWl5M?hn|&ZcN1d-F4MfDttyrUwChH5eV?6dvVdBZV$OV%?)5!@A#s0{p}c^s!! zQ+Q9Yo+6PYSuj*9_*l|JRbT4p{h|J>efPO;FS$Hpb z2nvzC8fF~-cd>5yHSdThazq7F$+ljkWZ6j`yn|*j2k8E}h2PSSmv}<0X4%oy5(NM! z5Q*?QHLE!~u33b^OPoYRFx}n{sYYQ2Ap}q}4;&Ph>Y0+h*a^D zNwI6l7@{pGln8*X0-{1}VN-ImPIdCSGRczz;Rxg@G^emecpSgF#Ud>#jYmK>qN{Xgw++KR+O+)wl@dn>d zp)*5c_LL>(O5JpWZH8$NgcIPGFxPH8-e2isUZR=2!jW9W6lcYJ;Y@F_`kWRJkL?w&{@J+(83|GQt!NN?1{=Hr48oby943U zwajmEuEGBwjt?i6xAiG?cJz0aE>fx`bI4Vkbv+S56)^}K<2u(6=_OF)7+&%wOXcJ` z02?>n_RVl`*5ImID@!h1YE?r}Rbw01G(@<%62IoRm?KC*O0M;TGq|N{mRv*>3IZ7r zexO3QW&qAu=uZHcCE@y3P&gpW0ssII5&)e6Dlh;r06t+Pjzy#*As##g06+!=wg66< zbRTch{K%@`)qeVMa}^GQ&I_rz6Ojk|yu<0O&jDOZxyc>Ub0MW$fQT0P9Ki-8h1OR&dsrS-G0^lmA=l zqLl1HrSP7eUf*Vyo>&s+_s9SM0RH&0W*w-@75=K~6?D7|`f519?w)c87E!Z>!gn>n z@MOcU-4EDTt%$-IB8t7Tn_4`KyNLCgKGaiH27>3pu zIXO7AYsr^U6h(*vEi%CwnsCYf5L@0eGU(W_OgJUNJ;vX^7UV8Iu98!%&mMK|9wLWB zOM~vfBKq?oHc zPB4an3~~5nC40DL(Dm8Crneozgu|or^NZIn{4J~F{6X{rC$w&x z+!<;=baYD7rJWn+w56F##R{@-O?E2v;~-#0YRJ}!YfI6F7kWV9;2w<=h9%;FEekpr zGS3KO{6g62%pBcFhnOH~iKTgU(RNf=ob7o^ZNk65#H7$8lCeVQG%z}%I`ps7#)1Cm O{-IKsclA1T1ONbCV9fad literal 0 HcmV?d00001 diff --git a/assets/effects/projectiles/staff_orb.webp.import b/assets/effects/projectiles/staff_orb.webp.import new file mode 100644 index 0000000..9f24ef9 --- /dev/null +++ b/assets/effects/projectiles/staff_orb.webp.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cgefm2og8yotn" +path="res://.godot/imported/staff_orb.webp-68de30a3e15c0087a324f5b809a66e64.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/effects/projectiles/staff_orb.webp" +dest_files=["res://.godot/imported/staff_orb.webp-68de30a3e15c0087a324f5b809a66e64.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=1 diff --git a/assets/effects/vfx/laser_body.webp b/assets/effects/vfx/laser_body.webp new file mode 100644 index 0000000000000000000000000000000000000000..c88ced3b99238656ea0f261fe1a7367400f33ba5 GIT binary patch literal 420 zcmV;V0bBl3Nk>0RRA3MM6+kP&il$0000G0000}000*N06|PpNHzcf00B3k761@7 z$k2{pe*qK`As+V&FcjH_3M#0ef(k0=pJQ)s5&h4A2@5tHxbWV-aA3oN2?GLFP&gn! z0RR9n4FH`1DpUX%06uLllSZT>p`kLeOE7>9iEIF_RRBK#J76~LfPTPif7s>!`BVS@ zalNn}mVbbGC;rEK|J~U7|N4LGZBv%(T%bHY)sSaPu)`PWHFHmv%tb`UEYlb|G%Fg! z`@-#Ygi@%zS&szi0092}#-9J}d=+Y9Zl&hx%21nJ7lDeztT!tg-Q|5!=+^;DiCAua zntSqV`Q%D`>jNK^`YbJZ0qz~S`E3Cunavby>cr%~yvc83;U)_ykZUpm!TJixDkSyG zc_rrQ##81JnTu&}`Kst4@xF}K`^hYyraC;fKehZn1J1P7F1>S_)tmpA(J~vJmx^Pj z3|O)kHI;ZQ+Wus+Dql%%Bb?^3N&Ux>V}A%J7i?fO9rbLF50r3>gpfrtI4e9dwW&j6 O^%%>SuFdw`6+i&<+`JV4 literal 0 HcmV?d00001 diff --git a/assets/effects/vfx/laser_body.webp.import b/assets/effects/vfx/laser_body.webp.import new file mode 100644 index 0000000..1e414ab --- /dev/null +++ b/assets/effects/vfx/laser_body.webp.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://i0oat3dopdxg" +path="res://.godot/imported/laser_body.webp-a24d4c8e3f176fa4750f52357edb0e9d.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/effects/vfx/laser_body.webp" +dest_files=["res://.godot/imported/laser_body.webp-a24d4c8e3f176fa4750f52357edb0e9d.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=1 diff --git a/assets/effects/vfx/laser_core.webp b/assets/effects/vfx/laser_core.webp new file mode 100644 index 0000000000000000000000000000000000000000..d41894d21f439c0542d8fcc28013fe7b37608b82 GIT binary patch literal 1284 zcmV+f1^fC^Nk&He1ONb6MM6+kP&il$0000G0000f001cf06|PpNRt8p00EG#{r}lW z|NN^A@;uGhlPR*}>6nM*a%>yL*lUe($fGqcz_yNd$7}lr#vA-|1rfo(Z6ihAW5&)O z0B4hs&Xhu-aFa^_Qo|q=$z0r&v|8=yA`?M~@#o-0MJ12QGW?S#ReS4eC>82Pp2_c4Y!wC;MDXVhi#O%WIn>TM3XHSekYoOZLFvn; z?k(~2_5M^@s=@;@(9xBBFV=Xs7ss39=>K32WRhv{>PP-QUY*&ML=q4H=%vo8zO{zb z=FM4ZKk+6iyt4BVnm2nZ!)XZsqA0`2*+n!zE{qtGaUhW;r=Pg}8O^_YC(^Zqm~=@s z&#tYmA-36u0&lsqTZV064Y@5?i~XD_Wohp+y5*jRU`j!k?|Xyp&0b5GLZKGaULo{a zLId19vzivsEjE=>u5tknB+MJXdm>Fsh=C~DG=5?x-$JC2ZY99c{!q&Ckiy!r8u<%{_7eqpAiAb}?A*|V@{3gsE2A95eG zT+`-(!Pl350tq8)Y)?%n7oWzCD`Rw3pAD;WsnZ^}5hCc-$~iTITES(aizU zMzEmqUj=3_@Au;ye?qI3R5)R>Ywf!KyEK00WSCUtAJ0Dz^>PDhyl;4!EEt6`z7B8r zM@{G{iPu<=2@NukbihaN4%|NM(z{~`DP{y}mhwm62s#!00Q^aBKOe>!yn_dXq)aeKw@5r6>8l6O}C literal 0 HcmV?d00001 diff --git a/assets/effects/vfx/laser_core.webp.import b/assets/effects/vfx/laser_core.webp.import new file mode 100644 index 0000000..1516c28 --- /dev/null +++ b/assets/effects/vfx/laser_core.webp.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b0iusoujp21aq" +path="res://.godot/imported/laser_core.webp-7f57b5ebdb9a25408a4cb260c897237f.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/effects/vfx/laser_core.webp" +dest_files=["res://.godot/imported/laser_core.webp-7f57b5ebdb9a25408a4cb260c897237f.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=1 diff --git a/assets/effects/vfx/magic_circle.webp b/assets/effects/vfx/magic_circle.webp new file mode 100644 index 0000000000000000000000000000000000000000..d4555335449ab3b7affceea39b10548d0b4f7996 GIT binary patch literal 4822 zcmV;{5-IIcNk&G_5&!^KMM6+kP&il$0000G0001g0RVpi06|PpNbv&z00Bp!kZlC$ z5YAurhlq%T_>fiGZrDnpPzs_T3Zftiq96*wJ-NMB6DL2K^;Sgx9Vw6-Hly2ugH{Dq zR`P=5533LngAkGV<(-v?_&`L2h|Djqt#Dy}d10j^%rCEZOLh0m{Inx8-5qbT^UM2` ztJ$mbjBNIBQo#A;>1QKiS&gb9iku-ts490~riw@Bmq(w?%&N*vRXvWw-4S;`j-!)_ zs*+TfN9UKPDDKW1HL-ss{2EK@@9rWpzdS-AomN#Ng0JNfJ|*HL$q^Brt*YX`01@Vw z%@yuWRk{7k_V&wyt7^y8{Ia>CXQ-SZSgX!9%xUx!`&!+fA?Sq*^UIz}RmB;B{gy1y zI&TB7dBYeH2=;$;R8>TnUp7=e-{J1qW^Y-%lWMOmd?WxqwI{j(PzCBm}* z6JdV2UE#invix24w>I^d8NLw2t0lT?Dq9KePAzlt6SpIOs4$t|^ z$Qja6_7wP=MDpPI<*p~YJITD$MK=^W9^IXoopdnm%*2l{mjygFj%B&HcQc!G_=gIe z{&_L?&U?$x80MGjlgEY`5uN+*BOa`(OZy@+zudJkGqdKC_rJY%wr4XAj)?Bw%4CDM z-S~hbA~>g2m4Ah2CLbD~GCF3ae}z{SW+Ec<%M~+QBla8`@3lX_T%YZOhQ{Tf_>(Zd z{HpM=t*RahpItMvBg`^pwp2)B?*3fdeQ}$nVP=0n@cwh-{PO;L%lV}*{FLp6+-QGz zdH?UGhLofM#tNY4>)Fq6Pli(B0@3|kp~TpW+o!&on$h z*`<9EnQoSI8r~K$XLlzLe#DdA{Y(IYzqlN8%nTbQ_DpWX%r<{A|CzXVGn-%T?Vkp* z`GZA-d+hvQ3^Mm@&(08Z;qDs>OH<8_*{~xbJZEBr3wtW~GodN|PciTCv{6A{OJPF= zb3lPVd3f2x5xR(orm(rS`DGJd2=|3K;0I2(I5M+{APV=j{9WdkO@yjK&?yo4(TmS^ zERYWs_*^=_>?Xt+fmchcz_Q==3y4W#|L4^vmtu6ZROXk>SL>t{q#CS%G=549>Aw`1 zUp5q$zEqXpWU+>s`DsUHVy*ZsRaKj&=9kTdh~W15*9Dju4w=}T@xqHW!Q^?is)`FV zuFNm{3uOG`japR^UZqYZs><6q){ran%l^W0UG|Tv%FHVvsdm=>Bg_+z@-|*oq@Hqy zWG0^^G1cbT{~!4EZD4azb}BUbZx~0I7bxI0d;3ng=9gD0V6%ru1TWX*@}5FdnP1*k zSSs_&3kwE;%JlMgRU80TP&gpA4gdhqXaJo7Du4li0X}Uqlt-o`A|WYQS?ItGiDPcg zlL`8tFp9(9%>MvC05QORfPVnR0QqMB0r&y9m1z8ce*pIdvQL_OQ1Nj~I1kr<3ci=0 zeeL~b|AF%Fb1Htazq0y+|8n%}>393*pnuoTR$uxat-sE6-+IUYW%U659q8xl8~ukR zU#S0pAD|y*{r}P@;{X4h{{Qtp7a|5*@kMqcoU|ETRzQduZ^afZe3ZAuG);sKWYk4N zy+jCsmi$p-*O?@;(g5T~ZBr4iR7dy-ftLJHVuC_Ug+5%|-Rv{Ru-|>DYrJvuxwoR%6ZNw^yw;c9ChocxRi>yZwfrpnWMKc2VwQEkjYDIsu#{*v`H~p^y6F& zqr(CcRx>~HnxX!IDQ~SZax^<{Aw79@#V>zO>29e&PF# zU6GZoaR9xi&JMD`!Z>a428w}l~*V$xeWrYDr{FJB(N9OnK^XsC9KnG{h(liru4uQ>r4P@ zsL_DZLT_MWsG7|7qSXu;%p}#L#mA z){e-cSTsBG@u#SU&dY%5{)B{p1iV&Xkoe^q`F<5CU~)=-A;vpOsIhC~r2e5`3hFh% z{hw!9RMW48tXga1txvW?d{JW8$4TH0zw>YYb}ePXuBo{|D)v*R`$#;k1s6lEQztH- zD6wi#)$(jHC*CX%Z-F1DBZ&ho_@c$Hl9u|tWl`m)hQ+UvlK=qzL68A${7)>=XEb84e)tj8BY#+hP6^{4gFrRZq`Y!;a60?(%r9RG#(R)9gbO3tX2qS)gsY7{`drnLsI3eRh1FkL!L&hkwkiT1xsF;D?*@1%3K6m}|mi@vK zlZxbC)>oG*=p)e69amP0Ds!88L7buj7b_OHE&}hUlK@~+n~gY)y?EEeC>33&2(y1G zPF^cDK`!lxUkdT#_ri{OPDS_^y$}F*{wH}BE9b1>_aZ|id6UwK&!K$h21_`v$tlt% z|B~8zz-8kfI<{Mj)PwX4ldj8M5O#jhK{<$Z~z9eS9Sxa3<0W_x8&w4ty!?u*To5#H@+LVGbM zl^G1xK0{-Hibu_gpMHm0jekaP##|8It7k)YvSRnykp_*tK%2F|)j7g<$LP~aoa!TT zv@-5ab2&2)Q?rNrQQbPp8?53Ylu2PT7Px2dHyGV^tGJEm1!Oh!guE5a{OxeZa_GTZ zur~2+{kj0vD-_fB;=8YTC2|Jp*<-QSJ7!Y%)DqcyH}$Kg^m77aXOcz4b9?eDCaC+1V6P_xI{(C#zyP;CQ>#Bs@>Z*>$TtTJAbQqbKA(?$~tTsIme zZ=rahG~Mk?1YWUCCcDY#8_ekX0;}FIV;~7$wT$;{+M1}O={7#-%z77NJ}P1b{g_zZ z#9{>kT9+tc)kzYToqB4Vn3!Hb)CjC4=P+I@ceC@rg1I0xG)+d5h6LW|s4wJC4t^UJRv zEViRI7h#iNQ3ba8Ko}p1@3@bsOjo-Y!fQ^wr=?W>o*2sbV8Fu<+O-E5Jtf2z)!DI& zWC*6=FTOn)q5q%6Dax|XwD>nHr!2?zpCt*>j#B z6!PI5FEi@*_lejou1-Vy5eb`5-?W?)nX~DHK8(O^VAASHpjF>$sCGM zDm+BeGw6*3bKn33uUl@@kl;*)0Y>*$nHq+1L_Ql$V7f%}dbhlXEsTyO>i*sQuKZ|m zAWp=~;r#N~+(=G6inwcpa3-x>{4FqfP4FEtmDWlVDt?rnx!{R*c*>Q;T{7x6s192$ z1*Cyg+F^%-56yF91tLwW0NnC=8Cs zTssUE5^OJWoErcxSXPT)VJB{Vi%kE0;TMQ{!&%6eKl6!~{wtbvt_+~S8am3A%D_cw z*a{^hRubiRe))QOdrH})hv=zWZ}r`BlYFn-t9Z=8PpEe&=|+BeM1#fq^0m2tW! z0hw)vx4JMBDk>xi=odL$r0(;&ThweYMwM3T45ob~xC}?Ra@M2c(qGrAss|O>s!5Nw zk}ZncOm&so@i5L%00E3GI=aV~;4sbhY*w#!#jvFc$hUh3JYRLInJweZYa;5<57K>y zu|c|L#E3evSt7Szb`152flG_H045te=&+lZRZr9NfyN%g=p2G%gbH5a=SV#Q8Cm3+ z%JsjaGF=BP9MB*c##myxL89=zl(+zfyi0Ib)-56u5AD@j+US{GPkvV*6n#wln|bn@ z$zdp3$?^3GitPX8yk@3~0u(jV{pQaU<5G0|x&4(U!2VDv8$-34q5=uW%9Zz1 zp#WGQ+}l2%1w)i{hh}S@#rd50>Wj?9Q}h4_qgs#}iHF3gZufnQjlnCGO&Uh*^;#zd zRUkRrfab>#x$411wyxpuv#*Q6e-$iC;NPGgINzgj0@oOO=@|g0C%usY%BYnll)pSz z0B!AkPGz>nOf?aZv)G)_*`{OzWpM&JY-Ki}4{B!g)Xi6%YqP6}j60^%&=8)vo#D7? zc6s?20$vS&8HBZl#F=|m1v%uo%nLwB#(N`h%v1Ye0ci=&!{0R%S}*_oS@>7RiY&QL z@lqP$KmbLR*irFEYV;9WV>ScTU9=vf71A&6+Y{bSdE@r^o*=~;u1|%%ZT>Y_UT09A z8`sT2q_I%3^#IcD&E@{W0`}n2QZosfr6H7pOtSEW^4{om+`7VJK=f;spUyq*oC!v8|d=V_(3 zoZRnbyDESH5S9ARVpEm7&ngAjt^%KwYjwoeL^ziP@h%O$R>R@I35HLDNTK2H^H!L- zFnhl(bd6F={Dghb!fN__&qs#{)7N(ZmHzW2ADYfy4(FxS2~>qpvLJ9iPg-IkT>Ez& z4%f`=EkI)YyU=U7)77&?x(vm@-EzMx<_kpNA-OElT!1bec%Zx5+Mc=+x#b+5 zq^|yA6&`{#92F6`{q(daefG$HL+o4w!%Z9QnJdM`+`P}Ro&XHpQ)-2A!Lh9gATA5A zt9*R#m(7HT((C@-?1ym2MT%j_gUOmL*ek6SO}OBiT03eN29rUoIeXF-MkitQ)^Hrx z9sNq?z7Sx}k>RH2-P3LUY@laiKP=_M_HyZyI21=vnMHiq-wk@ROx3!1`~ILGCNH`M zjLm1oVR2FBp5u*VFIJv9uzyxTQyJd45Hym`&rL;+6W)k>s`*lvKDS50P968mHT(l& z1xOP|000o|N!Tb?Jc=pn|iSkVOv6(*qpR<1bLXKG5nNk^j&ig4! z6GWmzriX)NQ;d~=q^Z<~Ca5an!Pon$z1(5&Tg!TA#Q}xF$4&P~>VHwUmtE!9u#=iI z9;AJqh*)fltN2i$5@p_R5}stzJCy7-DTSCAR#tcb_I6FdvnY_*(T+GiY~~r#Vy1Wi z0W5_mu*|xvv#eow8L3{`kTLhd0co>_0{Kd<#0X7sY#RHw48`U*#fDLD_2E;5Wv!>b zkn&doQ2^&hV2|!TyaNH%P+6}@uo=Nf7RKG$kNvA<5!QHgAA-Pjt3WrRUpw@}sV0{# zFvzdq4+VJ80002W34Dj>uSbI&G$NXTsW3C{fl}r void: add_to_group("player") hurtbox.area_entered.connect(_on_hurtbox_area_entered) _setup_weapon_visual() + _setup_sanctuary_back_visual() _setup_visual_shapes() _build_animations() slash_arc.visible = false @@ -274,6 +285,8 @@ func _find_upgrade_definition(upgrade_id: String) -> Dictionary: func on_attack_landed(attack_name: StringName, target: Node) -> void: gain_inspiration(inspiration_gain_on_attack_hit) + if buff_active and skill3_restore_defense_upgrade: + restore_defense(max_defense * 0.1) AccessoryManager.apply_on_hit_effects(self, attack_name, target) func sync_visuals() -> void: @@ -283,6 +296,8 @@ func sync_visuals() -> void: slash_arc.position = _attack_visual_offset() if slash_arc.visible: slash_arc.rotation = _attack_facing().angle() + if sanctuary_back_sprite != null: + sanctuary_back_sprite.visible = buff_active and hp > 0.0 if hp <= 0.0: modulate = Color(0.35, 0.35, 0.35, 1.0) elif dodge_invincible: @@ -387,7 +402,9 @@ func consume_queued_skill() -> StringName: return skill_name func start_attack() -> void: - cooldowns["attack"] = attack_interval + var attack_speed := get_attack_speed_multiplier() + animation_player.speed_scale = attack_speed + cooldowns["attack"] = attack_interval / attack_speed current_attack_targets.clear() current_attack_name = &"attack" attack_started.emit(current_attack_name) @@ -395,9 +412,10 @@ func start_attack() -> void: slash_arc.position = _attack_visual_offset() slash_arc.rotation = _attack_facing().angle() play_animation(&"attack") - _animate_weapon_swing(-34.0, 20.0, 0.12, 0.18) + _animate_weapon_swing(-34.0, 20.0, get_attack_windup_duration(), get_attack_recovery_duration()) func finish_attack() -> void: + animation_player.speed_scale = 1.0 if current_attack_name != &"": attack_finished.emit(current_attack_name) slash_arc.visible = false @@ -462,6 +480,8 @@ func try_hit_dash_targets_along_segment(start_position: Vector2, end_position: V func finish_dash() -> void: velocity = Vector2.ZERO + var burst_targets: Array[Node] = [] + apply_damage_to_targets(get_scaled_damage(skill1_post_dash_damage), skill1_post_dash_radius, &"skill1_burst", burst_targets) if current_attack_name != &"": attack_finished.emit(current_attack_name) current_attack_name = &"" @@ -500,7 +520,7 @@ func _distance_to_segment(point: Vector2, start_position: Vector2, end_position: return point.distance_to(closest_point) func trigger_normal_attack_hit() -> void: - apply_damage_to_targets_in_arc(attack_damage, attack_range, attack_arc_degrees, &"attack") + apply_damage_to_targets_in_arc(get_scaled_damage(attack_damage), attack_range, attack_arc_degrees, &"attack") _show_melee_slash_effect(attack_range, attack_arc_degrees) _flash_melee_impact() @@ -539,7 +559,7 @@ func trigger_shockwave() -> void: var payload := {} if skill2_knock_up_upgrade: payload["knock_up_duration"] = knock_up_duration - apply_damage_to_targets(skill2_damage, shockwave_radius, &"skill2", shockwave_targets, payload) + apply_damage_to_targets(get_scaled_damage(skill2_damage + 150.0), shockwave_radius, &"skill2", shockwave_targets, payload) if skill2_shield_upgrade: activate_guard() attack_finished.emit(&"skill2") @@ -565,19 +585,21 @@ func end_guard() -> void: shield_changed.emit(shield) func apply_sanctuary() -> void: - if skill3_heal_upgrade: - heal((max_hp - hp) * 0.1) + heal(max_hp * 0.5) if skill3_restore_defense_upgrade: if health_component.has_method("restore_defense_full"): health_component.restore_defense_full() else: health_component.defense = health_component.max_defense health_component.defense_changed.emit(health_component.defense, health_component.max_defense) - active_damage_multiplier = sanctuary_damage_multiplier + active_damage_multiplier = 1.0 + skill3_attack_damage_bonus + active_attack_speed_multiplier = skill3_attack_speed_bonus + active_move_speed_bonus = skill3_move_speed_bonus active_damage_reduction = sanctuary_damage_reduction health_component.set_damage_reduction(active_damage_reduction) buff_active = true sanctuary_time_remaining = buff_duration + sanctuary_heal_tick_elapsed = 0.0 attack_started.emit(&"skill3") _show_sanctuary_activation() attack_finished.emit(&"skill3") @@ -586,10 +608,15 @@ func clear_sanctuary() -> void: if not buff_active and sanctuary_time_remaining <= 0.0: return active_damage_multiplier = 1.0 + active_attack_speed_multiplier = 0.0 + active_move_speed_bonus = 0.0 active_damage_reduction = 0.0 + sanctuary_heal_tick_elapsed = 0.0 health_component.set_damage_reduction(0.0) buff_active = false sanctuary_time_remaining = 0.0 + if sanctuary_back_sprite != null: + sanctuary_back_sprite.visible = false sanctuary_ring.visible = false sanctuary_ring.rotation = 0.0 sanctuary_ring.scale = Vector2.ONE @@ -602,11 +629,32 @@ func cast_skill3() -> void: func heal(amount: float) -> void: health_component.heal(amount) +func restore_defense(amount: float) -> void: + if amount <= 0.0 or hp <= 0.0: + return + var previous_defense: float = float(health_component.defense) + health_component.defense = minf(health_component.defense + amount, health_component.max_defense) + health_component.defense_regen_timer = 0.0 + if not is_equal_approx(previous_defense, health_component.defense): + health_component.defense_changed.emit(health_component.defense, health_component.max_defense) + func get_scaled_damage(base_damage: float) -> float: return base_damage * active_damage_multiplier +func get_attack_speed_multiplier() -> float: + return 1.0 + active_attack_speed_multiplier + +func get_attack_windup_duration() -> float: + return attack_windup / get_attack_speed_multiplier() + +func get_attack_hit_frame_duration() -> float: + return attack_hit_frame / get_attack_speed_multiplier() + +func get_attack_recovery_duration() -> float: + return attack_recovery / get_attack_speed_multiplier() + func get_current_move_speed() -> float: - return move_speed * slow_factor + return move_speed * (1.0 + active_move_speed_bonus) * slow_factor func get_effective_move_speed() -> float: return get_current_move_speed() @@ -781,6 +829,19 @@ func _setup_weapon_visual() -> void: weapon_sprite.position = Vector2(27.0, 0.0) weapon.add_child(weapon_sprite) +func _setup_sanctuary_back_visual() -> void: + sanctuary_back_sprite = Sprite2D.new() + sanctuary_back_sprite.texture = TEXTURE_LOADER.load_texture(SANCTUARY_BACK_TEXTURE_PATH) + sanctuary_back_sprite.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST + sanctuary_back_sprite.centered = true + sanctuary_back_sprite.position = Vector2(0.0, -18.0) + sanctuary_back_sprite.scale = Vector2.ONE * 0.34 + sanctuary_back_sprite.modulate = Color(1.0, 1.0, 1.0, 0.8) + sanctuary_back_sprite.visible = false + sanctuary_back_sprite.z_index = -1 + sanctuary_back_sprite.show_behind_parent = true + add_child(sanctuary_back_sprite) + func _sync_weapon_visual() -> void: if weapon == null: return @@ -915,6 +976,11 @@ func _update_guard(delta: float) -> void: func _update_sanctuary(delta: float) -> void: if not buff_active: return + if skill3_heal_upgrade: + sanctuary_heal_tick_elapsed += delta + while sanctuary_heal_tick_elapsed >= 1.0: + sanctuary_heal_tick_elapsed -= 1.0 + heal(max_hp * 0.05) sanctuary_time_remaining = maxf(sanctuary_time_remaining - delta, 0.0) _spawn_sanctuary_visual(delta) if sanctuary_time_remaining <= 0.0: @@ -922,6 +988,8 @@ func _update_sanctuary(delta: float) -> void: func _spawn_sanctuary_visual(delta: float) -> void: sanctuary_ring.rotation += delta * 2.5 + if sanctuary_back_sprite != null: + sanctuary_back_sprite.rotation += delta * 0.7 func spawn_damage_number(amount: float, is_critical: bool, world_position: Vector2) -> void: var damage_number := DAMAGE_NUMBER_SCENE.instantiate() diff --git a/characters/knight/states/buff_state.gd b/characters/knight/states/buff_state.gd index be68778..8f50f6d 100644 --- a/characters/knight/states/buff_state.gd +++ b/characters/knight/states/buff_state.gd @@ -18,7 +18,8 @@ func enter() -> void: func physics_update(delta: float) -> void: elapsed += delta - actor.velocity = actor.velocity.move_toward(Vector2.ZERO, actor.move_speed * delta * 10.0) + var move_speed: float = float(actor.get_current_move_speed()) if actor.has_method("get_current_move_speed") else float(actor.move_speed) + actor.velocity = actor.velocity.move_toward(Vector2.ZERO, move_speed * delta * 10.0) func evaluate_transitions() -> void: if elapsed >= actor.buff_duration: diff --git a/characters/knight/states/charge_state.gd b/characters/knight/states/charge_state.gd index 3fed2ac..f9705da 100644 --- a/characters/knight/states/charge_state.gd +++ b/characters/knight/states/charge_state.gd @@ -18,7 +18,8 @@ func enter() -> void: func physics_update(delta: float) -> void: elapsed += delta - actor.velocity = actor.velocity.move_toward(Vector2.ZERO, actor.move_speed * delta * 8.0) + var move_speed: float = float(actor.get_current_move_speed()) if actor.has_method("get_current_move_speed") else float(actor.move_speed) + actor.velocity = actor.velocity.move_toward(Vector2.ZERO, move_speed * delta * 8.0) func evaluate_transitions() -> void: if elapsed >= actor.charge_duration: diff --git a/characters/knight/states/guard_state.gd b/characters/knight/states/guard_state.gd index f6061eb..2e011b5 100644 --- a/characters/knight/states/guard_state.gd +++ b/characters/knight/states/guard_state.gd @@ -18,7 +18,8 @@ func enter() -> void: func physics_update(delta: float) -> void: elapsed += delta - actor.velocity = actor.velocity.move_toward(Vector2.ZERO, actor.move_speed * delta * 10.0) + var move_speed: float = float(actor.get_current_move_speed()) if actor.has_method("get_current_move_speed") else float(actor.move_speed) + actor.velocity = actor.velocity.move_toward(Vector2.ZERO, move_speed * delta * 10.0) func evaluate_transitions() -> void: if elapsed >= actor.guard_duration or actor.shield <= 0.0: diff --git a/characters/knight/states/hit_state.gd b/characters/knight/states/hit_state.gd index 0cf814d..b58ddd9 100644 --- a/characters/knight/states/hit_state.gd +++ b/characters/knight/states/hit_state.gd @@ -19,7 +19,8 @@ func enter() -> void: func physics_update(delta: float) -> void: elapsed += delta - actor.velocity = actor.velocity.move_toward(Vector2.ZERO, actor.move_speed * delta * 14.0) + var move_speed: float = float(actor.get_current_move_speed()) if actor.has_method("get_current_move_speed") else float(actor.move_speed) + actor.velocity = actor.velocity.move_toward(Vector2.ZERO, move_speed * delta * 14.0) func evaluate_transitions() -> void: if elapsed >= actor.hit_stun_duration: diff --git a/characters/knight/states/skill_state.gd b/characters/knight/states/skill_state.gd index d921ec1..7d7fbba 100644 --- a/characters/knight/states/skill_state.gd +++ b/characters/knight/states/skill_state.gd @@ -25,7 +25,8 @@ func enter() -> void: func physics_update(delta: float) -> void: elapsed += delta - actor.velocity = actor.velocity.move_toward(Vector2.ZERO, actor.move_speed * delta * 10.0) + var move_speed: float = float(actor.get_current_move_speed()) if actor.has_method("get_current_move_speed") else float(actor.move_speed) + actor.velocity = actor.velocity.move_toward(Vector2.ZERO, move_speed * delta * 10.0) func evaluate_transitions() -> void: state_machine.transition_to(&"Idle") diff --git a/effects/projectiles/arcane_bolt.gd b/effects/projectiles/arcane_bolt.gd index 03f0a1a..7c857fa 100644 --- a/effects/projectiles/arcane_bolt.gd +++ b/effects/projectiles/arcane_bolt.gd @@ -1,10 +1,13 @@ extends Area2D +const TEXTURE_LOADER := preload("res://combat/runtime_texture_loader.gd") +const PLAYER_ORB_TEXTURE_PATH := "res://assets/effects/projectiles/player_staff_orb.webp" + @export var speed: float = 620.0 @export var lifetime: float = 1.5 @export var hit_radius: float = 18.0 -@onready var bolt: Polygon2D = $Bolt +@onready var bolt: Sprite2D = $Bolt @onready var collision_shape: CollisionShape2D = $CollisionShape2D var direction: Vector2 = Vector2.RIGHT @@ -19,6 +22,8 @@ var trail_timer: float = 0.0 func _ready() -> void: add_to_group("arcane_bolt_test") + _setup_texture_visual() + _setup_collision_shape() body_entered.connect(_on_body_entered) area_entered.connect(_on_area_entered) @@ -29,7 +34,6 @@ func setup(owner_actor: Node, travel_direction: Vector2, hit_damage: float, hit_ crit_rate = hit_crit_rate attack_name = attack_label extra_payload = hit_extra_payload.duplicate(true) - bolt.polygon = _pixel_bolt_polygon() rotation = direction.angle() var timer := get_tree().create_timer(lifetime) timer.timeout.connect(queue_free) @@ -37,7 +41,7 @@ func setup(owner_actor: Node, travel_direction: Vector2, hit_damage: float, hit_ func _physics_process(delta: float) -> void: global_position += direction * speed * delta pulse_time += delta - bolt.scale = Vector2.ONE * (1.06 if int(pulse_time * 16.0) % 2 == 0 else 0.94) + bolt.scale = Vector2.ONE * (0.72 if int(pulse_time * 16.0) % 2 == 0 else 0.66) trail_timer -= delta if trail_timer <= 0.0: trail_timer = 0.045 @@ -115,18 +119,12 @@ func _consume_bolt() -> void: call_deferred("queue_free") func _spawn_hit_flash() -> void: - var flash := Polygon2D.new() - flash.polygon = PackedVector2Array([ - Vector2(-14.0, -4.0), - Vector2(-4.0, -14.0), - Vector2(4.0, -14.0), - Vector2(14.0, -4.0), - Vector2(14.0, 4.0), - Vector2(4.0, 14.0), - Vector2(-4.0, 14.0), - Vector2(-14.0, 4.0) - ]) - flash.color = Color(0.82, 0.92, 1.0, 0.92) + var flash := Sprite2D.new() + flash.texture = TEXTURE_LOADER.load_texture(PLAYER_ORB_TEXTURE_PATH) + flash.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST + flash.centered = true + flash.scale = Vector2.ONE * 0.82 + flash.modulate = Color(0.82, 0.92, 1.0, 0.92) flash.global_position = global_position flash.rotation = rotation get_tree().current_scene.add_child(flash) @@ -135,37 +133,32 @@ func _spawn_hit_flash() -> void: tween.parallel().tween_property(flash, "modulate:a", 0.0, 0.1) tween.finished.connect(flash.queue_free) -func _pixel_bolt_polygon() -> PackedVector2Array: - return PackedVector2Array([ - Vector2(-16.0, -4.0), - Vector2(-6.0, -10.0), - Vector2(4.0, -10.0), - Vector2(12.0, -4.0), - Vector2(18.0, 0.0), - Vector2(12.0, 4.0), - Vector2(4.0, 10.0), - Vector2(-6.0, 10.0), - Vector2(-16.0, 4.0), - Vector2(-10.0, 0.0) - ]) - func _spawn_pixel_trail() -> void: var scene_root := get_tree().current_scene if scene_root == null: return - var rune := Polygon2D.new() - rune.color = Color(0.72, 0.88, 1.0, 0.34) - rune.polygon = PackedVector2Array([ - Vector2(-3.0, -5.0), - Vector2(3.0, -5.0), - Vector2(5.0, 0.0), - Vector2(3.0, 5.0), - Vector2(-3.0, 5.0), - Vector2(-5.0, 0.0) - ]) + var rune := Sprite2D.new() + rune.texture = TEXTURE_LOADER.load_texture(PLAYER_ORB_TEXTURE_PATH) + rune.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST + rune.centered = true + rune.scale = Vector2.ONE * 0.32 + rune.modulate = Color(0.72, 0.88, 1.0, 0.34) rune.global_position = global_position - direction * 10.0 scene_root.add_child(rune) var tween := rune.create_tween() tween.tween_property(rune, "scale", Vector2.ONE * 0.7, 0.12) tween.parallel().tween_property(rune, "modulate:a", 0.0, 0.12) tween.finished.connect(rune.queue_free) + +func _setup_texture_visual() -> void: + bolt.texture = TEXTURE_LOADER.load_texture(PLAYER_ORB_TEXTURE_PATH) + bolt.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST + bolt.centered = true + bolt.scale = Vector2.ONE * 0.68 + +func _setup_collision_shape() -> void: + if collision_shape == null: + return + var shape := CircleShape2D.new() + shape.radius = hit_radius + collision_shape.shape = shape diff --git a/effects/projectiles/arcane_bolt.tscn b/effects/projectiles/arcane_bolt.tscn index 921a4d4..e7fa990 100644 --- a/effects/projectiles/arcane_bolt.tscn +++ b/effects/projectiles/arcane_bolt.tscn @@ -2,9 +2,8 @@ [ext_resource type="Script" path="res://effects/projectiles/arcane_bolt.gd" id="1"] -[sub_resource type="CapsuleShape2D" id="1"] -radius = 6.0 -height = 28.0 +[sub_resource type="CircleShape2D" id="1"] +radius = 18.0 [node name="ArcaneBolt" type="Area2D"] script = ExtResource("1") @@ -13,9 +12,8 @@ collision_mask = 1 monitoring = true monitorable = false -[node name="Bolt" type="Polygon2D" parent="."] -color = Color(0.68, 0.86, 1, 1) -polygon = PackedVector2Array(-14, -4, 10, -6, 18, 0, 10, 6, -14, 4, -6, 0) +[node name="Bolt" type="Sprite2D" parent="."] +texture_filter = 1 [node name="CollisionShape2D" type="CollisionShape2D" parent="."] shape = SubResource("1") diff --git a/effects/projectiles/enemy_bolt.gd b/effects/projectiles/enemy_bolt.gd index 7f3981f..df5a421 100644 --- a/effects/projectiles/enemy_bolt.gd +++ b/effects/projectiles/enemy_bolt.gd @@ -1,10 +1,16 @@ extends Area2D +const TEXTURE_LOADER := preload("res://combat/runtime_texture_loader.gd") +const BASIC_BULLET_TEXTURE_PATH := "res://assets/effects/projectiles/basic_bullet.webp" +const APPRENTICE_ORB_TEXTURE_PATH := "res://assets/effects/projectiles/apprentice_orb.webp" +const ARCANIST_MISSILE_TEXTURE_PATH := "res://assets/effects/projectiles/arcanist_missile.webp" + @export var speed: float = 420.0 @export var lifetime: float = 2.4 @export var hit_radius: float = 18.0 -@onready var bolt: Polygon2D = $Bolt +@onready var bolt: Sprite2D = $Bolt +@onready var collision_shape: CollisionShape2D = $CollisionShape2D var direction: Vector2 = Vector2.RIGHT var damage: float = 10.0 @@ -15,6 +21,8 @@ var pulse_time: float = 0.0 var trail_timer: float = 0.0 func _ready() -> void: + _setup_texture_visual(BASIC_BULLET_TEXTURE_PATH) + _setup_collision_shape() body_entered.connect(_on_body_entered) area_entered.connect(_on_area_entered) @@ -23,8 +31,8 @@ func setup(owner_actor: Node, travel_direction: Vector2, hit_damage: float, colo direction = travel_direction.normalized() if travel_direction != Vector2.ZERO else Vector2.RIGHT damage = hit_damage speed = new_speed - bolt.color = color - bolt.polygon = _pixel_bolt_polygon() + bolt.modulate = color + _setup_texture_visual(_texture_for_color(color)) extra_payload = payload.duplicate(true) rotation = direction.angle() var timer := get_tree().create_timer(lifetime) @@ -34,7 +42,7 @@ func _physics_process(delta: float) -> void: global_position += direction * speed * delta pulse_time += delta var pixel_pulse := 1.08 if int(pulse_time * 18.0) % 2 == 0 else 0.96 - bolt.scale = Vector2.ONE * pixel_pulse + bolt.scale = Vector2.ONE * (0.72 * pixel_pulse) trail_timer -= delta if trail_timer <= 0.0: trail_timer = 0.055 @@ -103,18 +111,12 @@ func _resolve_damage_target(target: Variant) -> Node: return null func _spawn_hit_flash() -> void: - var flash := Polygon2D.new() - flash.polygon = PackedVector2Array([ - Vector2(-12, -4), - Vector2(-4, -12), - Vector2(4, -12), - Vector2(12, -4), - Vector2(12, 4), - Vector2(4, 12), - Vector2(-4, 12), - Vector2(-12, 4) - ]) - flash.color = bolt.color + var flash := Sprite2D.new() + flash.texture = bolt.texture + flash.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST + flash.centered = true + flash.scale = Vector2.ONE * 0.82 + flash.modulate = bolt.modulate flash.global_position = global_position flash.rotation = rotation get_tree().current_scene.add_child(flash) @@ -123,29 +125,16 @@ func _spawn_hit_flash() -> void: tween.parallel().tween_property(flash, "modulate:a", 0.0, 0.1) tween.finished.connect(flash.queue_free) -func _pixel_bolt_polygon() -> PackedVector2Array: - return PackedVector2Array([ - Vector2(-14.0, -4.0), - Vector2(-6.0, -8.0), - Vector2(10.0, -8.0), - Vector2(18.0, 0.0), - Vector2(10.0, 8.0), - Vector2(-6.0, 8.0), - Vector2(-14.0, 4.0) - ]) - func _spawn_pixel_trail() -> void: var scene_root := get_tree().current_scene if scene_root == null: return - var chip := Polygon2D.new() - chip.color = Color(bolt.color.r, bolt.color.g, bolt.color.b, 0.46) - chip.polygon = PackedVector2Array([ - Vector2(-3.0, -3.0), - Vector2(3.0, -3.0), - Vector2(3.0, 3.0), - Vector2(-3.0, 3.0) - ]) + var chip := Sprite2D.new() + chip.texture = bolt.texture + chip.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST + chip.centered = true + chip.scale = Vector2.ONE * 0.26 + chip.modulate = Color(bolt.modulate.r, bolt.modulate.g, bolt.modulate.b, 0.46) chip.global_position = global_position - direction * 12.0 chip.rotation = rotation scene_root.add_child(chip) @@ -153,3 +142,24 @@ func _spawn_pixel_trail() -> void: tween.tween_property(chip, "global_position", chip.global_position - direction * 10.0, 0.12) tween.parallel().tween_property(chip, "modulate:a", 0.0, 0.12) tween.finished.connect(chip.queue_free) + +func _texture_for_color(color: Color) -> String: + if color.r > 0.9 and color.g < 0.72: + return ARCANIST_MISSILE_TEXTURE_PATH + if color.b > color.r and color.b > color.g: + return APPRENTICE_ORB_TEXTURE_PATH + return BASIC_BULLET_TEXTURE_PATH + +func _setup_texture_visual(texture_path: String) -> void: + bolt.texture = TEXTURE_LOADER.load_texture(texture_path) + bolt.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST + bolt.centered = true + bolt.scale = Vector2.ONE * 0.72 + +func _setup_collision_shape() -> void: + if collision_shape == null: + return + var shape := CapsuleShape2D.new() + shape.radius = 7.5 + shape.height = 34.0 + collision_shape.shape = shape diff --git a/effects/projectiles/enemy_bolt.tscn b/effects/projectiles/enemy_bolt.tscn index 2bcb5b6..dcfa5b1 100644 --- a/effects/projectiles/enemy_bolt.tscn +++ b/effects/projectiles/enemy_bolt.tscn @@ -2,8 +2,9 @@ [ext_resource type="Script" path="res://effects/projectiles/enemy_bolt.gd" id="1"] -[sub_resource type="CircleShape2D" id="1"] -radius = 7.0 +[sub_resource type="CapsuleShape2D" id="1"] +radius = 7.5 +height = 34.0 [node name="EnemyBolt" type="Area2D"] script = ExtResource("1") @@ -12,9 +13,8 @@ collision_mask = 1 monitoring = true monitorable = false -[node name="Bolt" type="Polygon2D" parent="."] -color = Color(0.95, 0.88, 0.6, 1) -polygon = PackedVector2Array(-12, -6, 8, -8, 16, 0, 8, 8, -12, 6, -4, 0) +[node name="Bolt" type="Sprite2D" parent="."] +texture_filter = 1 [node name="CollisionShape2D" type="CollisionShape2D" parent="."] shape = SubResource("1") diff --git a/effects/projectiles/piercing_arrow.gd b/effects/projectiles/piercing_arrow.gd index 2f9c6f5..8bddd42 100644 --- a/effects/projectiles/piercing_arrow.gd +++ b/effects/projectiles/piercing_arrow.gd @@ -1,9 +1,12 @@ extends Area2D +const TEXTURE_LOADER := preload("res://combat/runtime_texture_loader.gd") +const ARROW_TEXTURE_PATH := "res://assets/effects/projectiles/arrow.webp" + @export var speed: float = 700.0 @export var lifetime: float = 1.4 -@onready var trail: Polygon2D = $Trail +@onready var trail: Sprite2D = $Trail var direction: Vector2 = Vector2.RIGHT var damage: float = 100.0 @@ -16,6 +19,7 @@ var expired: bool = false var trail_timer: float = 0.0 func _ready() -> void: + _setup_texture_visual() body_entered.connect(_on_body_entered) area_entered.connect(_on_area_entered) @@ -25,7 +29,6 @@ func setup(owner_actor: Node, travel_direction: Vector2, hit_damage: float, hit_ damage = hit_damage crit_rate = hit_crit_rate attack_name = attack_label - trail.polygon = _pixel_arrow_polygon() rotation = direction.angle() var timer := get_tree().create_timer(lifetime) timer.timeout.connect(queue_free) @@ -33,8 +36,8 @@ func setup(owner_actor: Node, travel_direction: Vector2, hit_damage: float, hit_ func _physics_process(delta: float) -> void: global_position += direction * speed * delta pulse_time += delta - trail.scale = Vector2(1.08 if int(pulse_time * 20.0) % 2 == 0 else 0.98, 1.0) - trail.color = Color(1.0, 0.96, 0.72, 1.0) + trail.scale = Vector2.ONE * (0.82 if int(pulse_time * 20.0) % 2 == 0 else 0.76) + trail.modulate = Color(1.0, 0.96, 0.72, 1.0) trail_timer -= delta if trail_timer <= 0.0: trail_timer = 0.035 @@ -85,16 +88,12 @@ func _resolve_damage_target(target: Variant) -> Node: return null func _spawn_hit_flash() -> void: - var flash := Polygon2D.new() - flash.polygon = PackedVector2Array([ - Vector2(-10.0, -4.0), - Vector2(6.0, -4.0), - Vector2(14.0, 0.0), - Vector2(6.0, 4.0), - Vector2(-10.0, 4.0), - Vector2(-4.0, 0.0) - ]) - flash.color = Color(1.0, 1.0, 0.8, 0.9) + var flash := Sprite2D.new() + flash.texture = TEXTURE_LOADER.load_texture(ARROW_TEXTURE_PATH) + flash.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST + flash.centered = true + flash.scale = Vector2.ONE * 0.92 + flash.modulate = Color(1.0, 1.0, 0.8, 0.9) flash.global_position = global_position flash.rotation = rotation get_tree().current_scene.add_child(flash) @@ -103,18 +102,6 @@ func _spawn_hit_flash() -> void: tween.parallel().tween_property(flash, "modulate:a", 0.0, 0.08) tween.finished.connect(flash.queue_free) -func _pixel_arrow_polygon() -> PackedVector2Array: - return PackedVector2Array([ - Vector2(-20.0, -3.0), - Vector2(4.0, -3.0), - Vector2(4.0, -7.0), - Vector2(20.0, 0.0), - Vector2(4.0, 7.0), - Vector2(4.0, 3.0), - Vector2(-20.0, 3.0), - Vector2(-12.0, 0.0) - ]) - func _spawn_pixel_trail() -> void: var scene_root := get_tree().current_scene if scene_root == null: @@ -134,3 +121,9 @@ func _spawn_pixel_trail() -> void: tween.tween_property(chip, "global_position", chip.global_position - direction * 12.0, 0.1) tween.parallel().tween_property(chip, "modulate:a", 0.0, 0.1) tween.finished.connect(chip.queue_free) + +func _setup_texture_visual() -> void: + trail.texture = TEXTURE_LOADER.load_texture(ARROW_TEXTURE_PATH) + trail.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST + trail.centered = true + trail.scale = Vector2.ONE * 0.78 diff --git a/effects/projectiles/piercing_arrow.tscn b/effects/projectiles/piercing_arrow.tscn index 10a1de8..1b3576e 100644 --- a/effects/projectiles/piercing_arrow.tscn +++ b/effects/projectiles/piercing_arrow.tscn @@ -3,8 +3,8 @@ [ext_resource type="Script" path="res://effects/projectiles/piercing_arrow.gd" id="1"] [sub_resource type="CapsuleShape2D" id="1"] -radius = 6.0 -height = 34.0 +radius = 5.0 +height = 38.0 [node name="PiercingArrow" type="Area2D"] script = ExtResource("1") @@ -13,9 +13,8 @@ collision_mask = 1 monitoring = true monitorable = false -[node name="Trail" type="Polygon2D" parent="."] -color = Color(0.96, 0.92, 0.58, 1) -polygon = PackedVector2Array(-18, -4, 18, 0, -18, 4, -6, 0) +[node name="Trail" type="Sprite2D" parent="."] +texture_filter = 1 [node name="CollisionShape2D" type="CollisionShape2D" parent="."] shape = SubResource("1") diff --git a/effects/projectiles/royal_bolt.gd b/effects/projectiles/royal_bolt.gd index f6e29c9..b952d1f 100644 --- a/effects/projectiles/royal_bolt.gd +++ b/effects/projectiles/royal_bolt.gd @@ -1,9 +1,14 @@ extends Area2D +const TEXTURE_LOADER := preload("res://combat/runtime_texture_loader.gd") +const BOSS_RED_TEXTURE_PATH := "res://assets/effects/projectiles/boss_bullet_red.webp" +const BOSS_YELLOW_TEXTURE_PATH := "res://assets/effects/projectiles/boss_bullet_yellow.webp" + @export var speed: float = 440.0 @export var lifetime: float = 3.0 -@onready var bolt: Polygon2D = $Bolt +@onready var bolt: Sprite2D = $Bolt +@onready var collision_shape: CollisionShape2D = $CollisionShape2D var direction: Vector2 = Vector2.RIGHT var damage: float = 18.0 @@ -14,6 +19,8 @@ var pulse_time: float = 0.0 var trail_timer: float = 0.0 func _ready() -> void: + _setup_texture_visual(BOSS_YELLOW_TEXTURE_PATH) + _setup_collision_shape() body_entered.connect(_on_body_entered) area_entered.connect(_on_area_entered) @@ -22,7 +29,7 @@ func setup(owner_actor: Node, travel_direction: Vector2, hit_damage: float, payl direction = travel_direction.normalized() if travel_direction != Vector2.ZERO else Vector2.RIGHT damage = hit_damage extra_payload = payload.duplicate(true) - bolt.polygon = _pixel_bolt_polygon() + _setup_texture_visual(BOSS_RED_TEXTURE_PATH if hit_damage > 22.0 else BOSS_YELLOW_TEXTURE_PATH) rotation = direction.angle() var timer := get_tree().create_timer(lifetime) timer.timeout.connect(queue_free) @@ -30,7 +37,7 @@ func setup(owner_actor: Node, travel_direction: Vector2, hit_damage: float, payl func _physics_process(delta: float) -> void: global_position += direction * speed * delta pulse_time += delta - bolt.scale = Vector2.ONE * (1.08 if int(pulse_time * 18.0) % 2 == 0 else 0.96) + bolt.scale = Vector2.ONE * (0.9 if int(pulse_time * 18.0) % 2 == 0 else 0.82) trail_timer -= delta if trail_timer <= 0.0: trail_timer = 0.05 @@ -85,18 +92,12 @@ func _resolve_damage_target(target: Variant) -> Node: return null func _spawn_hit_flash() -> void: - var flash := Polygon2D.new() - flash.polygon = PackedVector2Array([ - Vector2(-12.0, -4.0), - Vector2(-4.0, -12.0), - Vector2(4.0, -12.0), - Vector2(12.0, -4.0), - Vector2(12.0, 4.0), - Vector2(4.0, 12.0), - Vector2(-4.0, 12.0), - Vector2(-12.0, 4.0) - ]) - flash.color = Color(1.0, 0.84, 0.52, 0.9) + var flash := Sprite2D.new() + flash.texture = bolt.texture + flash.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST + flash.centered = true + flash.scale = Vector2.ONE + flash.modulate = Color(1.0, 0.84, 0.52, 0.9) flash.global_position = global_position flash.rotation = rotation get_tree().current_scene.add_child(flash) @@ -105,17 +106,6 @@ func _spawn_hit_flash() -> void: tween.parallel().tween_property(flash, "modulate:a", 0.0, 0.1) tween.finished.connect(flash.queue_free) -func _pixel_bolt_polygon() -> PackedVector2Array: - return PackedVector2Array([ - Vector2(-12.0, -4.0), - Vector2(-4.0, -9.0), - Vector2(6.0, -9.0), - Vector2(18.0, 0.0), - Vector2(6.0, 9.0), - Vector2(-4.0, 9.0), - Vector2(-12.0, 4.0) - ]) - func _spawn_pixel_trail() -> void: var scene_root := get_tree().current_scene if scene_root == null: @@ -135,3 +125,17 @@ func _spawn_pixel_trail() -> void: tween.tween_property(chip, "global_position", chip.global_position - direction * 8.0, 0.12) tween.parallel().tween_property(chip, "modulate:a", 0.0, 0.12) tween.finished.connect(chip.queue_free) + +func _setup_texture_visual(texture_path: String) -> void: + bolt.texture = TEXTURE_LOADER.load_texture(texture_path) + bolt.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST + bolt.centered = true + bolt.scale = Vector2.ONE * 0.84 + +func _setup_collision_shape() -> void: + if collision_shape == null: + return + var shape := CapsuleShape2D.new() + shape.radius = 8.0 + shape.height = 34.0 + collision_shape.shape = shape diff --git a/effects/projectiles/royal_bolt.tscn b/effects/projectiles/royal_bolt.tscn index 37baf70..ca3466d 100644 --- a/effects/projectiles/royal_bolt.tscn +++ b/effects/projectiles/royal_bolt.tscn @@ -2,8 +2,9 @@ [ext_resource type="Script" path="res://effects/projectiles/royal_bolt.gd" id="1"] -[sub_resource type="CircleShape2D" id="1"] +[sub_resource type="CapsuleShape2D" id="1"] radius = 8.0 +height = 34.0 [node name="RoyalBolt" type="Area2D"] script = ExtResource("1") @@ -12,9 +13,8 @@ collision_mask = 1 monitoring = true monitorable = false -[node name="Bolt" type="Polygon2D" parent="."] -color = Color(1, 0.82, 0.48, 1) -polygon = PackedVector2Array(-12, -6, 8, -8, 16, 0, 8, 8, -12, 6, -4, 0) +[node name="Bolt" type="Sprite2D" parent="."] +texture_filter = 1 [node name="CollisionShape2D" type="CollisionShape2D" parent="."] shape = SubResource("1") diff --git a/systems/pickups/run_pickup.gd b/systems/pickups/run_pickup.gd index d7e8076..bca65e6 100644 --- a/systems/pickups/run_pickup.gd +++ b/systems/pickups/run_pickup.gd @@ -2,6 +2,10 @@ extends Node2D signal collected(kind: String, amount: float, world_position: Vector2) +const TEXTURE_LOADER := preload("res://combat/runtime_texture_loader.gd") +const GOLD_TEXTURE_PATH := "res://assets/effects/pickups/gold.webp" +const EXPERIENCE_TEXTURE_PATH := "res://assets/effects/pickups/experience.webp" + var kind: String = "gold" var amount: float = 0.0 var tint: Color = Color(1.0, 0.88, 0.42, 1.0) @@ -18,7 +22,7 @@ func setup(pickup_kind: String, pickup_amount: float, options: Dictionary = {}) amount = maxf(pickup_amount, 0.0) tint = options.get("tint", _default_tint(kind)) var icon_path := String(options.get("icon", "")) - icon_texture = load(icon_path) as Texture2D if not icon_path.is_empty() else null + icon_texture = TEXTURE_LOADER.load_texture(icon_path) if not icon_path.is_empty() else _default_texture(kind) var launch_speed := float(options.get("launch_speed", randf_range(34.0, 62.0))) var launch_angle := float(options.get("launch_angle", randf() * TAU)) velocity = Vector2.RIGHT.rotated(launch_angle) * launch_speed @@ -64,7 +68,11 @@ func _draw() -> void: draw_circle(center, outer_radius + 2.0, Color(0.02, 0.03, 0.05, 0.52)) if icon_texture != null: var icon_size := Vector2(28.0, 28.0) - if kind == "accessory": + if kind == "gold": + icon_size = Vector2(20.0, 26.0) + elif kind == "inspiration": + icon_size = Vector2(26.0, 26.0) + elif kind == "accessory": icon_size = Vector2(26.0, 26.0) elif kind == "consumable": icon_size = Vector2(30.0, 30.0) @@ -131,3 +139,12 @@ func _default_tint(pickup_kind: String) -> Color: return Color(1.0, 0.88, 0.52, 1.0) _: return Color(1.0, 0.88, 0.42, 1.0) + +func _default_texture(pickup_kind: String) -> Texture2D: + match pickup_kind: + "gold": + return TEXTURE_LOADER.load_texture(GOLD_TEXTURE_PATH) + "inspiration": + return TEXTURE_LOADER.load_texture(EXPERIENCE_TEXTURE_PATH) + _: + return null diff --git a/tools/character_debug_world.gd b/tools/character_debug_world.gd index 8cab0e7..9b2e29d 100644 --- a/tools/character_debug_world.gd +++ b/tools/character_debug_world.gd @@ -15,6 +15,7 @@ const TWIN_PRINCES_BOSS_SCENE := preload("res://actors/bosses/town/twin_princes_ const RANGER_BOSS_SCENE := preload("res://actors/bosses/town/ranger_boss.tscn") const MAGE_BOSS_SCENE := preload("res://actors/bosses/town/mage_boss.tscn") const EMPEROR_BOSS_SCENE := preload("res://actors/bosses/town/emperor_boss.tscn") +const DEBUG_PANEL_SCENE := preload("res://ui/debug_panel.tscn") const PLAYER_SCENES := { &"knight": KNIGHT_SCENE, @@ -44,9 +45,11 @@ const ENEMY_SCENES := { @onready var debug_status: CanvasLayer = $CharacterDebugStatus @onready var help_label: Label = $DebugOverlay/Panel/Margin/HelpLabel @onready var camera: Camera2D = $Camera2D +var debug_panel: CanvasLayer = null var player_character: Node2D = null var active_target: Node2D = null +var current_encounter: Node = null var active_enemy_id: StringName = &"dummy" var active_enemy_elite: bool = false @@ -63,6 +66,8 @@ func _ready() -> void: enemy_select.visible = false if debug_status != null: debug_status.visible = false + _build_debug_overlay_controls() + _build_debug_panel() _refresh_help_text() if Music != null: Music.play_profile(&"title", true) @@ -73,6 +78,48 @@ func _ready() -> void: call_deferred("_on_character_selected", StringName(pending["character_id"])) +func _build_debug_overlay_controls() -> void: + if help_label == null: + return + var margin := help_label.get_parent() + if margin == null: + return + var control_row := HBoxContainer.new() + control_row.add_theme_constant_override("separation", 12) + control_row.size_flags_horizontal = Control.SIZE_EXPAND_FILL + margin.remove_child(help_label) + margin.add_child(control_row) + control_row.add_child(help_label) + help_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL + help_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + var upgrade_button := Button.new() + upgrade_button.text = "升级" + upgrade_button.custom_minimum_size = Vector2(120.0, 34.0) + upgrade_button.tooltip_text = "打开升级面板" + upgrade_button.pressed.connect(_on_upgrade_requested) + control_row.add_child(upgrade_button) + + +func _build_debug_panel() -> void: + if DEBUG_PANEL_SCENE == null: + return + debug_panel = DEBUG_PANEL_SCENE.instantiate() as CanvasLayer + if debug_panel == null: + return + debug_panel.name = "DebugPanel" + add_child(debug_panel) + if debug_panel.has_method("bind_world"): + debug_panel.bind_world(self) + debug_panel.visible = false + + +func _on_upgrade_requested() -> void: + if debug_panel == null or not is_instance_valid(debug_panel): + return + if debug_panel.has_method("toggle"): + debug_panel.toggle() + else: + debug_panel.visible = not debug_panel.visible func _process(_delta: float) -> void: _refresh_help_text() @@ -123,8 +170,10 @@ func _spawn_debug_target(enemy_id: StringName, elite: bool) -> void: if active_target != null and is_instance_valid(active_target): active_target.queue_free() active_target = null + current_encounter = null var scene: PackedScene = ENEMY_SCENES.get(enemy_id, TRAINING_DUMMY_SCENE) active_target = scene.instantiate() as Node2D + current_encounter = active_target active_target.position = target_spawn.position active_target.z_index = 3 if active_target.get("elite") != null: @@ -138,6 +187,7 @@ func _spawn_debug_target(enemy_id: StringName, elite: bool) -> void: func _on_debug_target_defeated() -> void: active_target = null + current_encounter = null var timer := get_tree().create_timer(0.45) timer.timeout.connect(func() -> void: if is_instance_valid(self): @@ -168,6 +218,7 @@ func _return_to_character_select() -> void: if active_target != null and is_instance_valid(active_target): active_target.queue_free() active_target = null + current_encounter = null if enemy_select != null and enemy_select.has_method("close"): enemy_select.close() if debug_status != null and debug_status.has_method("clear"): diff --git a/ui/battle_status.gd b/ui/battle_status.gd index 3cbbd3f..93e164e 100644 --- a/ui/battle_status.gd +++ b/ui/battle_status.gd @@ -1,5 +1,7 @@ extends CanvasLayer +signal debug_requested + const RunEffects := preload("res://systems/run/run_effects.gd") const UISkin := preload("res://ui/ui_skin.gd") @@ -33,6 +35,7 @@ var metric_caption_labels: Array[Label] = [] var layout_size_override: Vector2 = Vector2.ZERO var context_state: Dictionary = {} var run_panel_root: PanelContainer +var debug_button: Button func _ready() -> void: context_state = _default_context_state() @@ -95,6 +98,13 @@ func _build_ui() -> void: UISkin.label(title_label, 24, Color(0.98, 0.90, 0.66)) content.add_child(title_label) + debug_button = Button.new() + debug_button.text = "Debug" + debug_button.tooltip_text = "Open the test panel" + UISkin.button_styles(debug_button, "thin") + debug_button.pressed.connect(_on_debug_pressed) + content.add_child(debug_button) + subtitle_label = Label.new() subtitle_label.text = _locale_text( "Pick a champion, then build toward the bosses instead of only surviving the next room.", @@ -417,6 +427,9 @@ func _default_context_state() -> Dictionary: ) } +func _on_debug_pressed() -> void: + debug_requested.emit() + func _refresh_context() -> void: if objective_value_label == null: return diff --git a/ui/debug_panel.gd b/ui/debug_panel.gd index a42db43..731074b 100644 --- a/ui/debug_panel.gd +++ b/ui/debug_panel.gd @@ -8,13 +8,20 @@ const UISkin := preload("res://ui/ui_skin.gd") var target_world: Node = null var layout_size_override: Vector2 = Vector2.ZERO +var content_stack: VBoxContainer = null +var upgrade_title_label: Label = null +var upgrade_action_row: HBoxContainer = null +var clear_upgrades_button: Button = null +var upgrade_scroll: ScrollContainer = null +var upgrade_list: VBoxContainer = null +var active_upgrade_target: Node = null +var upgrade_buttons: Array[Button] = [] func _ready() -> void: layer = 40 visible = false panel.add_theme_stylebox_override("panel", UISkin.content_panel_style()) - UISkin.label(label, 12, Color(0.82, 0.90, 0.98)) - label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + _build_upgrade_ui() if get_viewport() != null and not get_viewport().size_changed.is_connected(_queue_layout_refresh): get_viewport().size_changed.connect(_queue_layout_refresh) _queue_layout_refresh() @@ -29,6 +36,8 @@ func _process(_delta: float) -> void: if not visible or target_world == null or not is_instance_valid(target_world): return var player: Node = target_world.player_character + if player != active_upgrade_target: + _rebuild_upgrade_controls(player) var encounter: Node = target_world.current_encounter var lines := PackedStringArray() lines.append("Debug") @@ -67,7 +76,123 @@ func _process(_delta: float) -> void: float(player.cooldowns.get("skill3", 0.0)) ]) label.text = "\n".join(lines) + _sync_upgrade_button_states() + +func _build_upgrade_ui() -> void: + if content_stack != null: + return + var content_root := label.get_parent() + if content_root == null: + return + content_stack = VBoxContainer.new() + content_stack.name = "DebugStack" + content_stack.add_theme_constant_override("separation", 8) + content_root.remove_child(label) + content_stack.add_child(label) + content_root.add_child(content_stack) + label.size_flags_horizontal = Control.SIZE_EXPAND_FILL + label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + + upgrade_title_label = Label.new() + upgrade_title_label.text = "Upgrades" + upgrade_title_label.add_theme_font_size_override("font_size", 14) + upgrade_title_label.add_theme_color_override("font_color", Color(1.0, 0.9, 0.64)) + content_stack.add_child(upgrade_title_label) + + upgrade_action_row = HBoxContainer.new() + upgrade_action_row.add_theme_constant_override("separation", 8) + content_stack.add_child(upgrade_action_row) + + clear_upgrades_button = Button.new() + clear_upgrades_button.text = "Clear Upgrades" + clear_upgrades_button.toggle_mode = false + clear_upgrades_button.custom_minimum_size = Vector2(0.0, 34.0) + UISkin.button_styles(clear_upgrades_button, "thin") + clear_upgrades_button.pressed.connect(_on_clear_upgrades_pressed) + upgrade_action_row.add_child(clear_upgrades_button) + + upgrade_scroll = ScrollContainer.new() + upgrade_scroll.custom_minimum_size = Vector2(0.0, 168.0) + upgrade_scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL + upgrade_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL + content_stack.add_child(upgrade_scroll) + + upgrade_list = VBoxContainer.new() + upgrade_list.add_theme_constant_override("separation", 6) + upgrade_list.size_flags_horizontal = Control.SIZE_EXPAND_FILL + upgrade_scroll.add_child(upgrade_list) +func _rebuild_upgrade_controls(player: Node) -> void: + active_upgrade_target = player + _clear_upgrade_entries() + if clear_upgrades_button != null: + clear_upgrades_button.disabled = player == null or not is_instance_valid(player) or not player.has_method("clear_all_upgrades") + if player == null or not is_instance_valid(player): + return + if not player.has_method("get_upgrade_sections") or not player.has_method("is_upgrade_enabled") or not player.has_method("set_upgrade_enabled"): + return + var sections: Array = player.get_upgrade_sections() + for section_variant in sections: + var section: Dictionary = section_variant + var section_title := Label.new() + section_title.text = String(section.get("title", "Upgrades")) + section_title.add_theme_font_size_override("font_size", 12) + section_title.add_theme_color_override("font_color", Color(0.86, 0.91, 0.98)) + upgrade_list.add_child(section_title) + for upgrade_variant in section.get("upgrades", []): + var upgrade: Dictionary = upgrade_variant + var upgrade_id := String(upgrade.get("id", "")) + var upgrade_label := String(upgrade.get("label", upgrade_id)) + var upgrade_description := String(upgrade.get("description", "")) + var button := Button.new() + button.toggle_mode = true + button.text = upgrade_label + button.tooltip_text = upgrade_description + button.custom_minimum_size = Vector2(0.0, 34.0) + button.size_flags_horizontal = Control.SIZE_EXPAND_FILL + UISkin.button_styles(button, "thin") + button.set_meta("upgrade_id", upgrade_id) + button.button_pressed = bool(player.is_upgrade_enabled(upgrade_id)) + button.toggled.connect(func(pressed: bool) -> void: + if active_upgrade_target == null or not is_instance_valid(active_upgrade_target): + return + if not active_upgrade_target.has_method("set_upgrade_enabled"): + return + active_upgrade_target.set_upgrade_enabled(upgrade_id, pressed) + ) + upgrade_list.add_child(button) + upgrade_buttons.append(button) + _sync_upgrade_button_states() + +func _sync_upgrade_button_states() -> void: + if active_upgrade_target == null or not is_instance_valid(active_upgrade_target): + return + if not active_upgrade_target.has_method("is_upgrade_enabled"): + return + for button in upgrade_buttons: + if button == null or not is_instance_valid(button): + continue + if not button.has_meta("upgrade_id"): + continue + var upgrade_id := String(button.get_meta("upgrade_id")) + var should_be_pressed := bool(active_upgrade_target.is_upgrade_enabled(upgrade_id)) + if button.button_pressed != should_be_pressed: + button.button_pressed = should_be_pressed + +func _clear_upgrade_entries() -> void: + upgrade_buttons.clear() + if upgrade_list == null: + return + for child in upgrade_list.get_children(): + child.queue_free() + +func _on_clear_upgrades_pressed() -> void: + if active_upgrade_target == null or not is_instance_valid(active_upgrade_target): + return + if not active_upgrade_target.has_method("clear_all_upgrades"): + return + active_upgrade_target.clear_all_upgrades() + _sync_upgrade_button_states() func _queue_layout_refresh() -> void: call_deferred("_refresh_layout") diff --git a/world.gd b/world.gd index e177cdd..1404e7f 100644 --- a/world.gd +++ b/world.gd @@ -1,4 +1,4 @@ -extends Node2D +extends Node2D const KNIGHT_SCENE := preload("res://characters/knight/knight.tscn") const RANGER_SCENE := preload("res://characters/ranger/ranger.tscn") @@ -142,6 +142,8 @@ func _ready() -> void: settings_panel.closed.connect(_on_settings_panel_closed) if debug_panel != null and debug_panel.has_method("bind_world"): debug_panel.bind_world(self) + if battle_status != null and battle_status.has_signal("debug_requested") and not battle_status.debug_requested.is_connected(_on_battle_status_debug_requested): + battle_status.debug_requested.connect(_on_battle_status_debug_requested) _refresh_battle_status( _ui_text("Town Boss Trial", "城镇王战试炼", "城鎮王戰試煉"), _ui_text("Pick a champion, then claim relics between encounters.", "先选择角色,再在战斗间隙领取饰品。", "先選擇角色,再在戰鬥間隙領取飾品。"), @@ -2140,6 +2142,11 @@ func _on_title_settings_requested() -> void: settings_panel.open() _refresh_battle_status() +func _on_battle_status_debug_requested() -> void: + if debug_panel == null or not debug_panel.has_method("toggle"): + return + debug_panel.toggle() + func _on_audio_settings_panel_closed() -> void: if return_pause_after_audio_panel: return_pause_after_audio_panel = false