Compare commits
3
Commits
10416a83a0
...
c9d18ffe23
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9d18ffe23 | ||
|
|
3488b8e005 | ||
|
|
909b25548c |
@@ -167,6 +167,50 @@ func update_state(state: String, speed: float, is_crouching: bool = false) -> vo
|
||||
if is_crouching and current_state == "ground":
|
||||
current_state = "crouch"
|
||||
|
||||
var is_holding_weapon: bool = false
|
||||
|
||||
func set_weapon(script_path: String) -> void:
|
||||
# Clear existing weapons from root_pivot
|
||||
for child in root_pivot.get_children():
|
||||
if child.has_meta("is_third_person_weapon"):
|
||||
child.queue_free()
|
||||
|
||||
if script_path == "":
|
||||
is_holding_weapon = false
|
||||
return
|
||||
|
||||
is_holding_weapon = true
|
||||
|
||||
var script = load(script_path)
|
||||
if not script:
|
||||
return
|
||||
|
||||
var w = script.new()
|
||||
w.name = "ThirdPersonWeapon"
|
||||
w.set_meta("is_third_person_weapon", true)
|
||||
|
||||
# Connect to ready so we can override the weapon's own _ready() calls
|
||||
w.ready.connect(func():
|
||||
w.set_process(false)
|
||||
w.set_process_input(false)
|
||||
if shadows_only:
|
||||
_set_shadows_recursive(w)
|
||||
|
||||
# Force position after the weapon's _build_model() sets it for 1st person
|
||||
# root_pivot is rotated 180 deg around Y, so local +Z is FORWARD and local -X is RIGHT
|
||||
w.position = Vector3(-0.15, 1.0, 0.4)
|
||||
w.rotation_degrees = Vector3(0, 180, 0)
|
||||
)
|
||||
|
||||
# Attach to root_pivot so it stays steady and points perfectly forward
|
||||
root_pivot.add_child(w)
|
||||
|
||||
func _set_shadows_recursive(node: Node) -> void:
|
||||
if node is GeometryInstance3D:
|
||||
node.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_SHADOWS_ONLY
|
||||
for child in node.get_children():
|
||||
_set_shadows_recursive(child)
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
var anim_speed = 1.0
|
||||
if current_state == "ground" and movement_speed > 1.0:
|
||||
@@ -319,6 +363,18 @@ func _process(delta: float) -> void:
|
||||
t_calf_l_rot.x = 0.0
|
||||
t_thigh_r_rot.x = deg_to_rad(10)
|
||||
t_calf_r_rot.x = 0.0
|
||||
|
||||
# Override right arm (and left) if holding a weapon
|
||||
if is_holding_weapon and current_state != "death":
|
||||
# Pose the right arm to look like it's holding the weapon handle
|
||||
t_upper_arm_r_rot.x = -0.6
|
||||
t_upper_arm_r_rot.z = 0.15
|
||||
t_lower_arm_r_rot.x = -1.0
|
||||
|
||||
# Pose the left arm to look like it's holding the foregrip
|
||||
t_upper_arm_l_rot.x = -0.5
|
||||
t_upper_arm_l_rot.z = -0.15
|
||||
t_lower_arm_l_rot.x = -1.1
|
||||
|
||||
var lerp_speed = 40.0 * delta
|
||||
root_pivot.position = root_pivot.position.lerp(t_root_pos, lerp_speed)
|
||||
|
||||
@@ -56,9 +56,9 @@ func _process(delta: float) -> void:
|
||||
_target_fov = params.base_fov
|
||||
var hspeed := Vector2(player.velocity.x, player.velocity.z).length()
|
||||
|
||||
# Dash FOV (highest priority)
|
||||
if hspeed > params.walk_speed * 1.5:
|
||||
_target_fov = params.dash_fov
|
||||
# Smoothly scale FOV based on speed above walking speed
|
||||
var speed_factor = clampf((hspeed - params.walk_speed) / (params.walk_speed * 0.5), 0.0, 1.0)
|
||||
_target_fov = lerpf(params.base_fov, params.dash_fov, speed_factor)
|
||||
|
||||
if _weapon_fov_override > 0.0:
|
||||
_target_fov = _weapon_fov_override
|
||||
|
||||
@@ -12,6 +12,7 @@ var camera: Camera3D
|
||||
var muzzle_flash: OmniLight3D
|
||||
var fire_sound: AudioStreamPlayer3D
|
||||
var reload_sound: AudioStreamPlayer3D
|
||||
var model_root: Node3D
|
||||
|
||||
func _ready() -> void:
|
||||
set_process_input(true)
|
||||
@@ -22,6 +23,9 @@ func _build_model() -> void:
|
||||
# Simple FPS weapon placement (bottom right)
|
||||
position = Vector3(0.3, -0.3, -0.6)
|
||||
|
||||
model_root = Node3D.new()
|
||||
add_child(model_root)
|
||||
|
||||
# Stock
|
||||
var stock = MeshInstance3D.new()
|
||||
var stock_mesh = BoxMesh.new()
|
||||
@@ -31,7 +35,7 @@ func _build_model() -> void:
|
||||
stock_mesh.material = stock_mat
|
||||
stock.mesh = stock_mesh
|
||||
stock.position = Vector3(0, 0, 0.2)
|
||||
add_child(stock)
|
||||
model_root.add_child(stock)
|
||||
|
||||
# Left Barrel
|
||||
var barrel_l = MeshInstance3D.new()
|
||||
@@ -47,14 +51,14 @@ func _build_model() -> void:
|
||||
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)
|
||||
model_root.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)
|
||||
model_root.add_child(barrel_r)
|
||||
|
||||
# Muzzle Flash Light
|
||||
muzzle_flash = OmniLight3D.new()
|
||||
@@ -62,7 +66,7 @@ func _build_model() -> void:
|
||||
muzzle_flash.light_energy = 0.0
|
||||
muzzle_flash.omni_range = 4.0
|
||||
muzzle_flash.position = Vector3(0, 0.03, -0.45)
|
||||
add_child(muzzle_flash)
|
||||
model_root.add_child(muzzle_flash)
|
||||
|
||||
# Audio Players
|
||||
fire_sound = AudioStreamPlayer3D.new()
|
||||
@@ -83,12 +87,12 @@ func _process(delta: float) -> void:
|
||||
|
||||
# Spin vertically (flip) over the duration of the reload
|
||||
var spin_speed = (PI * 2.0) / reload_time
|
||||
rotation.x += spin_speed * delta
|
||||
model_root.rotation.x += spin_speed * delta
|
||||
|
||||
if reload_timer <= 0.0:
|
||||
shells = 2
|
||||
reloading = false
|
||||
rotation.x = 0.0 # Snap back to perfectly level
|
||||
model_root.rotation.x = 0.0 # Snap back to perfectly level
|
||||
print("Shotgun Reloaded!")
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
|
||||
+52
-8
@@ -12,8 +12,7 @@ func _init() -> void:
|
||||
max_range = 100.0
|
||||
automatic = true
|
||||
spread_angle = 0.02
|
||||
|
||||
projectile_speed = 60.0 # Fast but requires lead
|
||||
projectile_speed = 90.0 # Extremely fast, less lead and drop
|
||||
projectile_gravity = 5.0 # Slight drop
|
||||
|
||||
func _build_model() -> void:
|
||||
@@ -106,18 +105,63 @@ func _spawn_custom_projectile(origin: Vector3, fire_dir: Vector3) -> void:
|
||||
mesh_instance.rotation.x = deg_to_rad(-90)
|
||||
proj.add_child(mesh_instance)
|
||||
|
||||
# Tracer streak
|
||||
var tracer = MeshInstance3D.new()
|
||||
var t_mesh = CylinderMesh.new()
|
||||
t_mesh.top_radius = 0.02
|
||||
t_mesh.bottom_radius = 0.0
|
||||
t_mesh.height = 1.5
|
||||
var t_mat = StandardMaterial3D.new()
|
||||
t_mat.albedo_color = Color(1.0, 0.8, 0.1) # Bright yellow/orange
|
||||
t_mat.emission_enabled = true
|
||||
t_mat.emission = Color(1.0, 0.8, 0.1)
|
||||
t_mat.emission_energy_multiplier = 3.0
|
||||
t_mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
t_mat.albedo_color.a = 0.6
|
||||
t_mesh.material = t_mat
|
||||
tracer.mesh = t_mesh
|
||||
tracer.rotation.x = deg_to_rad(-90)
|
||||
tracer.position.z = 0.75 # Trail behind the nail
|
||||
proj.add_child(tracer)
|
||||
|
||||
var spawn_pos = origin
|
||||
var actual_fire_dir = fire_dir
|
||||
|
||||
if muzzle_flash and camera:
|
||||
spawn_pos = muzzle_flash.global_position
|
||||
# Map the position through the viewmodel camera to fix FOV mismatch
|
||||
var wman = get_parent()
|
||||
if wman and "vm_camera" in wman and is_instance_valid(wman.vm_camera):
|
||||
if wman.vm_camera.is_position_behind(muzzle_flash.global_position) == false:
|
||||
var screen_pos = wman.vm_camera.unproject_position(muzzle_flash.global_position)
|
||||
var dist = camera.global_position.distance_to(muzzle_flash.global_position)
|
||||
spawn_pos = camera.project_position(screen_pos, dist)
|
||||
|
||||
var target_pos = origin + fire_dir * max_range
|
||||
var space_state = camera.get_world_3d().direct_space_state
|
||||
var query = PhysicsRayQueryParameters3D.create(origin, target_pos)
|
||||
if player:
|
||||
query.exclude = [player.get_rid()]
|
||||
var result = space_state.intersect_ray(query)
|
||||
if result:
|
||||
target_pos = result.position
|
||||
|
||||
actual_fire_dir = (target_pos - spawn_pos).normalized()
|
||||
|
||||
# Projectile rotation so it faces velocity
|
||||
var up_vec = Vector3.UP
|
||||
if abs(fire_dir.dot(up_vec)) > 0.99:
|
||||
if abs(actual_fire_dir.dot(up_vec)) > 0.99:
|
||||
up_vec = Vector3.RIGHT
|
||||
proj.transform.basis = Basis.looking_at(fire_dir, up_vec)
|
||||
|
||||
proj.position = muzzle_flash.global_position if muzzle_flash else origin
|
||||
proj.transform.basis = Basis.looking_at(actual_fire_dir, up_vec)
|
||||
|
||||
# Push the projectile forward slightly so the 1.5m long tracer tail
|
||||
# doesn't clip backward past the gun and look like it's coming from beside the player's head.
|
||||
proj.position = spawn_pos + actual_fire_dir * 1.5
|
||||
proj.logical_source = origin
|
||||
|
||||
proj.velocity = fire_dir * projectile_speed
|
||||
proj.velocity = actual_fire_dir * projectile_speed
|
||||
if player:
|
||||
proj.velocity += fire_dir * max(0.0, player.velocity.dot(fire_dir))
|
||||
proj.velocity += actual_fire_dir * max(0.0, player.velocity.dot(actual_fire_dir))
|
||||
proj.damage = base_damage
|
||||
proj.min_damage = min_damage
|
||||
proj.falloff_start = falloff_start
|
||||
|
||||
+117
-4
@@ -7,13 +7,59 @@ var camera: Camera3D
|
||||
var active_slot: int = 1 # 1: Primary 1, 2: Primary 2, 3: Special, 4: Melee
|
||||
var weapons: Dictionary = {} # slot_index: Node3D
|
||||
|
||||
var canvas_layer: CanvasLayer
|
||||
var sub_viewport: SubViewport
|
||||
var vm_camera: Camera3D
|
||||
|
||||
func _ready() -> void:
|
||||
set_process_input(true)
|
||||
set_process(true)
|
||||
|
||||
_setup_viewmodel_viewport()
|
||||
|
||||
# Wait one frame for LoadoutManager to be fully ready if needed
|
||||
await get_tree().process_frame
|
||||
_build_loadout()
|
||||
|
||||
func _setup_viewmodel_viewport() -> void:
|
||||
if not camera: return
|
||||
|
||||
# Remove viewmodel layer (20) from main camera
|
||||
camera.cull_mask &= ~(1 << 19)
|
||||
|
||||
canvas_layer = CanvasLayer.new()
|
||||
canvas_layer.layer = 0 # On top of 3D, but below UI
|
||||
add_child(canvas_layer)
|
||||
|
||||
var sv_container = SubViewportContainer.new()
|
||||
sv_container.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
sv_container.stretch = true
|
||||
sv_container.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
canvas_layer.add_child(sv_container)
|
||||
|
||||
sub_viewport = SubViewport.new()
|
||||
sub_viewport.transparent_bg = true
|
||||
# Shared world so it renders main scene's objects that have layer 20
|
||||
# (In Godot 4, own_world_3d is false by default, but let's be explicit)
|
||||
sub_viewport.own_world_3d = false
|
||||
sv_container.add_child(sub_viewport)
|
||||
|
||||
vm_camera = Camera3D.new()
|
||||
vm_camera.cull_mask = 1 << 19 # Only see layer 20
|
||||
sub_viewport.add_child(vm_camera)
|
||||
|
||||
# Add a light specifically for the viewmodel in case main lights don't reach
|
||||
var dir_light = DirectionalLight3D.new()
|
||||
dir_light.layers = 1 << 19
|
||||
dir_light.rotation_degrees = Vector3(-60, 45, 0)
|
||||
dir_light.light_energy = 0.5
|
||||
add_child(dir_light)
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if is_instance_valid(vm_camera) and is_instance_valid(camera):
|
||||
vm_camera.global_transform = camera.global_transform
|
||||
vm_camera.fov = camera.fov
|
||||
|
||||
func _build_loadout() -> void:
|
||||
# Clear existing weapons
|
||||
for w in weapons.values():
|
||||
@@ -51,6 +97,70 @@ func _spawn_weapon(slot: int, weapon_id: String) -> void:
|
||||
weapons[slot] = w
|
||||
w.visible = false
|
||||
w.set_process_input(false)
|
||||
w.set_meta("script_path", script_path)
|
||||
|
||||
_add_procedural_arms(w)
|
||||
|
||||
# Set weapons to viewmodel layer
|
||||
_set_layer_recursive(w, 1 << 19)
|
||||
|
||||
func _set_layer_recursive(node: Node, layer_mask: int) -> void:
|
||||
if node is VisualInstance3D:
|
||||
if node is GeometryInstance3D:
|
||||
node.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
|
||||
if node is Light3D:
|
||||
# Lights (e.g. muzzle flash) illuminate both world and viewmodel
|
||||
node.layers = 1 | layer_mask
|
||||
else:
|
||||
node.layers = layer_mask
|
||||
|
||||
for child in node.get_children():
|
||||
_set_layer_recursive(child, layer_mask)
|
||||
|
||||
func _add_procedural_arms(weapon: Node3D) -> void:
|
||||
# Attach to weapon instead of model_root to avoid arms spinning on reload
|
||||
|
||||
var mat = StandardMaterial3D.new()
|
||||
mat.albedo_color = Color(0.2, 0.4, 0.8) # Player color
|
||||
mat.roughness = 0.8
|
||||
|
||||
# Right Arm (Main Grip)
|
||||
var r_arm = MeshInstance3D.new()
|
||||
var r_mesh = BoxMesh.new()
|
||||
r_mesh.size = Vector3(0.08, 0.7, 0.08)
|
||||
r_mesh.material = mat
|
||||
r_arm.mesh = r_mesh
|
||||
|
||||
var r_pivot = Node3D.new()
|
||||
var r_shoulder = Vector3(0.25, -0.3, 0.5)
|
||||
var r_hand = Vector3(0.04, -0.05, 0.05)
|
||||
r_pivot.position = r_shoulder
|
||||
r_pivot.look_at_from_position(r_shoulder, r_hand, Vector3.UP)
|
||||
|
||||
r_arm.rotation_degrees.x = 90 # Mesh points UP by default, we need it to point forward along -Z
|
||||
r_arm.position.z = -0.35
|
||||
r_pivot.add_child(r_arm)
|
||||
weapon.add_child(r_pivot)
|
||||
|
||||
# Left Arm (Foregrip)
|
||||
if "weapon_name" in weapon and weapon.weapon_name != "Knife":
|
||||
var l_arm = MeshInstance3D.new()
|
||||
var l_mesh = BoxMesh.new()
|
||||
l_mesh.size = Vector3(0.08, 0.7, 0.08)
|
||||
l_mesh.material = mat
|
||||
l_arm.mesh = l_mesh
|
||||
|
||||
var l_pivot = Node3D.new()
|
||||
var l_shoulder = Vector3(-0.25, -0.3, 0.4)
|
||||
var l_hand = Vector3(-0.02, -0.02, -0.3)
|
||||
l_pivot.position = l_shoulder
|
||||
l_pivot.look_at_from_position(l_shoulder, l_hand, Vector3.UP)
|
||||
|
||||
l_arm.rotation_degrees.x = 90
|
||||
l_arm.position.z = -0.35
|
||||
l_pivot.add_child(l_arm)
|
||||
weapon.add_child(l_pivot)
|
||||
|
||||
func _equip_slot(slot: int) -> void:
|
||||
if weapons.has(active_slot):
|
||||
@@ -68,10 +178,13 @@ func _equip_slot(slot: int) -> void:
|
||||
var w = weapons[active_slot]
|
||||
w.visible = true
|
||||
w.set_process_input(true)
|
||||
print("Equipped slot ", slot)
|
||||
else:
|
||||
print("Slot ", slot, " is empty!")
|
||||
|
||||
|
||||
# Sync 3rd person weapon
|
||||
if player and player.has_node("HumanoidModel"):
|
||||
var humanoid = player.get_node("HumanoidModel")
|
||||
if humanoid.has_method("set_weapon") and w.has_meta("script_path"):
|
||||
humanoid.set_weapon(w.get_meta("script_path"))
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if Input.get_mouse_mode() != Input.MOUSE_MODE_CAPTURED:
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user