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 var attack_range := 2.4 @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_duration_remaining := 0.0 var dash_cooldown_remaining := 0.0 var dashing := false var dash_velocity := Vector3.ZERO var _swing_tween: Tween var _references_resolved := false func _ready() -> void: process_physics_priority = -1 if sword != null: sword.rotation = SLASH_REST_ANGLE var definition := SimulationItems.get_weapon(SimulationIds.ITEM_SWORD) if definition != null: attack_cooldown_seconds = definition.attack_cooldown attack_range = definition.reach 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) _advance_timers(delta) if _player_is_downed(): attack_timer = 0.0 dash_duration_remaining = 0.0 dash_cooldown_remaining = 0.0 dashing = false return if dashing: return if Input.is_action_just_pressed("attack"): _perform_attack() if Input.is_action_just_pressed("dash"): _perform_dash() func _advance_timers(delta: float) -> void: if not is_finite(delta) or delta <= 0.0: return attack_timer = maxf(attack_timer - delta, 0.0) dash_duration_remaining = maxf(dash_duration_remaining - delta, 0.0) dash_cooldown_remaining = maxf(dash_cooldown_remaining - delta, 0.0) if dashing and dash_duration_remaining <= 0.0: dashing = false 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: if attack_timer > 0.0 or not _simulation_attack_ready(): return 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: if dash_cooldown_remaining > 0.0 or _player_is_downed(): return dash_duration_remaining = dash_duration dash_cooldown_remaining = maxf(dash_cooldown_seconds, 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 _simulation_attack_ready() -> bool: if simulation_manager == null or not simulation_manager.has_method("player_attack_ready"): return true return simulation_manager.player_attack_ready() 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 func get_dash_velocity() -> Vector3: return dash_velocity if dashing else Vector3.ZERO func get_dash_cooldown_remaining() -> float: return dash_cooldown_remaining