A Third-Person-Shooter Godot game.
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

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)