extends CharacterBody3D const GRAVITY := -15.0 const MOVE_SPEED := 5.0 const ACCELERATION := 12.0 const FRICTION := 10.0 const ROTATION_SPEED := 8.0 const ATTACK_COOLDOWN := 0.5 const ATTACK_RANGE := 2.5 const STATIONARY_REQUIREMENT := 0.1 const ATTACK_ANIM_DURATION := 0.3 var camera_pivot: Node3D var _attack_cooldown := 0.0 var _attack_anim_timer := 0.0 var _stationary_timer := 0.0 var _is_flashing := false func _ready() -> void: camera_pivot = $CameraPivot add_to_group("combatants") func _physics_process(delta: float) -> void: if not is_on_floor(): velocity.y += GRAVITY * delta _attack_cooldown = maxf(0.0, _attack_cooldown - delta) _attack_anim_timer = maxf(0.0, _attack_anim_timer - delta) if _attack_anim_timer > 0.0: velocity.x = 0.0 velocity.z = 0.0 _stationary_timer += delta else: var input_dir := _get_movement_input() var move_dir := input_dir.normalized() if move_dir: _stationary_timer = 0.0 var forward := -camera_pivot.global_transform.basis.z forward.y = 0.0 forward = forward.normalized() var right := camera_pivot.global_transform.basis.x right.y = 0.0 right = right.normalized() var direction := (forward * move_dir.y + right * move_dir.x).normalized() velocity.x = lerp(velocity.x, direction.x * MOVE_SPEED, ACCELERATION * delta) velocity.z = lerp(velocity.z, direction.z * MOVE_SPEED, ACCELERATION * delta) var target_angle := atan2(direction.x, direction.z) var current_angle := rotation.y var diff := target_angle - current_angle while diff > PI: diff -= TAU while diff < -PI: diff += TAU rotation.y += diff * clampf(ROTATION_SPEED * delta, 0.0, 1.0) else: velocity.x = move_toward(velocity.x, 0.0, FRICTION * delta) velocity.z = move_toward(velocity.z, 0.0, FRICTION * delta) _stationary_timer += delta if Input.is_action_just_pressed("action_1") and _stationary_timer >= STATIONARY_REQUIREMENT and _attack_cooldown <= 0.0: _attack_cooldown = ATTACK_COOLDOWN _attack_anim_timer = ATTACK_ANIM_DURATION _stationary_timer = 0.0 _perform_attack() move_and_slide() func _perform_attack() -> void: var forward := _forward_dir() for enemy in _find_all_enemies(): var to_enemy: Vector3 = enemy.global_position - global_position to_enemy.y = 0.0 if to_enemy.length() > ATTACK_RANGE: continue if to_enemy.normalized().dot(forward) > 0.5: enemy.take_hit() func _find_all_enemies() -> Array[CharacterBody3D]: var enemies: Array[CharacterBody3D] = [] _collect_enemies(get_parent(), enemies) return enemies func _collect_enemies(node: Node, result: Array[CharacterBody3D]) -> void: for child in node.get_children(): if child is CharacterBody3D and child != self: result.append(child) _collect_enemies(child, result) func _forward_dir() -> Vector3: var forward := transform.basis.z forward.y = 0.0 return forward.normalized() func take_hit() -> void: if _is_flashing: return _is_flashing = true _flash_meshes(Color.WHITE, Color.WHITE, 0.15) func _flash_meshes(flash_color: Color, emissive_color: Color, duration: float) -> void: var flash_mat := StandardMaterial3D.new() flash_mat.albedo_color = flash_color flash_mat.emission = emissive_color flash_mat.emission_energy = 2.0 for child in get_children(): if child is MeshInstance3D: var old: Material = child.material_override child.material_override = flash_mat await get_tree().create_timer(duration).timeout child.material_override = old _is_flashing = false func _get_movement_input() -> Vector2: return Input.get_vector("move_left", "move_right", "move_down", "move_up")