feat: add state machine movement classes, double barrel shotgun weapon, and test level builder utility
This commit is contained in:
@@ -64,8 +64,8 @@ func _build_input_map() -> void:
|
||||
"sprint": [KEY_SHIFT],
|
||||
"crouch": [KEY_CTRL],
|
||||
"dash": [KEY_SHIFT],
|
||||
"fire": [MOUSE_BUTTON_RIGHT, KEY_F],
|
||||
"fire_alt": [MOUSE_BUTTON_LEFT],
|
||||
"fire": [MOUSE_BUTTON_LEFT, KEY_F],
|
||||
"fire_alt": [MOUSE_BUTTON_RIGHT],
|
||||
"reload": [KEY_R],
|
||||
"interact": [KEY_E],
|
||||
"move_forward": [KEY_W],
|
||||
@@ -262,8 +262,9 @@ func _build_lighting() -> void:
|
||||
func _build_player() -> void:
|
||||
var player := CharacterBody3D.new()
|
||||
player.name = "Player"
|
||||
player.position = Vector3(0, 1.0, 8)
|
||||
player.position = Vector3(0, 1.0, 0)
|
||||
add_child(player)
|
||||
_player = player
|
||||
|
||||
# Collision shape
|
||||
var col_shape := CollisionShape3D.new()
|
||||
@@ -316,6 +317,16 @@ func _build_player() -> void:
|
||||
camera.current = true
|
||||
camera.fov = 90.0
|
||||
head_pivot.add_child(camera)
|
||||
|
||||
# ── Temporary Weapon Setup ──────────────────────────────────────────────────
|
||||
# TODO: Change how the player is given weapons in the future to a loadout schema.
|
||||
var shotgun_script = preload("res://weapons/double_barrel_shotgun.gd")
|
||||
if shotgun_script:
|
||||
var shotgun = shotgun_script.new()
|
||||
shotgun.name = "DoubleBarrelShotgun"
|
||||
shotgun.player = player
|
||||
shotgun.camera = camera
|
||||
camera.add_child(shotgun)
|
||||
|
||||
var rig_script = preload("res://characters/player/fps_camera_rig.gd")
|
||||
if rig_script:
|
||||
|
||||
@@ -31,20 +31,23 @@ func update(delta: float) -> void:
|
||||
# ── Air control ───────────────────────────────────────────────────────
|
||||
var wish_dir: Vector3 = machine.wish_dir_world
|
||||
var hvel := Vector3(vel.x, 0.0, vel.z)
|
||||
var current_speed := hvel.length()
|
||||
|
||||
if wish_dir.length_squared() > 0.01:
|
||||
var target_speed := maxf(current_speed, params.walk_speed)
|
||||
target_speed = minf(target_speed, params.max_air_speed)
|
||||
wish_dir = wish_dir.normalized()
|
||||
var current_speed = hvel.length()
|
||||
|
||||
var target_vel := wish_dir * target_speed
|
||||
var new_hvel = hvel.lerp(target_vel, 1.0 - exp(-params.air_control * params.air_acceleration * delta))
|
||||
# Add a strong force in the wish direction
|
||||
var air_accel = params.air_strafe_accel * delta
|
||||
var new_hvel = hvel + wish_dir * air_accel
|
||||
|
||||
# Prevent speed loss during sharp turns, but allow braking if holding backward
|
||||
if hvel.dot(target_vel) >= 0.0 and current_speed > params.walk_speed:
|
||||
if new_hvel.length_squared() > 0.01:
|
||||
new_hvel = new_hvel.normalized() * current_speed
|
||||
|
||||
# Limit the speed so we don't gain speed purely from air control.
|
||||
# We only allow air control to change our direction (strafing)
|
||||
# or to accelerate us up to our walk speed if we started slow.
|
||||
var max_allowed_speed = maxf(current_speed, params.walk_speed)
|
||||
|
||||
if new_hvel.length() > max_allowed_speed:
|
||||
new_hvel = new_hvel.normalized() * max_allowed_speed
|
||||
|
||||
hvel = new_hvel
|
||||
else:
|
||||
# No input: slight air drag (very subtle)
|
||||
@@ -64,7 +67,7 @@ func update(delta: float) -> void:
|
||||
# Bunny hop: if jump was buffered or pressed on landing frame
|
||||
if machine.input_jump_pressed or machine.jump_buffer_time > 0.0:
|
||||
var land_speed := Vector3(player.velocity.x, 0.0, player.velocity.z).length()
|
||||
var bhop_speed := minf(land_speed + params.bunny_hop_speed_gain, params.bunny_hop_speed_cap)
|
||||
var bhop_speed := maxf(land_speed, minf(land_speed + params.bunny_hop_speed_gain, params.bunny_hop_speed_cap))
|
||||
# Maintain horizontal direction, boost speed
|
||||
var hdir := Vector3(player.velocity.x, 0.0, player.velocity.z)
|
||||
if hdir.length_squared() > 0.01:
|
||||
|
||||
@@ -26,6 +26,33 @@ func update(delta: float) -> void:
|
||||
var player := machine.player
|
||||
var vel: Vector3 = player.velocity
|
||||
|
||||
# ── Jump (coyote time + jump buffer) ──────────────────────────────────
|
||||
if machine.input_jump_just_pressed and machine.coyote_timer > 0.0:
|
||||
player.velocity.y = params.jump_velocity
|
||||
machine.current_jump_count = 1
|
||||
machine.on_ground = false
|
||||
machine.coyote_timer = 0.0
|
||||
machine.register_chain_mechanic("jump")
|
||||
machine.switch_to("air")
|
||||
return
|
||||
|
||||
# Jump buffer: player pressed jump just before landing
|
||||
if machine.on_ground and machine.jump_buffer_time > 0.0:
|
||||
player.velocity.y = params.jump_velocity
|
||||
machine.current_jump_count = 1
|
||||
machine.on_ground = false
|
||||
machine.jump_buffer_time = 0.0
|
||||
machine.register_chain_mechanic("jump")
|
||||
machine.switch_to("air")
|
||||
return
|
||||
|
||||
# ── Slide (crouch while moving fast enough) ───────────────────────────
|
||||
if machine.input_crouch and machine.slide_cooldown_timer <= 0.0:
|
||||
var hspeed := Vector3(player.velocity.x, 0.0, player.velocity.z).length()
|
||||
if hspeed > params.slide_min_speed:
|
||||
machine.switch_to("slide")
|
||||
return
|
||||
|
||||
# ── Determine target speed ────────────────────────────────────────────
|
||||
var wish_dir: Vector3 = machine.wish_dir_world
|
||||
var speed := params.walk_speed
|
||||
@@ -66,38 +93,11 @@ func update(delta: float) -> void:
|
||||
else:
|
||||
machine.on_ground = false
|
||||
|
||||
# ── Jump (coyote time + jump buffer) ──────────────────────────────────
|
||||
if machine.input_jump_just_pressed and machine.coyote_timer > 0.0:
|
||||
player.velocity.y = params.jump_velocity
|
||||
machine.current_jump_count = 1
|
||||
machine.on_ground = false
|
||||
machine.coyote_timer = 0.0
|
||||
machine.register_chain_mechanic("jump")
|
||||
machine.switch_to("air")
|
||||
return
|
||||
|
||||
# Jump buffer: player pressed jump just before landing
|
||||
if machine.on_ground and machine.jump_buffer_time > 0.0:
|
||||
player.velocity.y = params.jump_velocity
|
||||
machine.current_jump_count = 1
|
||||
machine.on_ground = false
|
||||
machine.jump_buffer_time = 0.0
|
||||
machine.register_chain_mechanic("jump")
|
||||
machine.switch_to("air")
|
||||
return
|
||||
|
||||
# ── Falling off ledge → air state ─────────────────────────────────────
|
||||
if not machine.on_ground and machine.coyote_timer <= 0.0:
|
||||
machine.switch_to("air")
|
||||
return
|
||||
|
||||
# ── Slide (crouch while moving fast enough) ───────────────────────────
|
||||
if machine.input_crouch and machine.slide_cooldown_timer <= 0.0:
|
||||
var hspeed := Vector3(player.velocity.x, 0.0, player.velocity.z).length()
|
||||
if hspeed > params.slide_min_speed:
|
||||
machine.switch_to("slide")
|
||||
return
|
||||
|
||||
# ── Wall run check ────────────────────────────────────────────────────
|
||||
var wall_n := machine.detect_wall_horizontal()
|
||||
if wall_n != Vector3.ZERO and machine.input_dir.length() > 0.1:
|
||||
|
||||
@@ -93,7 +93,8 @@ func update(delta: float) -> void:
|
||||
var hvel := Vector3(player.velocity.x, 0.0, player.velocity.z)
|
||||
if hvel.length_squared() > 0.01:
|
||||
var dir := hvel.normalized()
|
||||
var jump_speed := minf(hvel.length() + params.slide_jump_speed_boost, params.bunny_hop_speed_cap)
|
||||
var current_speed := hvel.length()
|
||||
var jump_speed := maxf(current_speed, minf(current_speed + params.slide_jump_speed_boost, params.bunny_hop_speed_cap))
|
||||
player.velocity.x = dir.x * jump_speed
|
||||
player.velocity.z = dir.z * jump_speed
|
||||
machine.current_jump_count = 1
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
extends Node3D
|
||||
class_name DoubleBarrelShotgun
|
||||
|
||||
@export var reload_time: float = 1.0
|
||||
|
||||
var shells: int = 2
|
||||
var reloading: bool = false
|
||||
var reload_timer: float = 0.0
|
||||
|
||||
var player: CharacterBody3D
|
||||
var camera: Camera3D
|
||||
|
||||
func _ready() -> void:
|
||||
set_process_input(true)
|
||||
set_process(true)
|
||||
_build_model()
|
||||
|
||||
func _build_model() -> void:
|
||||
# Simple FPS weapon placement (bottom right)
|
||||
position = Vector3(0.3, -0.3, -0.6)
|
||||
|
||||
# Stock
|
||||
var stock = MeshInstance3D.new()
|
||||
var stock_mesh = BoxMesh.new()
|
||||
stock_mesh.size = Vector3(0.1, 0.1, 0.4)
|
||||
var stock_mat = StandardMaterial3D.new()
|
||||
stock_mat.albedo_color = Color(0.4, 0.2, 0.1) # Brown wood
|
||||
stock_mesh.material = stock_mat
|
||||
stock.mesh = stock_mesh
|
||||
stock.position = Vector3(0, 0, 0.2)
|
||||
add_child(stock)
|
||||
|
||||
# Left Barrel
|
||||
var barrel_l = MeshInstance3D.new()
|
||||
var barrel_mesh = CylinderMesh.new()
|
||||
barrel_mesh.top_radius = 0.03
|
||||
barrel_mesh.bottom_radius = 0.03
|
||||
barrel_mesh.height = 0.6
|
||||
var barrel_mat = StandardMaterial3D.new()
|
||||
barrel_mat.albedo_color = Color(0.2, 0.2, 0.2) # Dark grey metal
|
||||
barrel_mat.metallic = 0.8
|
||||
barrel_mat.roughness = 0.4
|
||||
barrel_mesh.material = barrel_mat
|
||||
barrel_l.mesh = barrel_mesh
|
||||
barrel_l.rotation.x = deg_to_rad(90)
|
||||
barrel_l.position = Vector3(-0.04, 0.03, -0.1)
|
||||
add_child(barrel_l)
|
||||
|
||||
# Right Barrel
|
||||
var barrel_r = MeshInstance3D.new()
|
||||
barrel_r.mesh = barrel_mesh
|
||||
barrel_r.rotation.x = deg_to_rad(90)
|
||||
barrel_r.position = Vector3(0.04, 0.03, -0.1)
|
||||
add_child(barrel_r)
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if reloading:
|
||||
reload_timer -= delta
|
||||
|
||||
# Spin vertically (flip) over the duration of the reload
|
||||
var spin_speed = (PI * 2.0) / reload_time
|
||||
rotation.x += spin_speed * delta
|
||||
|
||||
if reload_timer <= 0.0:
|
||||
shells = 2
|
||||
reloading = false
|
||||
rotation.x = 0.0 # Snap back to perfectly level
|
||||
print("Shotgun Reloaded!")
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if Input.get_mouse_mode() != Input.MOUSE_MODE_CAPTURED:
|
||||
return
|
||||
|
||||
if event.is_action_pressed("fire"):
|
||||
_try_fire()
|
||||
elif event.is_action_released("fire"):
|
||||
_try_fire()
|
||||
|
||||
if event.is_action_pressed("reload"):
|
||||
if shells < 2 and not reloading:
|
||||
reloading = true
|
||||
reload_timer = reload_time
|
||||
|
||||
func _try_fire() -> void:
|
||||
if shells > 0 and not reloading:
|
||||
shells -= 1
|
||||
print("BANG! Shells left: ", shells)
|
||||
_apply_impulse()
|
||||
|
||||
# Start reload timer if we just shot the LAST shell
|
||||
if shells == 0:
|
||||
reloading = true
|
||||
reload_timer = reload_time
|
||||
|
||||
elif shells == 0 and not reloading:
|
||||
# Empty click, force reload
|
||||
reloading = true
|
||||
reload_timer = reload_time
|
||||
|
||||
func _apply_impulse() -> void:
|
||||
if not player or not camera:
|
||||
return
|
||||
|
||||
# Get the shotgun impulse parameter from the player's movement controller
|
||||
var impulse_strength = 20.0
|
||||
if player.has_method("get") and "params" in player:
|
||||
if player.params and "shotgun_jump_impulse" in player.params:
|
||||
impulse_strength = player.params.shotgun_jump_impulse
|
||||
|
||||
# Impulse direction is opposite to the camera's forward direction
|
||||
var push_dir = camera.global_transform.basis.z.normalized()
|
||||
|
||||
# Apply impulse to player velocity
|
||||
player.velocity += push_dir * impulse_strength
|
||||
|
||||
# Small hack to ensure we aren't considered on floor if we shoot down
|
||||
if push_dir.y > 0:
|
||||
# Just slightly lift the player to break floor contact immediately
|
||||
player.position.y += 0.05
|
||||
@@ -0,0 +1 @@
|
||||
uid://chgdeglyrlf07
|
||||
Reference in New Issue
Block a user