Compare commits
8
Commits
1e1568f748
...
0706d5ca73
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0706d5ca73 | ||
|
|
ee1114948b | ||
|
|
0b8dcaf3ce | ||
|
|
cb5708e19e | ||
|
|
6aae02bef8 | ||
|
|
acd495893d | ||
|
|
8984739334 | ||
|
|
72346803a9 |
@@ -0,0 +1,230 @@
|
||||
extends Node3D
|
||||
class_name HumanoidModel
|
||||
|
||||
@export var color: Color = Color.WHITE
|
||||
@export var shadows_only: bool = false
|
||||
|
||||
# Internal Pivots
|
||||
var root_pivot: Node3D
|
||||
var torso: MeshInstance3D
|
||||
var head_pivot: Node3D
|
||||
var head: MeshInstance3D
|
||||
var arm_l_pivot: Node3D
|
||||
var arm_l: MeshInstance3D
|
||||
var arm_r_pivot: Node3D
|
||||
var arm_r: MeshInstance3D
|
||||
var leg_l_pivot: Node3D
|
||||
var leg_l: MeshInstance3D
|
||||
var leg_r_pivot: Node3D
|
||||
var leg_r: MeshInstance3D
|
||||
|
||||
# Animation State Variables
|
||||
var current_state: String = "idle"
|
||||
var movement_speed: float = 0.0
|
||||
var _anim_time: float = 0.0
|
||||
|
||||
func _ready() -> void:
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.albedo_color = color
|
||||
mat.roughness = 0.8
|
||||
|
||||
var shadow_setting = GeometryInstance3D.SHADOW_CASTING_SETTING_SHADOWS_ONLY if shadows_only else GeometryInstance3D.SHADOW_CASTING_SETTING_ON
|
||||
|
||||
root_pivot = Node3D.new()
|
||||
add_child(root_pivot)
|
||||
|
||||
# Torso
|
||||
torso = MeshInstance3D.new()
|
||||
var t_mesh = BoxMesh.new()
|
||||
t_mesh.size = Vector3(0.4, 0.7, 0.25)
|
||||
t_mesh.material = mat
|
||||
torso.mesh = t_mesh
|
||||
torso.position = Vector3(0, 1.15, 0)
|
||||
torso.cast_shadow = shadow_setting
|
||||
root_pivot.add_child(torso)
|
||||
|
||||
# Head
|
||||
head_pivot = Node3D.new()
|
||||
head_pivot.position = Vector3(0, 1.5, 0)
|
||||
root_pivot.add_child(head_pivot)
|
||||
|
||||
head = MeshInstance3D.new()
|
||||
var h_mesh = BoxMesh.new()
|
||||
h_mesh.size = Vector3(0.25, 0.25, 0.25)
|
||||
h_mesh.material = mat
|
||||
head.mesh = h_mesh
|
||||
head.position = Vector3(0, 0.15, 0) # Offset from neck
|
||||
head.cast_shadow = shadow_setting
|
||||
head_pivot.add_child(head)
|
||||
|
||||
# Arms
|
||||
var a_mesh = BoxMesh.new()
|
||||
a_mesh.size = Vector3(0.12, 0.65, 0.12)
|
||||
a_mesh.material = mat
|
||||
|
||||
arm_l_pivot = Node3D.new()
|
||||
arm_l_pivot.position = Vector3(-0.28, 1.45, 0) # Shoulder
|
||||
root_pivot.add_child(arm_l_pivot)
|
||||
arm_l = MeshInstance3D.new()
|
||||
arm_l.mesh = a_mesh
|
||||
arm_l.position = Vector3(0, -0.3, 0) # Offset down from shoulder
|
||||
arm_l.cast_shadow = shadow_setting
|
||||
arm_l_pivot.add_child(arm_l)
|
||||
|
||||
arm_r_pivot = Node3D.new()
|
||||
arm_r_pivot.position = Vector3(0.28, 1.45, 0) # Shoulder
|
||||
root_pivot.add_child(arm_r_pivot)
|
||||
arm_r = MeshInstance3D.new()
|
||||
arm_r.mesh = a_mesh
|
||||
arm_r.position = Vector3(0, -0.3, 0) # Offset down from shoulder
|
||||
arm_r.cast_shadow = shadow_setting
|
||||
arm_r_pivot.add_child(arm_r)
|
||||
|
||||
# Legs
|
||||
var l_mesh = BoxMesh.new()
|
||||
l_mesh.size = Vector3(0.15, 0.8, 0.15)
|
||||
l_mesh.material = mat
|
||||
|
||||
leg_l_pivot = Node3D.new()
|
||||
leg_l_pivot.position = Vector3(-0.12, 0.8, 0) # Hip
|
||||
root_pivot.add_child(leg_l_pivot)
|
||||
leg_l = MeshInstance3D.new()
|
||||
leg_l.mesh = l_mesh
|
||||
leg_l.position = Vector3(0, -0.4, 0) # Offset down from hip
|
||||
leg_l.cast_shadow = shadow_setting
|
||||
leg_l_pivot.add_child(leg_l)
|
||||
|
||||
leg_r_pivot = Node3D.new()
|
||||
leg_r_pivot.position = Vector3(0.12, 0.8, 0) # Hip
|
||||
root_pivot.add_child(leg_r_pivot)
|
||||
leg_r = MeshInstance3D.new()
|
||||
leg_r.mesh = l_mesh
|
||||
leg_r.position = Vector3(0, -0.4, 0) # Offset down from hip
|
||||
leg_r.cast_shadow = shadow_setting
|
||||
leg_r_pivot.add_child(leg_r)
|
||||
|
||||
func update_state(state: String, speed: float, is_crouching: bool = false) -> void:
|
||||
current_state = state
|
||||
movement_speed = speed
|
||||
if is_crouching and current_state == "ground":
|
||||
current_state = "crouch"
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
# Advance time. For walk, speed it up based on movement speed.
|
||||
var anim_speed = 1.0
|
||||
if current_state == "ground" and movement_speed > 1.0:
|
||||
anim_speed = movement_speed * 0.4
|
||||
|
||||
_anim_time += delta * anim_speed
|
||||
|
||||
# Target values
|
||||
var t_root_pos := Vector3.ZERO
|
||||
var t_root_rot := Vector3.ZERO
|
||||
var t_arm_l_rot := Vector3.ZERO
|
||||
var t_arm_r_rot := Vector3.ZERO
|
||||
var t_leg_l_rot := Vector3.ZERO
|
||||
var t_leg_r_rot := Vector3.ZERO
|
||||
|
||||
match current_state:
|
||||
"ground", "idle":
|
||||
if movement_speed > 1.0:
|
||||
# Walk / Run
|
||||
var swing = sin(_anim_time * 15.0)
|
||||
t_arm_l_rot.x = swing * 0.8
|
||||
t_arm_r_rot.x = -swing * 0.8
|
||||
t_leg_l_rot.x = -swing * 0.8
|
||||
t_leg_r_rot.x = swing * 0.8
|
||||
# Bounce slightly
|
||||
t_root_pos.y = abs(cos(_anim_time * 15.0)) * 0.1
|
||||
else:
|
||||
# Idle
|
||||
var breath = sin(_anim_time * 2.0)
|
||||
t_root_pos.y = breath * 0.02
|
||||
t_arm_l_rot.z = 0.05 + breath * 0.01
|
||||
t_arm_r_rot.z = -0.05 - breath * 0.01
|
||||
|
||||
"crouch":
|
||||
t_root_pos.y = -0.5
|
||||
# Bend legs to simulate squat
|
||||
t_leg_l_rot.x = -1.0
|
||||
t_leg_r_rot.x = -1.0
|
||||
# Arms tucked
|
||||
t_arm_l_rot.x = 0.5
|
||||
t_arm_r_rot.x = 0.5
|
||||
|
||||
"slide":
|
||||
t_root_pos.y = -0.6
|
||||
t_root_rot.x = deg_to_rad(-60) # Lean back
|
||||
# Legs extended
|
||||
t_leg_l_rot.x = deg_to_rad(-45)
|
||||
t_leg_r_rot.x = deg_to_rad(60)
|
||||
# Arms swept back for balance
|
||||
t_arm_l_rot.x = deg_to_rad(60)
|
||||
t_arm_r_rot.x = deg_to_rad(60)
|
||||
|
||||
"air":
|
||||
# Falling / Jumping
|
||||
t_leg_l_rot.x = -0.2
|
||||
t_leg_r_rot.x = 0.2
|
||||
t_arm_l_rot.x = -0.5
|
||||
t_arm_r_rot.x = -0.5
|
||||
t_arm_l_rot.z = 0.3
|
||||
t_arm_r_rot.z = -0.3
|
||||
|
||||
"wall_run":
|
||||
# Tilt away from wall slightly
|
||||
t_root_rot.z = deg_to_rad(-15)
|
||||
# Fast cycle legs
|
||||
var cycle = sin(_anim_time * 25.0)
|
||||
t_leg_l_rot.x = cycle * 0.6
|
||||
t_leg_r_rot.x = -cycle * 0.6
|
||||
t_arm_l_rot.x = -cycle * 0.6
|
||||
t_arm_r_rot.x = cycle * 0.6
|
||||
|
||||
"wall_cling":
|
||||
# Hanging onto the wall
|
||||
t_leg_l_rot.x = deg_to_rad(-45)
|
||||
t_leg_r_rot.x = deg_to_rad(30)
|
||||
t_arm_l_rot.x = deg_to_rad(-160)
|
||||
t_arm_r_rot.x = deg_to_rad(-160)
|
||||
|
||||
"grapple":
|
||||
# One arm forward toward target, legs dangling
|
||||
t_arm_l_rot.x = deg_to_rad(-120)
|
||||
t_arm_r_rot.x = deg_to_rad(10)
|
||||
t_leg_l_rot.x = deg_to_rad(15)
|
||||
t_leg_r_rot.x = deg_to_rad(-10)
|
||||
|
||||
"dash":
|
||||
# Superman pose
|
||||
t_root_rot.x = deg_to_rad(-70)
|
||||
t_arm_l_rot.x = deg_to_rad(-160)
|
||||
t_arm_r_rot.x = deg_to_rad(-160)
|
||||
t_leg_l_rot.x = deg_to_rad(20)
|
||||
t_leg_r_rot.x = deg_to_rad(20)
|
||||
|
||||
"death":
|
||||
# Fall backwards like a plank
|
||||
t_root_rot.x = deg_to_rad(-90)
|
||||
t_root_pos.y = -0.5 # Shift down slightly so it rests on floor
|
||||
t_arm_l_rot.x = deg_to_rad(170)
|
||||
t_arm_r_rot.x = deg_to_rad(170)
|
||||
t_leg_l_rot.x = deg_to_rad(-10)
|
||||
t_leg_r_rot.x = deg_to_rad(10)
|
||||
|
||||
# Smoothly interpolate towards target values
|
||||
var lerp_speed = 15.0 * delta
|
||||
root_pivot.position = root_pivot.position.lerp(t_root_pos, lerp_speed)
|
||||
root_pivot.rotation = _lerp_vec3(root_pivot.rotation, t_root_rot, lerp_speed)
|
||||
|
||||
arm_l_pivot.rotation = _lerp_vec3(arm_l_pivot.rotation, t_arm_l_rot, lerp_speed)
|
||||
arm_r_pivot.rotation = _lerp_vec3(arm_r_pivot.rotation, t_arm_r_rot, lerp_speed)
|
||||
leg_l_pivot.rotation = _lerp_vec3(leg_l_pivot.rotation, t_leg_l_rot, lerp_speed)
|
||||
leg_r_pivot.rotation = _lerp_vec3(leg_r_pivot.rotation, t_leg_r_rot, lerp_speed)
|
||||
|
||||
func _lerp_vec3(a: Vector3, b: Vector3, t: float) -> Vector3:
|
||||
return Vector3(
|
||||
lerp_angle(a.x, b.x, t),
|
||||
lerp_angle(a.y, b.y, t),
|
||||
lerp_angle(a.z, b.z, t)
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bl23osdva6gy6
|
||||
@@ -0,0 +1,95 @@
|
||||
extends Node3D
|
||||
class_name ProceduralRagdoll
|
||||
|
||||
var torso_body: RigidBody3D
|
||||
|
||||
func build_ragdoll(color: Color) -> void:
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.albedo_color = color
|
||||
mat.roughness = 0.8
|
||||
|
||||
# Create bodies
|
||||
torso_body = _create_body(Vector3(0.4, 0.7, 0.25), mat, Vector3(0, 1.15, 0), 20.0)
|
||||
var head_body = _create_body(Vector3(0.25, 0.25, 0.25), mat, Vector3(0, 1.65, 0), 5.0)
|
||||
var arm_l_body = _create_body(Vector3(0.12, 0.65, 0.12), mat, Vector3(-0.28, 1.15, 0), 5.0)
|
||||
var arm_r_body = _create_body(Vector3(0.12, 0.65, 0.12), mat, Vector3(0.28, 1.15, 0), 5.0)
|
||||
var leg_l_body = _create_body(Vector3(0.15, 0.8, 0.15), mat, Vector3(-0.12, 0.4, 0), 8.0)
|
||||
var leg_r_body = _create_body(Vector3(0.15, 0.8, 0.15), mat, Vector3(0.12, 0.4, 0), 8.0)
|
||||
|
||||
add_child(torso_body)
|
||||
add_child(head_body)
|
||||
add_child(arm_l_body)
|
||||
add_child(arm_r_body)
|
||||
add_child(leg_l_body)
|
||||
add_child(leg_r_body)
|
||||
|
||||
# Need to wait a frame for paths to be valid before creating joints
|
||||
call_deferred("_setup_joints", head_body, arm_l_body, arm_r_body, leg_l_body, leg_r_body)
|
||||
|
||||
func _setup_joints(head_body, arm_l_body, arm_r_body, leg_l_body, leg_r_body) -> void:
|
||||
_create_joint(torso_body, head_body, Vector3(0, 1.5, 0))
|
||||
_create_joint(torso_body, arm_l_body, Vector3(-0.28, 1.45, 0))
|
||||
_create_joint(torso_body, arm_r_body, Vector3(0.28, 1.45, 0))
|
||||
_create_joint(torso_body, leg_l_body, Vector3(-0.12, 0.8, 0))
|
||||
_create_joint(torso_body, leg_r_body, Vector3(0.12, 0.8, 0))
|
||||
|
||||
func apply_initial_velocities(linear_vel: Vector3, impulse: Vector3) -> void:
|
||||
for child in get_children():
|
||||
if child is RigidBody3D:
|
||||
child.linear_velocity = linear_vel
|
||||
# Add random tumbling velocity so they don't fall perfectly still
|
||||
child.angular_velocity = Vector3(randf_range(-5, 5), randf_range(-5, 5), randf_range(-5, 5))
|
||||
|
||||
# Apply the weapon impulse mostly to the torso so it yanks the limbs
|
||||
if is_instance_valid(torso_body):
|
||||
torso_body.apply_central_impulse(impulse * 2.0)
|
||||
|
||||
func _create_body(size: Vector3, mat: Material, pos: Vector3, mass: float) -> RigidBody3D:
|
||||
var body = RigidBody3D.new()
|
||||
body.position = pos
|
||||
body.mass = mass
|
||||
|
||||
# Damping prevents insane spinning and explosion
|
||||
body.linear_damp = 0.5
|
||||
body.angular_damp = 2.0
|
||||
|
||||
var shape = CollisionShape3D.new()
|
||||
var box_shape = BoxShape3D.new()
|
||||
box_shape.size = size
|
||||
shape.shape = box_shape
|
||||
body.add_child(shape)
|
||||
|
||||
var mesh_inst = MeshInstance3D.new()
|
||||
var box_mesh = BoxMesh.new()
|
||||
box_mesh.size = size
|
||||
box_mesh.material = mat
|
||||
mesh_inst.mesh = box_mesh
|
||||
mesh_inst.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_ON
|
||||
body.add_child(mesh_inst)
|
||||
|
||||
return body
|
||||
|
||||
func _create_joint(node_a: RigidBody3D, node_b: RigidBody3D, pos: Vector3) -> void:
|
||||
var joint = Generic6DOFJoint3D.new()
|
||||
add_child(joint)
|
||||
joint.position = pos
|
||||
joint.node_a = node_a.get_path()
|
||||
joint.node_b = node_b.get_path()
|
||||
|
||||
# Free angular axes to allow flopping, lock linear
|
||||
joint.set_flag_x(Generic6DOFJoint3D.FLAG_ENABLE_LINEAR_LIMIT, true)
|
||||
joint.set_flag_y(Generic6DOFJoint3D.FLAG_ENABLE_LINEAR_LIMIT, true)
|
||||
joint.set_flag_z(Generic6DOFJoint3D.FLAG_ENABLE_LINEAR_LIMIT, true)
|
||||
|
||||
# Restrict angular limits so it doesn't bend 360 degrees
|
||||
joint.set_flag_x(Generic6DOFJoint3D.FLAG_ENABLE_ANGULAR_LIMIT, true)
|
||||
joint.set_param_x(Generic6DOFJoint3D.PARAM_ANGULAR_LOWER_LIMIT, -PI * 0.4)
|
||||
joint.set_param_x(Generic6DOFJoint3D.PARAM_ANGULAR_UPPER_LIMIT, PI * 0.4)
|
||||
|
||||
joint.set_flag_y(Generic6DOFJoint3D.FLAG_ENABLE_ANGULAR_LIMIT, true)
|
||||
joint.set_param_y(Generic6DOFJoint3D.PARAM_ANGULAR_LOWER_LIMIT, -PI * 0.2)
|
||||
joint.set_param_y(Generic6DOFJoint3D.PARAM_ANGULAR_UPPER_LIMIT, PI * 0.2)
|
||||
|
||||
joint.set_flag_z(Generic6DOFJoint3D.FLAG_ENABLE_ANGULAR_LIMIT, true)
|
||||
joint.set_param_z(Generic6DOFJoint3D.PARAM_ANGULAR_LOWER_LIMIT, -PI * 0.2)
|
||||
joint.set_param_z(Generic6DOFJoint3D.PARAM_ANGULAR_UPPER_LIMIT, PI * 0.2)
|
||||
@@ -0,0 +1 @@
|
||||
uid://comgwjpn00i7g
|
||||
+13
-38
@@ -222,6 +222,11 @@ func _build_target_dummy() -> void:
|
||||
dummy.position = Vector3(0, 0, -10)
|
||||
add_child(dummy)
|
||||
|
||||
var killable = load("res://entities/killable_dummy.gd").new()
|
||||
killable.name = "KillableDummy"
|
||||
killable.position = Vector3(5, 0, -10)
|
||||
add_child(killable)
|
||||
|
||||
# ── Lighting ──────────────────────────────────────────────────────────────────
|
||||
|
||||
func _build_lighting() -> void:
|
||||
@@ -268,6 +273,14 @@ func _build_player() -> void:
|
||||
if mover_script:
|
||||
player.set_script(mover_script)
|
||||
|
||||
# Humanoid Model for Player (Shadows only so it doesn't clip with camera)
|
||||
var humanoid = load("res://characters/humanoid_model.gd").new()
|
||||
humanoid.name = "HumanoidModel"
|
||||
humanoid.color = Color(0.2, 0.4, 0.8) # Blueish for player
|
||||
humanoid.shadows_only = true
|
||||
humanoid.position = Vector3(0, -0.9, 0) # Offset from center to feet
|
||||
player.add_child(humanoid)
|
||||
|
||||
# Movement State Machine
|
||||
var sm := Node.new()
|
||||
sm.name = "MovementStateMachine"
|
||||
@@ -424,44 +437,6 @@ func _build_hud() -> void:
|
||||
_chain_label.add_theme_color_override("font_color", Color(1.0, 0.8, 0.3))
|
||||
vbox.add_child(_chain_label)
|
||||
|
||||
# ── Help text ─────────────────────────────────────────────────────────
|
||||
var help_panel := PanelContainer.new()
|
||||
help_panel.name = "HelpPanel"
|
||||
help_panel.set_anchors_preset(Control.PRESET_BOTTOM_LEFT)
|
||||
help_panel.offset_left = 12
|
||||
help_panel.offset_top = -90
|
||||
help_panel.offset_right = 620
|
||||
help_panel.offset_bottom = -12
|
||||
var help_style := StyleBoxFlat.new()
|
||||
help_style.bg_color = Color(0, 0, 0, 0.4)
|
||||
help_style.corner_radius_top_left = 6
|
||||
help_style.corner_radius_top_right = 6
|
||||
help_style.corner_radius_bottom_left = 6
|
||||
help_style.corner_radius_bottom_right = 6
|
||||
help_style.content_margin_left = 10
|
||||
help_style.content_margin_top = 6
|
||||
help_style.content_margin_right = 10
|
||||
help_style.content_margin_bottom = 6
|
||||
help_panel.add_theme_stylebox_override("panel", help_style)
|
||||
canvas.add_child(help_panel)
|
||||
|
||||
var help := Label.new()
|
||||
help.name = "HelpLabel"
|
||||
|
||||
var k_f = _get_key_name("move_forward")
|
||||
var k_l = _get_key_name("move_left")
|
||||
var k_b = _get_key_name("move_back")
|
||||
var k_r = _get_key_name("move_right")
|
||||
var k_j = _get_key_name("jump")
|
||||
var k_c = _get_key_name("crouch")
|
||||
var k_d = _get_key_name("dash")
|
||||
var k_fire = _get_key_name("fire")
|
||||
|
||||
var help_text = "%s%s%s%s Move | %s Jump | %s (in air) Double Jump\n%s (moving fast) Slide | %s Dash | %s Fire | ESC Pause Menu" % [k_f, k_l, k_b, k_r, k_j, k_j, k_c, k_d, k_fire]
|
||||
help.text = help_text
|
||||
help.add_theme_font_size_override("font_size", 13)
|
||||
help.add_theme_color_override("font_color", Color(0.8, 0.8, 0.8, 0.7))
|
||||
help_panel.add_child(help)
|
||||
|
||||
# ── Weapon & Ammo Panel ───────────────────────────────────────────────
|
||||
var wp_panel := PanelContainer.new()
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
extends StaticBody3D
|
||||
class_name KillableDummy
|
||||
|
||||
var visual_node: Node3D
|
||||
var col_shape: CollisionShape3D
|
||||
var label: Label3D
|
||||
|
||||
var max_health: float = 100.0
|
||||
var health: float = 100.0
|
||||
var max_shield: float = 100.0
|
||||
var shield: float = 100.0
|
||||
|
||||
var is_dead: bool = false
|
||||
var ragdoll_instance: Node3D = null
|
||||
var pending_impulse: Vector3 = Vector3.ZERO
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("enemies")
|
||||
|
||||
col_shape = CollisionShape3D.new()
|
||||
col_shape.shape = CapsuleShape3D.new()
|
||||
col_shape.shape.radius = 0.5
|
||||
col_shape.shape.height = 2.0
|
||||
col_shape.position = Vector3(0, 1.0, 0)
|
||||
add_child(col_shape)
|
||||
|
||||
visual_node = load("res://characters/humanoid_model.gd").new()
|
||||
visual_node.name = "HumanoidModel"
|
||||
visual_node.color = Color(0.8, 0.5, 0.1) # Orange for killable
|
||||
add_child(visual_node)
|
||||
|
||||
label = Label3D.new()
|
||||
label.pixel_size = 0.01
|
||||
label.billboard = BaseMaterial3D.BILLBOARD_ENABLED
|
||||
label.position = Vector3(0, 2.3, 0)
|
||||
label.modulate = Color(1, 1, 1)
|
||||
add_child(label)
|
||||
|
||||
_update_label()
|
||||
|
||||
func take_damage(amount: float, hit_position: Vector3, source: Node = null) -> void:
|
||||
if is_dead:
|
||||
return
|
||||
|
||||
var dmg_taken = amount
|
||||
if shield > 0:
|
||||
if shield >= amount:
|
||||
shield -= amount
|
||||
amount = 0
|
||||
else:
|
||||
amount -= shield
|
||||
shield = 0
|
||||
|
||||
if amount > 0:
|
||||
health -= amount
|
||||
|
||||
_update_label()
|
||||
|
||||
if source and source.has_method("spawn_damage_number"):
|
||||
source.spawn_damage_number(dmg_taken, hit_position)
|
||||
|
||||
if health <= 0:
|
||||
_die(hit_position, source, dmg_taken)
|
||||
else:
|
||||
_wiggle()
|
||||
|
||||
func _update_label() -> void:
|
||||
label.text = "HP: %.0f / SH: %.0f" % [health, shield]
|
||||
|
||||
func apply_impulse(impulse: Vector3) -> void:
|
||||
pending_impulse += impulse
|
||||
|
||||
func _die(_hit_position: Vector3, source: Node, damage_amount: float) -> void:
|
||||
is_dead = true
|
||||
visual_node.visible = false
|
||||
col_shape.set_deferred("disabled", true)
|
||||
label.visible = false
|
||||
|
||||
# Spawn ragdoll
|
||||
var ragdoll_script = preload("res://characters/procedural_ragdoll.gd")
|
||||
if ragdoll_script:
|
||||
ragdoll_instance = ragdoll_script.new()
|
||||
get_tree().current_scene.add_child(ragdoll_instance)
|
||||
ragdoll_instance.global_position = global_position
|
||||
ragdoll_instance.build_ragdoll(Color(0.8, 0.5, 0.1))
|
||||
|
||||
# Calculate impulse
|
||||
var linear_vel = Vector3.ZERO
|
||||
var impulse = Vector3.ZERO
|
||||
|
||||
if pending_impulse.length_squared() > 0.01:
|
||||
# Scale down the massive direct impulse values to reasonable ragdoll velocities
|
||||
linear_vel = pending_impulse * 0.25
|
||||
impulse = pending_impulse * 0.2
|
||||
pending_impulse = Vector3.ZERO
|
||||
else:
|
||||
var hit_dir = Vector3.BACK
|
||||
|
||||
# Try to get direction from the player's camera
|
||||
if source and source is CharacterBody3D:
|
||||
var cam = source.get_node_or_null("HeadPivot/Camera3D")
|
||||
if cam:
|
||||
hit_dir = (_hit_position - cam.global_position).normalized()
|
||||
|
||||
linear_vel = hit_dir * (damage_amount * 0.02)
|
||||
impulse = hit_dir * (damage_amount * 0.5)
|
||||
|
||||
get_tree().create_timer(0.01).timeout.connect(func():
|
||||
if is_instance_valid(ragdoll_instance):
|
||||
ragdoll_instance.apply_initial_velocities(linear_vel, impulse)
|
||||
)
|
||||
|
||||
get_tree().create_timer(3.0).timeout.connect(func():
|
||||
_respawn()
|
||||
)
|
||||
|
||||
func _respawn() -> void:
|
||||
if is_instance_valid(ragdoll_instance):
|
||||
ragdoll_instance.queue_free()
|
||||
|
||||
health = max_health
|
||||
shield = max_shield
|
||||
is_dead = false
|
||||
|
||||
visual_node.visible = true
|
||||
col_shape.set_deferred("disabled", false)
|
||||
label.visible = true
|
||||
_update_label()
|
||||
|
||||
func _wiggle() -> void:
|
||||
var tween = create_tween()
|
||||
var orig_pos = Vector3.ZERO
|
||||
var wiggle_dir = Vector3(randf_range(-0.1, 0.1), 0, randf_range(-0.1, 0.1))
|
||||
|
||||
tween.tween_property(visual_node, "position", orig_pos + wiggle_dir, 0.05)
|
||||
tween.tween_property(visual_node, "position", orig_pos - wiggle_dir, 0.05)
|
||||
tween.tween_property(visual_node, "position", orig_pos, 0.05)
|
||||
@@ -0,0 +1 @@
|
||||
uid://3d2ftchff0t4
|
||||
+10
-18
@@ -1,7 +1,7 @@
|
||||
extends StaticBody3D
|
||||
class_name TargetDummy
|
||||
|
||||
var mesh_instance: MeshInstance3D
|
||||
var visual_node: Node3D
|
||||
var dps_label: Label3D
|
||||
|
||||
# Array of dictionaries: { "time": float, "amount": float }
|
||||
@@ -18,19 +18,11 @@ func _ready() -> void:
|
||||
shape.position = Vector3(0, 1.0, 0)
|
||||
add_child(shape)
|
||||
|
||||
# Mesh
|
||||
mesh_instance = MeshInstance3D.new()
|
||||
var mesh = CapsuleMesh.new()
|
||||
mesh.radius = 0.5
|
||||
mesh.height = 2.0
|
||||
mesh_instance.mesh = mesh
|
||||
|
||||
var mat = StandardMaterial3D.new()
|
||||
mat.albedo_color = Color(0.8, 0.2, 0.2) # Red dummy
|
||||
mat.roughness = 0.7
|
||||
mesh.material = mat
|
||||
mesh_instance.position = Vector3(0, 1.0, 0)
|
||||
add_child(mesh_instance)
|
||||
# Humanoid Model
|
||||
visual_node = load("res://characters/humanoid_model.gd").new()
|
||||
visual_node.name = "HumanoidModel"
|
||||
visual_node.color = Color(0.8, 0.2, 0.2)
|
||||
add_child(visual_node)
|
||||
|
||||
# DPS Label
|
||||
dps_label = Label3D.new()
|
||||
@@ -75,11 +67,11 @@ func _update_dps() -> void:
|
||||
func _wiggle() -> void:
|
||||
# Small tween to shake the dummy
|
||||
var tween = create_tween()
|
||||
var orig_pos = Vector3(0, 1.0, 0)
|
||||
var orig_pos = Vector3.ZERO
|
||||
var wiggle_dir = Vector3(randf_range(-0.1, 0.1), 0, randf_range(-0.1, 0.1))
|
||||
|
||||
tween.tween_property(mesh_instance, "position", orig_pos + wiggle_dir, 0.05)
|
||||
tween.tween_property(mesh_instance, "position", orig_pos - wiggle_dir, 0.05)
|
||||
tween.tween_property(mesh_instance, "position", orig_pos, 0.05)
|
||||
tween.tween_property(visual_node, "position", orig_pos + wiggle_dir, 0.05)
|
||||
tween.tween_property(visual_node, "position", orig_pos - wiggle_dir, 0.05)
|
||||
tween.tween_property(visual_node, "position", orig_pos, 0.05)
|
||||
|
||||
|
||||
|
||||
@@ -10,6 +10,23 @@ var head_pivot: Node3D = null # FPSCameraRig node
|
||||
var camera: Camera3D = null
|
||||
var _damage_layer: CanvasLayer = null
|
||||
|
||||
# Health and Shield
|
||||
var max_health: float = 100.0
|
||||
var health: float = 100.0
|
||||
var max_shield: float = 100.0
|
||||
var shield: float = 100.0
|
||||
var time_since_last_damage: float = 0.0
|
||||
var is_dead: bool = false
|
||||
|
||||
# UI
|
||||
var health_bar: ProgressBar
|
||||
var shield_bar: ProgressBar
|
||||
var health_label: Label
|
||||
var shield_label: Label
|
||||
var death_screen: Control
|
||||
|
||||
var ragdoll_instance: Node3D
|
||||
|
||||
# Audio
|
||||
var footstep_player: AudioStreamPlayer
|
||||
var dash_player: AudioStreamPlayer
|
||||
@@ -50,6 +67,7 @@ func _ready() -> void:
|
||||
_setup_audio()
|
||||
_setup_hit_marker()
|
||||
_setup_grapple()
|
||||
_setup_hud()
|
||||
|
||||
set_process(true)
|
||||
set_physics_process(true)
|
||||
@@ -188,10 +206,31 @@ func apply_impulse(force: Vector3) -> void:
|
||||
# Tiny upward bump helps move_and_slide detach from the floor
|
||||
global_position.y += 0.1
|
||||
|
||||
func take_damage(amount: float, _hit_pos: Vector3, _source: Node3D = null) -> void:
|
||||
func take_damage(amount: float, _hit_pos: Vector3, _source: Node3D = null, impulse: Vector3 = Vector3.ZERO) -> void:
|
||||
if is_dead:
|
||||
if is_instance_valid(ragdoll_instance):
|
||||
ragdoll_instance.apply_initial_velocities(Vector3.ZERO, impulse)
|
||||
return
|
||||
|
||||
# Show damage number for self-damage
|
||||
spawn_damage_number(amount, _hit_pos)
|
||||
|
||||
time_since_last_damage = 0.0
|
||||
|
||||
if shield > 0.0:
|
||||
if shield >= amount:
|
||||
shield -= amount
|
||||
amount = 0.0
|
||||
else:
|
||||
amount -= shield
|
||||
shield = 0.0
|
||||
|
||||
if amount > 0.0:
|
||||
health -= amount
|
||||
if health <= 0.0:
|
||||
health = 0.0
|
||||
die(impulse)
|
||||
|
||||
func _ensure_machine() -> MovementStateMachine:
|
||||
if is_instance_valid(_machine):
|
||||
return _machine
|
||||
@@ -278,6 +317,12 @@ func _physics_process(_delta: float) -> void:
|
||||
sm.input_dir = raw_input
|
||||
sm.wish_dir_world = world_dir
|
||||
|
||||
# Update humanoid model animation state
|
||||
var humanoid = get_node_or_null("HumanoidModel")
|
||||
if humanoid:
|
||||
var h_speed = Vector2(velocity.x, velocity.z).length()
|
||||
humanoid.update_state(sm.current_state, h_speed, sm.input_crouch)
|
||||
|
||||
|
||||
func _on_movement_event(ev: String, data: Dictionary) -> void:
|
||||
if ev == "chain_updated":
|
||||
@@ -286,3 +331,221 @@ func _on_movement_event(ev: String, data: Dictionary) -> void:
|
||||
grapple_shoot_player.play()
|
||||
elif ev == "grapple_latch":
|
||||
grapple_latch_player.play()
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if is_dead:
|
||||
# Continuously follow the ragdoll torso
|
||||
if is_instance_valid(ragdoll_instance) and is_instance_valid(camera):
|
||||
var torso = ragdoll_instance.torso_body
|
||||
if is_instance_valid(torso) and ragdoll_instance.has_meta("spring_arm"):
|
||||
var spring_arm = ragdoll_instance.get_meta("spring_arm")
|
||||
if is_instance_valid(spring_arm):
|
||||
# Follow torso position exactly to avoid lerp bobbing
|
||||
spring_arm.global_position = torso.global_position
|
||||
return
|
||||
|
||||
# Shield recharge logic
|
||||
time_since_last_damage += delta
|
||||
if time_since_last_damage >= 5.0 and shield < max_shield:
|
||||
shield += 20.0 * delta
|
||||
if shield > max_shield:
|
||||
shield = max_shield
|
||||
|
||||
# Update UI
|
||||
if is_instance_valid(health_bar):
|
||||
health_bar.value = health
|
||||
health_label.text = "%d / %d" % [ceil(health), max_health]
|
||||
if is_instance_valid(shield_bar):
|
||||
shield_bar.value = shield
|
||||
shield_label.text = "%d / %d" % [ceil(shield), max_shield]
|
||||
|
||||
func _setup_hud() -> void:
|
||||
var margin = MarginContainer.new()
|
||||
margin.set_anchors_preset(Control.PRESET_BOTTOM_LEFT)
|
||||
margin.offset_left = 20
|
||||
margin.offset_bottom = -20
|
||||
margin.grow_vertical = Control.GROW_DIRECTION_BEGIN
|
||||
|
||||
var vbox = VBoxContainer.new()
|
||||
margin.add_child(vbox)
|
||||
|
||||
# Shield Bar (Top)
|
||||
var shield_box = VBoxContainer.new()
|
||||
var s_title = Label.new()
|
||||
s_title.text = "SHIELD"
|
||||
s_title.add_theme_font_size_override("font_size", 12)
|
||||
s_title.add_theme_color_override("font_color", Color(0.4, 0.7, 1.0))
|
||||
shield_box.add_child(s_title)
|
||||
|
||||
shield_bar = ProgressBar.new()
|
||||
shield_bar.custom_minimum_size = Vector2(200, 20)
|
||||
shield_bar.max_value = max_shield
|
||||
shield_bar.value = shield
|
||||
shield_bar.show_percentage = false
|
||||
|
||||
var s_sb = StyleBoxFlat.new()
|
||||
s_sb.bg_color = Color(0.1, 0.4, 0.8)
|
||||
shield_bar.add_theme_stylebox_override("fill", s_sb)
|
||||
|
||||
shield_label = Label.new()
|
||||
shield_label.text = "100 / 100"
|
||||
shield_label.set_anchors_preset(Control.PRESET_CENTER)
|
||||
shield_bar.add_child(shield_label)
|
||||
|
||||
shield_box.add_child(shield_bar)
|
||||
vbox.add_child(shield_box)
|
||||
|
||||
# Spacer
|
||||
vbox.add_child(Control.new())
|
||||
|
||||
# Health Bar (Bottom)
|
||||
var health_box = VBoxContainer.new()
|
||||
var h_title = Label.new()
|
||||
h_title.text = "HEALTH"
|
||||
h_title.add_theme_font_size_override("font_size", 12)
|
||||
h_title.add_theme_color_override("font_color", Color(1.0, 0.3, 0.3))
|
||||
health_box.add_child(h_title)
|
||||
|
||||
health_bar = ProgressBar.new()
|
||||
health_bar.custom_minimum_size = Vector2(200, 20)
|
||||
health_bar.max_value = max_health
|
||||
health_bar.value = health
|
||||
health_bar.show_percentage = false
|
||||
|
||||
var h_sb = StyleBoxFlat.new()
|
||||
h_sb.bg_color = Color(0.8, 0.1, 0.1)
|
||||
health_bar.add_theme_stylebox_override("fill", h_sb)
|
||||
|
||||
health_label = Label.new()
|
||||
health_label.text = "100 / 100"
|
||||
health_label.set_anchors_preset(Control.PRESET_CENTER)
|
||||
health_bar.add_child(health_label)
|
||||
|
||||
health_box.add_child(health_bar)
|
||||
vbox.add_child(health_box)
|
||||
|
||||
_damage_layer.add_child(margin)
|
||||
|
||||
# Setup Death Screen
|
||||
death_screen = ColorRect.new()
|
||||
death_screen.color = Color(0, 0, 0, 0.7)
|
||||
death_screen.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
death_screen.visible = false
|
||||
|
||||
var center = CenterContainer.new()
|
||||
center.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
death_screen.add_child(center)
|
||||
|
||||
var d_vbox = VBoxContainer.new()
|
||||
d_vbox.add_theme_constant_override("separation", 20)
|
||||
center.add_child(d_vbox)
|
||||
|
||||
var d_title = Label.new()
|
||||
d_title.text = "SYSTEM FAILURE"
|
||||
d_title.add_theme_font_size_override("font_size", 48)
|
||||
d_title.add_theme_color_override("font_color", Color(1.0, 0.2, 0.2))
|
||||
d_vbox.add_child(d_title)
|
||||
|
||||
var respawn_btn = Button.new()
|
||||
respawn_btn.text = "REBOOT"
|
||||
respawn_btn.add_theme_font_size_override("font_size", 24)
|
||||
respawn_btn.pressed.connect(_on_respawn_pressed)
|
||||
d_vbox.add_child(respawn_btn)
|
||||
|
||||
_damage_layer.add_child(death_screen)
|
||||
|
||||
func die(impulse: Vector3 = Vector3.ZERO) -> void:
|
||||
if is_dead: return
|
||||
is_dead = true
|
||||
|
||||
# Disable movement state machine inputs
|
||||
if _machine:
|
||||
_machine.process_mode = Node.PROCESS_MODE_DISABLED
|
||||
|
||||
# Hide old animated model
|
||||
var humanoid = get_node_or_null("HumanoidModel")
|
||||
if humanoid:
|
||||
humanoid.visible = false
|
||||
|
||||
# Disable collision so player doesn't block bullets
|
||||
var col = get_node_or_null("CollisionShape3D")
|
||||
if col:
|
||||
col.set_deferred("disabled", true)
|
||||
|
||||
# Spawn true physics ragdoll
|
||||
var ragdoll = load("res://characters/procedural_ragdoll.gd").new()
|
||||
ragdoll_instance = ragdoll
|
||||
get_tree().current_scene.add_child(ragdoll)
|
||||
ragdoll.global_transform = global_transform
|
||||
ragdoll.build_ragdoll(Color(0.2, 0.4, 0.8)) # Blueish player color
|
||||
|
||||
# Wait a frame for physics to initialize then apply velocity
|
||||
get_tree().create_timer(0.01).timeout.connect(func():
|
||||
if is_instance_valid(ragdoll):
|
||||
ragdoll.apply_initial_velocities(velocity, impulse)
|
||||
)
|
||||
|
||||
# Move camera to 3rd person view using SpringArm3D
|
||||
if is_instance_valid(camera):
|
||||
# Hide the weapon manager so it isn't floating in front of the 3rd person camera
|
||||
var wman = camera.get_node_or_null("WeaponManager")
|
||||
if wman:
|
||||
wman.visible = false
|
||||
|
||||
var cam_trans = camera.global_transform
|
||||
camera.get_parent().remove_child(camera)
|
||||
|
||||
# Determine fixed offset direction for camera to follow from
|
||||
var back_dir = cam_trans.basis.z.normalized()
|
||||
back_dir.y = 0 # Flatten it
|
||||
if back_dir.length_squared() < 0.1:
|
||||
back_dir = Vector3.BACK
|
||||
else:
|
||||
back_dir = back_dir.normalized()
|
||||
|
||||
var spring_arm = SpringArm3D.new()
|
||||
spring_arm.name = "DeathSpringArm"
|
||||
spring_arm.spring_length = 1.5
|
||||
spring_arm.margin = 0.5
|
||||
|
||||
# Give it collision capability against the world but ignore the ragdoll shapes
|
||||
spring_arm.collision_mask = 1 # Environment mask
|
||||
for child in ragdoll.get_children():
|
||||
if child is CollisionObject3D:
|
||||
spring_arm.add_excluded_object(child.get_rid())
|
||||
|
||||
get_tree().current_scene.add_child(spring_arm)
|
||||
|
||||
# Put the arm at the torso's current position
|
||||
spring_arm.global_position = ragdoll.torso_body.global_position
|
||||
|
||||
# Calculate upper-right position
|
||||
var right_dir = back_dir.cross(Vector3.UP).normalized()
|
||||
if right_dir.length_squared() < 0.1:
|
||||
right_dir = Vector3.RIGHT
|
||||
|
||||
# The vector only defines direction, length is controlled by spring_length
|
||||
var desired_cam_pos = spring_arm.global_position + back_dir * 2.5 + right_dir * 1.5 + Vector3.UP * 1.5
|
||||
|
||||
# SpringArm3D extends along its local +Z axis.
|
||||
# look_at() points the local -Z axis at the target.
|
||||
# To point +Z at desired_cam_pos, we must look_at the OPPOSITE direction.
|
||||
var opposite_dir = spring_arm.global_position + (spring_arm.global_position - desired_cam_pos)
|
||||
spring_arm.look_at(opposite_dir, Vector3.UP)
|
||||
|
||||
spring_arm.add_child(camera)
|
||||
|
||||
# Since +Z points outwards to the camera, -Z points inwards to the torso.
|
||||
# Camera3D natively looks down -Z. Setting rotation to ZERO makes it perfectly look at the torso!
|
||||
camera.position = Vector3.ZERO
|
||||
camera.rotation = Vector3.ZERO
|
||||
|
||||
ragdoll_instance.set_meta("spring_arm", spring_arm)
|
||||
|
||||
# Show death screen
|
||||
death_screen.visible = true
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
|
||||
func _on_respawn_pressed() -> void:
|
||||
# Reload scene
|
||||
get_tree().reload_current_scene()
|
||||
|
||||
+2
-2
@@ -11,8 +11,8 @@ func _init() -> void:
|
||||
fire_rate = 1.5 # Slow
|
||||
max_ammo = 3
|
||||
reload_time = 3.0
|
||||
base_damage = 150.0 # 1 hit kill
|
||||
min_damage = 130.0 # Long falloff still kills
|
||||
base_damage = 200.0 # 1 hit kill
|
||||
min_damage = 130.0 # Long falloff
|
||||
falloff_start = 100.0
|
||||
max_range = 300.0
|
||||
automatic = false
|
||||
|
||||
@@ -193,6 +193,9 @@ func _shoot_hitscan() -> void:
|
||||
var falloff_factor = clampf((hit_dist - falloff_start) / (max_range - falloff_start), 0.0, 1.0)
|
||||
damage = lerpf(base_dmg, min_dmg, falloff_factor)
|
||||
|
||||
if result.collider.has_method("apply_impulse"):
|
||||
result.collider.apply_impulse(pellet_dir * 2.5) # Apply strong physical force for ragdoll
|
||||
|
||||
if result.collider.has_method("take_damage"):
|
||||
result.collider.take_damage(damage, result.position, player)
|
||||
else:
|
||||
|
||||
+3
-3
@@ -6,8 +6,8 @@ func _init() -> void:
|
||||
fire_rate = 1.5 # Slow
|
||||
max_ammo = 1
|
||||
reload_time = 3.0 # Extremely long
|
||||
base_damage = 200.0 # Huge damage
|
||||
min_damage = 20.0
|
||||
base_damage = 230.0 # Huge damage
|
||||
min_damage = 50.0
|
||||
falloff_start = 0.0
|
||||
max_range = 500.0
|
||||
automatic = false
|
||||
@@ -72,7 +72,7 @@ func _spawn_custom_projectile(origin: Vector3, fire_dir: Vector3) -> void:
|
||||
# Mortar Bouncing logic
|
||||
proj.fuse_time = -1.0 # Don't start ticking until it bounces
|
||||
proj.bounce_fuse_delay = 3.0 # Explodes 3 seconds after first bounce
|
||||
proj.impact_detonate_time = 2.0 # If flying > 2s, explode on impact instead
|
||||
proj.impact_detonate_time = 1.0 # If flying > 1s, explode on impact instead
|
||||
proj.bounciness = 0.4 # Heavy, loses momentum on bounce
|
||||
|
||||
# Explosive stats
|
||||
|
||||
@@ -10,7 +10,7 @@ func _init() -> void:
|
||||
fire_rate = 1.0 # 1 click fires all 8
|
||||
max_ammo = 4 # 4 swarms
|
||||
reload_time = 3.0
|
||||
base_damage = 20.0 # Per mini rocket
|
||||
base_damage = 35.0 # Per mini rocket
|
||||
min_damage = 20.0
|
||||
falloff_start = 0.0
|
||||
max_range = 300.0
|
||||
|
||||
Reference in New Issue
Block a user