165 lines
4.8 KiB
GDScript
165 lines
4.8 KiB
GDScript
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 _input_is_locked():
|
|
dash_duration_remaining = 0.0
|
|
dashing = false
|
|
dash_velocity = Vector3.ZERO
|
|
return
|
|
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 _input_is_locked() or 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 _input_is_locked() or 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 and not _input_is_locked() else Vector3.ZERO
|
|
|
|
|
|
func _input_is_locked() -> bool:
|
|
return (
|
|
player != null
|
|
and player.has_method("is_dialogue_input_locked")
|
|
and bool(player.call("is_dialogue_input_locked"))
|
|
)
|
|
|
|
|
|
func get_dash_cooldown_remaining() -> float:
|
|
return dash_cooldown_remaining
|