Base Weapons

Place in world, pick up and drop working on multiplayer
This commit is contained in:
Twirpytherobot
2025-11-15 16:03:38 +00:00
parent 791b1557c7
commit 86db6865b7
144 changed files with 3582 additions and 2 deletions
+16
View File
@@ -0,0 +1,16 @@
[gd_resource type="Resource" script_class="WeaponData" load_steps=3 format=3]
[ext_resource type="Script" path="res://level/scripts/weapon_data.gd" id="1"]
[ext_resource type="PackedScene" path="res://level/scenes/weapons/shield_mesh.tscn" id="2"]
[resource]
script = ExtResource("1")
weapon_name = "Wooden Shield"
description = "A sturdy wooden shield. Can be used to bash enemies."
damage = 8.0
attack_range = 2.5
attack_cooldown = 0.8
attack_animation = "Attack1"
mesh_scene = ExtResource("2")
pickup_radius = 1.5
weight = 3.0
+16
View File
@@ -0,0 +1,16 @@
[gd_resource type="Resource" script_class="WeaponData" load_steps=3 format=3]
[ext_resource type="Script" path="res://level/scripts/weapon_data.gd" id="1"]
[ext_resource type="PackedScene" path="res://level/scenes/weapons/sword_mesh.tscn" id="2"]
[resource]
script = ExtResource("1")
weapon_name = "Iron Sword"
description = "A simple iron sword. Good for close combat."
damage = 15.0
attack_range = 3.5
attack_cooldown = 0.6
attack_animation = "Attack1"
mesh_scene = ExtResource("2")
pickup_radius = 1.5
weight = 2.0
+9
View File
@@ -0,0 +1,9 @@
[gd_scene load_steps=2 format=3 uid="uid://c76hql634gf5"]
[ext_resource type="ArrayMesh" uid="uid://cc1kxfbkvpo2d" path="res://assets/Objects/swordlowpoly.obj" id="1_8p8s3"]
[node name="BaseWeaponScene" type="Node3D"]
[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 4.5366993, -2.9846973, -3.4840996)
mesh = ExtResource("1_8p8s3")
+7 -1
View File
@@ -4780,12 +4780,13 @@ properties/3/path = NodePath("3DGodotRobot:rotation")
properties/3/spawn = true
properties/3/replication_mode = 1
[node name="Player" type="CharacterBody3D" node_paths=PackedStringArray("_body", "_spring_arm_offset")]
[node name="Player" type="CharacterBody3D" node_paths=PackedStringArray("_body", "_spring_arm_offset", "_weapon_attachment")]
transform = Transform3D(-1, 0, -8.74228e-08, 0, 1, 0, 8.74228e-08, 0, -1, 0, 0, 0)
collision_mask = 3
script = ExtResource("1_tdh26")
_body = NodePath("3DGodotRobot")
_spring_arm_offset = NodePath("SpringArmOffset")
_weapon_attachment = NodePath("3DGodotRobot/RobotArmature/Skeleton3D/WeaponPoint")
blue_texture = ExtResource("4_max2b")
yellow_texture = ExtResource("3_l3dv8")
green_texture = ExtResource("4_74ree")
@@ -5006,6 +5007,11 @@ mesh = SubResource("ArrayMesh_0hcp6")
skin = SubResource("Skin_gooby")
surface_material_override/0 = SubResource("StandardMaterial3D_1ln88")
[node name="WeaponPoint" type="BoneAttachment3D" parent="3DGodotRobot/RobotArmature/Skeleton3D"]
transform = Transform3D(-0.95200163, -0.30448738, -0.031308167, 0.30294672, -0.9519023, 0.045883153, -0.043773204, 0.03419612, 0.99845606, -0.64481956, 1.0056509, 0.02542795)
bone_name = "hand.R"
bone_idx = 16
[node name="AnimationPlayer" type="AnimationPlayer" parent="3DGodotRobot"]
libraries = {
&"": SubResource("AnimationLibrary_f1v7x")
+9
View File
@@ -0,0 +1,9 @@
[gd_scene load_steps=2 format=3 uid="uid://rbvk4mg40ceg"]
[ext_resource type="ArrayMesh" uid="uid://rsoymmi6yqhp" path="res://assets/Objects/shield.obj" id="1"]
[node name="ShieldMesh" type="Node3D"]
[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.28306955, -0.70267653)
mesh = ExtResource("1")
+9
View File
@@ -0,0 +1,9 @@
[gd_scene load_steps=2 format=3 uid="uid://dyjfaq654xne3"]
[ext_resource type="ArrayMesh" uid="uid://dmottq5u3my52" path="res://assets/characters/Lobster/10029_Lobster_v1_iterations-2.obj" id="1_olchg"]
[node name="SwordMesh" type="Node3D"]
[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.3645025, -0.7985528, -1.0164871)
mesh = ExtResource("1_olchg")
@@ -0,0 +1,17 @@
[gd_scene load_steps=4 format=3]
[ext_resource type="Script" path="res://level/scripts/world_weapon.gd" id="1"]
[ext_resource type="Resource" path="res://level/resources/weapon_shield.tres" id="2"]
[sub_resource type="BoxShape3D" id="1"]
size = Vector3(0.8, 1.0, 0.2)
[node name="WorldWeaponShield" type="RigidBody3D"]
collision_layer = 4
collision_mask = 2
mass = 3.0
script = ExtResource("1")
weapon_data = ExtResource("2")
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
shape = SubResource("1")
@@ -0,0 +1,17 @@
[gd_scene load_steps=4 format=3]
[ext_resource type="Script" path="res://level/scripts/world_weapon.gd" id="1"]
[ext_resource type="Resource" path="res://level/resources/weapon_sword.tres" id="2"]
[sub_resource type="BoxShape3D" id="1"]
size = Vector3(0.3, 0.3, 1.2)
[node name="WorldWeaponSword" type="RigidBody3D"]
collision_layer = 4
collision_mask = 2
mass = 2.0
script = ExtResource("1")
weapon_data = ExtResource("2")
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
shape = SubResource("1")
+105
View File
@@ -0,0 +1,105 @@
extends Node3D
class_name BaseWeapon
## Base class for equipped weapons
## Attached to player's hand via BoneAttachment3D
## Provides common weapon functionality and stats
signal attack_performed()
@export var weapon_data: WeaponData
# Runtime references
var owner_character: Character = null
var _mesh_instance: Node3D = null
var _attack_timer: float = 0.0
func _ready():
if weapon_data and weapon_data.mesh_scene:
_spawn_mesh()
func _process(delta):
if _attack_timer > 0:
_attack_timer -= delta
## Spawn the visual mesh for this weapon
func _spawn_mesh():
# Remove old mesh if exists
if _mesh_instance:
_mesh_instance.queue_free()
# Instantiate new mesh
_mesh_instance = weapon_data.mesh_scene.instantiate()
add_child(_mesh_instance)
## Perform an attack with this weapon
## Called by the character who owns this weapon
func perform_attack() -> bool:
if not weapon_data or not owner_character:
return false
# Check cooldown
if _attack_timer > 0:
return false
_attack_timer = weapon_data.attack_cooldown
# Play attack animation on owner
if owner_character._body:
owner_character._body.play_attack()
# Find targets in range
_find_and_damage_targets()
attack_performed.emit()
return true
## Find targets in range and apply damage
func _find_and_damage_targets():
if not owner_character:
return
# Check if the owner character has authority (not this node)
if not owner_character.is_multiplayer_authority():
return
var space_state = get_world_3d().direct_space_state
var query = PhysicsShapeQueryParameters3D.new()
var sphere = SphereShape3D.new()
sphere.radius = weapon_data.attack_range
query.shape = sphere
query.transform = global_transform
query.collision_mask = 1 # Player layer
var results = space_state.intersect_shape(query)
for result in results:
var hit_body = result["collider"]
if hit_body != owner_character and hit_body is BaseUnit:
var attacker_id = multiplayer.get_unique_id()
# If we're the server, apply damage directly
if multiplayer.is_server():
owner_character._server_apply_damage(hit_body.name, weapon_data.damage, attacker_id)
else:
# Otherwise, request server to apply damage
owner_character.rpc_id(1, "_server_apply_damage", hit_body.name, weapon_data.damage, attacker_id)
break # Only hit one target per attack
## Check if weapon can attack
func can_attack() -> bool:
return _attack_timer <= 0
## Set the character who owns this weapon
func set_owner_character(character: Character):
owner_character = character
## Get weapon stats
func get_damage() -> float:
return weapon_data.damage if weapon_data else 0.0
func get_range() -> float:
return weapon_data.attack_range if weapon_data else 0.0
func get_cooldown() -> float:
return weapon_data.attack_cooldown if weapon_data else 0.0
+1
View File
@@ -0,0 +1 @@
uid://bknbrlgutvk6h
+71
View File
@@ -4,10 +4,14 @@ extends Node3D
@onready var nick_input: LineEdit = $Menu/MainContainer/MainMenu/Option1/NickInput
@onready var address_input: LineEdit = $Menu/MainContainer/MainMenu/Option3/AddressInput
@onready var players_container: Node3D = $PlayersContainer
@onready var weapons_container: Node3D = null # Will be created if doesn't exist
@onready var menu: Control = $Menu
@onready var main_menu: VBoxContainer = $Menu/MainContainer/MainMenu
@export var player_scene: PackedScene
# Weapon spawning counter (server-side only)
var _weapon_spawn_counter: int = 0
# multiplayer chat
@onready var message: LineEdit = $MultiplayerChat/VBoxContainer/HBoxContainer/Message
@onready var send: Button = $MultiplayerChat/VBoxContainer/HBoxContainer/Send
@@ -24,12 +28,49 @@ func _ready():
# Add quick-fill preset buttons
_create_preset_buttons()
# Create or find weapons container
if has_node("WeaponsContainer"):
weapons_container = get_node("WeaponsContainer")
else:
weapons_container = Node3D.new()
weapons_container.name = "WeaponsContainer"
add_child(weapons_container)
print("Created WeaponsContainer")
if not multiplayer.is_server():
return
Network.connect("player_connected", Callable(self, "_on_player_connected"))
multiplayer.peer_disconnected.connect(_remove_player)
# Spawn initial weapons when server starts
_spawn_initial_weapons()
func _spawn_initial_weapons():
if not multiplayer.is_server():
return
# Wait a frame for everything to be ready
await get_tree().process_frame
# Spawn a sword
_weapon_spawn_counter += 1
rpc("spawn_world_weapon",
"res://level/resources/weapon_sword.tres",
Vector3(5, 1, 0),
Vector3.ZERO,
_weapon_spawn_counter
)
# Spawn a shield
_weapon_spawn_counter += 1
rpc("spawn_world_weapon",
"res://level/resources/weapon_shield.tres",
Vector3(-5, 1, 0),
Vector3.ZERO,
_weapon_spawn_counter
)
func _on_player_connected(peer_id, player_info):
# for id in Network.players.keys():
# var player_data = Network.players[id]
@@ -162,3 +203,33 @@ func _on_jemz_preset():
nick_input.text = "Jemz"
skin_input.text = "Red"
address_input.text = "127.0.0.1"
# ---------- WEAPON SPAWNING ----------
## Spawn a weapon in the world (called from server, syncs to all clients)
@rpc("authority", "call_local", "reliable")
func spawn_world_weapon(weapon_data_path: String, spawn_position: Vector3, initial_velocity: Vector3, weapon_id: int):
if not weapons_container:
push_error("WeaponsContainer not found in level!")
return
# Load the weapon data resource
var weapon_data = load(weapon_data_path) as WeaponData
if not weapon_data:
push_error("Failed to load weapon data from: " + weapon_data_path)
return
# Create WorldWeapon instance
var world_weapon = WorldWeapon.new()
world_weapon.weapon_data = weapon_data
world_weapon.name = "WorldWeapon_" + str(weapon_id) # Deterministic name
world_weapon.position = spawn_position
# Add to weapons container
weapons_container.add_child(world_weapon, true)
print("Spawned weapon: ", world_weapon.name, " at ", spawn_position)
# Apply velocity after physics is ready
if initial_velocity != Vector3.ZERO:
await get_tree().process_frame
world_weapon.linear_velocity = initial_velocity
+192 -1
View File
@@ -13,6 +13,7 @@ enum SkinColor { BLUE, YELLOW, GREEN, RED }
@export_category("Objects")
@export var _body: Node3D = null
@export var _spring_arm_offset: Node3D = null
@export var _weapon_attachment: BoneAttachment3D = null # WeaponPoint bone attachment
@export_category("Skin Colors")
@export var blue_texture : CompressedTexture2D
@@ -28,6 +29,10 @@ enum SkinColor { BLUE, YELLOW, GREEN, RED }
var _current_speed: float
var gravity = ProjectSettings.get_setting("physics/3d/default_gravity")
# Weapon system
var equipped_weapon: BaseWeapon = null
var _nearby_weapons: Array[WorldWeapon] = []
# Attack system
@export var attack_damage: float = 10.0
@export var attack_range: float = 3.0
@@ -67,6 +72,18 @@ func _ready():
if is_multiplayer_authority():
_create_health_ui()
# Setup weapon pickup detection area
_setup_weapon_pickup_area()
# Auto-find weapon attachment if not set
if _weapon_attachment == null:
var bone_attach = get_node_or_null("3DGodotRobot/RobotArmature/Skeleton3D/BoneAttachment3D")
if bone_attach:
_weapon_attachment = bone_attach
print("Auto-found weapon attachment point")
else:
push_warning("Could not find BoneAttachment3D for weapons!")
func _physics_process(delta):
# Check if multiplayer is ready
if multiplayer.multiplayer_peer == null:
@@ -122,9 +139,23 @@ func _process(delta):
_perform_dash()
# Handle attack input
if Input.is_action_just_pressed("attack") and _attack_timer <= 0 and not is_dead and not _is_dashing:
if Input.is_action_just_pressed("attack") and not is_dead and not _is_dashing:
_perform_attack()
# Handle weapon pickup/drop
if Input.is_action_just_pressed("pickup") and not is_dead:
print("Pickup pressed! Equipped: ", equipped_weapon != null, " Nearby: ", _nearby_weapons.size())
if equipped_weapon:
# Tell server to drop (server will handle spawning)
if multiplayer.is_server():
drop_weapon()
else:
rpc_id(1, "drop_weapon") # Only send to server
elif _nearby_weapons.size() > 0:
_pickup_nearest_weapon()
else:
print("No weapons nearby to pick up")
func freeze():
velocity.x = 0
velocity.z = 0
@@ -208,10 +239,19 @@ func _perform_attack():
if not is_multiplayer_authority() or is_dead:
return
# Use equipped weapon if available
if equipped_weapon and equipped_weapon.can_attack():
equipped_weapon.perform_attack()
return
# Fallback to default unarmed attack
# Don't attack if already attacking
if _body and _body.animation_player and _body.animation_player.current_animation == "Attack1":
return
if _attack_timer > 0:
return
_attack_timer = attack_cooldown
# Play attack animation once
@@ -399,3 +439,154 @@ func _perform_dash():
func _reset_dash_rotation(rotation_value: float):
if _body:
_body.rotation.x = rotation_value
## Weapon System
func _setup_weapon_pickup_area():
# Create an Area3D to detect nearby weapons
var pickup_area = Area3D.new()
pickup_area.name = "WeaponPickupArea"
pickup_area.collision_layer = 0
pickup_area.collision_mask = 4 # Will detect WorldWeapons (we'll use layer 3)
add_child(pickup_area)
# Create collision shape for pickup range
var pickup_collision = CollisionShape3D.new()
var sphere = SphereShape3D.new()
sphere.radius = 2.0 # Pickup range
pickup_collision.shape = sphere
pickup_area.add_child(pickup_collision)
# Connect signals to track nearby weapons
pickup_area.area_entered.connect(_on_weapon_area_entered)
pickup_area.area_exited.connect(_on_weapon_area_exited)
func _on_weapon_area_entered(area: Area3D):
# Check if the area belongs to a WorldWeapon
var weapon = area.get_parent()
if weapon is WorldWeapon and weapon not in _nearby_weapons:
_nearby_weapons.append(weapon)
if is_multiplayer_authority():
print("Weapon nearby: ", weapon.weapon_data.weapon_name if weapon.weapon_data else "Unknown")
func _on_weapon_area_exited(area: Area3D):
# Remove weapon from nearby list
var weapon = area.get_parent()
if weapon is WorldWeapon and weapon in _nearby_weapons:
_nearby_weapons.erase(weapon)
if is_multiplayer_authority():
print("Weapon left range: ", weapon.weapon_data.weapon_name if weapon.weapon_data else "Unknown")
## Equip a weapon from WorldWeapon data (receives resource path)
@rpc("any_peer", "call_local", "reliable")
func equip_weapon_from_world(weapon_data_path: String):
var data = load(weapon_data_path) as WeaponData
if data:
equip_weapon(data)
else:
push_error("Failed to load weapon data from: " + weapon_data_path)
## Equip a weapon with given data
func equip_weapon(data: WeaponData):
# Unequip current weapon first
if equipped_weapon:
unequip_weapon()
if not _weapon_attachment:
push_error("No weapon attachment point found!")
return
# Create new weapon instance
var weapon = BaseWeapon.new()
weapon.weapon_data = data
weapon.name = "EquippedWeapon"
weapon.set_owner_character(self)
# Attach to bone
_weapon_attachment.add_child(weapon)
equipped_weapon = weapon
if is_multiplayer_authority():
print("Equipped: ", data.weapon_name)
## Unequip current weapon (local only)
func unequip_weapon():
if equipped_weapon:
equipped_weapon.queue_free()
equipped_weapon = null
## Sync unequip across all clients
@rpc("any_peer", "call_local", "reliable")
func _unequip_weapon_sync():
unequip_weapon()
## Drop currently equipped weapon
@rpc("any_peer", "reliable")
func drop_weapon():
print("drop_weapon called on peer ", multiplayer.get_unique_id(), " is_server: ", multiplayer.is_server(), " has weapon: ", equipped_weapon != null)
if not equipped_weapon:
print("No weapon equipped, cannot drop")
return
# Only server spawns the world weapon
if multiplayer.is_server():
print("Server spawning dropped weapon")
_spawn_world_weapon(equipped_weapon.weapon_data)
# Tell all clients to unequip
rpc("_unequip_weapon_sync")
# Unequip locally
unequip_weapon()
if is_multiplayer_authority():
print("Dropped weapon")
## Spawn a weapon in the world (server only)
func _spawn_world_weapon(data: WeaponData):
if not multiplayer.is_server():
return
# Get the resource path
var resource_path = data.resource_path
if resource_path == "":
push_error("WeaponData has no resource path! Make sure to save it as a .tres file")
return
# Position in front of player
var spawn_pos = global_position + (-transform.basis.z * 2.0)
spawn_pos.y += 1.0 # Spawn at chest height
# Calculate forward velocity
var velocity = -transform.basis.z * 3.0
# Tell level to spawn weapon on all clients
var level = get_tree().get_current_scene()
if level and level.has_method("spawn_world_weapon"):
# Increment the level's weapon counter
level._weapon_spawn_counter += 1
level.rpc("spawn_world_weapon", resource_path, spawn_pos, velocity, level._weapon_spawn_counter)
## Pick up nearest weapon
func _pickup_nearest_weapon():
if _nearby_weapons.size() == 0:
return
# Find closest weapon
var closest_weapon: WorldWeapon = null
var closest_distance: float = INF
for weapon in _nearby_weapons:
if not is_instance_valid(weapon):
continue
var distance = global_position.distance_to(weapon.global_position)
if distance < closest_distance:
closest_distance = distance
closest_weapon = weapon
if closest_weapon:
# Request server to pickup (server validates)
if multiplayer.is_server():
closest_weapon.try_pickup(multiplayer.get_unique_id())
else:
closest_weapon.rpc_id(1, "try_pickup", multiplayer.get_unique_id())
+23
View File
@@ -0,0 +1,23 @@
extends Resource
class_name WeaponData
## Resource that stores weapon statistics and properties
## Can be reused for both equipped weapons and world pickups
@export_category("Weapon Info")
@export var weapon_name: String = "Weapon"
@export_multiline var description: String = ""
@export_category("Combat Stats")
@export var damage: float = 10.0
@export var attack_range: float = 3.0
@export var attack_cooldown: float = 0.5
@export var attack_animation: String = "Attack1" # Animation to play when attacking
@export_category("Visual")
@export var mesh_scene: PackedScene # The 3D mesh for this weapon
@export var icon: Texture2D # Optional icon for UI
@export_category("Physics (for world pickups)")
@export var pickup_radius: float = 1.5 # How close player needs to be to pick up
@export var weight: float = 1.0 # Mass when dropped in world
+1
View File
@@ -0,0 +1 @@
uid://d2homvlmrg6xs
+139
View File
@@ -0,0 +1,139 @@
extends RigidBody3D
class_name WorldWeapon
## Weapon as a physics object in the world
## Can be picked up by players
## Spawned when a weapon is dropped or placed in the level
@export var weapon_data: WeaponData
var _mesh_instance: Node3D = null
var _collision_shape: CollisionShape3D = null
var _pickup_area: Area3D = null
var _is_being_picked_up: bool = false
func _ready():
# Set collision layer to "weapon" (layer 3 = bit 4)
collision_layer = 4
collision_mask = 2 # Collide with world
# Set up physics
if weapon_data:
mass = weapon_data.weight
# Spawn mesh
if weapon_data and weapon_data.mesh_scene:
_spawn_mesh()
# Create collision shape if not exists
_setup_collision()
# Create pickup area
_setup_pickup_area()
# Only server manages pickup logic
if multiplayer.is_server():
_pickup_area.body_entered.connect(_on_body_entered_pickup_area)
func _spawn_mesh():
# Remove old mesh if exists
if _mesh_instance:
_mesh_instance.queue_free()
# Instantiate mesh
_mesh_instance = weapon_data.mesh_scene.instantiate()
add_child(_mesh_instance)
func _setup_collision():
# Check if we already have a collision shape
for child in get_children():
if child is CollisionShape3D:
_collision_shape = child
return
# Create a basic box collision if none exists
_collision_shape = CollisionShape3D.new()
var box_shape = BoxShape3D.new()
box_shape.size = Vector3(0.5, 0.5, 1.5) # Approximate weapon size
_collision_shape.shape = box_shape
add_child(_collision_shape)
func _setup_pickup_area():
# Create Area3D for pickup detection
_pickup_area = Area3D.new()
_pickup_area.name = "PickupArea"
_pickup_area.collision_layer = 4 # Layer 3 (weapon layer) so player can detect it
_pickup_area.collision_mask = 1 # Detect players
add_child(_pickup_area)
# Create sphere collision for pickup range
var pickup_collision = CollisionShape3D.new()
var sphere = SphereShape3D.new()
sphere.radius = weapon_data.pickup_radius if weapon_data else 1.5
pickup_collision.shape = sphere
_pickup_area.add_child(pickup_collision)
func _on_body_entered_pickup_area(body: Node3D):
if _is_being_picked_up:
return
# Check if it's a player trying to pick up
if body is Character and body.is_multiplayer_authority():
# Let the player handle the pickup
# The player will call try_pickup() via RPC
pass
## Called by player to attempt pickup
@rpc("any_peer", "reliable")
func try_pickup(player_id: int):
# Only server validates pickup
if not multiplayer.is_server():
return
if _is_being_picked_up:
return
# Find the player
var level = get_tree().get_current_scene()
if not level or not level.has_node("PlayersContainer"):
return
var players_container = level.get_node("PlayersContainer")
if not players_container.has_node(str(player_id)):
return
var player = players_container.get_node(str(player_id))
if not player is Character:
return
# Check distance
var distance = global_position.distance_to(player.global_position)
if distance > (weapon_data.pickup_radius if weapon_data else 1.5):
return
_is_being_picked_up = true
# Get the resource path
var resource_path = weapon_data.resource_path
if resource_path == "":
push_error("WeaponData has no resource path!")
return
# Tell the player to equip this weapon (on all clients)
player.rpc("equip_weapon_from_world", resource_path)
# Remove this world weapon from all clients
rpc("_remove_from_all_clients")
## Remove weapon from all clients
@rpc("any_peer", "call_local", "reliable")
func _remove_from_all_clients():
queue_free()
## Set weapon data and refresh
func set_weapon_data(data: WeaponData):
weapon_data = data
if is_inside_tree():
_spawn_mesh()
if weapon_data:
mass = weapon_data.weight
+1
View File
@@ -0,0 +1 @@
uid://ccnnd0y4jqiot