class_name PlayerCombatController extends Node3D const SLASH_REST_ANGLE := Vector3(-0.35, 0.0, 0.0) @export var player: CharacterBody3D @export var simulation_manager: Node @export var combat_presentation: Node @export var sword: Node3D @export_range(0.5, 4.0, 0.1) var attack_range := 2.6 @export_range(30.0, 160.0, 5.0) var attack_angle := 90.0 @export var dash_speed := 22.0 @export var dash_duration := 0.18 @export var dash_cooldown_seconds := 1.1 var attack_cooldown_seconds := 0.55 var attack_timer := 0.0 var dash_timer := 0.0 var dashing := false var dash_velocity := Vector3.ZERO var _swing_tween: Tween var _references_resolved := false func _ready() -> void: if sword == null: return sword.rotation = SLASH_REST_ANGLE var definition := SimulationItems.get_weapon(SimulationIds.ITEM_SWORD) if definition != null: attack_cooldown_seconds = definition.attack_cooldown attack_range = maxf(definition.reach + 0.2, attack_range) func _resolve_references() -> void: if simulation_manager == null: simulation_manager = get_tree().get_first_node_in_group("simulation_manager") if combat_presentation == null: combat_presentation = get_tree().get_first_node_in_group("combat_presentation") _references_resolved = true func _physics_process(delta: float) -> void: if not _references_resolved: _resolve_references() if simulation_manager != null and simulation_manager.has_method("update_player_combatant"): simulation_manager.update_player_combatant(player.global_position) attack_timer = maxf(attack_timer - delta, 0.0) dash_timer = maxf(dash_timer - delta, 0.0) if _player_is_downed(): attack_timer = 0.0 dash_timer = 0.0 dashing = false return if dashing: player.velocity = dash_velocity if dash_timer <= 0.0: dashing = false player.move_and_slide() return if Input.is_action_just_pressed("attack") and attack_timer <= 0.0: _perform_attack() if Input.is_action_just_pressed("dash") and dash_timer <= 0.0: _perform_dash() func _player_is_downed() -> bool: return ( simulation_manager != null and simulation_manager.has_method("player_is_downed") and simulation_manager.player_is_downed() ) func _perform_attack() -> void: attack_timer = attack_cooldown_seconds _play_swing() var facing := -player.global_transform.basis.z var hostile: CombatantStateRecord = null if combat_presentation != null and combat_presentation.has_method("find_hostile_in_arc"): hostile = combat_presentation.find_hostile_in_arc( player.global_position, facing, attack_range, attack_angle ) if hostile == null: return if simulation_manager != null and simulation_manager.has_method("player_attack"): var damage: float = simulation_manager.player_attack(hostile.get_combatant_id()) if damage > 0.0: _emit_combat_feedback("Struck %s" % hostile.get_display_name(), damage) func _perform_dash() -> void: dash_timer = dash_duration dashing = true var facing := -player.global_transform.basis.z facing.y = 0.0 facing = facing.normalized() dash_velocity = facing * dash_speed dash_velocity.y = 0.0 func _play_swing() -> void: if sword == null: return if _swing_tween != null and _swing_tween.is_valid(): _swing_tween.kill() sword.rotation = SLASH_REST_ANGLE _swing_tween = create_tween() _swing_tween.set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT) _swing_tween.tween_property(sword, "rotation", SLASH_REST_ANGLE + Vector3(-1.6, 0.6, 0.0), 0.12) _swing_tween.tween_property(sword, "rotation", SLASH_REST_ANGLE, 0.2) func _emit_combat_feedback(heading: String, damage: float) -> void: if player != null and player.has_signal("interaction_feedback"): player.interaction_feedback.emit(heading, "Dealt %.0f damage." % damage, true) func is_attacking() -> bool: return attack_timer > 0.0 func is_dashing() -> bool: return dashing