## TacticalWeaponHitscan — rollback-aware hitscan weapon for the tactical shooter. ## ## Extends NetworkWeaponHitscan3D with per-limb damage, team checks, ## and headshot detection. ## ## This script is ONLY loaded from client scenes where netfox is active. ## The server handles damage authority directly. extends "res://addons/netfox.extras/weapon/network-weapon-hitscan-3d.gd" class_name TacticalWeaponHitscan ## Weapon data resource (stats) — set on spawn var weapon_data: WeaponData = null ## Weapon owner's peer ID — set on spawn var owner_peer_id: int = -1 ## Cooldown tick for rate-of-fire limiting var _last_fire_tick: int = -1 func _can_fire() -> bool: if weapon_data == null: return false if weapon_data.fire_mode == WeaponData.FireMode.MELEE: return true # Melee always "can fire" # Rate-of-fire check using network tick var nt = Engine.get_singleton(&"NetworkTime") if nt == null: return true var current_tick := nt.tick if nt.has_method(&"get_tick") else 0 if _last_fire_tick >= 0: var tick_interval := maxi(1, int(weapon_data.fire_rate / 64.0)) if current_tick - _last_fire_tick < tick_interval: return false return true func _can_peer_use(peer_id: int) -> bool: return peer_id == owner_peer_id func _after_fire(): # Track fire tick for cooldown var nt = Engine.get_singleton(&"NetworkTime") if nt != null: _last_fire_tick = nt.tick if nt.has_method(&"get_tick") else 0 ## Handle a raycast hit — called on ALL peers with rollback reconciliation. func _on_hit(result: Dictionary): if weapon_data == null: return if not multiplayer.is_server() and not is_multiplayer_authority(): return # Only server/authority applies damage var collider := result.get("collider") if collider == null: return # Determine limb hit via collision groups var damage_multiplier: float = weapon_data.chest_multiplier var is_headshot := false if collider is Node: if collider.is_in_group(&"head"): damage_multiplier = weapon_data.headshot_multiplier is_headshot = true elif collider.is_in_group(&"chest"): damage_multiplier = weapon_data.chest_multiplier elif collider.is_in_group(&"waist"): damage_multiplier = weapon_data.waist_multiplier elif collider.is_in_group(&"legs"): damage_multiplier = weapon_data.leg_multiplier var final_damage := int(weapon_data.damage * damage_multiplier) # Find the player node from the collider var player := _find_player(collider) if player != null and player.has_method(&"apply_damage"): if is_headshot: print("[TacticalWeapon] HEADSHOT on %s for %d" % [player.name, final_damage]) player.apply_damage(final_damage, owner_peer_id, weapon_data.weapon_id) ## Find a player node walking up the scene tree. func _find_player(node: Node) -> Node: var current = node while current != null: if current.has_method(&"apply_damage") or current.is_in_group(&"players"): return current current = current.get_parent() return null