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 time_since_last_damage: float = 0.0 var ragdoll_instance: Node3D = null var pending_impulse: Vector3 = Vector3.ZERO var recent_attackers: Dictionary = {} 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 _process(delta: float) -> void: if is_dead: return 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() 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 _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, "Target 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 # 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(_apply_ragdoll_velocity.bind(ragdoll_instance, 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 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)