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)