197 lines
5 KiB
GDScript
197 lines
5 KiB
GDScript
class_name Player
|
|
extends CharacterBody3D
|
|
|
|
signal died
|
|
|
|
enum State {
|
|
NORMAL,
|
|
DASHING,
|
|
LOCKED,
|
|
}
|
|
|
|
const MOVE_SPEED := 7.5
|
|
const DASH_SPEED := 35.
|
|
const DASH_COST := .5
|
|
|
|
const MORTAR_IDX := 2
|
|
|
|
static var instance: Player
|
|
|
|
@export var shake_noise: FastNoiseLite
|
|
@export var cannon: Node3D
|
|
var shake_duration := 0.
|
|
@onready var camera_transform: Transform3D = %Camera3D.transform
|
|
|
|
var damage_clock := 0.
|
|
var aim_angle: float
|
|
var health := 3:
|
|
set(v):
|
|
health = v
|
|
if is_node_ready():
|
|
%HealthLabel.text = "Health: %d" % v
|
|
%HealthBar.update(v)
|
|
|
|
var state := State.NORMAL
|
|
var dash_direction: Vector2
|
|
var stamina := 1.0
|
|
|
|
var speed_mul := 1.
|
|
|
|
@export var guns: Array[Gun]
|
|
var gun_index := 0:
|
|
set(v):
|
|
gun_index = v % guns.size()
|
|
%WeaponUI.active_weapon = gun_index
|
|
var gun: Gun:
|
|
get: return guns[gun_index]
|
|
|
|
var distance_traveled := 0.
|
|
|
|
func clean_angle(theta: float) -> float:
|
|
theta = fposmod(theta, TAU)
|
|
if theta < PI: return theta
|
|
else: return PI - theta
|
|
|
|
func _init() -> void:
|
|
instance = self
|
|
|
|
func _ready() -> void:
|
|
health = health
|
|
|
|
if not Level.level:
|
|
await get_tree().process_frame
|
|
|
|
Level.level.level_started.connect(func(): %EngineSFX.play())
|
|
|
|
func _process_stamina(delta: float) -> void:
|
|
stamina = move_toward(stamina, 1., delta * 0.2)
|
|
%StaminaBar.value = stamina
|
|
|
|
func exp_lerp(a: Variant, b: Variant, decay: float, dt: float) -> Variant:
|
|
return lerp(a, b, 1 - exp(-decay * dt))
|
|
|
|
func _process_movement(delta: float) -> void:
|
|
var input = Input.get_vector("move_left", "move_right", "move_up", "move_down")
|
|
input.normalized()
|
|
var mul := 10. if DebugMenu.high_speed_hack else 1.
|
|
var desired_velocity = Vector3(input.x, 0., input.y) * MOVE_SPEED * mul * speed_mul
|
|
velocity = exp_lerp(velocity, desired_velocity, 5, delta)
|
|
move_and_slide()
|
|
|
|
if Input.is_action_just_pressed("dash") and stamina >= DASH_COST and not input.is_zero_approx():
|
|
stamina -= DASH_COST
|
|
dash_direction = input
|
|
state = State.DASHING
|
|
await get_tree().create_timer(.25, false).timeout
|
|
# state = State.LOCKED
|
|
# await get_tree().create_timer(.5, false).timeout
|
|
state = State.NORMAL
|
|
|
|
func _process_dash() -> void:
|
|
velocity = Vector3(dash_direction.x, 0., dash_direction.y) * DASH_SPEED
|
|
move_and_slide()
|
|
|
|
func _process_aim() -> void:
|
|
var viewport_mouse_pos := get_viewport().get_mouse_position()
|
|
var r_origin: Vector3 = %Camera3D.project_ray_origin(viewport_mouse_pos)
|
|
var r_dir: Vector3 = %Camera3D.project_ray_normal(viewport_mouse_pos)
|
|
|
|
# y = mx + b
|
|
# 0 = mx + b
|
|
# -b / m = x
|
|
|
|
var t := -r_origin.y / r_dir.y
|
|
var world_mouse_pos = r_origin + r_dir * t
|
|
var to_mouse_pos = to_local(world_mouse_pos)
|
|
var angle = Vector2(to_mouse_pos.x, -to_mouse_pos.z).angle()
|
|
aim_angle = angle
|
|
|
|
var cannon_to_mouse_pos = cannon.get_parent().to_local(world_mouse_pos)
|
|
cannon.rotation.y = Vector2(cannon_to_mouse_pos.x, -cannon_to_mouse_pos.z).angle() + PI / 2
|
|
|
|
%Reticle.position = to_mouse_pos
|
|
|
|
func _process_shoot(delta: float) -> void:
|
|
if gun_index == MORTAR_IDX and Input.is_action_pressed("fire"):
|
|
speed_mul = exp_lerp(speed_mul, 0.25, 5, delta)
|
|
else:
|
|
speed_mul = exp_lerp(speed_mul, 1., 5, delta)
|
|
|
|
if Input.is_action_pressed("fire") and gun.fire_clock <= 0.:
|
|
gun.fire(aim_angle)
|
|
|
|
func _process_cam_shake(delta: float) -> void:
|
|
shake_duration -= delta
|
|
%Camera3D.transform = camera_transform
|
|
|
|
if DebugMenu.zoomed_out:
|
|
%Camera3D.position += %Camera3D.basis.z * 20.
|
|
|
|
if shake_duration <= 0:
|
|
return
|
|
|
|
var x := shake_noise.get_noise_1d(shake_duration * 10000)
|
|
var y := shake_noise.get_noise_1d(-shake_duration * 10000)
|
|
var v := \
|
|
camera_transform.basis.x * x + \
|
|
camera_transform.basis.y * y
|
|
|
|
%Camera3D.position += v * 1.5
|
|
|
|
func _process_engine_sfx() -> void:
|
|
var speed := velocity.length()
|
|
%EngineSFX.pitch_scale = clampf(remap(speed, 0, MOVE_SPEED, 1., 1.25), 1., 1.5)
|
|
|
|
func _process(delta: float) -> void:
|
|
if not Level.is_active(): return
|
|
match state:
|
|
State.NORMAL: _process_movement(delta)
|
|
State.DASHING: _process_dash()
|
|
State.LOCKED: pass
|
|
distance_traveled += velocity.length() * delta
|
|
|
|
|
|
_process_aim()
|
|
_process_shoot(delta)
|
|
_process_cam_shake(delta)
|
|
_process_stamina(delta)
|
|
_process_engine_sfx()
|
|
|
|
damage_clock -= delta
|
|
|
|
if Input.is_action_just_pressed("sharpshooter"):
|
|
gun_index = 0
|
|
if Input.is_action_just_pressed("minigun"):
|
|
gun_index = 1
|
|
if Input.is_action_just_pressed("mortar"):
|
|
gun_index = 2
|
|
|
|
%MortarDecal.visible = gun_index == MORTAR_IDX
|
|
%MortarDecal.global_position = %Reticle.global_position
|
|
|
|
var facing_dir := Vector2.RIGHT.rotated(%Tank.rotation.y)
|
|
var new_dir := Vector2(velocity.x, -velocity.z).rotated(PI / 2)
|
|
var opp_dir := new_dir.rotated(PI)
|
|
|
|
var new_dot = facing_dir.dot(new_dir)
|
|
var opp_dot = facing_dir.dot(opp_dir)
|
|
|
|
if new_dot > opp_dot:
|
|
%Tank.rotation.y = new_dir.angle()
|
|
else:
|
|
%Tank.rotation.y = opp_dir.angle()
|
|
|
|
func damage(damager: Node3D, amount: int = 1) -> void:
|
|
if health <= 0: return
|
|
if state == State.DASHING:
|
|
if damager.has_method("kill"):
|
|
damager.kill(velocity.normalized())
|
|
else:
|
|
if damage_clock <= 0.:
|
|
%CrashSFX.play()
|
|
health -= amount
|
|
damage_clock = 3
|
|
shake_duration = .25
|
|
|
|
if health <= 0:
|
|
died.emit()
|