You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
445 lines
14 KiB
445 lines
14 KiB
extends KinematicBody
|
|
|
|
# Movement
|
|
var velocity = Vector3.ZERO
|
|
var strafe_dir = Vector3.ZERO
|
|
var strafe = Vector3.ZERO
|
|
|
|
# Vertical movement
|
|
const GRAVITY = 9.8
|
|
const TERMINAL_VELOCITY = -56
|
|
const MIN_AIRBORNE_TIME = 0.1 # minimum time to be considered off-ground
|
|
var velocity_y = 0
|
|
var weight = 2
|
|
var airborne_time = 0
|
|
|
|
# Special movement
|
|
var aim_turn = 0
|
|
|
|
var movement_speed = 0
|
|
var walk_speed = 2.0
|
|
var crouch_walk_speed = 1
|
|
var run_speed = 9
|
|
var acceleration = 5
|
|
var air_drag = 0.2
|
|
var angular_acceleration = 7
|
|
|
|
var jump_magnitude = 7
|
|
var roll_magnitude = 20
|
|
|
|
var walk_toggle = false # TODO
|
|
var walking = false
|
|
|
|
var aiming = false
|
|
var aim_speed = 10
|
|
|
|
var final_velocity = Vector3.ZERO
|
|
var model_local_final_velocity = Vector3.ZERO
|
|
var cumulative_x = 0
|
|
var cumulative_z = 0
|
|
|
|
# Camera
|
|
const CAMERA_MOUSE_ROTATION_SPEED = 0.001
|
|
const CAMERA_CONTROLLER_ROTATION_SPEED = 3.0
|
|
# A minimum angle lower than or equal to -90 breaks movement if the player is looking upward.
|
|
const CAMERA_X_ROT_MIN = -89.9
|
|
const CAMERA_X_ROT_MAX = 70
|
|
var camera_x_rot = 0.0
|
|
|
|
onready var initial_position = transform.origin
|
|
|
|
# Model movement towards velocity
|
|
const ROTATION_INTERPOLATE_SPEED = 0.1
|
|
|
|
onready var camera_base = $CameraBase
|
|
onready var camera_animation = camera_base.get_node(@"Animation")
|
|
onready var camera_rot = camera_base.get_node(@"CameraRot")
|
|
onready var camera_spring_arm = camera_rot.get_node(@"SpringArm")
|
|
onready var camera_camera = camera_spring_arm.get_node(@"Camera")
|
|
|
|
# Animation
|
|
onready var model = $CenterOfMass/Alunya
|
|
var wheel_rotation = 0
|
|
|
|
export(int) var health = 9
|
|
|
|
var current_weapon = 0
|
|
var fired_once = false # for non-auto weapons
|
|
var ForwardCollisionZOffset = 0
|
|
|
|
# Nodes
|
|
onready var animation_tree = $AnimationTree
|
|
onready var weapon_contoller = $WeaponController
|
|
onready var weapon = model.get_node(@"Colette_Armature/Skeleton/GunBone/Weapon")
|
|
onready var shoot_from = model.get_node(@"Colette_Armature/Skeleton/GunBone/ShootFrom")
|
|
|
|
onready var ui = $UI
|
|
onready var color_rect = ui.get_node(@"ColorRect")
|
|
onready var crosshair = ui.get_node(@"Crosshair")
|
|
onready var ui_health = ui.get_node(@"WeaponStatUI/ColorRect/Health")
|
|
onready var fire_cooldown = $FireCooldown
|
|
|
|
onready var sound_effect_land = $SoundEffects/Land
|
|
onready var sound_effect_jump = $SoundEffects/Jump
|
|
onready var sound_effect_step = $SoundEffects/Step
|
|
|
|
var achievements = []
|
|
onready var level = get_parent()
|
|
# Fading to black when falling
|
|
onready var out_of_bounds_y_start : float = level.get_out_of_bounds_y_start()
|
|
onready var out_of_bounds_y_end : float = level.get_out_of_bounds_y_end()
|
|
onready var out_of_bounds_y_fade = out_of_bounds_y_start - out_of_bounds_y_end
|
|
|
|
func _init():
|
|
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
|
|
|
|
|
|
func _ready():
|
|
ForwardCollisionZOffset = $ForwardCollision.transform.origin.z - $BodyCollision.transform.origin.z
|
|
$GlobalShootParticle.speed_scale = weapon_contoller.fire_rate() #/2
|
|
|
|
weapon_contoller.update_ammo()
|
|
update_health()
|
|
|
|
|
|
func _physics_process(delta):
|
|
# Order of operations:
|
|
# - Process camera
|
|
# - Process physical movement
|
|
# - Process movement-based rotation
|
|
# - Process inventory commands
|
|
# - Process shooting
|
|
# - Process animation
|
|
|
|
|
|
# Process camera
|
|
var camera_move = Vector2(
|
|
Input.get_action_strength("view_right") - Input.get_action_strength("view_left"),
|
|
Input.get_action_strength("view_up") - Input.get_action_strength("view_down"))
|
|
var camera_speed_this_frame = delta * CAMERA_CONTROLLER_ROTATION_SPEED
|
|
if aiming:
|
|
camera_speed_this_frame *= 0.5
|
|
rotate_camera(camera_move * camera_speed_this_frame)
|
|
|
|
# Get camera_rot relative to player, countering any rotation of the player base transform
|
|
var camera_basis = camera_rot.global_transform.basis
|
|
var camera_z = camera_basis.z
|
|
var camera_x = camera_basis.x
|
|
|
|
camera_z.y = 0
|
|
camera_z = camera_z.normalized()
|
|
camera_x.y = 0
|
|
camera_x = camera_x.normalized()
|
|
|
|
# Aiming camera
|
|
var current_aim = Input.is_action_pressed("aim")
|
|
var changed_aim = (aiming != current_aim)
|
|
# Trigger animation if we changed aim this frame
|
|
if changed_aim:
|
|
aiming = current_aim
|
|
if aiming:
|
|
camera_animation.play("shoot")
|
|
else:
|
|
camera_animation.play("far")
|
|
|
|
|
|
# Process movement
|
|
var orientation = global_transform
|
|
var input_target = Vector3.ZERO
|
|
# Try to move in input direction
|
|
if (
|
|
(
|
|
Input.is_action_pressed("move_forward")
|
|
or Input.is_action_pressed("move_back")
|
|
or Input.is_action_pressed("move_left")
|
|
or Input.is_action_pressed("move_right")
|
|
)
|
|
and is_on_floor()
|
|
):
|
|
|
|
var direction = Vector3(Input.get_action_strength("move_left") - Input.get_action_strength("move_right"),
|
|
0,
|
|
Input.get_action_strength("move_forward") - Input.get_action_strength("move_back"))
|
|
# Enforce a top ammount of 1
|
|
if direction.length() > 1:
|
|
direction = direction.normalized()
|
|
|
|
strafe_dir = direction
|
|
|
|
# Convert orientation to quaternions for interpolating rotation.
|
|
input_target = camera_x * direction.x + camera_z * direction.z
|
|
if input_target.length() > 0.001:
|
|
#var q_from = orientation.basis.get_rotation_quat()
|
|
var q_to = global_transform.looking_at(global_transform.origin - input_target, Vector3.UP).basis.get_rotation_quat()
|
|
# Interpolate current rotation with desired one.
|
|
#orientation.basis = Basis(q_from.slerp(q_to, delta * ROTATION_INTERPOLATE_SPEED))
|
|
orientation.basis = Basis(q_to)
|
|
|
|
var target_velocity
|
|
var model_local_delta_velocity = model_local_final_velocity
|
|
|
|
if aiming:
|
|
movement_speed = walk_speed
|
|
else:
|
|
movement_speed = run_speed
|
|
|
|
movement_speed *= input_target.length()
|
|
target_velocity = orientation.basis.z * movement_speed
|
|
|
|
# Calculate y velocity independently of input motion
|
|
velocity_y -= GRAVITY * 2 * delta
|
|
airborne_time += delta
|
|
if is_on_floor():
|
|
# If just landed after a non-trivial drop, play landing sound
|
|
if airborne_time > 0.5:
|
|
sound_effect_land.play()
|
|
airborne_time = 0
|
|
|
|
var on_air = airborne_time > MIN_AIRBORNE_TIME
|
|
|
|
if not on_air:
|
|
# Token down velocity to keep contact with ground
|
|
velocity_y = -1
|
|
# Jump logic
|
|
if Input.is_action_just_pressed("jump"):
|
|
velocity_y = jump_magnitude
|
|
on_air = true
|
|
# Increase airborne time so next frame on_air is certainly true
|
|
airborne_time = MIN_AIRBORNE_TIME
|
|
sound_effect_jump.play()
|
|
# Terminal velocity check
|
|
if velocity_y < TERMINAL_VELOCITY:
|
|
velocity_y = TERMINAL_VELOCITY
|
|
|
|
# Momentum dampener: sharply reduces recorded velocity after being stopped by a wall.
|
|
# Otherwise, requires a full deceleration before able to move the other direction.
|
|
# Making it equal to final_velocity seriously reduces ability to walk up a slope.
|
|
if get_slide_count() > 1:
|
|
velocity = lerp(velocity, Vector3(final_velocity.x, 0, final_velocity.z), 1)
|
|
|
|
var slide_y = Vector3.ZERO
|
|
if is_on_floor():
|
|
# Calculate movement velocity
|
|
velocity = lerp(velocity, target_velocity, delta * acceleration)
|
|
# Calculate vertical sliding
|
|
#slide_y = get_floor_normal() * GRAVITY * delta
|
|
|
|
else:
|
|
velocity = lerp(velocity, Vector3(0, velocity.y, 0), delta * air_drag)
|
|
|
|
# As of 3.4.0, I don't trust move_and_slide to give true velocity (or at least what I want).
|
|
# I was running into a corner and it said I was reaching 2 m/s while vibrating.
|
|
# TODO test in 3.4 and report
|
|
var position_before = global_transform.origin
|
|
move_and_slide(velocity + Vector3.UP * velocity_y - slide_y, Vector3.UP)
|
|
var position_after = global_transform.origin
|
|
final_velocity = (position_after - position_before) / delta
|
|
model_local_final_velocity = (model.to_local(position_after) - model.to_local(position_before)) / delta
|
|
|
|
var velocity_length = Vector3(final_velocity.x, 0, final_velocity.z).length()
|
|
|
|
# Rotate model towards actual moved velocity
|
|
if velocity_length > 0.001:
|
|
var q_from = model.global_transform.basis.get_rotation_quat()
|
|
var q_to = Transform().looking_at(Vector3(-final_velocity.x, 0, -final_velocity.z), Vector3.UP).basis.get_rotation_quat()
|
|
# Interpolate current rotation with desired one.
|
|
model.global_transform.basis = Basis(q_from.slerp(q_to, ROTATION_INTERPOLATE_SPEED))
|
|
|
|
# Add acceleration tilt
|
|
model_local_delta_velocity = model_local_delta_velocity - model_local_final_velocity
|
|
# Determine total cumulative acceleration. Should always balance out to 0 when stopped.
|
|
cumulative_x += model_local_delta_velocity.x
|
|
cumulative_z += model_local_delta_velocity.z
|
|
model.rotate_object_local(Vector3.BACK, cumulative_x/250)
|
|
model.rotate_object_local(Vector3.RIGHT, -cumulative_z/500)
|
|
|
|
# Process inventory commands
|
|
|
|
# Reload
|
|
if Input.is_action_pressed("reload"):
|
|
reload()
|
|
|
|
# Switch weapon keys
|
|
for i in weapon_contoller.weapons.size():
|
|
if Input.is_action_pressed("switch_weapon"+str(i+1)):
|
|
switch_weapon(i)
|
|
|
|
# Process shooting
|
|
|
|
# Checking: not reloading, not too soon after last fire, and
|
|
# not a manual fire weapon firing twice without release
|
|
if Input.is_action_pressed("shoot") && $ReloadTimer.is_stopped() and fire_cooldown.time_left == 0 and \
|
|
(!fired_once or weapon_contoller.auto()):
|
|
fired_once = true
|
|
|
|
var shoot_origin = shoot_from.global_transform.origin
|
|
|
|
# Get what crosshair is aiming at
|
|
var ch_pos = crosshair.get_global_rect().position + crosshair.rect_size * 0.5
|
|
var ray_from = camera_camera.project_ray_origin(ch_pos)
|
|
var ray_dir = camera_camera.project_ray_normal(ch_pos)
|
|
|
|
var shoot_target
|
|
var col = get_world().direct_space_state.intersect_ray(ray_from, ray_from + ray_dir * 1000, [self], 0b1000001)
|
|
if col.empty():
|
|
shoot_target = ray_from + ray_dir * 1000
|
|
else:
|
|
shoot_target = col.position
|
|
var shoot_dir = (shoot_target - shoot_origin).normalized()
|
|
|
|
# Check if (cosine of) angle of shooting too large (like standing against a wall)
|
|
if shoot_dir.dot(ray_dir) < 0.5:
|
|
shoot_dir = ray_dir
|
|
|
|
# Raycast from 'shoot from', rather than the camera
|
|
#col = get_world().direct_space_state.intersect_ray(shoot_origin, shoot_target, [self], 0b111)
|
|
|
|
if weapon_contoller.shoot(shoot_origin, shoot_dir, col):
|
|
fire_cooldown.start(1 / weapon_contoller.fire_rate())
|
|
weapon_contoller.play_shoot_sound()
|
|
camera_camera.add_recoil(weapon_contoller.recoil())
|
|
else:
|
|
# Attempt auto-reload
|
|
reload()
|
|
|
|
|
|
# Process animation
|
|
|
|
# Add procedural animation and stride wheel
|
|
var amount_to_turn = velocity_length * PI / 4 * delta
|
|
wheel_rotation += amount_to_turn
|
|
$"CenterOfMass/Alunya/stride wheel".rotate(Vector3(1, 0, 0), amount_to_turn)
|
|
|
|
var seconds = wheel_rotation / PI # was * (6.25/5) / PI. it's magic number, i aint gotta explain shit [not sure why it's 6.25/5]
|
|
|
|
$AnimationTree["parameters/run_seek/seek_position"] = seconds
|
|
|
|
$AnimationTree["parameters/walk_seek/seek_position"] = seconds * 4
|
|
|
|
$AnimationTree["parameters/blend_moving/blend_amount"] = clamp(velocity_length / (walk_speed),0,2) - 1
|
|
|
|
# DISABLED UNTIL ANIMATIONS EXIST
|
|
#if on_air:
|
|
# if (final_velocity.y > 0):
|
|
# animation_tree["parameters/state/current"] = 2
|
|
# else:
|
|
# animation_tree["parameters/state/current"] = 3
|
|
#if aiming:
|
|
# Change state to strafe.
|
|
# animation_tree["parameters/state/current"] = 0
|
|
|
|
# Trigger aiming weapon animation if we changed aim this frame
|
|
if changed_aim:
|
|
if aiming:
|
|
$AnimationTree["parameters/hold_weapon_blend/blend_amount"] = 1
|
|
else:
|
|
$AnimationTree["parameters/hold_weapon_blend/blend_amount"] = 0
|
|
|
|
# Fade out to black if falling out of the map. out_of_bounds_y_start is lower than
|
|
# the lowest valid position on the map.
|
|
# At out_of_bounds_y_fade units below out_of_bounds_y_start, the screen turns fully black.
|
|
if transform.origin.y < out_of_bounds_y_start:
|
|
color_rect.modulate.a = min((out_of_bounds_y_start - transform.origin.y) / out_of_bounds_y_fade, 1)
|
|
# If we're below (out_of_bounds_y_end - 5), respawn.
|
|
if transform.origin.y < out_of_bounds_y_end - 5:
|
|
color_rect.modulate.a = 0
|
|
respawn()
|
|
#TODO make it a parameter received from the map
|
|
|
|
|
|
func _input(event):
|
|
if event is InputEventMouseMotion:
|
|
var camera_speed_this_frame = CAMERA_MOUSE_ROTATION_SPEED
|
|
if aiming:
|
|
camera_speed_this_frame *= 0.75
|
|
rotate_camera(event.relative * camera_speed_this_frame)
|
|
|
|
if event.is_action_released("shoot"):
|
|
fired_once = false
|
|
|
|
func rotate_camera(move):
|
|
camera_base.rotate_y(-move.x)
|
|
# After relative transforms, camera needs to be renormalized.
|
|
camera_base.orthonormalize()
|
|
camera_x_rot += move.y
|
|
camera_x_rot = clamp(camera_x_rot, deg2rad(CAMERA_X_ROT_MIN), deg2rad(CAMERA_X_ROT_MAX))
|
|
camera_rot.rotation.x = camera_x_rot
|
|
|
|
func add_camera_shake_trauma(amount):
|
|
camera_camera.add_trauma(amount)
|
|
|
|
func switch_weapon(to):
|
|
if to < weapon_contoller.weapons.size() && (to != current_weapon):
|
|
# Check if they're allowed to use it
|
|
if !weapon_contoller.weapons[to]["available"]:
|
|
# TODO Play error noise or something
|
|
print_debug("tried to select unavailable weapon")
|
|
return
|
|
|
|
abort_reload()
|
|
|
|
# Switch
|
|
weapon.get_node(weapon_contoller.weapons[current_weapon]["name"]).hide()
|
|
current_weapon = to
|
|
weapon.get_node(weapon_contoller.weapons[current_weapon]["name"]).show()
|
|
|
|
# Set animations
|
|
animation_tree.set("parameters/hold_weapon/current", weapon_contoller.weapon_index())
|
|
|
|
# Set animation fire rate
|
|
var speed_scale = weapon_contoller.fire_rate()
|
|
$GlobalShootParticle.speed_scale = speed_scale
|
|
|
|
# Refresh UI stats
|
|
weapon_contoller.update_ammo()
|
|
|
|
func reload():
|
|
if (weapon_contoller.mag() != weapon_contoller.mag_size()
|
|
and weapon_contoller.ammo_backup() != 0
|
|
and $ReloadTimer.is_stopped()
|
|
):
|
|
animation_tree.set("parameters/reload_switch/active", true)
|
|
animation_tree.set("parameters/reload_scale/scale", weapon_contoller.reload_speed())
|
|
$ReloadTimer.start(1 / weapon_contoller.reload_speed())
|
|
|
|
func abort_reload():
|
|
$ReloadTimer.stop()
|
|
animation_tree.set("parameters/reload_switch/active", false)
|
|
|
|
func update_health():
|
|
ui_health.text = str(health)
|
|
|
|
func decrement_health(amount):
|
|
health -= amount
|
|
if health <= 0:
|
|
health = 9
|
|
respawn()
|
|
achievement("Achievement: Vendetta", "Alunya is an idea, and ideas cannot be killed.")
|
|
update_health()
|
|
|
|
func hit(col):
|
|
decrement_health(1)
|
|
self.add_camera_shake_trauma(13)
|
|
|
|
func respawn():
|
|
transform.origin = initial_position
|
|
# be nice (only does current weapon)
|
|
if weapon_contoller.ammo_backup() < weapon_contoller.mag_size() / 2:
|
|
weapon_contoller.weapons[current_weapon].ammo_backup = weapon_contoller.mag_size()
|
|
weapon_contoller.update_ammo()
|
|
|
|
func get_main_hitbox():
|
|
return model.get_node("Colette_Armature/Skeleton/ChestBone/Hitbox")
|
|
|
|
func _on_FireCooldown_timeout():
|
|
$GlobalShootParticle.emitting = false
|
|
|
|
func _on_ReloadTimer_timeout():
|
|
weapon_contoller.mag_fill()
|
|
|
|
func achievement(title, desc):
|
|
if !achievements.has(title):
|
|
achievements.append(title)
|
|
var message = preload("res://player/ui/HUDMessage.tscn").instance()
|
|
message.label(title, desc)
|
|
ui.add_child(message)
|
|
|