234 lines
6.6 KiB
GDScript
234 lines
6.6 KiB
GDScript
extends CharacterBody3D
|
|
class_name WalkingDummy
|
|
|
|
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 time_since_last_damage: float = 0.0
|
|
var recent_attackers: Dictionary = {} # attacker_id: timestamp
|
|
var ragdoll_instance: Node3D = null
|
|
var pending_impulse: Vector3 = Vector3.ZERO
|
|
|
|
@export var point_a: Vector3 = Vector3(15, 0, -10)
|
|
@export var point_b: Vector3 = Vector3(15, 0, 10)
|
|
@export var walk_speed: float = 11.0
|
|
|
|
var _target_point: Vector3
|
|
|
|
func _ready() -> void:
|
|
add_to_group("enemies")
|
|
|
|
_target_point = point_b
|
|
|
|
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.1, 0.5) # Purple-pink for walking
|
|
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 _physics_process(delta: float) -> void:
|
|
if is_dead:
|
|
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_label()
|
|
|
|
# Basic gravity
|
|
if not is_on_floor():
|
|
velocity.y -= 9.8 * delta
|
|
|
|
var target_dir = (_target_point - global_position)
|
|
target_dir.y = 0 # Ignore vertical distance
|
|
|
|
if target_dir.length() < 0.5:
|
|
# Reached point, swap
|
|
if _target_point == point_b:
|
|
_target_point = point_a
|
|
else:
|
|
_target_point = point_b
|
|
target_dir = (_target_point - global_position)
|
|
target_dir.y = 0
|
|
|
|
var move_dir = target_dir.normalized()
|
|
velocity.x = move_dir.x * walk_speed
|
|
velocity.z = move_dir.z * walk_speed
|
|
|
|
# Turn to face direction
|
|
if move_dir.length_squared() > 0.1:
|
|
var target_y_rot = atan2(move_dir.x, move_dir.z)
|
|
visual_node.rotation.y = lerp_angle(visual_node.rotation.y, target_y_rot, 10.0 * delta)
|
|
|
|
move_and_slide()
|
|
|
|
if visual_node and visual_node.has_method("update_state"):
|
|
var current_h_speed = Vector2(velocity.x, velocity.z).length()
|
|
visual_node.update_state("ground", current_h_speed)
|
|
|
|
func take_damage(amount: float, hit_position: Vector3, source: Node = null, _impulse: Vector3 = Vector3.ZERO) -> void:
|
|
if is_dead:
|
|
return
|
|
|
|
var attacker_id = 0
|
|
if source:
|
|
if source.has_method("get_multiplayer_authority"):
|
|
attacker_id = source.get_multiplayer_authority()
|
|
elif "owner_player" in source and source.owner_player:
|
|
attacker_id = int(str(source.owner_player.name))
|
|
elif source is CharacterBody3D:
|
|
attacker_id = int(str(source.name))
|
|
|
|
if attacker_id != 0:
|
|
recent_attackers[attacker_id] = Time.get_ticks_msec() / 1000.0
|
|
|
|
time_since_last_damage = 0.0
|
|
|
|
var dmg_taken = amount
|
|
if shield > 0:
|
|
if shield >= amount:
|
|
shield -= amount
|
|
amount = 0
|
|
else:
|
|
amount -= shield
|
|
shield = 0
|
|
|
|
if amount > 0:
|
|
health -= amount
|
|
if health <= 0:
|
|
health = 0
|
|
|
|
_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)
|
|
|
|
var nm = get_node_or_null("/root/NetworkManager")
|
|
if nm and multiplayer.has_multiplayer_peer():
|
|
var killer_id = 0
|
|
var weapon_name = "Killed"
|
|
if source:
|
|
if source.has_method("get_multiplayer_authority"):
|
|
killer_id = source.get_multiplayer_authority()
|
|
elif "owner_player" in source and source.owner_player:
|
|
killer_id = int(str(source.owner_player.name))
|
|
elif source is CharacterBody3D:
|
|
killer_id = int(str(source.name))
|
|
|
|
if source.has_method("get_weapon_name"):
|
|
weapon_name = source.get_weapon_name()
|
|
|
|
var assist_ids: Array = []
|
|
var now = Time.get_ticks_msec() / 1000.0
|
|
for aid in recent_attackers.keys():
|
|
if now - recent_attackers[aid] <= 10.0:
|
|
assist_ids.append(aid)
|
|
|
|
nm.register_kill.rpc(-1, killer_id, weapon_name, "Walking Dummy", assist_ids)
|
|
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
|
|
|
|
# Stop movement immediately
|
|
velocity = Vector3.ZERO
|
|
|
|
# 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.1, 0.5))
|
|
|
|
# Calculate impulse
|
|
var linear_vel = Vector3.ZERO
|
|
var impulse = Vector3.ZERO
|
|
|
|
if pending_impulse.length_squared() > 0.01:
|
|
linear_vel = pending_impulse * 0.25
|
|
impulse = pending_impulse * 0.2
|
|
pending_impulse = Vector3.ZERO
|
|
else:
|
|
var hit_dir = Vector3.BACK
|
|
|
|
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(_apply_ragdoll_velocity.bind(ragdoll_instance, velocity + linear_vel, impulse))
|
|
|
|
get_tree().create_timer(3.0).timeout.connect(_respawn)
|
|
|
|
func _apply_ragdoll_velocity(ragdoll: Node3D, l_vel: Vector3, imp: Vector3) -> void:
|
|
if is_instance_valid(ragdoll):
|
|
ragdoll.apply_initial_velocities(l_vel, imp)
|
|
|
|
func _respawn() -> void:
|
|
if is_instance_valid(ragdoll_instance):
|
|
ragdoll_instance.queue_free()
|
|
|
|
health = max_health
|
|
shield = max_shield
|
|
is_dead = false
|
|
|
|
# Respawn at point A and reset target
|
|
global_position = point_a
|
|
_target_point = point_b
|
|
|
|
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)
|