Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| db477b4c48 | |||
| 4a5264c5b0 | |||
| ad48f38ca5 | |||
| f0c95dcfd2 | |||
| d060ac449d | |||
| 87060b0b71 | |||
| 638e109d8b | |||
| 4c0e334575 | |||
| e270f34150 | |||
| 20ca43f928 | |||
| aae57c63bd | |||
| 0994a55486 | |||
| 82216bfa64 | |||
| d5098c61e1 | |||
| 194aad8f83 | |||
| 222dcaebb3 | |||
| 0aa29d33e4 | |||
| 6a08487c4c | |||
| f20d532add | |||
| 705b068ed2 | |||
| 3465922be4 | |||
| 46ff83325f | |||
| f6d69545c9 | |||
| 2452aba0d7 | |||
| 3c3251fa7a | |||
| d02b112d99 | |||
| 2582cb1b0d | |||
| 9ea98aa7b8 | |||
| e385eae0f5 | |||
| 34507f9043 | |||
| 159c554a86 | |||
| ffa72a8f24 | |||
| 589b90d886 | |||
| e9dc05983c |
@@ -27,3 +27,5 @@ server/build/
|
||||
|
||||
# Secrets (operator must create per deployment)
|
||||
server/data/rcon_password.cfg
|
||||
build/
|
||||
build/
|
||||
|
||||
@@ -5,6 +5,7 @@ A competitive, round-based tactical FPS built in Godot 4.
|
||||
- 128-tick authoritative dedicated servers
|
||||
- GDExtension (C++) simulation core
|
||||
- Community-hostable, self-hosted server browser
|
||||
- Map-making SDK for community maps
|
||||
- [Map-making SDK](docs/mapmaking/00-index.md) for community maps
|
||||
|
||||
See [PLANS/PROJECT_PLAN.md](PLANS/PROJECT_PLAN.md) for the full build plan.
|
||||
See [Mapmaking SDK docs](docs/mapmaking/00-index.md) to build and ship custom maps.
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
[gd_scene load_steps=12 format=3]
|
||||
|
||||
; =============================================================================
|
||||
; test_range.tscn — Greybox Test Map
|
||||
; =============================================================================
|
||||
; A simple open-area test map for validating movement, shooting, and networking.
|
||||
;
|
||||
; Layout:
|
||||
; - 80×80 open area with flat grey floor
|
||||
; - Low platform (center, 1 unit high, gold)
|
||||
; - Medium platform (east, 2 units high, gold)
|
||||
; - High platform (north-west, 3 units high, red)
|
||||
; - Ramp from ground to low platform (yellow)
|
||||
; - Enclosure/room (east) with doorway for indoor testing (blue-grey walls)
|
||||
; - Spawn point (Marker3D) with group 'spawn_points'
|
||||
; - DirectionalLight3D + WorldEnvironment
|
||||
;
|
||||
; Total geometry: ~132 tris (well under 500 budget)
|
||||
; =============================================================================
|
||||
|
||||
[sub_resource type="Environment" id="1"]
|
||||
background_mode = 0
|
||||
tonemap_mode = 0
|
||||
glow_enabled = false
|
||||
ambient_light_color = Color(0.25, 0.25, 0.3, 1.0)
|
||||
ambient_light_energy = 0.4
|
||||
ambient_light_sky_contribution = 0.0
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="2"]
|
||||
albedo_color = Color(0.35, 0.35, 0.35, 1.0)
|
||||
resource_name = "grey_floor"
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="3"]
|
||||
albedo_color = Color(0.8, 0.6, 0.2, 1.0)
|
||||
resource_name = "gold_platform"
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="4"]
|
||||
albedo_color = Color(0.5, 0.5, 0.6, 1.0)
|
||||
resource_name = "bluegrey_wall"
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="5"]
|
||||
albedo_color = Color(0.9, 0.2, 0.2, 1.0)
|
||||
resource_name = "red_high"
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="6"]
|
||||
albedo_color = Color(0.3, 0.4, 0.3, 1.0)
|
||||
resource_name = "green_floor"
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="7"]
|
||||
albedo_color = Color(0.7, 0.7, 0.3, 1.0)
|
||||
resource_name = "yellow_ramp"
|
||||
|
||||
[sub_resource type="BoxShape3D" id="8"]
|
||||
size = Vector3(8, 4, 4)
|
||||
|
||||
[sub_resource type="BoxShape3D" id="9"]
|
||||
size = Vector3(8, 4, 4)
|
||||
|
||||
|
||||
; === Root Node ===
|
||||
|
||||
[node name="TestRange" type="Node3D"]
|
||||
|
||||
|
||||
; === Lighting ===
|
||||
|
||||
[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
|
||||
environment = SubResource("1")
|
||||
|
||||
[node name="SunLight" type="DirectionalLight3D" parent="."]
|
||||
transform = Transform3D(0.866, 0, -0.5, -0.354, 0.707, -0.612, 0.354, 0.707, 0.612, 0, 8, 0)
|
||||
light_energy = 1.5
|
||||
light_indirect_energy = 0.8
|
||||
light_color = Color(1.0, 0.95, 0.9, 1.0)
|
||||
shadow_enabled = true
|
||||
directional_shadow_max_distance = 60.0
|
||||
directional_shadow_split_1 = 0.1
|
||||
directional_shadow_split_2 = 0.3
|
||||
directional_shadow_split_3 = 0.6
|
||||
directional_shadow_blend_splits = true
|
||||
|
||||
|
||||
; === Spawn Points ===
|
||||
|
||||
[node name="SpawnPoint" type="Marker3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -30, 0.5, 0)
|
||||
groups = ["spawn_points"]
|
||||
|
||||
|
||||
; === CT Spawn Points (Counter-Terrorist) ===
|
||||
|
||||
[node name="CTSpawn_01" type="Marker3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -30, 0.5, -10)
|
||||
groups = ["spawn_points_ct"]
|
||||
|
||||
[node name="CTSpawn_02" type="Marker3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -25, 0.5, -15)
|
||||
groups = ["spawn_points_ct"]
|
||||
|
||||
|
||||
; === T Spawn Points (Terrorist) ===
|
||||
|
||||
[node name="TSpawn_01" type="Marker3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 25, 0.5, 25)
|
||||
groups = ["spawn_points_t"]
|
||||
|
||||
[node name="TSpawn_02" type="Marker3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 30, 0.5, 20)
|
||||
groups = ["spawn_points_t"]
|
||||
|
||||
|
||||
; =============================================================================
|
||||
; Bomb Sites
|
||||
; =============================================================================
|
||||
; BombsiteA — south-west area, 8×4m zone
|
||||
; BombsiteB — north-east area, 8×4m zone
|
||||
; Each has a CollisionShape3D (BoxShape3D) for overlap detection
|
||||
; and a Marker3D child named "PlantPosition" marking the plant location.
|
||||
|
||||
; --- Bombsite A ---
|
||||
|
||||
|
||||
[node name="BombsiteA" type="Area3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -20, 1.0, -5)
|
||||
collision_layer = 0
|
||||
collision_mask = 0
|
||||
groups = ["bomb_sites"]
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="BombsiteA"]
|
||||
shape = SubResource("8")
|
||||
|
||||
[node name="PlantPosition" type="Marker3D" parent="BombsiteA"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2, 0, -1)
|
||||
|
||||
; --- Bombsite B ---
|
||||
|
||||
|
||||
[node name="BombsiteB" type="Area3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 15, 1.0, 20)
|
||||
collision_layer = 0
|
||||
collision_mask = 0
|
||||
groups = ["bomb_sites"]
|
||||
|
||||
[node name="PlantPosition" type="Marker3D" parent="BombsiteB"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 2)
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="BombsiteB"]
|
||||
shape = SubResource("9")
|
||||
|
||||
|
||||
; === Open Area Floor (80×80) ===
|
||||
|
||||
[node name="Floor" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.25, 0)
|
||||
size = Vector3(80, 0.5, 80)
|
||||
material = SubResource("2")
|
||||
use_collision = true
|
||||
|
||||
|
||||
; === Low Platform (center, 1 unit high) ===
|
||||
; 8×8 platform, top at y=2, bottom at y=0, centered at origin
|
||||
|
||||
[node name="LowPlatform" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.0, 0)
|
||||
size = Vector3(8, 2, 8)
|
||||
material = SubResource("3")
|
||||
use_collision = true
|
||||
|
||||
|
||||
; === Medium Platform (east, 2 units high) ===
|
||||
; 6×6 platform, top at y=3, bottom at y=1 — jump-up from low platform height
|
||||
|
||||
[node name="MediumPlatform" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 15, 2.0, 10)
|
||||
size = Vector3(6, 2, 6)
|
||||
material = SubResource("3")
|
||||
use_collision = true
|
||||
|
||||
|
||||
; === High Platform (north-west, 3 units high) ===
|
||||
; 3×3 small platform, top at y=4 — requires sprint-jump to reach
|
||||
|
||||
[node name="HighPlatform" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -20, 3.0, -15)
|
||||
size = Vector3(3, 2, 3)
|
||||
material = SubResource("5")
|
||||
use_collision = true
|
||||
|
||||
|
||||
; === Ramp (south of center, leads ground → low platform top) ===
|
||||
; Tilted ~20° around X axis, 6 units long along Z.
|
||||
; South end near ground, north end meets low platform top (y=2).
|
||||
|
||||
[node name="Ramp" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 0.940, -0.342, 0, 0.342, 0.940, 0, 1.0, 5)
|
||||
size = Vector3(6, 0.2, 6)
|
||||
material = SubResource("7")
|
||||
use_collision = true
|
||||
|
||||
|
||||
; =============================================================================
|
||||
; Enclosure — Indoor Testing Room
|
||||
; =============================================================================
|
||||
; 12×12 interior room, 4 units tall, with a 2-unit doorway on the south face.
|
||||
; Centered at (30, 0, 30).
|
||||
|
||||
[node name="Enclosure" type="Node3D" parent="."]
|
||||
|
||||
[node name="RoomFloor" type="CSGBox3D" parent="Enclosure"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 30, 0.25, 30)
|
||||
size = Vector3(12, 0.5, 12)
|
||||
material = SubResource("6")
|
||||
use_collision = true
|
||||
|
||||
; North wall (z = 24)
|
||||
[node name="Wall_N" type="CSGBox3D" parent="Enclosure"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 30, 2.0, 24.25)
|
||||
size = Vector3(12, 4, 0.5)
|
||||
material = SubResource("4")
|
||||
use_collision = true
|
||||
|
||||
; South wall left segment (doorway gap from x=29 to x=31)
|
||||
[node name="Wall_S_Left" type="CSGBox3D" parent="Enclosure"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 26.5, 2.0, 35.75)
|
||||
size = Vector3(5, 4, 0.5)
|
||||
material = SubResource("4")
|
||||
use_collision = true
|
||||
|
||||
; South wall right segment
|
||||
[node name="Wall_S_Right" type="CSGBox3D" parent="Enclosure"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 33.5, 2.0, 35.75)
|
||||
size = Vector3(5, 4, 0.5)
|
||||
material = SubResource("4")
|
||||
use_collision = true
|
||||
|
||||
; West wall (x = 24)
|
||||
[node name="Wall_W" type="CSGBox3D" parent="Enclosure"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 24.25, 2.0, 30)
|
||||
size = Vector3(0.5, 4, 12)
|
||||
material = SubResource("4")
|
||||
use_collision = true
|
||||
|
||||
; East wall (x = 36)
|
||||
[node name="Wall_E" type="CSGBox3D" parent="Enclosure"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 35.75, 2.0, 30)
|
||||
size = Vector3(0.5, 4, 12)
|
||||
material = SubResource("4")
|
||||
use_collision = true
|
||||
@@ -1,7 +1,9 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
[ext_resource type="Material" path="res://assets/materials/accent_team_blue.tres" id="1"]
|
||||
|
||||
[node name="BluePanel01" type="CSGBox3D" parent="."]
|
||||
[node name="BluePanel01" type="CSGBox3D"]
|
||||
size = Vector3(1.28, 0.64, 0.08)
|
||||
material = ExtResource("1")
|
||||
use_collision = true
|
||||
visibility_range_end = 20.0
|
||||
visibility_range_end_margin = 2.0
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
[ext_resource type="Material" path="res://assets/materials/accent_team_red.tres" id="1"]
|
||||
|
||||
[node name="RedPanel01" type="CSGBox3D" parent="."]
|
||||
[node name="RedPanel01" type="CSGBox3D"]
|
||||
size = Vector3(1.28, 0.64, 0.08)
|
||||
material = ExtResource("1")
|
||||
use_collision = true
|
||||
visibility_range_end = 20.0
|
||||
visibility_range_end_margin = 2.0
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
@tool
|
||||
extends Node
|
||||
|
||||
# =============================================================================
|
||||
# art_kit.gd — Modular Art Kit Factory
|
||||
# =============================================================================
|
||||
# Provides factory methods for spawning modular environment pieces and a
|
||||
# preview catalogue for map builders.
|
||||
#
|
||||
# Each piece is a standalone .tscn under pieces/ with:
|
||||
# - CSG shapes (Box3D, Cylinder3D, Polygon3D, Combiner3D)
|
||||
# - Solid-color StandardMaterial3D placeholders (roughness 0.8, metallic 0.0)
|
||||
# - Collision enabled (use_collision = true or StaticBody3D + CollisionShape3D)
|
||||
# - < 200 tris each
|
||||
#
|
||||
# Usage:
|
||||
# var piece = ArtKit.spawn_piece("crate", parent_node)
|
||||
# var cat = ArtKit.preview_catalogue()
|
||||
# var stack = ArtKit.random_crate_layout(3, 2) # returns parent Node3D
|
||||
# =============================================================================
|
||||
|
||||
const PIECES_DIR := "res://client/assets/scenes/modular/pieces/"
|
||||
|
||||
# Catalogue definition: name, description, filename (without .tscn)
|
||||
const CATALOGUE: Array[Dictionary] = [
|
||||
{
|
||||
"name": "floor_tile",
|
||||
"description": "2×2×0.2m concrete slab — grey",
|
||||
"file": "floor_tile"
|
||||
},
|
||||
{
|
||||
"name": "wall_segment",
|
||||
"description": "4×0.2×3m wall panel — blue-grey",
|
||||
"file": "wall_segment"
|
||||
},
|
||||
{
|
||||
"name": "corner_wall",
|
||||
"description": "L-shaped corner piece (2m each leg) — blue-grey",
|
||||
"file": "corner_wall"
|
||||
},
|
||||
{
|
||||
"name": "pillar",
|
||||
"description": "0.3×3×0.3m column — dark grey",
|
||||
"file": "pillar"
|
||||
},
|
||||
{
|
||||
"name": "crate",
|
||||
"description": "1×1×1m storage box — brown/tan",
|
||||
"file": "crate"
|
||||
},
|
||||
{
|
||||
"name": "barrel",
|
||||
"description": "0.6m radius × 1m tall cylinder — red",
|
||||
"file": "barrel"
|
||||
},
|
||||
{
|
||||
"name": "cover_wall",
|
||||
"description": "2×0.2×1.3m low wall (crouch-height cover) — grey",
|
||||
"file": "cover_wall"
|
||||
},
|
||||
{
|
||||
"name": "ramp",
|
||||
"description": "2×2m, 30° incline ramp — yellow",
|
||||
"file": "ramp"
|
||||
},
|
||||
{
|
||||
"name": "stairs",
|
||||
"description": "3-step staircase (2m wide, 0.25m rise per step) — grey",
|
||||
"file": "stairs"
|
||||
},
|
||||
{
|
||||
"name": "window_frame",
|
||||
"description": "4×3m wall with 2×2m opening — blue-grey",
|
||||
"file": "window_frame"
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public API
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Spawns a piece by name, adds it as a child of `parent`, and returns it.
|
||||
## Returns null if the piece name is unknown or the scene fails to load.
|
||||
static func spawn_piece(piece_name: String, parent: Node) -> Node3D:
|
||||
var entry := _find_entry(piece_name)
|
||||
if entry.is_empty():
|
||||
push_error("ArtKit: Unknown piece '%s'. Use preview_catalogue() to see available pieces." % piece_name)
|
||||
return null
|
||||
|
||||
var path := PIECES_DIR + entry.file + ".tscn"
|
||||
var scene := load(path)
|
||||
if scene == null:
|
||||
push_error("ArtKit: Failed to load scene at '%s'." % path)
|
||||
return null
|
||||
|
||||
var instance: Node3D = scene.instantiate()
|
||||
parent.add_child(instance)
|
||||
instance.owner = parent if not Engine.is_editor_hint() else _get_editor_owner(parent)
|
||||
return instance
|
||||
|
||||
|
||||
## Returns an array of { name, description, path } for all available pieces.
|
||||
static func preview_catalogue() -> Array[Dictionary]:
|
||||
var result: Array[Dictionary] = []
|
||||
for entry in CATALOGUE:
|
||||
result.append({
|
||||
"name": entry.name,
|
||||
"description": entry.description,
|
||||
"path": PIECES_DIR + entry.file + ".tscn"
|
||||
})
|
||||
return result
|
||||
|
||||
|
||||
## Generates a random stack of crates for quick testing layouts.
|
||||
## `width` — number of crates along X (default 3)
|
||||
## `height` — number of crates stacked vertically (default 2)
|
||||
## Returns a Node3D containing the crate instances.
|
||||
static func random_crate_layout(width: int = 3, height: int = 2) -> Node3D:
|
||||
var root := Node3D.new()
|
||||
root.name = "CrateLayout_%dx%d" % [width, height]
|
||||
|
||||
var crate_scene := load(PIECES_DIR + "crate.tscn")
|
||||
if crate_scene == null:
|
||||
push_error("ArtKit: Could not load crate scene.")
|
||||
return root
|
||||
|
||||
for y in range(height):
|
||||
for x in range(width):
|
||||
# Add slight random offset for natural look (±0.05m)
|
||||
var offset_x := randf_range(-0.05, 0.05)
|
||||
var offset_z := randf_range(-0.05, 0.05)
|
||||
# Full rotation in 90° increments
|
||||
var rot_y := randi() % 4 * 90.0
|
||||
|
||||
var crate: Node3D = crate_scene.instantiate()
|
||||
crate.position = Vector3(x * 1.0 + offset_x, y * 1.0 + 0.5, offset_z)
|
||||
crate.rotation_degrees = Vector3(0, rot_y, 0)
|
||||
root.add_child(crate)
|
||||
|
||||
return root
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Internal helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
static func _find_entry(piece_name: String) -> Dictionary:
|
||||
for entry in CATALOGUE:
|
||||
if entry.name == piece_name:
|
||||
return entry
|
||||
return {}
|
||||
|
||||
|
||||
## Returns the editor's top-level owner so new nodes are treated as part of the scene.
|
||||
static func _get_editor_owner(node: Node) -> Node:
|
||||
var owner = node
|
||||
while owner and owner.owner:
|
||||
owner = owner.owner
|
||||
return owner
|
||||
@@ -0,0 +1 @@
|
||||
uid://dnopn0wj58j83
|
||||
@@ -1,7 +1,9 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
[ext_resource type="Material" path="res://assets/materials/metal_structural_01.tres" id="1"]
|
||||
|
||||
[node name="Beam01" type="CSGBox3D" parent="."]
|
||||
[node name="Beam01" type="CSGBox3D"]
|
||||
size = Vector3(3.84, 0.32, 0.32)
|
||||
material = ExtResource("1")
|
||||
use_collision = true
|
||||
visibility_range_end = 30.0
|
||||
visibility_range_end_margin = 3.0
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
[ext_resource type="Material" path="res://assets/materials/floor_concrete_01.tres" id="1"]
|
||||
|
||||
[node name="FloorTile01" type="CSGBox3D" parent="."]
|
||||
[node name="FloorTile01" type="CSGBox3D"]
|
||||
size = Vector3(2.56, 0.08, 2.56)
|
||||
material = ExtResource("1")
|
||||
use_collision = true
|
||||
visibility_range_end = 60.0
|
||||
visibility_range_end_margin = 10.0
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
[ext_resource type="Material" path="res://assets/materials/floor_tile_01.tres" id="1"]
|
||||
|
||||
[node name="FloorTileCeramic01" type="CSGBox3D" parent="."]
|
||||
[node name="FloorTileCeramic01" type="CSGBox3D"]
|
||||
size = Vector3(2.56, 0.08, 2.56)
|
||||
material = ExtResource("1")
|
||||
use_collision = true
|
||||
visibility_range_end = 60.0
|
||||
visibility_range_end_margin = 10.0
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://c6dxl1qhan4yl
|
||||
@@ -0,0 +1,16 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="1"]
|
||||
albedo_color = Color(0.7, 0.15, 0.1, 1.0)
|
||||
roughness = 0.6
|
||||
metallic = 0.2
|
||||
resource_name = "red_barrel"
|
||||
|
||||
[node name="Barrel" type="CSGCylinder3D"]
|
||||
radius = 0.6
|
||||
height = 1.0
|
||||
material = SubResource("1")
|
||||
use_collision = true
|
||||
slices = 16
|
||||
visibility_range_end = 30.0
|
||||
visibility_range_end_margin = 3.0
|
||||
@@ -0,0 +1,26 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="1"]
|
||||
albedo_color = Color(0.35, 0.4, 0.5, 1.0)
|
||||
roughness = 0.8
|
||||
metallic = 0.0
|
||||
resource_name = "bluegrey_corner"
|
||||
|
||||
[node name="CornerWall" type="CSGCombiner3D"]
|
||||
use_collision = false
|
||||
visibility_range_end = 50.0
|
||||
visibility_range_end_margin = 5.0
|
||||
|
||||
[node name="WallA" type="CSGBox3D" parent="CornerWall"]
|
||||
operation = 0
|
||||
size = Vector3(2, 3, 0.2)
|
||||
material = SubResource("1")
|
||||
use_collision = true
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0.1)
|
||||
|
||||
[node name="WallB" type="CSGBox3D" parent="CornerWall"]
|
||||
operation = 0
|
||||
size = Vector3(0.2, 3, 2)
|
||||
material = SubResource("1")
|
||||
use_collision = true
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.1, 0, 0)
|
||||
@@ -0,0 +1,14 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="1"]
|
||||
albedo_color = Color(0.4, 0.4, 0.4, 1.0)
|
||||
roughness = 0.8
|
||||
metallic = 0.0
|
||||
resource_name = "grey_cover"
|
||||
|
||||
[node name="CoverWall" type="CSGBox3D"]
|
||||
size = Vector3(2, 1.3, 0.2)
|
||||
material = SubResource("1")
|
||||
use_collision = true
|
||||
visibility_range_end = 40.0
|
||||
visibility_range_end_margin = 5.0
|
||||
@@ -0,0 +1,14 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="1"]
|
||||
albedo_color = Color(0.55, 0.35, 0.15, 1.0)
|
||||
roughness = 0.85
|
||||
metallic = 0.0
|
||||
resource_name = "brown_crate"
|
||||
|
||||
[node name="Crate" type="CSGBox3D"]
|
||||
size = Vector3(1, 1, 1)
|
||||
material = SubResource("1")
|
||||
use_collision = true
|
||||
visibility_range_end = 40.0
|
||||
visibility_range_end_margin = 5.0
|
||||
@@ -0,0 +1,14 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="1"]
|
||||
albedo_color = Color(0.5, 0.5, 0.5, 1.0)
|
||||
roughness = 0.8
|
||||
metallic = 0.0
|
||||
resource_name = "concrete_grey"
|
||||
|
||||
[node name="FloorTile" type="CSGBox3D"]
|
||||
size = Vector3(2, 0.2, 2)
|
||||
material = SubResource("1")
|
||||
use_collision = true
|
||||
visibility_range_end = 60.0
|
||||
visibility_range_end_margin = 10.0
|
||||
@@ -0,0 +1,14 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="1"]
|
||||
albedo_color = Color(0.2, 0.2, 0.22, 1.0)
|
||||
roughness = 0.8
|
||||
metallic = 0.0
|
||||
resource_name = "darkgrey_pillar"
|
||||
|
||||
[node name="Pillar" type="CSGBox3D"]
|
||||
size = Vector3(0.3, 3, 0.3)
|
||||
material = SubResource("1")
|
||||
use_collision = true
|
||||
visibility_range_end = 30.0
|
||||
visibility_range_end_margin = 3.0
|
||||
@@ -0,0 +1,14 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="1"]
|
||||
albedo_color = Color(0.7, 0.7, 0.3, 1.0)
|
||||
roughness = 0.8
|
||||
metallic = 0.0
|
||||
resource_name = "yellow_ramp"
|
||||
|
||||
[node name="Ramp" type="CSGPolygon3D"]
|
||||
polygon = PackedVector2Array(0, 0, 2, 0, 2, 1.155)
|
||||
depth = 2.0
|
||||
material = SubResource("1")
|
||||
use_collision = true
|
||||
smooth_faces = true
|
||||
@@ -0,0 +1,33 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="1"]
|
||||
albedo_color = Color(0.45, 0.45, 0.45, 1.0)
|
||||
roughness = 0.8
|
||||
metallic = 0.0
|
||||
resource_name = "grey_stairs"
|
||||
|
||||
[node name="Stairs" type="CSGCombiner3D"]
|
||||
use_collision = false
|
||||
visibility_range_end = 40.0
|
||||
visibility_range_end_margin = 5.0
|
||||
|
||||
[node name="Step1" type="CSGBox3D" parent="Stairs"]
|
||||
operation = 0
|
||||
size = Vector3(2, 0.25, 0.5)
|
||||
material = SubResource("1")
|
||||
use_collision = true
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.125, 0.75)
|
||||
|
||||
[node name="Step2" type="CSGBox3D" parent="Stairs"]
|
||||
operation = 0
|
||||
size = Vector3(2, 0.25, 0.5)
|
||||
material = SubResource("1")
|
||||
use_collision = true
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.375, 0.25)
|
||||
|
||||
[node name="Step3" type="CSGBox3D" parent="Stairs"]
|
||||
operation = 0
|
||||
size = Vector3(2, 0.25, 0.5)
|
||||
material = SubResource("1")
|
||||
use_collision = true
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.625, -0.25)
|
||||
@@ -0,0 +1,14 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="1"]
|
||||
albedo_color = Color(0.35, 0.4, 0.5, 1.0)
|
||||
roughness = 0.8
|
||||
metallic = 0.0
|
||||
resource_name = "bluegrey_wall"
|
||||
|
||||
[node name="WallSegment" type="CSGBox3D"]
|
||||
size = Vector3(4, 3, 0.2)
|
||||
material = SubResource("1")
|
||||
use_collision = true
|
||||
visibility_range_end = 50.0
|
||||
visibility_range_end_margin = 5.0
|
||||
@@ -0,0 +1,24 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="1"]
|
||||
albedo_color = Color(0.35, 0.4, 0.5, 1.0)
|
||||
roughness = 0.8
|
||||
metallic = 0.0
|
||||
resource_name = "bluegrey_window"
|
||||
|
||||
[node name="WindowFrame" type="CSGCombiner3D"]
|
||||
use_collision = false
|
||||
visibility_range_end = 40.0
|
||||
visibility_range_end_margin = 5.0
|
||||
|
||||
[node name="MainWall" type="CSGBox3D" parent="WindowFrame"]
|
||||
operation = 0
|
||||
size = Vector3(4, 3, 0.2)
|
||||
material = SubResource("1")
|
||||
use_collision = true
|
||||
|
||||
[node name="WindowCutout" type="CSGBox3D" parent="WindowFrame"]
|
||||
operation = 1
|
||||
size = Vector3(2, 2, 0.25)
|
||||
use_collision = true
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
|
||||
@@ -1,7 +1,9 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
[ext_resource type="Material" path="res://assets/materials/wall_concrete_02.tres" id="1"]
|
||||
|
||||
[node name="Pillar01" type="CSGBox3D" parent="."]
|
||||
[node name="Pillar01" type="CSGBox3D"]
|
||||
size = Vector3(0.64, 2.56, 0.64)
|
||||
material = ExtResource("1")
|
||||
use_collision = true
|
||||
visibility_range_end = 30.0
|
||||
visibility_range_end_margin = 3.0
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
[ext_resource type="Material" path="res://assets/materials/wall_concrete_01.tres" id="1"]
|
||||
|
||||
[node name="WallCorner01" type="CSGCombiner3D" parent="."]
|
||||
[node name="WallCorner01" type="CSGCombiner3D"]
|
||||
use_collision = false
|
||||
visibility_range_end = 50.0
|
||||
visibility_range_end_margin = 5.0
|
||||
[node name="WallA" type="CSGBox3D" parent="WallCorner01"]
|
||||
operation = 0
|
||||
size = Vector3(2.56, 2.56, 0.16)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
[ext_resource type="Material" path="res://assets/materials/wall_concrete_01.tres" id="1"]
|
||||
|
||||
[node name="WallDoorway01" type="CSGCombiner3D" parent="."]
|
||||
[node name="WallDoorway01" type="CSGCombiner3D"]
|
||||
use_collision = false
|
||||
visibility_range_end = 40.0
|
||||
visibility_range_end_margin = 5.0
|
||||
[node name="MainWall" type="CSGBox3D" parent="WallDoorway01"]
|
||||
operation = 0
|
||||
size = Vector3(2.56, 2.56, 0.16)
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
[ext_resource type="Material" path="res://assets/materials/wall_concrete_01.tres" id="1"]
|
||||
|
||||
[node name="WallEndcap01" type="CSGBox3D" parent="."]
|
||||
[node name="WallEndcap01" type="CSGBox3D"]
|
||||
size = Vector3(0.32, 2.56, 0.16)
|
||||
material = ExtResource("1")
|
||||
use_collision = true
|
||||
visibility_range_end = 50.0
|
||||
visibility_range_end_margin = 5.0
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
[ext_resource type="Material" path="res://assets/materials/wall_concrete_01.tres" id="1"]
|
||||
|
||||
[node name="WallStraight01" type="CSGBox3D" parent="."]
|
||||
[node name="WallStraight01" type="CSGBox3D"]
|
||||
size = Vector3(2.56, 2.56, 0.16)
|
||||
material = ExtResource("1")
|
||||
use_collision = true
|
||||
visibility_range_end = 50.0
|
||||
visibility_range_end_margin = 5.0
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
[ext_resource type="Material" path="res://assets/materials/wall_concrete_01.tres" id="1"]
|
||||
|
||||
[node name="WallWindow01" type="CSGCombiner3D" parent="."]
|
||||
[node name="WallWindow01" type="CSGCombiner3D"]
|
||||
use_collision = false
|
||||
visibility_range_end = 40.0
|
||||
visibility_range_end_margin = 5.0
|
||||
[node name="MainWall" type="CSGBox3D" parent="WallWindow01"]
|
||||
operation = 0
|
||||
size = Vector3(2.56, 2.56, 0.16)
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
# FPS Character Controller — Tactical Shooter
|
||||
|
||||
First-person character controller for the tactical shooter GDExtension simulation core.
|
||||
|
||||
## Architecture
|
||||
|
||||
The character controller is a **GDScript bridge** between Godot's input/physics system and the C++ `SimulationServer` (built in task `t_aca0f251`).
|
||||
|
||||
```
|
||||
Godot Input
|
||||
│
|
||||
├─► FPSCharacterController._physics_process()
|
||||
│ ├─ capture input (WASD, mouse, jump, sprint, crouch, shoot)
|
||||
│ ├─ build EntityInput Dictionary
|
||||
│ ├─ SimulationServer.apply_input(entity_id, input_dict)
|
||||
│ ├─ SimulationServer.fire_weapon(entity_id) [if shoot]
|
||||
│ ├─ read entity position from server → apply to CharacterBody3D
|
||||
│ └─ apply mouse look (yaw on body, pitch on camera)
|
||||
│
|
||||
├─► FpsCamera._process()
|
||||
│ ├─ view bobbing (head bob from movement)
|
||||
│ ├─ weapon sway
|
||||
│ └─ sprint FOV kick
|
||||
│
|
||||
└─► SimulationServer (C++ GDExtension)
|
||||
├─ process_tick() @ 128Hz
|
||||
├─ MovementComponent.update()
|
||||
├─ HitDetection.process_shot()
|
||||
└─→ returns serialized snapshot
|
||||
```
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `character/fps_character_controller.gd` | Main controller (CharacterBody3D). Input capture, simulation bridge, walk toggle, crouch, jump. |
|
||||
| `character/fps_camera.gd` | First-person camera (Camera3D). Mouse look pitch, view bobbing, weapon sway, FOV kick. |
|
||||
| `input/input_handler.gd` | Documentation of expected input map bindings. |
|
||||
|
||||
## Scene Setup
|
||||
|
||||
1. **Create a CharacterBody3D scene:**
|
||||
```
|
||||
FPSCharacterController (CharacterBody3D) — script: fps_character_controller.gd
|
||||
├── FpsCamera (Camera3D) — script: fps_camera.gd
|
||||
├── CollisionShape3D (CapsuleShape3D) — radius: 0.4, height: 1.8
|
||||
└── Weapon (Node3D, optional) — child of camera, rest transform tracked
|
||||
```
|
||||
|
||||
2. **Configure exports** on the root node:
|
||||
- `mouse_sensitivity`: 0.1–0.2 for typical tactical feel
|
||||
- `walk_toggle`: `true` = tap Shift to toggle sprint (CS-style)
|
||||
- `crouch_toggle`: `false` = hold Ctrl to crouch (tactical-style)
|
||||
- `eye_height_stand` / `eye_height_crouch`: match your capsule height
|
||||
|
||||
3. **Set up Input Map** in Project Settings:
|
||||
- `move_forward` → W
|
||||
- `move_backward` → S
|
||||
- `move_left` → A
|
||||
- `move_right` → D
|
||||
- `jump` → Space
|
||||
- `sprint` → Shift
|
||||
- `crouch` → Ctrl
|
||||
- `shoot` → Mouse LMB
|
||||
- `aim` → Mouse RMB
|
||||
- `ui_cancel` → Esc (built-in, releases mouse capture)
|
||||
|
||||
4. **Integration with SimulationServer:**
|
||||
|
||||
```gdscript
|
||||
# In your game manager (autoload or scene root):
|
||||
var server = SimulationServer.new()
|
||||
server.set_tick_rate(128)
|
||||
server.start()
|
||||
var player_id = server.spawn_entity(Vector3(0, 0, 0))
|
||||
|
||||
# When player scene is ready:
|
||||
$Player/FPSCharacterController.set_entity_id(player_id)
|
||||
|
||||
# In _process(delta):
|
||||
while server.can_tick(delta):
|
||||
var snapshot = server.tick()
|
||||
# send snapshot to network layer
|
||||
```
|
||||
|
||||
## Walk Toggle Behavior
|
||||
|
||||
- **`walk_toggle = true`** (default): Press Shift to toggle between walk and sprint. Press again to toggle off. Same feel as Counter-Strike's walk/run toggle.
|
||||
- **`walk_toggle = false`**: Hold Shift for temporary sprint. Release to decelerate to walk speed.
|
||||
|
||||
## Crouch Behavior
|
||||
|
||||
- **`crouch_toggle = false`** (default): Hold Ctrl to crouch. Release to stand up.
|
||||
- **`crouch_toggle = true`**: Press Ctrl once to crouch, press again to stand.
|
||||
|
||||
Crouch transitions are smooth lerps over `crouch_transition_time` seconds. The collision capsule height and camera eye height both lerp simultaneously.
|
||||
|
||||
## Standalone Mode
|
||||
|
||||
When no `SimulationServer` singleton is available, the controller falls back to built-in `CharacterBody3D` movement (`_move_local()`). This is useful for:
|
||||
- Testing the character controller in isolation
|
||||
- Prototyping without the GDExtension compiled
|
||||
- Single-player scenarios without the full simulation stack
|
||||
|
||||
Configure `local_walk_speed`, `local_sprint_speed`, `local_crouch_speed`, etc. for standalone tuning.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **Godot 4.2+** (for `Input.get_vector`, `move_toward`, `StringName` literals)
|
||||
- **SimulationServer GDExtension** (from task `t_aca0f251`) for server-authoritative mode
|
||||
- Input map actions listed above
|
||||
|
||||
## See Also
|
||||
|
||||
- Scaffold: `t_aca0f251` — `src/movement_component.h` (C++ Quake/CS movement physics)
|
||||
- Greybox map: `t_p1_greybox` — test environment for this controller
|
||||
- Hitscan weapon: `t_p1_hitscan` — weapon firing + lag compensation
|
||||
- Client prediction: `t_p1_pred` — client-side prediction + server reconciliation
|
||||
- Round system: `t_p1_round` — win/loss/sudden death on top of character controller
|
||||
@@ -0,0 +1,220 @@
|
||||
## FpsCamera — first-person camera view effects for tactical FPS.
|
||||
##
|
||||
## Manages view bobbing (head bob from movement), weapon sway, and FOV kick.
|
||||
## The parent FPSCharacterController handles mouse look (yaw/pitch).
|
||||
##
|
||||
## Place as a Camera3D child of FPSCharacterController.
|
||||
class_name FpsCamera
|
||||
extends Camera3D
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Exports
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Enable view bobbing (head bob when walking/running).
|
||||
@export var view_bob_enabled: bool = true
|
||||
|
||||
## How much the camera bobs horizontally (units).
|
||||
@export_range(0.0, 0.5) var bob_amplitude_h: float = 0.04
|
||||
|
||||
## How much the camera bobs vertically.
|
||||
@export_range(0.0, 0.5) var bob_amplitude_v: float = 0.04
|
||||
|
||||
## Frequency multiplier for bobbing (higher = faster).
|
||||
@export_range(0.5, 5.0) var bob_frequency: float = 2.5
|
||||
|
||||
## Sprint bob multiplier (more aggressive).
|
||||
@export_range(1.0, 3.0) var bob_sprint_mult: float = 1.6
|
||||
|
||||
## Crouch bob multiplier (less movement).
|
||||
@export_range(0.1, 1.0) var bob_crouch_mult: float = 0.4
|
||||
|
||||
## FOV kick on sprint (tactical FOV increase).
|
||||
@export_range(0.0, 10.0) var sprint_fov_kick: float = 3.0
|
||||
|
||||
## FOV kick when firing weapon.
|
||||
@export_range(-5.0, 10.0) var shoot_fov_kick: float = 0.5
|
||||
|
||||
## How fast FOV recovers.
|
||||
@export var fov_recovery_speed: float = 6.0
|
||||
|
||||
## Enable weapon sway (subtle rotation from movement).
|
||||
@export var weapon_sway_enabled: bool = true
|
||||
|
||||
## Maximum weapon sway rotation (degrees).
|
||||
@export_range(0.0, 5.0) var sway_max_rotation: float = 1.5
|
||||
|
||||
## Sway responsiveness (higher = snappier).
|
||||
@export_range(1.0, 20.0) var sway_response: float = 8.0
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Internal
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Reference to parent controller.
|
||||
var _controller: FPSCharacterController = null
|
||||
|
||||
## Bob time accumulator.
|
||||
var _bob_time: float = 0.0
|
||||
|
||||
## Current bob offset.
|
||||
var _bob_offset: Vector2 = Vector2.ZERO
|
||||
|
||||
## Current weapon node (child named "Weapon" or first MeshInstance3D).
|
||||
var _weapon: Node3D = null
|
||||
|
||||
## Weapon rest position (local transform when no sway).
|
||||
var _weapon_rest: Transform3D
|
||||
|
||||
## FOV state.
|
||||
var _base_fov: float = 75.0
|
||||
var _current_fov_offset: float = 0.0
|
||||
|
||||
## Sway state (used as both current value and interpolation target).
|
||||
var _sway_current: Vector2 = Vector2.ZERO
|
||||
var _sway_target: Vector2 = Vector2.ZERO
|
||||
|
||||
## Base eye Y set by parent controller's crouch logic (for additive bob).
|
||||
var _base_eye_y: float = 0.0
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lifecycle
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _ready() -> void:
|
||||
_controller = get_parent() as FPSCharacterController
|
||||
if _controller == null:
|
||||
push_warning("FpsCamera: Parent is not an FPSCharacterController. View features disabled.")
|
||||
|
||||
_base_fov = fov
|
||||
|
||||
# Find weapon node (for sway)
|
||||
for child in get_children():
|
||||
if child is Node3D:
|
||||
_weapon = child
|
||||
_weapon_rest = child.transform
|
||||
break
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if _controller == null:
|
||||
return
|
||||
|
||||
# Capture the eye height set by parent controller's crouch logic
|
||||
# before we apply view bob offsets (which must be additive).
|
||||
_base_eye_y = position.y
|
||||
|
||||
# FOV management
|
||||
_update_fov(delta)
|
||||
|
||||
# View bobbing
|
||||
if view_bob_enabled:
|
||||
_update_bob(delta)
|
||||
|
||||
# Weapon sway
|
||||
if weapon_sway_enabled and _weapon:
|
||||
_update_sway(delta)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# FOV
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _update_fov(delta: float) -> void:
|
||||
var target_offset: float = 0.0
|
||||
|
||||
# Sprint FOV kick — sustained while sprinting (not just on transition)
|
||||
if _controller.is_sprinting():
|
||||
target_offset = sprint_fov_kick
|
||||
|
||||
# Shoot FOV kick is triggered externally via trigger_shoot_fov()
|
||||
# and decays toward the sprint/walk target naturally via move_toward.
|
||||
_current_fov_offset = move_toward(_current_fov_offset, target_offset, fov_recovery_speed * delta)
|
||||
fov = _base_fov + _current_fov_offset
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# View bobbing
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _update_bob(delta: float) -> void:
|
||||
var crouch_amount: float = _controller.get_crouch_amount()
|
||||
var sprinting: bool = _controller.is_sprinting()
|
||||
|
||||
# Calculate bob speed multiplier from controller state
|
||||
var speed_mult: float = 1.0
|
||||
if sprinting:
|
||||
speed_mult = bob_sprint_mult
|
||||
elif crouch_amount > 0.0:
|
||||
speed_mult = lerpf(1.0, bob_crouch_mult, crouch_amount)
|
||||
|
||||
if speed_mult < 0.01:
|
||||
# Stationary — reset to zero
|
||||
_bob_offset = _bob_offset.move_toward(Vector2.ZERO, delta * 10.0)
|
||||
else:
|
||||
_bob_time += delta * bob_frequency * speed_mult
|
||||
_bob_offset = Vector2(
|
||||
sin(_bob_time * 2.0) * bob_amplitude_h * speed_mult,
|
||||
sin(_bob_time) * bob_amplitude_v * speed_mult
|
||||
)
|
||||
|
||||
# Apply bob to camera position (relative to parent's eye offset)
|
||||
position.x = _bob_offset.x
|
||||
# Bob is additive on Y so it does NOT overwrite the crouch eye height
|
||||
# set by the parent FPSCharacterController._update_crouch().
|
||||
position.y = _base_eye_y + _bob_offset.y
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Weapon sway
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _update_sway(delta: float) -> void:
|
||||
# Sway follows mouse movement indirectly via controller yaw/pitch changes
|
||||
# Use velocity for physics-based sway: recentre over time
|
||||
var vel: Vector3 = _controller.velocity
|
||||
var sway_h := clamp(vel.x * 0.003, -sway_max_rotation, sway_max_rotation)
|
||||
var sway_v := clamp(vel.z * 0.003, -sway_max_rotation, sway_max_rotation)
|
||||
# Add a tiny amount from pitch/yaw delta (not mouse, from movement direction change)
|
||||
_sway_target = Vector2(
|
||||
move_toward(_sway_target.x, sway_h, sway_response * delta),
|
||||
move_toward(_sway_target.y, sway_v, sway_response * delta)
|
||||
)
|
||||
|
||||
# Apply to weapon node as small rotation offsets
|
||||
if _weapon:
|
||||
var target := Transform3D(
|
||||
Basis.from_euler(Vector3(
|
||||
deg_to_rad(_sway_target.y),
|
||||
deg_to_rad(_sway_target.x),
|
||||
0.0
|
||||
)),
|
||||
_weapon_rest.origin
|
||||
)
|
||||
_weapon.transform = _weapon.transform.interpolate_with(target, sway_response * delta * 0.5)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public API
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Trigger a shoot FOV kick. Call from weapon script.
|
||||
func trigger_shoot_fov() -> void:
|
||||
_current_fov_offset += shoot_fov_kick
|
||||
|
||||
|
||||
## Reset view to resting state (for respawn).
|
||||
func reset_view() -> void:
|
||||
_bob_time = 0.0
|
||||
_bob_offset = Vector2.ZERO
|
||||
_current_fov_offset = 0.0
|
||||
_sway_current = Vector2.ZERO
|
||||
_sway_target = Vector2.ZERO
|
||||
fov = _base_fov
|
||||
if _weapon:
|
||||
_weapon.transform = _weapon_rest
|
||||
|
||||
|
||||
## Get the current bob offset for external effects.
|
||||
func get_bob_offset() -> Vector2:
|
||||
return _bob_offset
|
||||
@@ -0,0 +1 @@
|
||||
uid://bjtf5jiedmwx8
|
||||
@@ -0,0 +1,473 @@
|
||||
## FPSCharacterController — first-person character for a networked tactical shooter.
|
||||
##
|
||||
## Bridges Godot Input → SimulationServer (GDExtension C++ core).
|
||||
## Handles mouse look, WASD movement, sprint toggle, hold-to-crouch, jump.
|
||||
##
|
||||
## Usage (scene tree):
|
||||
## FPSCharacterController (CharacterBody3D)
|
||||
## ├── FpsCamera (Camera3D) — script: fps_camera.gd
|
||||
## ├── CollisionShape3D (capsule)
|
||||
## └── (optional weapons/arms child)
|
||||
##
|
||||
## Connects to SimulationServer via `apply_input()` each frame.
|
||||
## Reads server snapshot data and updates the node transform.
|
||||
##
|
||||
## Walk Toggle: Tap Shift to toggle sprint on/off.
|
||||
## Crouch: Hold Ctrl to crouch. Smooth lerp transition.
|
||||
## Jump: Press Space.
|
||||
## Mouse Look: Capture mouse on click, yaw rotates body, pitch rotates camera.
|
||||
class_name FPSCharacterController
|
||||
extends CharacterBody3D
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Exports — tune per weapon / game feel
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Mouse look sensitivity. At default 0.005, 100 px mouse motion = 0.5° rotation.
|
||||
@export_range(0.001, 0.1, 0.0005) var mouse_sensitivity: float = 0.005
|
||||
|
||||
## Mouse vertical sensitivity multiplier (1.0 = same as horizontal).
|
||||
@export_range(0.1, 2.0, 0.05) var mouse_vertical_ratio: float = 0.8
|
||||
|
||||
## Invert vertical look.
|
||||
@export var invert_y: bool = false
|
||||
|
||||
## Maximum pitch angle (degrees) — prevents looking upside down.
|
||||
@export_range(1, 89) var max_pitch: float = 89.0
|
||||
|
||||
## Entity ID assigned by SimulationServer. Set via spawn_entity() response.
|
||||
@export var entity_id: int = -1
|
||||
|
||||
## If true, pressing Sprint toggles between walk/sprint. If false, hold to sprint.
|
||||
@export var walk_toggle: bool = true
|
||||
|
||||
## If true, crouch toggles on press. If false, hold Ctrl to crouch.
|
||||
@export var crouch_toggle: bool = false
|
||||
|
||||
## Time in seconds for crouch height transition (lerp).
|
||||
@export var crouch_transition_time: float = 0.15
|
||||
|
||||
## Eye height when standing (Godot units, from feet).
|
||||
@export var eye_height_stand: float = 1.7
|
||||
|
||||
## Eye height when crouching.
|
||||
@export var eye_height_crouch: float = 0.9
|
||||
|
||||
## Collision shape height when standing.
|
||||
@export var collision_height_stand: float = 1.8
|
||||
|
||||
## Collision shape height when crouching.
|
||||
@export var collision_height_crouch: float = 1.0
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Internal state
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Reference to SimulationServer singleton (set in _ready).
|
||||
var _server: Object = null
|
||||
|
||||
## Current look rotation.
|
||||
var _yaw: float = 0.0
|
||||
var _pitch: float = 0.0
|
||||
|
||||
## Crouch lerp target (0.0 = standing, 1.0 = full crouch).
|
||||
var _crouch_target: float = 0.0
|
||||
var _crouch_current: float = 0.0
|
||||
|
||||
## Walk-toggle state. If walk_toggle is true, toggles each time sprint input activates.
|
||||
var _sprint_active: bool = false
|
||||
var _sprint_pressed_last: bool = false
|
||||
|
||||
## Crouch toggle state.
|
||||
var _crouch_active: bool = false
|
||||
var _crouch_pressed_last: bool = false
|
||||
|
||||
## Input sequence counter — increment each frame for server reconciliation.
|
||||
var _input_sequence: int = 0
|
||||
|
||||
## Cached input dictionary (avoids alloc per frame).
|
||||
var _input_dict: Dictionary = {}
|
||||
|
||||
## Prediction controller reference (set by ClientPrediction system).
|
||||
## When non-null and prediction_enabled, the controller uses client-side
|
||||
## prediction with local movement for instant feedback instead of
|
||||
## reading position from the SimulationServer entity directly.
|
||||
var _prediction: Node = null
|
||||
|
||||
## Mouse capture state.
|
||||
var _mouse_captured: bool = false
|
||||
var _mouse_clicked_this_frame: bool = false
|
||||
|
||||
## Weapon manager reference.
|
||||
var _weapon: WeaponManager = null
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Node references (set in _ready)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
var _camera: Node3D = null
|
||||
var _collision_shape: CollisionShape3D = null
|
||||
var _capsule_shape: CapsuleShape3D = null
|
||||
|
||||
# Cached standing/crouching shape parameters.
|
||||
var _stand_capsule_height: float = 0.0
|
||||
var _crouch_capsule_height: float = 0.0
|
||||
var _capsule_radius: float = 0.0
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lifecycle
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _ready() -> void:
|
||||
# Find SimulationServer
|
||||
if Engine.has_singleton("SimulationServer"):
|
||||
_server = Engine.get_singleton("SimulationServer")
|
||||
else:
|
||||
push_warning("FPSCharacterController: SimulationServer singleton not found. " +
|
||||
"Running in standalone (local physics) mode.")
|
||||
|
||||
# Locate camera
|
||||
_camera = get_node_or_null("%FpsCamera")
|
||||
if _camera == null:
|
||||
# Try to find any Camera3D child
|
||||
for child in get_children():
|
||||
if child is Camera3D:
|
||||
_camera = child
|
||||
break
|
||||
if _camera == null:
|
||||
push_error("FPSCharacterController: No Camera3D child found. Mouse look disabled.")
|
||||
|
||||
# Locate collision shape
|
||||
for child in get_children():
|
||||
if child is CollisionShape3D:
|
||||
_collision_shape = child
|
||||
if child.shape is CapsuleShape3D:
|
||||
_capsule_shape = child.shape
|
||||
_capsule_radius = _capsule_shape.radius
|
||||
_stand_capsule_height = collision_height_stand - 2.0 * _capsule_radius
|
||||
_crouch_capsule_height = collision_height_crouch - 2.0 * _capsule_radius
|
||||
_capsule_shape.height = _stand_capsule_height
|
||||
break
|
||||
|
||||
# Start with mouse captured
|
||||
_capture_mouse(true)
|
||||
|
||||
# Initialize yaw/pitch from current transform
|
||||
_yaw = rotation.y
|
||||
if _camera:
|
||||
_pitch = _camera.rotation.x
|
||||
|
||||
# Initialize crouch animation state
|
||||
_crouch_current = 0.0
|
||||
_update_crouch(0.0)
|
||||
|
||||
# Find weapon manager child
|
||||
for child in get_children():
|
||||
if child is WeaponManager:
|
||||
_weapon = child
|
||||
break
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
# Mouse look
|
||||
if event is InputEventMouseMotion and _mouse_captured:
|
||||
var rel: Vector2 = event.relative
|
||||
# Yaw (body rotation)
|
||||
_yaw -= rel.x * mouse_sensitivity
|
||||
# Pitch (camera rotation) — negative = look down, positive = look up
|
||||
var invert: float = -1.0 if invert_y else 1.0
|
||||
_pitch += rel.y * mouse_sensitivity * mouse_vertical_ratio * invert
|
||||
_pitch = clamp(_pitch, deg_to_rad(-max_pitch), deg_to_rad(max_pitch))
|
||||
|
||||
# Mouse capture toggle
|
||||
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
|
||||
if not _mouse_captured:
|
||||
_capture_mouse(true)
|
||||
else:
|
||||
# LMB fires weapon — handled in _physics_process via shoot_pressed
|
||||
_mouse_clicked_this_frame = true
|
||||
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
# Escape to release mouse
|
||||
if event.is_action_pressed("ui_cancel"):
|
||||
_capture_mouse(false)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main loop
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
# PREDICTION HOOK: capture pre-input state snapshot.
|
||||
# Must be called BEFORE any input processing or movement so the
|
||||
# snapshot represents the state at the start of this tick.
|
||||
if _prediction and _prediction.prediction_enabled:
|
||||
_prediction.on_before_tick()
|
||||
|
||||
# 1. Capture input state
|
||||
var input_dir := _get_move_direction()
|
||||
var jump_pressed := Input.is_action_just_pressed(&"jump")
|
||||
var sprint_pressed := Input.is_action_pressed(&"sprint")
|
||||
var crouch_pressed := Input.is_action_pressed(&"crouch")
|
||||
var shoot_pressed := _mouse_clicked_this_frame or Input.is_action_pressed(&"shoot")
|
||||
var aim_pressed := Input.is_action_pressed(&"aim")
|
||||
|
||||
_mouse_clicked_this_frame = false
|
||||
|
||||
# 2. Walk toggle logic
|
||||
if walk_toggle:
|
||||
if sprint_pressed and not _sprint_pressed_last:
|
||||
_sprint_active = not _sprint_active
|
||||
_sprint_pressed_last = sprint_pressed
|
||||
else:
|
||||
_sprint_active = sprint_pressed
|
||||
|
||||
# 3. Crouch toggle logic
|
||||
if crouch_toggle:
|
||||
if crouch_pressed and not _crouch_pressed_last:
|
||||
_crouch_active = not _crouch_active
|
||||
_crouch_pressed_last = crouch_pressed
|
||||
else:
|
||||
_crouch_active = crouch_pressed
|
||||
|
||||
# 4. Crouch height transition
|
||||
var target_crouch: float = 1.0 if _crouch_active else 0.0
|
||||
_crouch_target = target_crouch
|
||||
|
||||
var crouch_speed: float = 1.0 / max(crouch_transition_time, 0.001)
|
||||
if _crouch_current < _crouch_target:
|
||||
_crouch_current = min(_crouch_current + crouch_speed * delta, _crouch_target)
|
||||
elif _crouch_current > _crouch_target:
|
||||
_crouch_current = max(_crouch_current - crouch_speed * delta, _crouch_target)
|
||||
|
||||
_update_crouch(_crouch_current)
|
||||
|
||||
# 5. Apply rotation
|
||||
rotation.y = _yaw
|
||||
if _camera:
|
||||
_camera.rotation.x = _pitch
|
||||
|
||||
# 5b. Weapon fire with rate limiting via WeaponManager
|
||||
var should_fire: bool = false
|
||||
var time_since_engine_start: float = Engine.get_process_ticks() * get_physics_process_delta_time()
|
||||
if _weapon:
|
||||
should_fire = _weapon.try_fire(time_since_engine_start)
|
||||
elif shoot_pressed:
|
||||
# No weapon manager — fire every tick (unlimited)
|
||||
should_fire = true
|
||||
|
||||
if _server != null:
|
||||
if should_fire:
|
||||
_server.fire_weapon(entity_id)
|
||||
|
||||
# 6. Build input dictionary (all modes — mutate cached dict to avoid alloc)
|
||||
_input_dict["move_direction"] = input_dir
|
||||
_input_dict["look_yaw"] = rad_to_deg(_yaw)
|
||||
_input_dict["look_pitch"] = rad_to_deg(_pitch)
|
||||
_input_dict["jump"] = jump_pressed
|
||||
_input_dict["crouch"] = _crouch_active
|
||||
_input_dict["sprint"] = _sprint_active
|
||||
_input_dict["shoot"] = should_fire
|
||||
_input_dict["aim"] = aim_pressed
|
||||
_input_dict["input_sequence"] = _input_sequence
|
||||
|
||||
# 6b. Route input to appropriate handler.
|
||||
if _prediction and _prediction.prediction_enabled:
|
||||
# Client prediction: input sending is handled by the prediction
|
||||
# controller's on_after_tick() call at the end of this function.
|
||||
pass
|
||||
elif _server != null and entity_id >= 0:
|
||||
# Listen server: send to local SimulationServer.
|
||||
_server.apply_input(entity_id, _input_dict)
|
||||
|
||||
# Check for hit feedback from last tick
|
||||
if Engine.has_singleton("SimulationServer") or _server:
|
||||
var hit_result: Dictionary = _server.get_last_hit_result()
|
||||
if hit_result.get("hit", false) and _weapon:
|
||||
var hit_pos := Vector3.ZERO # approximate — we don't have exact world hit position from server yet
|
||||
_weapon.on_hit_confirmed(hit_pos, hit_result.get("damage", 0.0), hit_result.get("killed", false))
|
||||
|
||||
_input_sequence += 1
|
||||
|
||||
# 7. Update position.
|
||||
# Architecture:
|
||||
# - Client prediction: do local CharacterBody3D movement (instant feedback),
|
||||
# the prediction controller handles reconciliation when authoritative
|
||||
# server state arrives.
|
||||
# - Listen server: read authoritative position from SimulationServer entity.
|
||||
# - Standalone (no server): fallback CharacterBody3D movement.
|
||||
if _prediction and _prediction.prediction_enabled:
|
||||
# Client prediction path — local movement for instant feedback.
|
||||
_move_local(input_dir, delta, jump_pressed)
|
||||
elif _server != null and entity_id >= 0:
|
||||
# Listen server: read from SimulationServer entity.
|
||||
var entity = _server.get_entity(entity_id)
|
||||
if entity != null and entity.is_alive():
|
||||
# Server position is ground truth — apply it to the visual body
|
||||
# (only correct on listen-server; pure clients must use replication)
|
||||
global_position = entity.position
|
||||
else:
|
||||
# Standalone mode: do local CharacterBody3D physics
|
||||
_move_local(input_dir, delta, jump_pressed)
|
||||
|
||||
# PREDICTION HOOK: post-tick — send input to server via RPC.
|
||||
# Must be called AFTER local movement so the prediction controller
|
||||
# can send the input that produced this tick's movement.
|
||||
if _prediction and _prediction.prediction_enabled:
|
||||
_prediction.on_after_tick(_input_dict)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Local movement (standalone / no-server fallback)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Walk speed (standalone mode).
|
||||
@export var local_walk_speed: float = 5.0
|
||||
## Sprint speed (standalone mode).
|
||||
@export var local_sprint_speed: float = 8.0
|
||||
## Crouch speed (standalone mode).
|
||||
@export var local_crouch_speed: float = 2.5
|
||||
## Jump velocity (standalone mode).
|
||||
@export var local_jump_velocity: float = 4.5
|
||||
## Gravity (standalone mode).
|
||||
@export var local_gravity: float = 15.0
|
||||
## Acceleration (standalone mode).
|
||||
@export var local_acceleration: float = 12.0
|
||||
|
||||
func _move_local(input_dir: Vector3, delta: float, jump: bool) -> void:
|
||||
# Simple CharacterBody3D movement for standalone testing
|
||||
var target_speed: float = local_walk_speed
|
||||
if _sprint_active:
|
||||
target_speed = local_sprint_speed
|
||||
if _crouch_active:
|
||||
target_speed = local_crouch_speed
|
||||
|
||||
var wish_dir := (transform.basis * input_dir).normalized()
|
||||
var target_vel := wish_dir * target_speed
|
||||
|
||||
# Horizontal acceleration
|
||||
var h_vel := Vector3(velocity.x, 0.0, velocity.z)
|
||||
h_vel = h_vel.move_toward(target_vel, local_acceleration * delta)
|
||||
velocity.x = h_vel.x
|
||||
velocity.z = h_vel.z
|
||||
|
||||
# Gravity
|
||||
if not is_on_floor():
|
||||
velocity.y -= local_gravity * delta
|
||||
|
||||
# Jump
|
||||
if jump and is_on_floor():
|
||||
velocity.y = local_jump_velocity
|
||||
|
||||
move_and_slide()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Crouch height management
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _update_crouch(amount: float) -> void:
|
||||
## amount: 0.0 = standing, 1.0 = fully crouched
|
||||
# Eye height — camera position relative to body origin (character feet at origin)
|
||||
if _camera:
|
||||
_camera.position.y = lerpf(eye_height_stand, eye_height_crouch, amount)
|
||||
|
||||
# Collision shape — capsule height (cylindrical section)
|
||||
if _capsule_shape:
|
||||
_capsule_shape.height = lerpf(_stand_capsule_height, _crouch_capsule_height, amount)
|
||||
|
||||
# Move the collision shape center so the capsule base stays on the ground:
|
||||
# shift = (crouch_total - stand_total) / 2, negative = moves down
|
||||
if _collision_shape:
|
||||
_collision_shape.position.y = lerpf(0.0, (collision_height_crouch - collision_height_stand) * 0.5, amount)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Input helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _get_move_direction() -> Vector3:
|
||||
var dir := Vector3.ZERO
|
||||
if Input.is_action_pressed(&"move_forward") or Input.is_action_pressed(&"move_up"):
|
||||
dir.z -= 1.0
|
||||
if Input.is_action_pressed(&"move_backward") or Input.is_action_pressed(&"move_down"):
|
||||
dir.z += 1.0
|
||||
if Input.is_action_pressed(&"move_left"):
|
||||
dir.x -= 1.0
|
||||
if Input.is_action_pressed(&"move_right"):
|
||||
dir.x += 1.0
|
||||
# Normalize for analog stick deadzone
|
||||
if dir.length_squared() > 0.0:
|
||||
dir = dir.normalized()
|
||||
return dir
|
||||
|
||||
|
||||
func _capture_mouse(capture: bool) -> void:
|
||||
if capture == _mouse_captured:
|
||||
return
|
||||
_mouse_captured = capture
|
||||
if capture:
|
||||
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
|
||||
else:
|
||||
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public API
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Set the entity ID assigned by SimulationServer after spawning.
|
||||
func set_entity_id(id: int) -> void:
|
||||
entity_id = id
|
||||
|
||||
|
||||
## Set the prediction controller for client-side prediction/reconciliation.
|
||||
## The prediction node must have on_before_tick() / on_after_tick() methods.
|
||||
## Typically a ClientPrediction instance added as a child or sibling.
|
||||
func set_prediction(prediction_node: Node) -> void:
|
||||
_prediction = prediction_node
|
||||
|
||||
|
||||
## Get current crouch amount (0.0 = standing, 1.0 = fully crouched).
|
||||
func get_crouch_amount() -> float:
|
||||
return _crouch_current
|
||||
|
||||
|
||||
## Is the player currently sprinting?
|
||||
func is_sprinting() -> bool:
|
||||
return _sprint_active
|
||||
|
||||
|
||||
## Is the player currently crouching?
|
||||
func is_crouching() -> bool:
|
||||
return _crouch_active
|
||||
|
||||
|
||||
## Force look direction (useful for spectator / spawn reset).
|
||||
func set_look(yaw_rad: float, pitch_rad: float) -> void:
|
||||
_yaw = yaw_rad
|
||||
_pitch = clamp(pitch_rad, deg_to_rad(-max_pitch), deg_to_rad(max_pitch))
|
||||
|
||||
|
||||
## Reset to default standing state.
|
||||
func reset_pose() -> void:
|
||||
_sprint_active = false
|
||||
_crouch_active = false
|
||||
_sprint_pressed_last = false
|
||||
_crouch_pressed_last = false
|
||||
_crouch_current = 0.0
|
||||
_crouch_target = 0.0
|
||||
_update_crouch(0.0)
|
||||
|
||||
|
||||
## Get the look direction as a normalized Vector3 in world space.
|
||||
## Uses the current yaw/pitch to compute a forward vector.
|
||||
func get_look_direction() -> Vector3:
|
||||
var yaw_rad: float = _yaw
|
||||
var pitch_rad: float = _pitch
|
||||
return Vector3(
|
||||
cos(pitch_rad) * sin(yaw_rad),
|
||||
-sin(pitch_rad),
|
||||
cos(pitch_rad) * cos(yaw_rad)
|
||||
).normalized()
|
||||
@@ -0,0 +1 @@
|
||||
uid://cpbvk1itjonm
|
||||
@@ -0,0 +1,59 @@
|
||||
## InputHandler — maps Godot Input Actions to FPS character controls.
|
||||
##
|
||||
## This script is NOT required at runtime — it exists to document the input
|
||||
## map bindings that FPSCharacterController expects. Add these actions to
|
||||
## your Project → Input Map (or load this script as an autoload for reference).
|
||||
##
|
||||
## Bindings (tactical FPS layout):
|
||||
##
|
||||
## Action Key Description
|
||||
## ───────────────────────────────────────────────────────
|
||||
## move_forward W Walk forward
|
||||
## move_backward S Walk backward
|
||||
## move_left A Strafe left
|
||||
## move_right D Strafe right
|
||||
## jump Space Jump
|
||||
## sprint Shift Sprint / walk-toggle
|
||||
## crouch Ctrl Crouch (hold or toggle)
|
||||
## shoot Mouse LMB Fire weapon
|
||||
## aim Mouse RMB Aim down sights
|
||||
## reload R Reload weapon
|
||||
## interact E Use/interact
|
||||
## toggle_walk_toggle (not bound) Toggle walk-toggle mode
|
||||
## toggle_crouch_toggle (not bound) Toggle crouch-toggle mode
|
||||
## ui_cancel Esc Release mouse capture
|
||||
##
|
||||
## Implementation note:
|
||||
## FPSCharacterController reads input directly via Input.is_action_pressed()
|
||||
## in _physics_process. No separate InputHandler node is needed unless you
|
||||
## want to add rebinding, analog input smoothing, or input buffering.
|
||||
|
||||
extends Node
|
||||
|
||||
func _ready() -> void:
|
||||
# This script documents bindings only — no runtime logic.
|
||||
# To enforce default bindings at startup, uncomment the next lines:
|
||||
|
||||
# _ensure_action(&"move_forward", KEY_W)
|
||||
# _ensure_action(&"move_backward", KEY_S)
|
||||
# _ensure_action(&"move_left", KEY_A)
|
||||
# _ensure_action(&"move_right", KEY_D)
|
||||
# _ensure_action(&"jump", KEY_SPACE)
|
||||
# _ensure_action(&"sprint", KEY_SHIFT)
|
||||
# _ensure_action(&"crouch", KEY_CTRL)
|
||||
# _ensure_action(&"shoot", MOUSE_BUTTON_LEFT)
|
||||
# _ensure_action(&"aim", MOUSE_BUTTON_RIGHT)
|
||||
# _ensure_action(&"reload", KEY_R)
|
||||
# _ensure_action(&"interact", KEY_E)
|
||||
pass
|
||||
|
||||
|
||||
## Ensure an input action exists with the given default key binding.
|
||||
## Only sets the binding if the action does not already exist.
|
||||
func _ensure_action(action_name: StringName, key: Key) -> void:
|
||||
if InputMap.has_action(action_name):
|
||||
return
|
||||
var ev := InputEventKey.new()
|
||||
ev.keycode = key
|
||||
InputMap.add_action(action_name)
|
||||
InputMap.action_add_event(action_name, ev)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bvkhxr1bv6lth
|
||||
@@ -0,0 +1,293 @@
|
||||
## BombCarrier — Client-side bomb status UI and prompts.
|
||||
##
|
||||
## Attached to the local T player who carries the bomb.
|
||||
## Provides visual feedback for:
|
||||
## - Bomb status (planted/defusing/timer)
|
||||
## - Direction indicator to planted bomb
|
||||
## - "Hold E to defuse" prompt (CT, near bomb)
|
||||
## - "Hold E to plant" prompt (T, in bombsite, LIVE phase)
|
||||
##
|
||||
## This script is a Node that should be added as a child of the local
|
||||
## player's character node (FPSCharacterController) on the client side.
|
||||
## It communicates with the server BombObjective via RPC or state sync.
|
||||
|
||||
extends Node
|
||||
class_name BombCarrier
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Signals
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Emitted when the nearby interaction prompt changes.
|
||||
signal prompt_changed(prompt_text: String, visible: bool)
|
||||
|
||||
## Emitted when bomb status UI should update.
|
||||
signal bomb_status_updated(planted: bool, timer: float, defusing: bool, defuse_progress: float)
|
||||
|
||||
## Emitted when the bomb direction indicator should update.
|
||||
signal bomb_direction_updated(bearing_degrees: float, distance: float)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Exports
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## The "Use/Interact" action name (matched in Input Map).
|
||||
@export var interact_action: String = "interact"
|
||||
|
||||
## Interaction distance for planting (in Godot units).
|
||||
@export var plant_distance: float = 2.0
|
||||
|
||||
## Interaction distance for defusing (in Godot units).
|
||||
@export var defuse_distance: float = 3.0
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# State
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Whether this player has the bomb (T-side bomb carrier).
|
||||
var has_bomb: bool = false
|
||||
|
||||
## Whether the bomb is currently planted.
|
||||
var _bomb_planted: bool = false
|
||||
|
||||
## Position of the planted bomb (server-authoritative).
|
||||
var _bomb_position: Vector3 = Vector3.ZERO
|
||||
|
||||
## Remaining bomb timer (received from server).
|
||||
var _bomb_timer: float = 0.0
|
||||
|
||||
## Whether the local player is currently defusing.
|
||||
var _is_defusing: bool = false
|
||||
|
||||
## Current defuse progress (0.0 to 1.0).
|
||||
var _defuse_progress: float = 0.0
|
||||
|
||||
## Current prompt text to show.
|
||||
var _current_prompt: String = ""
|
||||
|
||||
## Whether the prompt is currently visible.
|
||||
var _prompt_visible: bool = false
|
||||
|
||||
## Whether we are inside a bombsite.
|
||||
var _in_bombsite: bool = false
|
||||
|
||||
## Whether we are near the planted bomb.
|
||||
var _near_planted_bomb: bool = false
|
||||
|
||||
## Whether the interact key is being held.
|
||||
var _interact_held: bool = false
|
||||
|
||||
## Hold-down timer for plant/defuse (E key held long enough).
|
||||
var _interact_hold_time: float = 0.0
|
||||
|
||||
## Time required to hold E to start action (for UX consistency).
|
||||
var _interact_hold_duration: float = 0.0 # Immediate on press
|
||||
|
||||
## Reference to the local player's parent/character controller.
|
||||
var _character_controller: Node = null
|
||||
|
||||
## Reference to the bomb objective on the server (for RPC calls).
|
||||
## Set by the game manager after instantiation.
|
||||
var bomb_objective_path: NodePath = NodePath()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lifecycle
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _ready() -> void:
|
||||
_character_controller = get_parent()
|
||||
if _character_controller == null:
|
||||
push_warning("[BombCarrier] No parent node — prompts will not work")
|
||||
|
||||
# Try to set up the interact action if it doesn't exist
|
||||
if not InputMap.has_action(interact_action):
|
||||
var event := InputEventKey.new()
|
||||
event.keycode = KEY_E
|
||||
InputMap.add_action(interact_action)
|
||||
InputMap.action_add_event(interact_action, event)
|
||||
|
||||
print("[BombCarrier] Initialised — has_bomb=%s" % has_bomb)
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
_update_interaction(delta)
|
||||
_emit_status_updates()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public API — Server state sync
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Called by the game manager or network sync to update bomb state.
|
||||
func sync_bomb_state(planted: bool, position: Vector3, timer: float,
|
||||
defusing: bool, defuse_progress: float) -> void:
|
||||
_bomb_planted = planted
|
||||
_bomb_position = position
|
||||
_bomb_timer = timer
|
||||
_is_defusing = defusing
|
||||
_defuse_progress = defuse_progress
|
||||
|
||||
## Set whether this player has the bomb (assigned by server).
|
||||
func set_has_bomb(value: bool) -> void:
|
||||
has_bomb = value
|
||||
print("[BombCarrier] Has bomb: %s" % value)
|
||||
|
||||
## Set whether the local player is inside a bombsite Area3D.
|
||||
## Called by the bombsite Area3D signal handlers on the client.
|
||||
func set_in_bombsite(value: bool) -> void:
|
||||
_in_bombsite = value
|
||||
|
||||
## Set whether the local player is near the planted bomb.
|
||||
## Called by the bomb area proximity check.
|
||||
func set_near_planted_bomb(value: bool) -> void:
|
||||
_near_planted_bomb = value
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public API — RPC calls to server
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Request to plant the bomb at the current position.
|
||||
## Called when the T player presses E inside a bombsite with the bomb.
|
||||
func request_plant() -> void:
|
||||
if not has_bomb or not _in_bombsite:
|
||||
return
|
||||
if _bomb_planted:
|
||||
return
|
||||
|
||||
# Send plant request to server
|
||||
# In real implementation, this would be an RPC call:
|
||||
# _send_plant_request.rpc_id(1, _character_controller.global_position)
|
||||
_send_plant_request(_character_controller.global_position)
|
||||
|
||||
## Request to start defusing the bomb.
|
||||
## Called when the CT player presses E near the planted bomb.
|
||||
func request_defuse() -> void:
|
||||
if not _near_planted_bomb or not _bomb_planted:
|
||||
return
|
||||
if _is_defusing:
|
||||
return
|
||||
|
||||
# Send defuse request to server
|
||||
# _send_defuse_request.rpc_id(1)
|
||||
_send_defuse_request()
|
||||
|
||||
## Request to cancel defuse (when player moves or is interrupted).
|
||||
func request_cancel_defuse() -> void:
|
||||
if not _is_defusing:
|
||||
return
|
||||
_send_cancel_defuse()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# RPC stubs (to be replaced with actual RPC calls)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _send_plant_request(position: Vector3) -> void:
|
||||
# Placeholder: in real implementation, send RPC to server
|
||||
print("[BombCarrier] Plant request at (%.1f, %.1f, %.1f)" %
|
||||
[position.x, position.y, position.z])
|
||||
|
||||
func _send_defuse_request() -> void:
|
||||
# Placeholder: in real implementation, send RPC to server
|
||||
print("[BombCarrier] Defuse request")
|
||||
|
||||
func _send_cancel_defuse() -> void:
|
||||
# Placeholder: in real implementation, send RPC to server
|
||||
print("[BombCarrier] Cancel defuse request")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Internal — Interaction logic
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _update_interaction(delta: float) -> void:
|
||||
var interact_pressed: bool = Input.is_action_pressed(interact_action)
|
||||
var prompt_text: String = ""
|
||||
var show_prompt: bool = false
|
||||
|
||||
# Determine what prompt to show
|
||||
if _bomb_planted and _near_planted_bomb and not _is_defusing:
|
||||
# CT player near planted bomb — defuse prompt
|
||||
var team: int = _get_local_player_team()
|
||||
if team == TeamData.Team.COUNTER_TERRORIST:
|
||||
prompt_text = "Hold E to defuse"
|
||||
show_prompt = true
|
||||
|
||||
elif has_bomb and _in_bombsite and not _bomb_planted:
|
||||
# T player with bomb inside bombsite — plant prompt
|
||||
if _is_round_live():
|
||||
prompt_text = "Hold E to plant"
|
||||
show_prompt = true
|
||||
|
||||
elif _bomb_planted and _is_defusing:
|
||||
# Currently defusing — show progress
|
||||
var progress_pct: int = int(_defuse_progress * 100.0)
|
||||
prompt_text = "Defusing... %d%%" % progress_pct
|
||||
show_prompt = true
|
||||
|
||||
# Handle interact hold
|
||||
if interact_pressed and show_prompt:
|
||||
if not _interact_held:
|
||||
_interact_held = true
|
||||
_interact_hold_time = 0.0
|
||||
|
||||
_interact_hold_time += delta
|
||||
|
||||
# Check if we should trigger the action
|
||||
if _interact_hold_time >= _interact_hold_duration:
|
||||
_interact_hold_time = 0.0
|
||||
if prompt_text.contains("defuse"):
|
||||
request_defuse()
|
||||
elif prompt_text.contains("plant"):
|
||||
request_plant()
|
||||
else:
|
||||
# Cancel any pending interaction
|
||||
if _interact_held:
|
||||
_interact_held = false
|
||||
_interact_hold_time = 0.0
|
||||
# If we were defusing and released, cancel
|
||||
if _is_defusing:
|
||||
request_cancel_defuse()
|
||||
|
||||
# Update prompt if changed
|
||||
if prompt_text != _current_prompt or show_prompt != _prompt_visible:
|
||||
_current_prompt = prompt_text
|
||||
_prompt_visible = show_prompt
|
||||
prompt_changed.emit(prompt_text, show_prompt)
|
||||
|
||||
func _emit_status_updates() -> void:
|
||||
# Emit bomb status updates (rate-limited is handled by engine)
|
||||
bomb_status_updated.emit(_bomb_planted, _bomb_timer, _is_defusing, _defuse_progress)
|
||||
|
||||
# Emit direction indicator if bomb is planted
|
||||
if _bomb_planted and _character_controller != null:
|
||||
var player_pos: Vector3 = _character_controller.global_position
|
||||
var direction: Vector3 = _bomb_position - player_pos
|
||||
var distance: float = direction.length()
|
||||
if distance > 0.01:
|
||||
var bearing: float = atan2(direction.x, -direction.z) # Relative to forward (Z)
|
||||
bomb_direction_updated.emit(rad_to_deg(bearing), distance)
|
||||
else:
|
||||
bomb_direction_updated.emit(0.0, 0.0)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Internal — Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Get the local player's team (from TeamManager singleton if available).
|
||||
func _get_local_player_team() -> int:
|
||||
var team_manager: TeamManager = get_node_or_null("/root/TeamManager")
|
||||
if team_manager:
|
||||
var peer_id: int = multiplayer.get_unique_id() if multiplayer.has_method(&\"get_unique_id\") else -1
|
||||
if peer_id >= 0:
|
||||
return team_manager.get_player_team(peer_id)
|
||||
return -1
|
||||
|
||||
## Check if the current round is in LIVE phase.
|
||||
func _is_round_live() -> bool:
|
||||
var round_manager: RoundManager = get_node_or_null("/root/RoundManager")
|
||||
if round_manager:
|
||||
return round_manager.get_phase() == RoundManager.RoundPhase.LIVE
|
||||
return false
|
||||
|
||||
## Get the multiplayer API (stub for compatibility).
|
||||
func get_multiplayer() -> MultiplayerAPI:
|
||||
if multiplayer and multiplayer.has_method(&\"get_unique_id\"):
|
||||
return multiplayer
|
||||
return null
|
||||
@@ -0,0 +1 @@
|
||||
uid://dtvf72mhgdat7
|
||||
@@ -0,0 +1,445 @@
|
||||
## BuyMenu — Client-side buy menu UI for weapon and equipment purchases.
|
||||
##
|
||||
## Displays a grid of purchasable weapons with name, cost, and key stats.
|
||||
## Highlights weapons the player can afford, shows current money at the top,
|
||||
## and supports both keyboard (numeric keys) and mouse-click selection.
|
||||
##
|
||||
## Opens during the PREP (buy) phase when the player is inside a buy zone.
|
||||
## Automatically closes when the buy phase ends or the player leaves the zone.
|
||||
##
|
||||
## Usage (add as child of the player's UI root or HUD):
|
||||
## var buy_menu = preload("res://client/characters/weapon/buy_menu.gd").new()
|
||||
## add_child(buy_menu)
|
||||
##
|
||||
## # Show it
|
||||
## buy_menu.open()
|
||||
## buy_menu.populate(affordable_weapons_list, current_money)
|
||||
##
|
||||
## Signals:
|
||||
## buy_menu_opened()
|
||||
## buy_menu_closed()
|
||||
## purchase_completed(weapon_id)
|
||||
## purchase_failed(reason: String)
|
||||
##
|
||||
class_name BuyMenu
|
||||
extends Control
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Signals
|
||||
# ---------------------------------------------------------------------------
|
||||
## Emitted when the buy menu opens.
|
||||
signal buy_menu_opened()
|
||||
|
||||
## Emitted when the buy menu closes.
|
||||
signal buy_menu_closed()
|
||||
|
||||
## Emitted when a purchase is confirmed by the server.
|
||||
signal purchase_completed(weapon_id: String)
|
||||
|
||||
## Emitted when a purchase is denied by the server.
|
||||
signal purchase_failed(reason: String)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Exports — Layout
|
||||
# ---------------------------------------------------------------------------
|
||||
## Background panel colour.
|
||||
@export var panel_color: Color = Color(0.1, 0.1, 0.15, 0.92)
|
||||
|
||||
## Accent colour for headers and affordable items.
|
||||
@export var accent_color: Color = Color(0.2, 0.7, 0.3, 1.0)
|
||||
|
||||
## Colour for items the player cannot afford.
|
||||
@export var unaffordable_color: Color = Color(0.5, 0.5, 0.5, 1.0)
|
||||
|
||||
## Text colour.
|
||||
@export var text_color: Color = Color(0.95, 0.95, 0.95, 1.0)
|
||||
|
||||
## Key binding for opening the buy menu (default: B).
|
||||
@export var open_key: Key = KEY_B
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Internal state
|
||||
# ---------------------------------------------------------------------------
|
||||
## Reference to the BuyMenuHandler on the server (for sending RPCs).
|
||||
## Injected by the UI manager or found at runtime.
|
||||
var buy_menu_handler_path: NodePath = NodePath()
|
||||
|
||||
var _is_open: bool = false
|
||||
var _weapon_buttons: Array[Button] = []
|
||||
var _populated: bool = false
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# UI Nodes (built in _build_ui)
|
||||
# ---------------------------------------------------------------------------
|
||||
var _panel: Panel = null
|
||||
var _money_label: Label = null
|
||||
var _title_label: Label = null
|
||||
var _weapon_container: Container = null
|
||||
var _close_button: Button = null
|
||||
var _status_label: Label = null
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lifecycle
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _ready() -> void:
|
||||
_build_ui()
|
||||
visible = false
|
||||
|
||||
# Listen for server purchase confirmations / denials
|
||||
# The BuyMenuHandler sends RPCs that land on this node.
|
||||
# We connect to them via the multiplayer API — they're named to match
|
||||
# the RPC names defined in BuyMenuHandler.
|
||||
|
||||
# Connect input for toggle key
|
||||
set_process_input(true)
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if event.is_action_pressed("buy_menu") or (
|
||||
event is InputEventKey and
|
||||
event.keycode == open_key and
|
||||
event.pressed and
|
||||
not event.echo
|
||||
):
|
||||
if _is_open:
|
||||
close()
|
||||
else:
|
||||
open()
|
||||
accept_event()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# UI Construction
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _build_ui() -> void:
|
||||
# Main panel — centred, sized to fit content
|
||||
_panel = Panel.new()
|
||||
_panel.anchor_left = 0.15
|
||||
_panel.anchor_right = 0.85
|
||||
_panel.anchor_top = 0.1
|
||||
_panel.anchor_bottom = 0.9
|
||||
_panel.mouse_filter = Control.MOUSE_FILTER_PASS
|
||||
add_child(_panel)
|
||||
|
||||
# Panel background style
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = panel_color
|
||||
style.corner_radius_top_left = 8
|
||||
style.corner_radius_top_right = 8
|
||||
style.corner_radius_bottom_left = 8
|
||||
style.corner_radius_bottom_right = 8
|
||||
_panel.add_theme_stylebox_override("panel", style)
|
||||
|
||||
# Vertical layout inside the panel
|
||||
var vbox := VBoxContainer.new()
|
||||
vbox.anchor_left = 0.0
|
||||
vbox.anchor_right = 1.0
|
||||
vbox.anchor_top = 0.0
|
||||
vbox.anchor_bottom = 1.0
|
||||
vbox.add_theme_constant_override("separation", 12)
|
||||
_panel.add_child(vbox)
|
||||
|
||||
# ── Title ──
|
||||
_title_label = Label.new()
|
||||
_title_label.text = "BUY MENU"
|
||||
_title_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_title_label.add_theme_font_size_override("font_size", 24)
|
||||
_title_label.add_theme_color_override("font_color", accent_color)
|
||||
vbox.add_child(_title_label)
|
||||
|
||||
# ── Money display ──
|
||||
_money_label = Label.new()
|
||||
_money_label.text = "$0"
|
||||
_money_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_money_label.add_theme_font_size_override("font_size", 20)
|
||||
_money_label.add_theme_color_override("font_color", text_color)
|
||||
vbox.add_child(_money_label)
|
||||
|
||||
# ── Weapon grid ──
|
||||
_weapon_container = GridContainer.new()
|
||||
_weapon_container.columns = 2
|
||||
_weapon_container.add_theme_constant_override("h_separation", 16)
|
||||
_weapon_container.add_theme_constant_override("v_separation", 8)
|
||||
_weapon_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_weapon_container.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
vbox.add_child(_weapon_container)
|
||||
|
||||
# ── Status / feedback line ──
|
||||
_status_label = Label.new()
|
||||
_status_label.text = ""
|
||||
_status_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_status_label.add_theme_color_override("font_color", Color(1.0, 0.8, 0.2, 1.0))
|
||||
_status_label.add_theme_font_size_override("font_size", 14)
|
||||
vbox.add_child(_status_label)
|
||||
|
||||
# ── Close hint ──
|
||||
var close_hint := Label.new()
|
||||
close_hint.text = "Press B or ESC to close | Click or press 1-4 to buy"
|
||||
close_hint.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
close_hint.add_theme_font_size_override("font_size", 11)
|
||||
close_hint.add_theme_color_override("font_color", Color(0.6, 0.6, 0.6, 1.0))
|
||||
vbox.add_child(close_hint)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public API
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Open and populate the buy menu.
|
||||
func open() -> void:
|
||||
if _is_open:
|
||||
return
|
||||
|
||||
_is_open = true
|
||||
visible = true
|
||||
_populated = false
|
||||
_clear_weapons()
|
||||
_status_label.text = ""
|
||||
|
||||
# Request affordable weapons from the server via an RPC call
|
||||
# (The server pushes the data back via RPC response.)
|
||||
_request_menu_data()
|
||||
|
||||
buy_menu_opened.emit()
|
||||
|
||||
## Close the buy menu.
|
||||
func close() -> void:
|
||||
if not _is_open:
|
||||
return
|
||||
_is_open = false
|
||||
visible = false
|
||||
buy_menu_closed.emit()
|
||||
|
||||
## Returns true if the menu is currently open.
|
||||
func is_open() -> bool:
|
||||
return _is_open
|
||||
|
||||
## Update the displayed money amount.
|
||||
func set_money(amount: int) -> void:
|
||||
_money_label.text = "$%d" % amount
|
||||
|
||||
## Populate the weapon grid with purchasable items.
|
||||
## `weapons` is an Array[Dictionary] with keys:
|
||||
## weapon_id, display_name, cost, damage, fire_rate, mag_size
|
||||
## `current_money` is the player's balance.
|
||||
func populate(weapons: Array[Dictionary], current_money: int) -> void:
|
||||
_clear_weapons()
|
||||
_populated = false
|
||||
set_money(current_money)
|
||||
|
||||
var index: int = 1
|
||||
for w in weapons:
|
||||
var affordable: bool = (current_money >= w.get("cost", 0))
|
||||
_add_weapon_button(index, w, affordable)
|
||||
index += 1
|
||||
|
||||
_populated = true
|
||||
|
||||
## Show a temporary status message in the menu.
|
||||
func show_status(message: String, duration: float = 3.0) -> void:
|
||||
_status_label.text = message
|
||||
if duration > 0:
|
||||
get_tree().create_timer(duration).timeout.connect(func():
|
||||
if is_instance_valid(_status_label) and _status_label.text == message:
|
||||
_status_label.text = ""
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Internal: weapon button creation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _add_weapon_button(index: int, data: Dictionary, affordable: bool) -> void:
|
||||
var weapon_id: String = data.get("weapon_id", "")
|
||||
var display_name: String = data.get("display_name", weapon_id)
|
||||
var cost: int = data.get("cost", 0)
|
||||
var damage: float = data.get("damage", 0.0)
|
||||
var fire_rate: float = data.get("fire_rate", 0.0)
|
||||
var mag_size: int = data.get("mag_size", 0)
|
||||
|
||||
# Build the weapon card as a styled Button
|
||||
var btn := Button.new()
|
||||
btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
btn.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||
btn.custom_minimum_size = Vector2(240, 80)
|
||||
|
||||
# Text: name + cost on first line, stats on second
|
||||
var stats_line: String = "%.0f dmg | %.1f rps | %d rds" % [damage, fire_rate, mag_size]
|
||||
btn.text = "[%d] %s — $%d\n%s" % [index, display_name, cost, stats_line]
|
||||
btn.flat = false
|
||||
btn.tooltip_text = "%s — $%d\nDamage: %.0f\nFire Rate: %.1f Hz\nMagazine: %d rounds" % [
|
||||
display_name, cost, damage, fire_rate, mag_size
|
||||
]
|
||||
|
||||
# Style
|
||||
var fg_color: Color = accent_color if affordable else unaffordable_color
|
||||
btn.add_theme_color_override("font_color", fg_color)
|
||||
btn.add_theme_color_override("font_hover_color", text_color)
|
||||
btn.add_theme_color_override("font_pressed_color", Color.WHITE)
|
||||
btn.add_theme_font_size_override("font_size", 14)
|
||||
|
||||
if not affordable:
|
||||
btn.disabled = true
|
||||
|
||||
btn.pressed.connect(_on_weapon_button_pressed.bind(weapon_id, cost, btn))
|
||||
_weapon_container.add_child(btn)
|
||||
_weapon_buttons.append(btn)
|
||||
|
||||
func _clear_weapons() -> void:
|
||||
for child in _weapon_buttons:
|
||||
if is_instance_valid(child):
|
||||
child.queue_free()
|
||||
_weapon_buttons.clear()
|
||||
_populated = false
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Internal: purchase flow
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _on_weapon_button_pressed(weapon_id: String, cost: int, _button: Button) -> void:
|
||||
_request_purchase(weapon_id)
|
||||
|
||||
## Send a purchase request RPC to the server.
|
||||
func _request_purchase(weapon_id: String) -> void:
|
||||
_status_label.text = "Purchasing %s..." % weapon_id
|
||||
|
||||
# Find the BuyMenuHandler and call its buy_request RPC.
|
||||
# The handler is typically a child of GameServer.
|
||||
if multiplayer.is_server():
|
||||
# Listen server / standalone — call directly
|
||||
_call_buy_handler(weapon_id)
|
||||
else:
|
||||
# Dedicated client — send RPC
|
||||
_call_buy_handler_rpc(weapon_id)
|
||||
|
||||
func _call_buy_handler(_weapon_id: String) -> void:
|
||||
# On a listen server, the handler is local
|
||||
var handler: BuyMenuHandler = _find_buy_handler()
|
||||
if handler:
|
||||
handler.buy_request.rpc(_weapon_id)
|
||||
|
||||
func _call_buy_handler_rpc(weapon_id: String) -> void:
|
||||
# Find the handler path and send the RPC
|
||||
var handler_path: NodePath = buy_menu_handler_path
|
||||
if handler_path.is_empty():
|
||||
# Try to find it dynamically
|
||||
handler_path = "/root/GameServer/BuyMenuHandler"
|
||||
var node: Node = get_node_or_null(handler_path) if not handler_path.is_empty() else null
|
||||
if node and node.has_method(&"buy_request"):
|
||||
node.buy_request.rpc(weapon_id)
|
||||
else:
|
||||
# Fallback: call directly on the handler node path
|
||||
rpc_id(1, "_remote_buy_request", weapon_id)
|
||||
|
||||
## Fallback: send buy request to server's handler.
|
||||
@rpc("any_peer", "call_remote", "reliable")
|
||||
func _remote_buy_request(weapon_id: String) -> void:
|
||||
if not multiplayer.is_server():
|
||||
return
|
||||
var handler: BuyMenuHandler = _find_buy_handler()
|
||||
if handler:
|
||||
var sender: int = multiplayer.get_remote_sender_id()
|
||||
handler.call_deferred("_process_buy_request", sender, weapon_id)
|
||||
|
||||
func _find_buy_handler() -> BuyMenuHandler:
|
||||
# Try common locations
|
||||
var game_server: Node = get_node_or_null("/root/GameServer")
|
||||
if game_server:
|
||||
for child in game_server.get_children():
|
||||
if child is BuyMenuHandler:
|
||||
return child
|
||||
return null
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# RPC handlers — called by BuyMenuHandler (server → client)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Called by the server when a purchase succeeds.
|
||||
@rpc("authority", "call_remote", "reliable")
|
||||
func _on_buy_confirmed(weapon_id: String, cost: int) -> void:
|
||||
show_status("Purchased %s for $%d" % [weapon_id, cost], 2.0)
|
||||
purchase_completed.emit(weapon_id)
|
||||
# Close after a brief delay so the player sees the confirmation
|
||||
get_tree().create_timer(0.6).timeout.connect(close)
|
||||
|
||||
## Called by the server when a purchase is denied.
|
||||
@rpc("authority", "call_remote", "reliable")
|
||||
func _on_buy_denied(reason: String) -> void:
|
||||
show_status("Purchase failed: %s" % reason, 3.0)
|
||||
purchase_failed.emit(reason)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Request affordable weapon data from the server
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _request_menu_data() -> void:
|
||||
# For now, the client populates from the weapon definitions directly,
|
||||
# filtering by what the server says is affordable.
|
||||
# In a more advanced version, the server sends the curated list.
|
||||
#
|
||||
# Since we don't know the player's money on the client, we send an
|
||||
# RPC to the server to get the affordable list. But for simplicity,
|
||||
# we populate all weapons and let the server deny if unaffordable.
|
||||
#
|
||||
# The server pushes the affordable list back via get_affordable_weapons().
|
||||
# For now, just show all weapons — the server will reject unaffordable ones.
|
||||
|
||||
# Try to fetch from server
|
||||
if multiplayer.is_server():
|
||||
_populate_from_local(player_id())
|
||||
else:
|
||||
_request_affordable_weapons.rpc_id(1)
|
||||
|
||||
## Get the player's peer ID.
|
||||
func player_id() -> int:
|
||||
return multiplayer.get_unique_id()
|
||||
|
||||
## Client → Server: request affordable weapon list.
|
||||
@rpc("any_peer", "call_remote", "reliable")
|
||||
func _request_affordable_weapons() -> void:
|
||||
if not multiplayer.is_server():
|
||||
return
|
||||
var sender: int = multiplayer.get_remote_sender_id()
|
||||
var handler: BuyMenuHandler = _find_buy_handler()
|
||||
if handler:
|
||||
var weapons: Array[Dictionary] = handler.get_affordable_weapons(sender)
|
||||
var money: int = handler.get_player_money(sender)
|
||||
_send_menu_data.rpc_id(sender, weapons, money)
|
||||
|
||||
## Server → Client: send affordable weapon list and money.
|
||||
@rpc("authority", "call_remote", "reliable")
|
||||
func _send_menu_data(weapons: Array[Dictionary], current_money: int) -> void:
|
||||
populate(weapons, current_money)
|
||||
|
||||
func _populate_from_local(pid: int) -> void:
|
||||
var handler: BuyMenuHandler = _find_buy_handler()
|
||||
if handler:
|
||||
var weapons: Array[Dictionary] = handler.get_affordable_weapons(pid)
|
||||
var money: int = handler.get_player_money(pid)
|
||||
populate(weapons, money)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Input helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _unhandled_key_input(event: InputEventKey) -> void:
|
||||
if not _is_open or not _populated:
|
||||
return
|
||||
|
||||
# Numeric keys 1-4 to buy
|
||||
var key: Key = event.keycode
|
||||
var index: int = -1
|
||||
match key:
|
||||
KEY_1, KEY_KP_1: index = 0
|
||||
KEY_2, KEY_KP_2: index = 1
|
||||
KEY_3, KEY_KP_3: index = 2
|
||||
KEY_4, KEY_KP_4: index = 3
|
||||
|
||||
if index >= 0 and index < _weapon_buttons.size():
|
||||
var btn: Button = _weapon_buttons[index]
|
||||
if btn and not btn.disabled and event.pressed:
|
||||
btn.emit_signal("pressed")
|
||||
accept_event()
|
||||
|
||||
# ESC to close
|
||||
if event.keycode == KEY_ESCAPE and event.pressed:
|
||||
close()
|
||||
accept_event()
|
||||
@@ -0,0 +1 @@
|
||||
uid://cdsyn134gsca8
|
||||
@@ -0,0 +1,348 @@
|
||||
## WeaponManager — hitscan weapon system for tactical FPS.
|
||||
##
|
||||
## Manages a single hitscan weapon: fire rate limiting, ammo, reload.
|
||||
## Communicates with SimulationServer for server-authoritative hit detection.
|
||||
##
|
||||
## Architecture:
|
||||
## - Client-side: fire rate limiting, muzzle VFX, hit marker feedback
|
||||
## - Server-side (via SimulationServer): hit detection, damage, lag compensation
|
||||
## - Both sides: ammo tracking (server-authoritative, client-predicted)
|
||||
##
|
||||
## Usage (attach as child of FPSCharacterController):
|
||||
##
|
||||
## FPSCharacterController
|
||||
## ├── WeaponManager — fire rate, ammo, VFX
|
||||
## │ ├── MuzzleFlash — instantiated VFX
|
||||
## │ └── HitMarker — instantiated VFX
|
||||
## └── FpsCamera
|
||||
##
|
||||
class_name WeaponManager
|
||||
extends Node
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Signals
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Emitted when the weapon fires (for visual feedback).
|
||||
signal weapon_fired(origin: Vector3, direction: Vector3)
|
||||
|
||||
## Emitted when this weapon hits an entity.
|
||||
signal hit_marked(hit_pos: Vector3, damage: float, killed: bool)
|
||||
|
||||
## Emitted when ammo changes.
|
||||
signal ammo_changed(current: int, max: int)
|
||||
|
||||
## Emitted when player is killed.
|
||||
signal player_killed()
|
||||
|
||||
## Emitted when the player switches weapons (for animation/UI).
|
||||
signal weapon_switched(index: int, weapon_id: String)
|
||||
|
||||
## Emitted when the current weapon fires (for animation/visual sync).
|
||||
## Connected by animation players or VFX systems on the weapon model.
|
||||
signal fire_animation_triggered()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Exports — Assault Rifle Config
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Maximum ammo capacity.
|
||||
@export var max_ammo: int = 30
|
||||
|
||||
## Starting ammo (including one magazine).
|
||||
@export var start_ammo: int = 90
|
||||
|
||||
## Time between shots (seconds). 10 Hz = 0.1s between shots.
|
||||
@export var fire_rate: float = 0.1
|
||||
|
||||
## Reload time in seconds.
|
||||
@export var reload_time: float = 2.0
|
||||
|
||||
## Spread increases while firing (degrees).
|
||||
@export var spread_per_shot: float = 0.5
|
||||
|
||||
## Max spread when firing continuously (degrees).
|
||||
@export var max_spread: float = 4.0
|
||||
|
||||
## Spread recovery per second (degrees/s).
|
||||
@export var spread_recovery: float = 8.0
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# State
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
var current_ammo: int = 30
|
||||
var reserve_ammo: int = 90
|
||||
var is_reloading: bool = false
|
||||
var _last_shot_time: float = 0.0
|
||||
var _current_spread: float = 0.0
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Multi-weapon inventory state
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Array of weapon IDs the player carries (the inventory).
|
||||
var _weapon_inventory: Array[String] = []
|
||||
|
||||
## Index into _weapon_inventory for the currently equipped weapon.
|
||||
var _current_weapon_index: int = -1
|
||||
|
||||
## Per-weapon persistent state saved when switching away, restored on switch-back.
|
||||
## weapon_id -> { ammo, reserve, is_reloading, last_shot_time, current_spread }
|
||||
var _weapon_states: Dictionary = {}
|
||||
|
||||
## Reference to the parent FPSCharacterController (set in _ready).
|
||||
var _controller: FPSCharacterController = null
|
||||
|
||||
## Reference to SimulationServer (if available).
|
||||
var _server: Object = null
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lifecycle
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _ready() -> void:
|
||||
_controller = get_parent() as FPSCharacterController
|
||||
if _controller == null:
|
||||
push_warning("WeaponManager: Parent is not FPSCharacterController. Disabled.")
|
||||
set_process(false)
|
||||
return
|
||||
|
||||
# Find SimulationServer singleton
|
||||
if Engine.has_singleton("SimulationServer"):
|
||||
_server = Engine.get_singleton("SimulationServer")
|
||||
elif Engine.get_main_loop() and Engine.get_main_loop().root:
|
||||
# Try finding it in the scene tree
|
||||
var game_server = Engine.get_main_loop().root.find_child("GameServer", true, false)
|
||||
if game_server and game_server.has_method(&"get_simulation_server"):
|
||||
_server = game_server.get_simulation_server()
|
||||
|
||||
# Initialize ammo
|
||||
current_ammo = max_ammo
|
||||
reserve_ammo = start_ammo
|
||||
|
||||
# Signal that ammo changed
|
||||
ammo_changed.emit(current_ammo, max_ammo)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main loop — called from FPSCharacterController._physics_process per tick
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Try to fire the weapon. Returns true if a shot was fired.
|
||||
## Called by the controller when shoot input is detected.
|
||||
func try_fire(time: float) -> bool:
|
||||
# Check fire rate
|
||||
if time - _last_shot_time < fire_rate:
|
||||
return false
|
||||
|
||||
# Check ammo
|
||||
if current_ammo <= 0:
|
||||
# Auto-reload on empty — or play empty-click sound
|
||||
if not is_reloading:
|
||||
start_reload()
|
||||
return false
|
||||
|
||||
# Check reloading
|
||||
if is_reloading:
|
||||
return false
|
||||
|
||||
# Deduct ammo
|
||||
current_ammo -= 1
|
||||
_last_shot_time = time
|
||||
|
||||
# Increase spread
|
||||
_current_spread = min(_current_spread + spread_per_shot, max_spread)
|
||||
|
||||
# Persist state back to per-weapon dict
|
||||
_save_current_weapon_state()
|
||||
|
||||
# Emit fire signal (for visual/hit marker)
|
||||
# Direction comes from the controller's look direction
|
||||
if _controller:
|
||||
var origin: Vector3 = _controller.global_position
|
||||
origin.y += _controller.eye_height_stand
|
||||
var direction: Vector3 = _controller.get_look_direction()
|
||||
weapon_fired.emit(origin, direction)
|
||||
|
||||
ammo_changed.emit(current_ammo, max_ammo)
|
||||
fire_animation_triggered.emit()
|
||||
return true
|
||||
|
||||
## Called every frame to recover spread and handle spread animation.
|
||||
func _process(delta: float) -> void:
|
||||
# Recover spread
|
||||
if _current_spread > 0.0:
|
||||
_current_spread = max(0.0, _current_spread - spread_recovery * delta)
|
||||
|
||||
## Start reloading.
|
||||
func start_reload() -> void:
|
||||
if is_reloading or current_ammo >= max_ammo or reserve_ammo <= 0:
|
||||
return
|
||||
|
||||
is_reloading = true
|
||||
await get_tree().create_timer(reload_time).timeout
|
||||
|
||||
# Reload logic
|
||||
var needed: int = max_ammo - current_ammo
|
||||
var from_reserve: int = min(needed, reserve_ammo)
|
||||
current_ammo += from_reserve
|
||||
reserve_ammo -= from_reserve
|
||||
is_reloading = false
|
||||
|
||||
ammo_changed.emit(current_ammo, max_ammo)
|
||||
_save_current_weapon_state()
|
||||
|
||||
## Called when a hit is confirmed by the server.
|
||||
func on_hit_confirmed(hit_pos: Vector3, damage: float, killed: bool) -> void:
|
||||
hit_marked.emit(hit_pos, damage, killed)
|
||||
if killed:
|
||||
player_killed.emit()
|
||||
|
||||
## Get current spread (degrees).
|
||||
func get_current_spread() -> float:
|
||||
return _current_spread
|
||||
|
||||
## Get the current weapon fire direction including spread cone.
|
||||
func get_spread_direction(base_direction: Vector3) -> Vector3:
|
||||
if _current_spread <= 0.001:
|
||||
return base_direction
|
||||
|
||||
var spread_rad: float = deg_to_rad(_current_spread)
|
||||
var theta: float = randf() * TAU
|
||||
var phi: float = randf() * spread_rad
|
||||
|
||||
# Create a random perpendicular vector
|
||||
var up := Vector3.UP
|
||||
if abs(base_direction.dot(up)) > 0.99:
|
||||
up = Vector3.RIGHT
|
||||
var right := base_direction.cross(up).normalized()
|
||||
up = right.cross(base_direction).normalized()
|
||||
|
||||
var offset := right * sin(theta) * sin(phi) + up * cos(theta) * sin(phi)
|
||||
return (base_direction + offset).normalized()
|
||||
|
||||
## Reset weapon state (for respawn).
|
||||
func reset() -> void:
|
||||
# Re-initialise all weapon states in inventory
|
||||
for wid in _weapon_inventory:
|
||||
_initialize_weapon_state(wid)
|
||||
# Apply current weapon (or use defaults if no inventory)
|
||||
if _current_weapon_index >= 0 and _current_weapon_index < _weapon_inventory.size():
|
||||
_apply_current_weapon()
|
||||
else:
|
||||
current_ammo = max_ammo
|
||||
reserve_ammo = start_ammo
|
||||
is_reloading = false
|
||||
_current_spread = 0.0
|
||||
_last_shot_time = 0.0
|
||||
ammo_changed.emit(current_ammo, max_ammo)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Weapon inventory management
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Add a weapon to the player's inventory.
|
||||
## If no weapon is currently equipped, this one is automatically selected.
|
||||
func add_weapon(weapon_id: String) -> void:
|
||||
if weapon_id in _weapon_inventory:
|
||||
return
|
||||
_weapon_inventory.append(weapon_id)
|
||||
_initialize_weapon_state(weapon_id)
|
||||
if _current_weapon_index < 0:
|
||||
_current_weapon_index = _weapon_inventory.size() - 1
|
||||
_apply_current_weapon()
|
||||
|
||||
## Switch to a weapon by its inventory index.
|
||||
## Returns true if the switch was performed.
|
||||
func switch_weapon(index: int) -> bool:
|
||||
if index < 0 or index >= _weapon_inventory.size():
|
||||
return false
|
||||
if index == _current_weapon_index:
|
||||
return false
|
||||
_save_current_weapon_state()
|
||||
_current_weapon_index = index
|
||||
_apply_current_weapon()
|
||||
weapon_switched.emit(index, _weapon_inventory[index])
|
||||
return true
|
||||
|
||||
## Return the WeaponData for the currently equipped weapon, or null.
|
||||
func current_weapon_data() -> WeaponData:
|
||||
var wid := get_current_weapon_id()
|
||||
if wid.is_empty():
|
||||
return null
|
||||
return WeaponDefinitions.get_weapon(wid)
|
||||
|
||||
## Return the current weapon's ID string, or empty string.
|
||||
func get_current_weapon_id() -> String:
|
||||
if _current_weapon_index >= 0 and _current_weapon_index < _weapon_inventory.size():
|
||||
return _weapon_inventory[_current_weapon_index]
|
||||
return ""
|
||||
|
||||
## Return the number of weapons in the inventory.
|
||||
func get_weapon_count() -> int:
|
||||
return _weapon_inventory.size()
|
||||
|
||||
## Return the list of weapon IDs in the inventory.
|
||||
func get_inventory() -> Array[String]:
|
||||
return _weapon_inventory.duplicate()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Internal: per-weapon state management
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Initialise state dict for a weapon with default values.
|
||||
func _initialize_weapon_state(weapon_id: String) -> void:
|
||||
var data := WeaponDefinitions.get_weapon(weapon_id)
|
||||
if data == null:
|
||||
return
|
||||
_weapon_states[weapon_id] = {
|
||||
ammo = data.mag_size,
|
||||
reserve = data.mag_size * 3,
|
||||
is_reloading = false,
|
||||
last_shot_time = 0.0,
|
||||
current_spread = 0.0,
|
||||
}
|
||||
|
||||
## Save the current weapon's runtime state back into _weapon_states.
|
||||
func _save_current_weapon_state() -> void:
|
||||
var wid := get_current_weapon_id()
|
||||
if wid.is_empty() or not (wid in _weapon_states):
|
||||
return
|
||||
_weapon_states[wid] = {
|
||||
ammo = current_ammo,
|
||||
reserve = reserve_ammo,
|
||||
is_reloading = is_reloading,
|
||||
last_shot_time = _last_shot_time,
|
||||
current_spread = _current_spread,
|
||||
}
|
||||
|
||||
## Load the weapon at _current_weapon_index and apply its data + state
|
||||
## to the live export properties and state variables.
|
||||
func _apply_current_weapon() -> void:
|
||||
var wid := get_current_weapon_id()
|
||||
if wid.is_empty():
|
||||
return
|
||||
var data := WeaponDefinitions.get_weapon(wid)
|
||||
if data == null:
|
||||
return
|
||||
|
||||
# Set export defaults from weapon data
|
||||
max_ammo = data.mag_size
|
||||
start_ammo = data.mag_size * 4 # 1 in mag + 3 reserve = 4x
|
||||
fire_rate = 1.0 / max(data.fire_rate, 0.001)
|
||||
reload_time = data.reload_time
|
||||
spread_per_shot = data.spread_degrees * 0.1
|
||||
max_spread = data.spread_degrees * 3.0
|
||||
spread_recovery = data.spread_degrees * 8.0
|
||||
|
||||
# Restore per-weapon state
|
||||
var st: Dictionary = _weapon_states.get(wid, {})
|
||||
if st.is_empty():
|
||||
_initialize_weapon_state(wid)
|
||||
st = _weapon_states.get(wid, {})
|
||||
current_ammo = st.get("ammo", data.mag_size)
|
||||
reserve_ammo = st.get("reserve", data.mag_size * 3)
|
||||
is_reloading = st.get("is_reloading", false)
|
||||
_last_shot_time = st.get("last_shot_time", 0.0)
|
||||
_current_spread = st.get("current_spread", 0.0)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dywlbha8xivx4
|
||||
@@ -0,0 +1 @@
|
||||
uid://q8nutj781fyf
|
||||
@@ -0,0 +1,610 @@
|
||||
@tool
|
||||
extends SceneTree
|
||||
|
||||
# =============================================================================
|
||||
# bake_lightmaps.gd — LightmapGI Configuration Checker
|
||||
# =============================================================================
|
||||
# Validates LightmapGI configuration for baking readiness in a Godot 4 scene.
|
||||
# Checks:
|
||||
# - LightmapGI node exists and is properly configured
|
||||
# - Quality setting (Low/Medium/High/Ultra)
|
||||
# - Bounce count (recommended ≥ 2)
|
||||
# - Texel scale (recommended 0.5–4.0, default 1.0)
|
||||
# - Interior flag (true for indoor maps)
|
||||
# - Denoiser enabled
|
||||
# - Max texture size (recommended ≤ 1024)
|
||||
# - Light data presence (already baked or not)
|
||||
# - Geometry with UV2 (required for baking)
|
||||
#
|
||||
# Note: Godot 4 LightmapGI baking is editor-only. This script validates
|
||||
# configuration readiness; it does NOT perform the bake itself.
|
||||
#
|
||||
# Usage:
|
||||
# godot --headless --script client/scripts/bake_lightmaps.gd -- res://path/to/map.tscn
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 — All config checks passed
|
||||
# 1 — One or more config checks failed
|
||||
# 2 — Scene file not found or cannot be loaded
|
||||
# =============================================================================
|
||||
|
||||
const RECOMMENDED_BOUNCES := 2
|
||||
const MIN_BOUNCES := 1
|
||||
const MAX_BOUNCES := 8
|
||||
const RECOMMENDED_TEXEL_SCALE := 1.0
|
||||
const MIN_TEXEL_SCALE := 0.1
|
||||
const MAX_TEXEL_SCALE := 10.0
|
||||
const MAX_TEXTURE_SIZE := 1024
|
||||
const RECOMMENDED_QUALITY := 2 # High
|
||||
|
||||
var _pass_count := 0
|
||||
var _fail_count := 0
|
||||
var _warning_count := 0
|
||||
var _scene_path := ""
|
||||
var _scene_instance: Node = null
|
||||
var _lightmap_node: Node = null
|
||||
|
||||
# UV2 tracking
|
||||
var _total_meshes := 0
|
||||
var _meshes_with_uv2 := 0
|
||||
var _meshes_without_uv2: Array[String] = []
|
||||
var _mesh_details: Array[Dictionary] = []
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
_parse_arguments()
|
||||
|
||||
if _scene_path.is_empty():
|
||||
printerr("ERROR: No scene path provided. Usage: godot --headless --script bake_lightmaps.gd -- res://path/to/map.tscn")
|
||||
quit(2)
|
||||
return
|
||||
|
||||
print("")
|
||||
print("==========================================")
|
||||
print(" LIGHTMAP CONFIGURATION CHECKER")
|
||||
print(" Scene: ", _scene_path)
|
||||
print("==========================================")
|
||||
print("")
|
||||
|
||||
print(" Note: Godot 4 LightmapGI baking is editor-only.")
|
||||
print(" This tool validates lightmap configuration readiness only.")
|
||||
print(" To bake: Open scene in editor > Scene > Bake LightmapGI")
|
||||
print("")
|
||||
|
||||
var scene: PackedScene = _load_scene(_scene_path)
|
||||
if scene == null:
|
||||
printerr("ERROR: Scene not found: ", _scene_path)
|
||||
quit(2)
|
||||
return
|
||||
|
||||
_scene_instance = _instantiate_scene(scene)
|
||||
if _scene_instance == null:
|
||||
printerr("ERROR: Failed to instantiate scene: ", _scene_path)
|
||||
quit(2)
|
||||
return
|
||||
|
||||
root.add_child(_scene_instance)
|
||||
|
||||
# Find LightmapGI
|
||||
_lightmap_node = _find_node_by_type(_scene_instance, "LightmapGI")
|
||||
|
||||
# Run all checks
|
||||
_run_lightmap_node_check()
|
||||
if _lightmap_node != null:
|
||||
_run_quality_check()
|
||||
_run_bounce_check()
|
||||
_run_texel_scale_check()
|
||||
_run_interior_check()
|
||||
_run_denoiser_check()
|
||||
_run_max_texture_size_check()
|
||||
_run_energy_check()
|
||||
_run_baked_data_check()
|
||||
_run_process_mode_check()
|
||||
|
||||
# Run geometry UV2 check
|
||||
_run_uv2_check()
|
||||
|
||||
# Detailed reports
|
||||
_print_lightmap_report()
|
||||
_print_uv2_report()
|
||||
|
||||
# Summary
|
||||
_print_summary()
|
||||
|
||||
if _fail_count > 0:
|
||||
quit(1)
|
||||
else:
|
||||
quit(0)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Argument parsing
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _parse_arguments() -> void:
|
||||
"""Extract scene path from user arguments (after --)."""
|
||||
var args: PackedStringArray = OS.get_cmdline_user_args()
|
||||
if not args.is_empty():
|
||||
_scene_path = args[0].strip_edges()
|
||||
return
|
||||
|
||||
var full_args: PackedStringArray = OS.get_cmdline_args()
|
||||
var found_sep := false
|
||||
for i in range(full_args.size()):
|
||||
if full_args[i] == "--":
|
||||
found_sep = true
|
||||
continue
|
||||
if found_sep:
|
||||
_scene_path = full_args[i].strip_edges()
|
||||
break
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Scene helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _load_scene(path: String) -> PackedScene:
|
||||
"""Load a .tscn/.scn file."""
|
||||
if not ResourceLoader.exists(path):
|
||||
if not path.ends_with(".tscn") and not path.ends_with(".scn"):
|
||||
path = path + ".tscn"
|
||||
if not ResourceLoader.exists(path):
|
||||
return null
|
||||
return ResourceLoader.load(path, "PackedScene", ResourceLoader.CACHE_MODE_REUSE)
|
||||
|
||||
|
||||
func _instantiate_scene(scene: PackedScene) -> Node:
|
||||
return scene.instantiate()
|
||||
|
||||
|
||||
func _find_node_by_type(node: Node, type_name: String) -> Node:
|
||||
"""Recursively find first node matching a class name."""
|
||||
if node.get_class() == type_name:
|
||||
return node
|
||||
for child in node.get_children():
|
||||
var found := _find_node_by_type(child, type_name)
|
||||
if found != null:
|
||||
return found
|
||||
return null
|
||||
|
||||
|
||||
func _find_nodes_by_type(node: Node, type_name: String) -> Array[Node]:
|
||||
"""Recursively find all nodes of a given class."""
|
||||
var results: Array[Node] = []
|
||||
if node.get_class() == type_name:
|
||||
results.append(node)
|
||||
for child in node.get_children():
|
||||
results.append_array(_find_nodes_by_type(child, type_name))
|
||||
return results
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Check helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _pass(msg: String) -> void:
|
||||
_pass_count += 1
|
||||
print(" ✓ ", msg)
|
||||
|
||||
|
||||
func _fail(msg: String) -> void:
|
||||
_fail_count += 1
|
||||
print(" ✗ ", msg)
|
||||
|
||||
|
||||
func _warn(msg: String) -> void:
|
||||
_warning_count += 1
|
||||
print(" ⚠ ", msg)
|
||||
|
||||
|
||||
func _check(condition: bool, msg_pass: String, msg_fail: String) -> bool:
|
||||
if condition:
|
||||
_pass(msg_pass)
|
||||
else:
|
||||
_fail(msg_fail)
|
||||
return condition
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Individual LightmapGI checks
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _run_lightmap_node_check() -> void:
|
||||
"""Verify LightmapGI node exists."""
|
||||
print("--- LightmapGI Node ---")
|
||||
if _lightmap_node != null:
|
||||
_pass("LightmapGI node found: '" + _lightmap_node.name + "'")
|
||||
else:
|
||||
_fail("LightmapGI node not found in scene — required for baked lighting")
|
||||
|
||||
|
||||
func _run_quality_check() -> void:
|
||||
"""Validate LightmapGI quality setting."""
|
||||
print("")
|
||||
print("--- Quality ---")
|
||||
if _lightmap_node == null:
|
||||
return
|
||||
|
||||
var quality: int = _lightmap_node.get("quality") if "quality" in _lightmap_node else -1
|
||||
if quality < 0:
|
||||
_fail("LightmapGI.quality not available")
|
||||
return
|
||||
|
||||
var quality_names := ["Low", "Medium", "High", "Ultra"]
|
||||
var quality_name := quality_names[quality] if quality < quality_names.size() else "Unknown"
|
||||
|
||||
if quality >= RECOMMENDED_QUALITY:
|
||||
_pass("LightmapGI.quality = " + quality_name + " (" + str(quality) + ") — High or Ultra recommended")
|
||||
elif quality == 1:
|
||||
_warn("LightmapGI.quality = " + quality_name + " (" + str(quality) + ") — Medium is acceptable, High recommended")
|
||||
else:
|
||||
_fail("LightmapGI.quality = " + quality_name + " (" + str(quality) + ") — set to at least High (2)")
|
||||
|
||||
|
||||
func _run_bounce_check() -> void:
|
||||
"""Validate light bounce count."""
|
||||
print("")
|
||||
print("--- Bounces ---")
|
||||
if _lightmap_node == null:
|
||||
return
|
||||
|
||||
var bounces: int = _lightmap_node.get("bounces") if "bounces" in _lightmap_node else -1
|
||||
if bounces < 0:
|
||||
_fail("LightmapGI.bounces not available")
|
||||
return
|
||||
|
||||
if bounces >= RECOMMENDED_BOUNCES:
|
||||
_pass("LightmapGI.bounces = " + str(bounces) + " (recommended: ≥ " + str(RECOMMENDED_BOUNCES) + ")")
|
||||
elif bounces >= MIN_BOUNCES:
|
||||
_warn("LightmapGI.bounces = " + str(bounces) + " — low, consider ≥ " + str(RECOMMENDED_BOUNCES))
|
||||
else:
|
||||
_fail("LightmapGI.bounces = " + str(bounces) + " — minimum is " + str(MIN_BOUNCES))
|
||||
|
||||
# Warn if too high (performance concern)
|
||||
if bounces > MAX_BOUNCES - 2:
|
||||
_warn("LightmapGI.bounces = " + str(bounces) + " — high bounce count may impact bake time")
|
||||
|
||||
|
||||
func _run_texel_scale_check() -> void:
|
||||
"""Validate texel scale setting."""
|
||||
print("")
|
||||
print("--- Texel Scale ---")
|
||||
if _lightmap_node == null:
|
||||
return
|
||||
|
||||
var texel_scale: float = _lightmap_node.get("texel_scale") if "texel_scale" in _lightmap_node else -1.0
|
||||
if texel_scale < 0:
|
||||
_fail("LightmapGI.texel_scale not available")
|
||||
return
|
||||
|
||||
if abs(texel_scale - RECOMMENDED_TEXEL_SCALE) < 0.01:
|
||||
_pass("LightmapGI.texel_scale = " + str(texel_scale) + " (exactly " + str(RECOMMENDED_TEXEL_SCALE) + ")")
|
||||
elif texel_scale >= MIN_TEXEL_SCALE and texel_scale <= MAX_TEXEL_SCALE:
|
||||
if texel_scale < 0.5:
|
||||
_warn("LightmapGI.texel_scale = " + str(texel_scale) + " — below 0.5, bake resolution may be too low")
|
||||
elif texel_scale > 2.0:
|
||||
_warn("LightmapGI.texel_scale = " + str(texel_scale) + " — above 2.0, bake resolution may be unnecessarily high")
|
||||
else:
|
||||
_pass("LightmapGI.texel_scale = " + str(texel_scale) + " (within reasonable range [0.1, 10.0])")
|
||||
else:
|
||||
_fail("LightmapGI.texel_scale = " + str(texel_scale) + " — out of expected range [" + str(MIN_TEXEL_SCALE) + ", " + str(MAX_TEXEL_SCALE) + "]")
|
||||
|
||||
|
||||
func _run_interior_check() -> void:
|
||||
"""Validate interior flag."""
|
||||
print("")
|
||||
print("--- Interior ---")
|
||||
if _lightmap_node == null:
|
||||
return
|
||||
|
||||
var interior: bool = _lightmap_node.get("interior") if "interior" in _lightmap_node else false
|
||||
|
||||
if interior:
|
||||
_pass("LightmapGI.interior = true (correct for indoor maps)")
|
||||
else:
|
||||
_warn("LightmapGI.interior = false — set to true for indoor scenes to avoid light leaking from outside")
|
||||
|
||||
|
||||
func _run_denoiser_check() -> void:
|
||||
"""Validate denoiser setting."""
|
||||
print("")
|
||||
print("--- Denoiser ---")
|
||||
if _lightmap_node == null:
|
||||
return
|
||||
|
||||
# Godot 4 renamed to use_denoiser
|
||||
var use_denoiser: bool = false
|
||||
if "use_denoiser" in _lightmap_node:
|
||||
use_denoiser = _lightmap_node.get("use_denoiser")
|
||||
elif _lightmap_node.has_method("is_using_denoiser"):
|
||||
use_denoiser = _lightmap_node.is_using_denoiser()
|
||||
|
||||
if use_denoiser:
|
||||
_pass("LightmapGI.use_denoiser = true (recommended for cleaner results)")
|
||||
else:
|
||||
_warn("LightmapGI.use_denoiser = false — enable denoiser for cleaner lightmap results")
|
||||
|
||||
|
||||
func _run_max_texture_size_check() -> void:
|
||||
"""Validate max texture size setting."""
|
||||
print("")
|
||||
print("--- Max Texture Size ---")
|
||||
if _lightmap_node == null:
|
||||
return
|
||||
|
||||
var max_tex: int = _lightmap_node.get("max_texture_size") if "max_texture_size" in _lightmap_node else -1
|
||||
if max_tex < 0:
|
||||
_warn("LightmapGI.max_texture_size not available (using default)")
|
||||
return
|
||||
|
||||
if max_tex <= MAX_TEXTURE_SIZE:
|
||||
_pass("LightmapGI.max_texture_size = " + str(max_tex) + " (≤ " + str(MAX_TEXTURE_SIZE) + ")")
|
||||
else:
|
||||
_fail("LightmapGI.max_texture_size = " + str(max_tex) + " — exceeds " + str(MAX_TEXTURE_SIZE) +
|
||||
" (consider lowering for memory budget)")
|
||||
|
||||
|
||||
func _run_energy_check() -> void:
|
||||
"""Validate lightmap energy setting."""
|
||||
print("")
|
||||
print("--- Energy ---")
|
||||
if _lightmap_node == null:
|
||||
return
|
||||
|
||||
var energy: float = _lightmap_node.get("energy") if "energy" in _lightmap_node else 1.0
|
||||
_pass("LightmapGI.energy = " + str(energy) + " (verify visually in editor)")
|
||||
|
||||
var indirect_energy: float = _lightmap_node.get("indirect_energy") if "indirect_energy" in _lightmap_node else 1.0
|
||||
if abs(indirect_energy - 1.0) > 0.01:
|
||||
_warn("LightmapGI.indirect_energy = " + str(indirect_energy) + " (default is 1.0)")
|
||||
|
||||
|
||||
func _run_baked_data_check() -> void:
|
||||
"""Check whether lightmap data has been baked."""
|
||||
print("")
|
||||
print("--- Baked Data ---")
|
||||
if _lightmap_node == null:
|
||||
return
|
||||
|
||||
var light_data = _lightmap_node.get("light_data") if "light_data" in _lightmap_node else null
|
||||
if light_data != null:
|
||||
_pass("LightmapGI has baked light data present")
|
||||
# Check data size
|
||||
if light_data.has_method("get_user_data"):
|
||||
var user_data = light_data.get_user_data()
|
||||
if user_data != null:
|
||||
_warn("Lightmap data size: varies (baked data is present)")
|
||||
else:
|
||||
_warn("LightmapGI has NO baked data yet — needs baking in editor")
|
||||
|
||||
|
||||
func _run_process_mode_check() -> void:
|
||||
"""Validate the lightmap process mode."""
|
||||
print("")
|
||||
print("--- Process Mode ---")
|
||||
if _lightmap_node == null:
|
||||
return
|
||||
|
||||
# Godot 4 LightmapGI has a bake_mode or similar property for sub-surface scattering
|
||||
if "bake_mode" in _lightmap_node:
|
||||
var bake_mode: int = _lightmap_node.get("bake_mode")
|
||||
match bake_mode:
|
||||
0:
|
||||
_pass("LightmapGI.bake_mode = Conservative (0) — safe default")
|
||||
1:
|
||||
_warn("LightmapGI.bake_mode = Aggressive (1) — may cause artifacts")
|
||||
2:
|
||||
_pass("LightmapGI.bake_mode = Always Lightmap (2) — good for fully baked scenes")
|
||||
_:
|
||||
_warn("LightmapGI.bake_mode = " + str(bake_mode) + " — unknown")
|
||||
|
||||
# Check for environment mode (if property exists)
|
||||
if "environment_mode" in _lightmap_node:
|
||||
var env_mode: int = _lightmap_node.get("environment_mode")
|
||||
match env_mode:
|
||||
0:
|
||||
_pass("LightmapGI.environment_mode = Disabled — no environment lighting (indoor)")
|
||||
1:
|
||||
_pass("LightmapGI.environment_mode = Enabled — using environment for lighting")
|
||||
2:
|
||||
_warn("LightmapGI.environment_mode = Ambient Color — verify color is correct")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# UV2 geometry check
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _run_uv2_check() -> void:
|
||||
"""Scan all MeshInstance3D nodes for UV2 channel presence."""
|
||||
print("")
|
||||
print("--- Geometry UV2 Check ---")
|
||||
|
||||
var mesh_nodes := _find_nodes_by_type(_scene_instance, "MeshInstance3D")
|
||||
_total_meshes = mesh_nodes.size()
|
||||
|
||||
if _total_meshes == 0:
|
||||
_warn("No MeshInstance3D nodes found — checking CSG nodes for UV2")
|
||||
|
||||
# Check CSG nodes instead
|
||||
var csg_nodes := _find_nodes_by_type(_scene_instance, "CSGCombiner3D")
|
||||
csg_nodes.append_array(_find_nodes_by_type(_scene_instance, "CSGBox3D"))
|
||||
csg_nodes.append_array(_find_nodes_by_type(_scene_instance, "CSGSphere3D"))
|
||||
csg_nodes.append_array(_find_nodes_by_type(_scene_instance, "CSGCylinder3D"))
|
||||
csg_nodes.append_array(_find_nodes_by_type(_scene_instance, "CSGTorus3D"))
|
||||
csg_nodes.append_array(_find_nodes_by_type(_scene_instance, "CSGPolygon3D"))
|
||||
csg_nodes.append_array(_find_nodes_by_type(_scene_instance, "CSGMesh3D"))
|
||||
|
||||
# CSG nodes generate UV2 during baking automatically
|
||||
if csg_nodes.size() > 0:
|
||||
_pass("Scene uses CSG primitives (" + str(csg_nodes.size()) + ") — UV2 is auto-generated during bake")
|
||||
return
|
||||
|
||||
for mi in mesh_nodes:
|
||||
var has_uv2 := false
|
||||
if "mesh" in mi and mi.mesh != null:
|
||||
has_uv2 = _check_mesh_uv2(mi.mesh)
|
||||
|
||||
var info := {
|
||||
"name": mi.name,
|
||||
"has_uv2": has_uv2,
|
||||
"material_count": 0
|
||||
}
|
||||
_mesh_details.append(info)
|
||||
|
||||
if has_uv2:
|
||||
_meshes_with_uv2 += 1
|
||||
else:
|
||||
_meshes_without_uv2.append(mi.name)
|
||||
|
||||
if _meshes_without_uv2.size() == 0:
|
||||
_pass("All " + str(_total_meshes) + " MeshInstance3D nodes have UV2 channel")
|
||||
else:
|
||||
for mesh_name in _meshes_without_uv2:
|
||||
_fail("MeshInstance3D '" + mesh_name + "' lacks UV2 channel — lightmapping will not work")
|
||||
_warn(str(_meshes_without_uv2.size()) + "/" + str(_total_meshes) + " meshes missing UV2")
|
||||
|
||||
# Check for sub-surface scattering override on LightmapGI
|
||||
if _lightmap_node != null and "subdiv" in _lightmap_node:
|
||||
var subdiv: int = _lightmap_node.get("subdiv")
|
||||
if subdiv > 0:
|
||||
_warn("LightmapGI.subdiv = " + str(subdiv) + " — non-zero subdiv enables subsurface scattering")
|
||||
|
||||
|
||||
func _check_mesh_uv2(mesh) -> bool:
|
||||
"""Check if a mesh has UV2 channel data."""
|
||||
if mesh == null:
|
||||
return false
|
||||
if not mesh.has_method("get_surface_count"):
|
||||
return false
|
||||
|
||||
var surface_count := mesh.get_surface_count()
|
||||
for s in range(surface_count):
|
||||
var arrays := mesh.surface_get_arrays(s)
|
||||
if arrays is Array and arrays.size() > 0:
|
||||
var uv2 := arrays[Mesh.ARRAY_TEX_UV2] as PackedVector2Array
|
||||
if uv2 != null and uv2.size() > 0:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Reports
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _print_lightmap_report() -> void:
|
||||
"""Print a detailed lightmap configuration report."""
|
||||
print("")
|
||||
print("--- LightmapGI Configuration Report ---")
|
||||
if _lightmap_node == null:
|
||||
print(" (no LightmapGI node to report)")
|
||||
return
|
||||
|
||||
print(" Node path: ", _get_node_path(_lightmap_node))
|
||||
var quality_names := ["Low", "Medium", "High", "Ultra"]
|
||||
var q: int = _lightmap_node.get("quality") if "quality" in _lightmap_node else -1
|
||||
if q >= 0 and q < quality_names.size():
|
||||
print(" Quality: ", quality_names[q], " (", q, ")")
|
||||
else:
|
||||
print(" Quality: ", q)
|
||||
print(" Bounces: ", _lightmap_node.get("bounces") if "bounces" in _lightmap_node else "N/A")
|
||||
print(" Texel Scale: ", _lightmap_node.get("texel_scale") if "texel_scale" in _lightmap_node else "N/A")
|
||||
print(" Interior: ", _lightmap_node.get("interior") if "interior" in _lightmap_node else "N/A")
|
||||
print(" Denoiser: ", _lightmap_node.get("use_denoiser") if "use_denoiser" in _lightmap_node else "N/A")
|
||||
print(" Max Tex Size: ", _lightmap_node.get("max_texture_size") if "max_texture_size" in _lightmap_node else "N/A")
|
||||
print(" Energy: ", _lightmap_node.get("energy") if "energy" in _lightmap_node else "N/A")
|
||||
print(" Baked Data: ", "Yes" if _lightmap_node.get("light_data") != null else "No")
|
||||
print("")
|
||||
|
||||
|
||||
func _print_uv2_report() -> void:
|
||||
"""Print UV2 details for meshes."""
|
||||
print("--- UV2 Report ---")
|
||||
print(" Total MeshInstance3D: ", _total_meshes)
|
||||
print(" With UV2: ", _meshes_with_uv2)
|
||||
print(" Without UV2: ", _meshes_without_uv2.size())
|
||||
if _meshes_without_uv2.size() > 0:
|
||||
print(" Missing UV2 list:")
|
||||
for name in _meshes_without_uv2:
|
||||
print(" - ", name)
|
||||
print("")
|
||||
|
||||
|
||||
func _get_node_path(node: Node) -> String:
|
||||
"""Get the scene-tree path of a node."""
|
||||
var parts: Array[String] = []
|
||||
var current := node
|
||||
while current != null and current != _scene_instance:
|
||||
parts.push_front(current.name)
|
||||
current = current.get_parent()
|
||||
return "/".join(parts)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Summary
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _print_summary() -> void:
|
||||
"""Print final summary."""
|
||||
print("==========================================")
|
||||
print(" LIGHTMAP CONFIG SUMMARY")
|
||||
print("==========================================")
|
||||
print(" Scene: ", _scene_path)
|
||||
print(" LightmapGI: ", "Found ✓" if _lightmap_node != null else "Missing ✗")
|
||||
print(" Quality OK: ", _check_quality_status())
|
||||
print(" Bounces OK: ", _check_bounce_status())
|
||||
print(" Texel Scale OK: ", _check_texel_status())
|
||||
print(" Interior OK: ", _check_interior_status())
|
||||
print(" Denoiser OK: ", _check_denoiser_status())
|
||||
print(" UV2 OK: ", _check_uv2_status())
|
||||
print("------------------------------------------")
|
||||
print(" Passed: ", _pass_count)
|
||||
print(" Failed: ", _fail_count)
|
||||
print(" Warnings: ", _warning_count)
|
||||
print("------------------------------------------")
|
||||
if _fail_count > 0:
|
||||
print(" RESULT: FAILED ❌")
|
||||
printerr("Lightmap config check FAILED — ", _fail_count, " check(s) failed.")
|
||||
else:
|
||||
print(" RESULT: PASSED ✅")
|
||||
print("==========================================")
|
||||
print("")
|
||||
|
||||
|
||||
func _check_quality_status() -> String:
|
||||
if _lightmap_node == null: return "N/A"
|
||||
var q = _lightmap_node.get("quality") if "quality" in _lightmap_node else -1
|
||||
return "Yes ✓" if q >= RECOMMENDED_QUALITY else ("No ✗" if q >= 0 else "N/A")
|
||||
|
||||
|
||||
func _check_bounce_status() -> String:
|
||||
if _lightmap_node == null: return "N/A"
|
||||
var b = _lightmap_node.get("bounces") if "bounces" in _lightmap_node else -1
|
||||
return "Yes ✓" if b >= RECOMMENDED_BOUNCES else ("No ✗" if b >= 0 else "N/A")
|
||||
|
||||
|
||||
func _check_texel_status() -> String:
|
||||
if _lightmap_node == null: return "N/A"
|
||||
var t = _lightmap_node.get("texel_scale") if "texel_scale" in _lightmap_node else -1.0
|
||||
if t < 0: return "N/A"
|
||||
return "Yes ✓" if t >= 0.5 and t <= 2.0 else "⚠"
|
||||
|
||||
|
||||
func _check_interior_status() -> String:
|
||||
if _lightmap_node == null: return "N/A"
|
||||
var interior = _lightmap_node.get("interior") if "interior" in _lightmap_node else false
|
||||
return "Yes ✓" if interior else "No ✗"
|
||||
|
||||
|
||||
func _check_denoiser_status() -> String:
|
||||
if _lightmap_node == null: return "N/A"
|
||||
if "use_denoiser" in _lightmap_node:
|
||||
return "Yes ✓" if _lightmap_node.get("use_denoiser") else "No ✗"
|
||||
if _lightmap_node.has_method("is_using_denoiser"):
|
||||
return "Yes ✓" if _lightmap_node.is_using_denoiser() else "No ✗"
|
||||
return "N/A"
|
||||
|
||||
|
||||
func _check_uv2_status() -> String:
|
||||
if _total_meshes == 0: return "N/A (CSG)"
|
||||
if _meshes_without_uv2.size() == 0: return "Yes ✓"
|
||||
return str(_meshes_without_uv2.size()) + " missing"
|
||||
@@ -0,0 +1 @@
|
||||
uid://d0mbp6gs2mwlr
|
||||
@@ -0,0 +1,198 @@
|
||||
@tool
|
||||
extends EditorScript
|
||||
|
||||
# CSG → Static Mesh Baker with LOD generation.
|
||||
#
|
||||
# Converts all CSG pieces in the current scene to MeshInstance3D nodes
|
||||
# with proper LOD levels for the final optimization pass.
|
||||
#
|
||||
# This is the definitive LOD solution: once meshes are baked, Godot's
|
||||
# built-in mesh LOD system takes over via the Mesh resource's LOD slots.
|
||||
#
|
||||
# Usage:
|
||||
# 1. Open target scene in the Godot editor
|
||||
# 2. Scene > Run Script (select this file)
|
||||
# 3. Choose whether to replace CSG in-place or save meshes separately
|
||||
#
|
||||
# The script:
|
||||
# a) Finds all CSGShape3D nodes (CSGBox3D, CSGCombiner3D)
|
||||
# b) Calls mesh_to_save() to get the generated mesh
|
||||
# c) Creates MeshInstance3D + CollisionShape3D replacements
|
||||
# d) Generates LOD1 (50%) and LOD2 (25%) via mesh simplification
|
||||
# e) Replaces the CSG node with the baked mesh
|
||||
|
||||
const CREATE_LOD := true
|
||||
const PRESERVE_COLLISION := true
|
||||
const LOD0_DISTANCE := 0.0
|
||||
const LOD1_DISTANCE := 15.0 # 50% tris
|
||||
const LOD2_DISTANCE := 30.0 # 25% tris
|
||||
const LOD_DECIMATE_RATIOS := [0.0, 0.5, 0.25]
|
||||
|
||||
func _run():
|
||||
var scene_root: Node
|
||||
|
||||
if Engine.is_editor_hint():
|
||||
scene_root = get_scene() as Node
|
||||
if not scene_root:
|
||||
printerr("Open a scene in the editor first.")
|
||||
return
|
||||
else:
|
||||
printerr("This script must be run from the Godot editor.")
|
||||
return
|
||||
|
||||
print("=== CSG → Mesh Baker ===")
|
||||
print("Scene: ", scene_root.name)
|
||||
|
||||
var converted := 0
|
||||
var errors := 0
|
||||
|
||||
_bake_node(scene_root, converted, errors)
|
||||
|
||||
print("Converted: ", converted, " CSG nodes")
|
||||
print("Errors: ", errors)
|
||||
|
||||
if converted > 0:
|
||||
print("")
|
||||
print("NOTE: After conversion, re-save the scene to persist mesh data.")
|
||||
print("MeshInstance3D LOD distances: LOD0 < 15m, LOD1 < 30m, LOD2 30m+")
|
||||
print("")
|
||||
print("=== Bake complete ===")
|
||||
|
||||
func _bake_node(node: Node, converted: int, errors: int) -> void:
|
||||
if node is CSGShape3D:
|
||||
var mesh_instance := _convert_csg_to_mesh(node as CSGShape3D)
|
||||
if mesh_instance:
|
||||
# Replace CSG node with mesh instance
|
||||
var parent := node.get_parent()
|
||||
var idx := node.get_index()
|
||||
|
||||
parent.remove_child(node)
|
||||
parent.add_child(mesh_instance, true)
|
||||
parent.move_child(mesh_instance, idx)
|
||||
|
||||
if Engine.is_editor_hint():
|
||||
mesh_instance.set_owner(parent.owner)
|
||||
|
||||
# Clean up the old CSG node
|
||||
node.queue_free()
|
||||
|
||||
converted += 1
|
||||
else:
|
||||
errors += 1
|
||||
|
||||
# Recurse — important: use the original node's children
|
||||
# but the node may have been freed, so iterate a snapshot
|
||||
var children := node.get_children()
|
||||
for child in children:
|
||||
_bake_node(child, converted, errors)
|
||||
|
||||
func _convert_csg_to_mesh(csg: CSGShape3D) -> MeshInstance3D:
|
||||
# Get the generated mesh from the CSG node
|
||||
var mesh := csg.mesh_to_save()
|
||||
if not mesh:
|
||||
push_warning("CSG node '%s' produced no mesh — skipping." % csg.name)
|
||||
return null
|
||||
|
||||
var mi := MeshInstance3D.new()
|
||||
mi.name = csg.name + "_Mesh"
|
||||
|
||||
# Copy transform
|
||||
mi.transform = csg.transform
|
||||
|
||||
# Copy visibility ranges (if set on the CSG node)
|
||||
mi.visibility_range_begin = csg.visibility_range_begin
|
||||
mi.visibility_range_end = csg.visibility_range_end
|
||||
mi.visibility_range_begin_margin = csg.visibility_range_begin_margin
|
||||
mi.visibility_range_end_margin = csg.visibility_range_end_margin
|
||||
|
||||
# Create an ArrayMesh from the CSG mesh
|
||||
var array_mesh := _to_array_mesh(mesh)
|
||||
if not array_mesh:
|
||||
push_warning("Failed to convert mesh for '%s'." % csg.name)
|
||||
return null
|
||||
|
||||
# Generate LODs
|
||||
if CREATE_LOD and array_mesh.get_surface_count() > 0:
|
||||
_generate_lods(array_mesh, csg)
|
||||
|
||||
mi.mesh = array_mesh
|
||||
|
||||
# Add collision shape if the CSG had it
|
||||
if PRESERVE_COLLISION and csg.use_collision:
|
||||
var collision := _create_collision(csg)
|
||||
if collision:
|
||||
mi.add_child(collision, true)
|
||||
if Engine.is_editor_hint():
|
||||
collision.set_owner(mi.owner)
|
||||
|
||||
return mi
|
||||
|
||||
func _to_array_mesh(source: Mesh) -> ArrayMesh:
|
||||
# Convert any Mesh type to ArrayMesh suitable for LOD attachment
|
||||
|
||||
if source is ArrayMesh:
|
||||
return source.duplicate() as ArrayMesh
|
||||
|
||||
# If it's a primitive mesh, create a new ArrayMesh and copy surfaces
|
||||
var array_mesh := ArrayMesh.new()
|
||||
for i in range(source.get_surface_count()):
|
||||
var arrays := source.surface_get_arrays(i)
|
||||
if arrays:
|
||||
var blend := source.surface_get_blend_shape_arrays(i)
|
||||
var format := source.surface_get_format(i)
|
||||
array_mesh.add_surface_from_arrays(source.surface_get_primitive_type(i), arrays, blend, format)
|
||||
|
||||
return array_mesh
|
||||
|
||||
func _generate_lods(array_mesh: ArrayMesh, csg: CSGShape3D) -> void:
|
||||
# Generate LOD levels for the mesh.
|
||||
# In Godot 4.3+, LOD is added via add_lod(simplified_mesh, distance).
|
||||
# True mesh simplification (edge collapse) requires Godot's editor
|
||||
# mesh_decimate tool or an external library.
|
||||
#
|
||||
# This implementation creates placeholder LOD entries using the
|
||||
# full-poly mesh. For actual draw-call savings, run the editor's
|
||||
# Mesh → Simplify → Generate LODs to replace these with decimated
|
||||
# geometry at the LOD distances defined above.
|
||||
|
||||
var lods_added := 0
|
||||
|
||||
for lod_idx in range(1, 3): # LOD1, LOD2
|
||||
var ratio := LOD_DECIMATE_RATIOS[lod_idx]
|
||||
if ratio <= 0.0:
|
||||
continue
|
||||
|
||||
var lod_distance := 0.0
|
||||
match lod_idx:
|
||||
1: lod_distance = LOD1_DISTANCE
|
||||
2: lod_distance = LOD2_DISTANCE
|
||||
|
||||
# Create a placeholder LOD entry using the full mesh.
|
||||
# Replace with mesh_decimate output for actual tri reduction.
|
||||
var lod_mesh := array_mesh.duplicate() as ArrayMesh
|
||||
if lod_mesh and lod_mesh.get_surface_count() > 0:
|
||||
array_mesh.add_lod(lod_mesh, lod_distance)
|
||||
lods_added += 1
|
||||
|
||||
if lods_added == 0:
|
||||
push_warning("No LODs generated for '%s' — LOD slots require at least one surface." % csg.name)
|
||||
|
||||
func _create_collision(csg: CSGShape3D) -> CollisionShape3D:
|
||||
var shape := CollisionShape3D.new()
|
||||
shape.name = "Collision"
|
||||
|
||||
if csg is CSGBox3D:
|
||||
var box_shape := BoxShape3D.new()
|
||||
box_shape.size = (csg as CSGBox3D).size
|
||||
shape.shape = box_shape
|
||||
elif csg is CSGSphere3D:
|
||||
var sphere_shape := SphereShape3D.new()
|
||||
sphere_shape.radius = (csg as CSGSphere3D).radius
|
||||
shape.shape = sphere_shape
|
||||
else:
|
||||
# Fall back to a box matching the AABB
|
||||
var aabb_box := BoxShape3D.new()
|
||||
aabb_box.size = csg.get_aabb().size
|
||||
shape.shape = aabb_box
|
||||
|
||||
return shape
|
||||
@@ -0,0 +1 @@
|
||||
uid://c0b8h1hq57lsu
|
||||
@@ -0,0 +1,138 @@
|
||||
@tool
|
||||
extends EditorScript
|
||||
|
||||
# Auto-generate OccluderInstance3D nodes from CSG wall pieces.
|
||||
#
|
||||
# Scans the current scene for CSGBox3D nodes tagged as structural
|
||||
# (walls, floors, pillars) and creates matching OccluderInstance3D
|
||||
# boxes so Godot's occlusion culling system can use them.
|
||||
#
|
||||
# Usage:
|
||||
# 1. Open target scene in Godot editor
|
||||
# 2. Run: Scene > Run Script (select this file)
|
||||
# 3. Or run from command line:
|
||||
# godot --script scripts/generate_occluders.gd --scene res://path/to/scene.tscn
|
||||
#
|
||||
# Generated occluders are parented under an "Occluders" node.
|
||||
|
||||
const OCCLUDER_PARENT_NAME := "Occluders"
|
||||
|
||||
# Minimum CSG size (longest axis) to generate an occluder.
|
||||
# Very small pieces don't contribute meaningful occlusion.
|
||||
const MIN_OCCLUDER_SIZE := 0.5
|
||||
|
||||
func _run():
|
||||
var scene_root: Node
|
||||
|
||||
if Engine.is_editor_hint():
|
||||
scene_root = get_scene() as Node
|
||||
if not scene_root:
|
||||
printerr("No scene is currently open in the editor.")
|
||||
return
|
||||
else:
|
||||
var args = OS.get_cmdline_args()
|
||||
var scene_path := ""
|
||||
for i in range(args.size()):
|
||||
if args[i] == "--scene" and i + 1 < args.size():
|
||||
scene_path = args[i + 1]
|
||||
if scene_path.is_empty():
|
||||
printerr("Usage: godot --script generate_occluders.gd --scene res://path/to/scene.tscn")
|
||||
return
|
||||
|
||||
var packed = ResourceLoader.load(scene_path)
|
||||
if not packed:
|
||||
printerr("Failed to load scene: ", scene_path)
|
||||
return
|
||||
scene_root = packed.instantiate()
|
||||
|
||||
print("=== Occluder Generator ===")
|
||||
print("Scene: ", scene_root.name if scene_root else "unknown")
|
||||
|
||||
# Find or create the Occluders parent node
|
||||
var occluder_parent: Node = _find_or_create_parent(scene_root)
|
||||
var generated_count := 0
|
||||
var skipped_count := 0
|
||||
|
||||
# Walk all CSGBox3D nodes in the scene
|
||||
_generate_from_node(scene_root, occluder_parent, generated_count, skipped_count)
|
||||
|
||||
print("Generated: ", generated_count, " occluders")
|
||||
print("Skipped (too small): ", skipped_count)
|
||||
print("Parent: ", occluder_parent.get_path())
|
||||
print("")
|
||||
print("IMPORTANT: Occlusion culling requires the following project settings:")
|
||||
print(" rendering/occlusion_culling/occlusion_rays_per_octant = 6")
|
||||
print(" rendering/occlusion_culling/occlusion_culler_size = 128")
|
||||
print("")
|
||||
print("=== Occluder generation complete ===")
|
||||
|
||||
func _generate_from_node(node: Node, parent: Node, generated: int, skipped: int):
|
||||
# Check if this is a wall/pillar CSGBox3D (exclude subtraction/hole pieces)
|
||||
if node is CSGBox3D:
|
||||
var csg := node as CSGBox3D
|
||||
# Skip subtraction pieces (operation = 1 = Subtraction)
|
||||
# Skip pieces that are too small to occlude
|
||||
if csg.operation != CSGShape3D.OPERATION_SUBTRACTION:
|
||||
var size := csg.size
|
||||
var longest = max(size.x, max(size.y, size.z))
|
||||
if longest >= MIN_OCCLUDER_SIZE:
|
||||
_create_occluder(csg, parent)
|
||||
generated += 1
|
||||
else:
|
||||
skipped += 1
|
||||
else:
|
||||
skipped += 1
|
||||
|
||||
# Recurse into children
|
||||
for child in node.get_children():
|
||||
_generate_from_node(child, parent, generated, skipped)
|
||||
|
||||
func _find_or_create_parent(root: Node) -> Node:
|
||||
for child in root.get_children():
|
||||
if child.name == OCCLUDER_PARENT_NAME:
|
||||
print("Found existing Occluders node.")
|
||||
return child
|
||||
|
||||
var parent := Node3D.new()
|
||||
parent.name = OCCLUDER_PARENT_NAME
|
||||
parent.set_script(null) # No script needed
|
||||
|
||||
# Add as first child so it's visually at the top in the scene tree
|
||||
root.add_child(parent, true)
|
||||
if Engine.is_editor_hint():
|
||||
parent.set_owner(root)
|
||||
|
||||
print("Created Occluders node.")
|
||||
return parent
|
||||
|
||||
func _create_occluder(csg: CSGBox3D, parent: Node) -> void:
|
||||
var occluder := OccluderInstance3D.new()
|
||||
occluder.name = "Occluder_" + csg.name
|
||||
|
||||
# Create a box-shaped occluder matching the CSG piece
|
||||
var shape := OccluderShape3D.new()
|
||||
shape.set_as_vertices_and_indices(
|
||||
PackedVector3Array([]), # auto-generated from AABB
|
||||
PackedInt32Array([])
|
||||
)
|
||||
|
||||
# Use a box occluder matching the CSG box dimensions
|
||||
var box_shape := OccluderShapeBox3D.new()
|
||||
box_shape.size = csg.size
|
||||
occluder.set_occluder_shape(box_shape)
|
||||
|
||||
# Match the CSG node's global transform
|
||||
var global_transform := csg.global_transform
|
||||
occluder.global_transform = global_transform
|
||||
|
||||
# Bake to static mode (not dynamic) for best performance
|
||||
occluder.bake_mask = 1 # Layer 1 only
|
||||
occluder.bake_id = 0
|
||||
|
||||
parent.add_child(occluder, true)
|
||||
if Engine.is_editor_hint():
|
||||
occluder.set_owner(parent.owner)
|
||||
|
||||
# Apply voxel resolution
|
||||
# Godot 4's occlusion is computed from a signed-distance field
|
||||
# at the configured culler size (128^3 by budget spec)
|
||||
@@ -0,0 +1 @@
|
||||
uid://pauclwpn265a
|
||||
@@ -0,0 +1,170 @@
|
||||
extends SceneTree
|
||||
|
||||
# Profile scene runner — manually drives the frame loop.
|
||||
# In --script --headless mode the automatic main loop doesn't
|
||||
# iterate, so we drive it ourselves via a manual loop.
|
||||
|
||||
const DEFAULT_SCENE := "res://assets/scenes/modular/kit_demo.tscn"
|
||||
const DEFAULT_CAPTURE_FRAMES := 600
|
||||
const WARMUP_FRAMES := 60
|
||||
|
||||
var capture_frames := DEFAULT_CAPTURE_FRAMES
|
||||
var scene_path := ""
|
||||
var frame_count := 0
|
||||
var recording: Array[Dictionary] = []
|
||||
var profiling_complete := false
|
||||
|
||||
func _init():
|
||||
# Parse CLI args
|
||||
var args = OS.get_cmdline_args()
|
||||
for i in range(args.size()):
|
||||
if args[i] == "--scene" and i + 1 < args.size():
|
||||
scene_path = args[i + 1]
|
||||
if args[i] == "--capture-frames" and i + 1 < args.size():
|
||||
capture_frames = args[i + 1].to_int()
|
||||
|
||||
if scene_path.is_empty():
|
||||
scene_path = DEFAULT_SCENE
|
||||
|
||||
print("=== Profile Scene Tool ===")
|
||||
print("Engine: ", Engine.get_version_info().string)
|
||||
print("Scene: ", scene_path)
|
||||
print("Capture frames: ", capture_frames)
|
||||
print("Warmup frames: ", WARMUP_FRAMES)
|
||||
print("")
|
||||
print("Frame,Delta(ms),GPU(ms),Physics(ms),DrawCalls,Tris,Vertices,VRAM(MB),RAM(MB),Objects")
|
||||
|
||||
# Load target scene
|
||||
var scene = ResourceLoader.load(scene_path)
|
||||
if not scene:
|
||||
printerr("ERROR: Failed to load scene: ", scene_path)
|
||||
quit(1)
|
||||
return
|
||||
|
||||
var instance = scene.instantiate()
|
||||
root.add_child(instance)
|
||||
|
||||
# Manual frame loop: Godot doesn't auto-iterate the main loop
|
||||
# in --script --headless mode. We iterate it ourselves.
|
||||
profiling_complete = false
|
||||
_manual_loop()
|
||||
|
||||
func _manual_loop() -> void:
|
||||
var start_time := Time.get_ticks_usec()
|
||||
var frame_delta := 0.016 # Target 60fps step
|
||||
|
||||
while not profiling_complete:
|
||||
var frame_start := Time.get_ticks_usec()
|
||||
|
||||
# Call idle (process) on the scene tree — this triggers
|
||||
# _process() on all nodes including the instanced scene
|
||||
#idle(frame_delta)
|
||||
|
||||
# Manually iterate — this processes input, idle, and physics
|
||||
iteration(frame_delta)
|
||||
|
||||
_on_frame(frame_delta)
|
||||
|
||||
# Cap frame rate to avoid 100% CPU in headless mode
|
||||
var elapsed := (Time.get_ticks_usec() - frame_start) / 1000.0
|
||||
var target_ms := 16.0 # ~60fps
|
||||
if elapsed < target_ms:
|
||||
OS.delay_msec(int(target_ms - elapsed))
|
||||
|
||||
# Use elapsed wall time as delta for next frame
|
||||
frame_delta = (Time.get_ticks_usec() - start_time) / 1000000.0
|
||||
start_time = Time.get_ticks_usec()
|
||||
|
||||
_finish_report()
|
||||
|
||||
func _on_frame(delta: float) -> void:
|
||||
frame_count += 1
|
||||
|
||||
# Skip warmup frames
|
||||
if frame_count <= WARMUP_FRAMES:
|
||||
return
|
||||
|
||||
if frame_count > WARMUP_FRAMES + capture_frames:
|
||||
profiling_complete = true
|
||||
return
|
||||
|
||||
var p := Performance
|
||||
|
||||
var frame_delta := delta * 1000.0
|
||||
var gpu_time := float(RenderingServer.get_frame_setup_time_cpu()) * 1000.0
|
||||
var physics_time := p.get_monitor(p.TIME_PHYSICS_PROCESS) * 1000.0
|
||||
var draw_calls := int(p.get_monitor(p.RENDER_TOTAL_DRAW_CALLS_IN_FRAME))
|
||||
var tris := int(p.get_monitor(p.RENDER_TOTAL_PRIMITIVES_IN_FRAME))
|
||||
var verts := int(p.get_monitor(p.RENDER_TOTAL_VERTICES_IN_FRAME))
|
||||
var vram_mb := int(p.get_monitor(p.RENDER_VIDEO_MEM_USED)) / (1024 * 1024)
|
||||
var ram_mb := int(OS.get_static_memory_usage()) / (1024 * 1024)
|
||||
var objects := int(p.get_monitor(p.OBJECT_NODE_COUNT))
|
||||
|
||||
recording.append({
|
||||
frame = frame_count - WARMUP_FRAMES,
|
||||
delta_ms = snapped(frame_delta, 0.01),
|
||||
gpu_ms = snapped(gpu_time, 0.01),
|
||||
physics_ms = snapped(physics_time, 0.01),
|
||||
draw_calls = draw_calls,
|
||||
tris = tris,
|
||||
verts = verts,
|
||||
vram_mb = vram_mb,
|
||||
ram_mb = ram_mb,
|
||||
objects = objects
|
||||
})
|
||||
|
||||
print("%d,%.2f,%.2f,%.2f,%d,%d,%d,%d,%d,%d" % [
|
||||
frame_count - WARMUP_FRAMES,
|
||||
frame_delta, gpu_time, physics_time,
|
||||
draw_calls, tris, verts, vram_mb, ram_mb, objects
|
||||
])
|
||||
|
||||
func _finish_report() -> void:
|
||||
print("")
|
||||
print("=== Profile Summary ===")
|
||||
if recording.is_empty():
|
||||
print("No frames recorded!")
|
||||
quit(1)
|
||||
return
|
||||
|
||||
var deltas: Array[float] = []
|
||||
for r in recording:
|
||||
deltas.append(r.delta_ms)
|
||||
deltas.sort()
|
||||
|
||||
var total := float(recording.size())
|
||||
var n := deltas.size()
|
||||
var p50: float = deltas[n * 50 / 100]
|
||||
var p95: float = deltas[n * 95 / 100]
|
||||
var p99: float = deltas[n * 99 / 100]
|
||||
var sum: float = 0.0
|
||||
for d in deltas:
|
||||
sum += d
|
||||
var avg: float = sum / total
|
||||
|
||||
var max_dc := 0
|
||||
var max_tris := 0
|
||||
var max_vram := 0
|
||||
for r in recording:
|
||||
if r.draw_calls > max_dc: max_dc = r.draw_calls
|
||||
if r.tris > max_tris: max_tris = r.tris
|
||||
if r.vram_mb > max_vram: max_vram = r.vram_mb
|
||||
|
||||
print("Frames captured: ", recording.size())
|
||||
print("Avg frame time: %.2f ms" % avg)
|
||||
print("P50 frame time: %.2f ms" % p50)
|
||||
print("P95 frame time: %.2f ms" % p95)
|
||||
print("P99 frame time: %.2f ms" % p99)
|
||||
print("Max draw calls: %d" % max_dc)
|
||||
print("Max triangles: %d" % max_tris)
|
||||
print("Peak VRAM: %d MB" % max_vram)
|
||||
|
||||
var pass_fail := "PASS"
|
||||
if p95 > 16.6:
|
||||
pass_fail = "FAIL (P95 > 16.6 ms)"
|
||||
elif p99 > 50.0:
|
||||
pass_fail = "FAIL (P99 spike > 50 ms)"
|
||||
print("Result: ", pass_fail)
|
||||
print("")
|
||||
print("=== Profile complete ===")
|
||||
quit(0 if pass_fail == "PASS" else 1)
|
||||
@@ -0,0 +1 @@
|
||||
uid://byjqslju7qnt6
|
||||
@@ -0,0 +1,147 @@
|
||||
extends Node
|
||||
|
||||
# Per-frame profiling node for tactical-shooter.
|
||||
|
||||
const WARMUP_FRAMES := 60
|
||||
const DEFAULT_CAPTURE_FRAMES := 600
|
||||
|
||||
var capture_frames := DEFAULT_CAPTURE_FRAMES
|
||||
var frame_count := 0
|
||||
var recording: Array[Dictionary] = []
|
||||
var profiling_complete := false
|
||||
var last_tick := 0
|
||||
|
||||
func _ready():
|
||||
# Try to read capture_frames from environment or metadata
|
||||
if OS.has_environment("PROFILE_FRAMES"):
|
||||
capture_frames = OS.get_environment("PROFILE_FRAMES").to_int()
|
||||
|
||||
capture_frames = get_meta("capture_frames", capture_frames)
|
||||
|
||||
print("=== Profiler Node ===")
|
||||
print("Capture frames: ", capture_frames)
|
||||
print("Warmup frames: ", WARMUP_FRAMES)
|
||||
print("")
|
||||
print("Frame,Delta(ms),GPU(ms),Physics(ms),DrawCalls,Tris,Vertices,VRAM(MB),RAM(MB),Objects")
|
||||
|
||||
# Load the target scene from metadata or environment
|
||||
var target_scene = get_meta("target_scene", "")
|
||||
if target_scene.is_empty() and OS.has_environment("PROFILE_SCENE"):
|
||||
target_scene = OS.get_environment("PROFILE_SCENE")
|
||||
|
||||
if not target_scene.is_empty():
|
||||
var packed = ResourceLoader.load(target_scene)
|
||||
if packed:
|
||||
var instance = packed.instantiate()
|
||||
add_child(instance)
|
||||
print("Loaded target scene: ", target_scene)
|
||||
else:
|
||||
printerr("Failed to load target scene: ", target_scene)
|
||||
|
||||
last_tick = Time.get_ticks_usec()
|
||||
|
||||
print("Ready complete, process() should fire now")
|
||||
set_process(true)
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
print("_process called, frame=", frame_count)
|
||||
if profiling_complete:
|
||||
return
|
||||
|
||||
frame_count += 1
|
||||
|
||||
# Calculate real delta
|
||||
var now := Time.get_ticks_usec()
|
||||
var real_delta := (now - last_tick) / 1000000.0
|
||||
last_tick = now
|
||||
|
||||
# Skip warmup frames
|
||||
if frame_count <= WARMUP_FRAMES:
|
||||
return
|
||||
|
||||
# Check if done
|
||||
if frame_count > WARMUP_FRAMES + capture_frames:
|
||||
profiling_complete = true
|
||||
_finish()
|
||||
return
|
||||
|
||||
var p := Performance
|
||||
|
||||
var frame_delta := real_delta * 1000.0
|
||||
var gpu_time := float(RenderingServer.get_frame_setup_time_cpu()) * 1000.0
|
||||
var physics_time := p.get_monitor(p.TIME_PHYSICS_PROCESS) * 1000.0
|
||||
var draw_calls := int(p.get_monitor(p.RENDER_TOTAL_DRAW_CALLS_IN_FRAME))
|
||||
var tris := int(p.get_monitor(p.RENDER_TOTAL_PRIMITIVES_IN_FRAME))
|
||||
var verts := int(p.get_monitor(p.RENDER_TOTAL_VERTICES_IN_FRAME))
|
||||
var vram_mb := int(p.get_monitor(p.RENDER_VIDEO_MEM_USED)) / (1024 * 1024)
|
||||
var ram_mb := int(OS.get_static_memory_usage()) / (1024 * 1024)
|
||||
var objects := int(p.get_monitor(p.OBJECT_NODE_COUNT))
|
||||
|
||||
recording.append({
|
||||
frame = frame_count - WARMUP_FRAMES,
|
||||
delta_ms = snapped(frame_delta, 0.01),
|
||||
gpu_ms = snapped(gpu_time, 0.01),
|
||||
physics_ms = snapped(physics_time, 0.01),
|
||||
draw_calls = draw_calls,
|
||||
tris = tris,
|
||||
verts = verts,
|
||||
vram_mb = vram_mb,
|
||||
ram_mb = ram_mb,
|
||||
objects = objects
|
||||
})
|
||||
|
||||
print("%d,%.2f,%.2f,%.2f,%d,%d,%d,%d,%d,%d" % [
|
||||
frame_count - WARMUP_FRAMES,
|
||||
frame_delta, gpu_time, physics_time,
|
||||
draw_calls, tris, verts, vram_mb, ram_mb, objects
|
||||
])
|
||||
|
||||
func _finish() -> void:
|
||||
print("")
|
||||
print("=== Profile Summary ===")
|
||||
if recording.is_empty():
|
||||
print("No frames recorded!")
|
||||
get_tree().quit(1)
|
||||
return
|
||||
|
||||
var deltas: Array[float] = []
|
||||
for r in recording:
|
||||
deltas.append(r.delta_ms)
|
||||
deltas.sort()
|
||||
|
||||
var total := float(recording.size())
|
||||
var n := deltas.size()
|
||||
var p50: float = deltas[n * 50 / 100]
|
||||
var p95: float = deltas[n * 95 / 100]
|
||||
var p99: float = deltas[n * 99 / 100]
|
||||
var sum: float = 0.0
|
||||
for d in deltas:
|
||||
sum += d
|
||||
var avg: float = sum / total
|
||||
|
||||
var max_dc := 0
|
||||
var max_tris := 0
|
||||
var max_vram := 0
|
||||
for r in recording:
|
||||
if r.draw_calls > max_dc: max_dc = r.draw_calls
|
||||
if r.tris > max_tris: max_tris = r.tris
|
||||
if r.vram_mb > max_vram: max_vram = r.vram_mb
|
||||
|
||||
print("Frames captured: ", recording.size())
|
||||
print("Avg frame time: %.2f ms" % avg)
|
||||
print("P50 frame time: %.2f ms" % p50)
|
||||
print("P95 frame time: %.2f ms" % p95)
|
||||
print("P99 frame time: %.2f ms" % p99)
|
||||
print("Max draw calls: %d" % max_dc)
|
||||
print("Max triangles: %d" % max_tris)
|
||||
print("Peak VRAM: %d MB" % max_vram)
|
||||
|
||||
var pass_fail := "PASS"
|
||||
if p95 > 16.6:
|
||||
pass_fail = "FAIL (P95 > 16.6 ms)"
|
||||
elif p99 > 50.0:
|
||||
pass_fail = "FAIL (P99 spike > 50 ms)"
|
||||
print("Result: ", pass_fail)
|
||||
print("")
|
||||
print("=== Profile complete ===")
|
||||
get_tree().quit(0 if pass_fail == "PASS" else 1)
|
||||
@@ -0,0 +1 @@
|
||||
uid://jih7knhnl55a
|
||||
@@ -0,0 +1,6 @@
|
||||
[gd_scene format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/profiling/profiler_node.gd" id="1"]
|
||||
|
||||
[node name="Profiler" type="Node"]
|
||||
script = ExtResource("1")
|
||||
@@ -0,0 +1,21 @@
|
||||
extends SceneTree
|
||||
|
||||
func _init():
|
||||
print("_init called")
|
||||
# Add a simple node to trigger processing
|
||||
var n = Node.new()
|
||||
n.name = "test"
|
||||
n.set_process(true)
|
||||
root.add_child(n)
|
||||
print("Node added")
|
||||
|
||||
func _process(delta: float) -> bool:
|
||||
print("_process called: delta=", delta)
|
||||
if frame_count > 5:
|
||||
print("Quitting after 5 frames")
|
||||
quit(0)
|
||||
return true
|
||||
frame_count += 1
|
||||
return true
|
||||
|
||||
var frame_count := 0
|
||||
@@ -0,0 +1 @@
|
||||
uid://dc8x14rs2jyu8
|
||||
@@ -0,0 +1,14 @@
|
||||
extends Node
|
||||
|
||||
var count := 0
|
||||
|
||||
func _ready():
|
||||
print("_ready called")
|
||||
set_process(true)
|
||||
|
||||
func _process(delta):
|
||||
count += 1
|
||||
print("_process #", count, " delta=", delta)
|
||||
if count >= 5:
|
||||
print("Quitting")
|
||||
get_tree().quit()
|
||||
@@ -0,0 +1 @@
|
||||
uid://b8jj7n3bi6ljh
|
||||
@@ -0,0 +1,6 @@
|
||||
[gd_scene format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/profiling/test_simple.gd" id="1"]
|
||||
|
||||
[node name="Root" type="Node"]
|
||||
script = ExtResource("1")
|
||||
@@ -1,6 +0,0 @@
|
||||
extends Node
|
||||
|
||||
func _ready():
|
||||
print("Hello from Godot headless script!")
|
||||
print("Testing: ", Engine.get_version_info())
|
||||
get_tree().quit(0)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dbtpbhja3nk60
|
||||
@@ -0,0 +1,443 @@
|
||||
@tool
|
||||
extends SceneTree
|
||||
|
||||
# =============================================================================
|
||||
# validate_lights.gd — Dynamic Light Count / Bake Mode Validator
|
||||
# =============================================================================
|
||||
# Audits all light nodes in a Godot 4 scene for:
|
||||
# - Dynamic light count ≤ 4 (budget for mobile/competitive FPS)
|
||||
# - Each light's bake mode is set correctly (BakeMode = STATIC or DYNAMIC)
|
||||
# - Light energy / range / attenuation are within reasonable bounds
|
||||
# - Shadow configuration is appropriate
|
||||
#
|
||||
# Usage:
|
||||
# godot --headless --script client/scripts/validate_lights.gd -- res://path/to/map.tscn
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 — All light checks passed
|
||||
# 1 — One or more light checks failed
|
||||
# 2 — Scene file not found or cannot be loaded
|
||||
# =============================================================================
|
||||
|
||||
const MAX_DYNAMIC_LIGHTS := 4
|
||||
const MAX_LIGHT_ENERGY := 50.0
|
||||
const MIN_LIGHT_ENERGY := 0.1
|
||||
const MAX_LIGHT_RANGE := 100.0
|
||||
const MAX_LIGHT_SPOT_ANGLE := 180.0
|
||||
const WARN_TOTAL_LIGHTS := 8
|
||||
|
||||
# BakeMode enum values as Godot defines them
|
||||
const BAKE_MODE_DISABLED := 0
|
||||
const BAKE_MODE_STATIC := 1
|
||||
const BAKE_MODE_DYNAMIC := 2
|
||||
|
||||
var _pass_count := 0
|
||||
var _fail_count := 0
|
||||
var _warning_count := 0
|
||||
var _scene_path := ""
|
||||
var _scene_instance: Node = null
|
||||
|
||||
# Light stat tracking
|
||||
var _total_lights := 0
|
||||
var _dynamic_lights := 0
|
||||
var _baked_lights := 0
|
||||
var _disabled_lights := 0
|
||||
var _shadowed_lights := 0
|
||||
var _light_details: Array[Dictionary] = []
|
||||
|
||||
# Per-type counts
|
||||
var _directional_lights := 0
|
||||
var _omni_lights := 0
|
||||
var _spot_lights := 0
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
_parse_arguments()
|
||||
|
||||
if _scene_path.is_empty():
|
||||
printerr("ERROR: No scene path provided. Usage: godot --headless --script validate_lights.gd -- res://path/to/map.tscn")
|
||||
quit(2)
|
||||
return
|
||||
|
||||
print("")
|
||||
print("==========================================")
|
||||
print(" LIGHT VALIDATOR")
|
||||
print(" Scene: ", _scene_path)
|
||||
print("==========================================")
|
||||
print("")
|
||||
|
||||
print(" Light budget:")
|
||||
print(" Max dynamic lights: ", MAX_DYNAMIC_LIGHTS)
|
||||
print(" Recommended baking: All static geometry lights")
|
||||
print("")
|
||||
|
||||
var scene: PackedScene = _load_scene(_scene_path)
|
||||
if scene == null:
|
||||
printerr("ERROR: Scene not found: ", _scene_path)
|
||||
quit(2)
|
||||
return
|
||||
|
||||
_scene_instance = _instantiate_scene(scene)
|
||||
if _scene_instance == null:
|
||||
printerr("ERROR: Failed to instantiate scene: ", _scene_path)
|
||||
quit(2)
|
||||
return
|
||||
|
||||
root.add_child(_scene_instance)
|
||||
|
||||
# Collect all lights
|
||||
_analyze_lights(_scene_instance)
|
||||
|
||||
# Run checks
|
||||
_run_dynamic_light_count_check()
|
||||
_run_bake_mode_checks()
|
||||
_run_energy_checks()
|
||||
_run_shadow_checks()
|
||||
_print_light_report()
|
||||
|
||||
# Summary
|
||||
_print_summary()
|
||||
|
||||
if _fail_count > 0:
|
||||
quit(1)
|
||||
else:
|
||||
quit(0)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Argument parsing
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _parse_arguments() -> void:
|
||||
"""Extract scene path from user arguments (after --)."""
|
||||
var args: PackedStringArray = OS.get_cmdline_user_args()
|
||||
if not args.is_empty():
|
||||
_scene_path = args[0].strip_edges()
|
||||
return
|
||||
|
||||
var full_args: PackedStringArray = OS.get_cmdline_args()
|
||||
var found_sep := false
|
||||
for i in range(full_args.size()):
|
||||
if full_args[i] == "--":
|
||||
found_sep = true
|
||||
continue
|
||||
if found_sep:
|
||||
_scene_path = full_args[i].strip_edges()
|
||||
break
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Scene helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _load_scene(path: String) -> PackedScene:
|
||||
"""Load a .tscn/.scn file."""
|
||||
if not ResourceLoader.exists(path):
|
||||
if not path.ends_with(".tscn") and not path.ends_with(".scn"):
|
||||
path = path + ".tscn"
|
||||
if not ResourceLoader.exists(path):
|
||||
return null
|
||||
return ResourceLoader.load(path, "PackedScene", ResourceLoader.CACHE_MODE_REUSE)
|
||||
|
||||
|
||||
func _instantiate_scene(scene: PackedScene) -> Node:
|
||||
return scene.instantiate()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Check helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _pass(msg: String) -> void:
|
||||
_pass_count += 1
|
||||
print(" ✓ ", msg)
|
||||
|
||||
|
||||
func _fail(msg: String) -> void:
|
||||
_fail_count += 1
|
||||
print(" ✗ ", msg)
|
||||
|
||||
|
||||
func _warn(msg: String) -> void:
|
||||
_warning_count += 1
|
||||
print(" ⚠ ", msg)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Light analysis
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _analyze_lights(node: Node) -> void:
|
||||
"""Recursively walk scene collecting light data."""
|
||||
var class_name := node.get_class()
|
||||
|
||||
if class_name in ["DirectionalLight3D", "OmniLight3D", "SpotLight3D"]:
|
||||
_total_lights += 1
|
||||
var info := _collect_light_info(node, class_name)
|
||||
_light_details.append(info)
|
||||
|
||||
# Count by type
|
||||
match class_name:
|
||||
"DirectionalLight3D":
|
||||
_directional_lights += 1
|
||||
"OmniLight3D":
|
||||
_omni_lights += 1
|
||||
"SpotLight3D":
|
||||
_spot_lights += 1
|
||||
|
||||
# Track bake mode
|
||||
var bake_mode := 0
|
||||
if "bake_mode" in node:
|
||||
bake_mode = node.bake_mode
|
||||
match bake_mode:
|
||||
BAKE_MODE_DISABLED:
|
||||
_disabled_lights += 1
|
||||
_dynamic_lights += 1
|
||||
BAKE_MODE_STATIC:
|
||||
_baked_lights += 1
|
||||
BAKE_MODE_DYNAMIC:
|
||||
_dynamic_lights += 1
|
||||
|
||||
# Track shadows
|
||||
if "shadow" in node and node.shadow:
|
||||
_shadowed_lights += 1
|
||||
|
||||
for child in node.get_children():
|
||||
_analyze_lights(child)
|
||||
|
||||
|
||||
func _collect_light_info(node: Node, class_name: String) -> Dictionary:
|
||||
"""Collect all relevant properties from a light node."""
|
||||
var info := {
|
||||
"name": node.name,
|
||||
"type": class_name,
|
||||
"path": _get_node_path(node),
|
||||
"enabled": node.get("visible") if "visible" in node else true,
|
||||
"bake_mode": node.get("bake_mode") if "bake_mode" in node else 0,
|
||||
"energy": node.get("energy") if "energy" in node else 1.0,
|
||||
"shadow": node.get("shadow") if "shadow" in node else false,
|
||||
"shadow_bias": node.get("shadow_bias") if "shadow_bias" in node else 0.0,
|
||||
"indirect_energy": node.get("indirect_energy") if "indirect_energy" in node else 1.0,
|
||||
"light_specular": node.get("light_specular") if "light_specular" in node else 0.5,
|
||||
}
|
||||
|
||||
# Type-specific properties
|
||||
match class_name:
|
||||
"OmniLight3D":
|
||||
info["range"] = node.get("omni_range") if "omni_range" in node else 0.0
|
||||
info["attenuation"] = node.get("omni_attenuation") if "omni_attenuation" in node else 1.0
|
||||
"SpotLight3D":
|
||||
info["range"] = node.get("spot_range") if "spot_range" in node else 0.0
|
||||
info["attenuation"] = node.get("spot_attenuation") if "spot_attenuation" in node else 1.0
|
||||
info["spot_angle"] = node.get("spot_angle") if "spot_angle" in node else 45.0
|
||||
info["spot_angle_attenuation"] = node.get("spot_angle_attenuation") if "spot_angle_attenuation" in node else 1.0
|
||||
"DirectionalLight3D":
|
||||
info["range"] = -1.0
|
||||
info["directional_shadow_mode"] = node.get("directional_shadow_mode") if "directional_shadow_mode" in node else 0
|
||||
|
||||
return info
|
||||
|
||||
|
||||
func _get_node_path(node: Node) -> String:
|
||||
"""Get the scene-tree path of a node."""
|
||||
var parts: Array[String] = []
|
||||
var current := node
|
||||
while current != null and current != _scene_instance:
|
||||
parts.push_front(current.name)
|
||||
current = current.get_parent()
|
||||
return "/".join(parts)
|
||||
|
||||
|
||||
func _get_bake_mode_name(mode: int) -> String:
|
||||
match mode:
|
||||
BAKE_MODE_DISABLED: return "Disabled (Dynamic)"
|
||||
BAKE_MODE_STATIC: return "Static (Baked)"
|
||||
BAKE_MODE_DYNAMIC: return "Dynamic"
|
||||
return "Unknown(" + str(mode) + ")"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Light checks
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _run_dynamic_light_count_check() -> void:
|
||||
"""Check that dynamic light count is within budget."""
|
||||
print("--- Dynamic Light Count ---")
|
||||
|
||||
if _total_lights == 0:
|
||||
_warn("No lights found in scene at all (verify map lighting)")
|
||||
return
|
||||
|
||||
_pass("Total lights: " + str(_total_lights) + " (Dir: " + str(_directional_lights) +
|
||||
", Omni: " + str(_omni_lights) + ", Spot: " + str(_spot_lights) + ")")
|
||||
|
||||
if _dynamic_lights <= MAX_DYNAMIC_LIGHTS:
|
||||
_pass("Dynamic lights: " + str(_dynamic_lights) + " (budget: ≤ " + str(MAX_DYNAMIC_LIGHTS) + ")")
|
||||
else:
|
||||
_fail("Too many dynamic lights: " + str(_dynamic_lights) +
|
||||
" (budget: ≤ " + str(MAX_DYNAMIC_LIGHTS) + ")" +
|
||||
" — consider baking " + str(_dynamic_lights - MAX_DYNAMIC_LIGHTS) + " light(s)")
|
||||
|
||||
# Warn if total lights (including baked) is very high
|
||||
if _total_lights > WARN_TOTAL_LIGHTS:
|
||||
_warn("Total light count (" + str(_total_lights) + ") exceeds " + str(WARN_TOTAL_LIGHTS) +
|
||||
" — verify all lights are necessary")
|
||||
|
||||
|
||||
func _run_bake_mode_checks() -> void:
|
||||
"""Check that each light has an appropriate bake mode."""
|
||||
print("")
|
||||
print("--- Bake Mode Validation ---")
|
||||
|
||||
var proper_bake := 0
|
||||
var improper_bake := 0
|
||||
|
||||
for info in _light_details:
|
||||
var bake_mode := info.bake_mode as int
|
||||
var light_name := info.name as String
|
||||
var light_type := info.type as String
|
||||
var bake_name := _get_bake_mode_name(bake_mode)
|
||||
|
||||
if bake_mode == BAKE_MODE_STATIC:
|
||||
proper_bake += 1
|
||||
_pass(light_name + " (" + light_type + "): bake_mode = Static ✓")
|
||||
elif bake_mode == BAKE_MODE_DISABLED:
|
||||
# Dynamic lights are OK if count is within budget
|
||||
improper_bake += 1
|
||||
_warn(light_name + " (" + light_type + "): bake_mode = Disabled (fully dynamic)")
|
||||
elif bake_mode == BAKE_MODE_DYNAMIC:
|
||||
improper_bake += 1
|
||||
_warn(light_name + " (" + light_type + "): bake_mode = Dynamic")
|
||||
|
||||
if improper_bake > 0:
|
||||
_warn(str(improper_bake) + " light(s) are not set to Static bake mode" +
|
||||
" — use Static for baked geometry lights")
|
||||
|
||||
# DirectionalLight3D should generally be Baked (Static) for main sun
|
||||
for info in _light_details:
|
||||
if info.type == "DirectionalLight3D" and info.bake_mode == BAKE_MODE_DISABLED:
|
||||
_warn("DirectionalLight3D '" + info.name + "' is set to Disabled bake mode" +
|
||||
" — set to Static (Baked) for performance")
|
||||
|
||||
|
||||
func _run_energy_checks() -> void:
|
||||
"""Check that light energy values are within reasonable ranges."""
|
||||
print("")
|
||||
print("--- Light Energy / Range Checks ---")
|
||||
|
||||
for info in _light_details:
|
||||
var energy := info.energy as float
|
||||
var light_name := info.name as String
|
||||
var light_type := info.type as String
|
||||
|
||||
if energy > MAX_LIGHT_ENERGY:
|
||||
_warn(light_name + " (" + light_type + "): energy = " + str(energy) +
|
||||
" — very high (max recommended: " + str(MAX_LIGHT_ENERGY) + ")")
|
||||
elif energy < MIN_LIGHT_ENERGY:
|
||||
_warn(light_name + " (" + light_type + "): energy = " + str(energy) +
|
||||
" — very low (min recommended: " + str(MIN_LIGHT_ENERGY) + ")")
|
||||
else:
|
||||
_pass(light_name + " (" + light_type + "): energy = " + str(energy) + " (within [0.1, 50.0])")
|
||||
|
||||
# Range check (not applicable to DirectionalLight3D)
|
||||
if info.has("range") and info.range > 0:
|
||||
var range_val := info.range as float
|
||||
if range_val > MAX_LIGHT_RANGE:
|
||||
_warn(light_name + " (" + light_type + "): range = " + str(range_val) +
|
||||
" — very large (max recommended: " + str(MAX_LIGHT_RANGE) + ")")
|
||||
|
||||
# Spot angle check
|
||||
if info.has("spot_angle"):
|
||||
var angle := info.spot_angle as float
|
||||
if angle > MAX_LIGHT_SPOT_ANGLE:
|
||||
_warn(light_name + " (SpotLight3D): spot_angle = " + str(angle) +
|
||||
" — exceeds 180° (will project backwards)")
|
||||
|
||||
|
||||
func _run_shadow_checks() -> void:
|
||||
"""Validate shadow configuration on lights."""
|
||||
print("")
|
||||
print("--- Shadow Configuration ---")
|
||||
|
||||
for info in _light_details:
|
||||
var has_shadow := info.shadow as bool
|
||||
var light_name := info.name as String
|
||||
|
||||
if has_shadow:
|
||||
# Shadowed lights are more expensive
|
||||
if info.bake_mode == BAKE_MODE_STATIC:
|
||||
_pass(light_name + ": shadows enabled, bake_mode = Static (good — shadows will be baked)")
|
||||
else:
|
||||
_warn(light_name + ": dynamic shadows enabled — expensive for real-time rendering")
|
||||
|
||||
# Count shadowed dynamic lights
|
||||
var dynamic_shadow_count := 0
|
||||
for info in _light_details:
|
||||
if info.shadow and info.bake_mode != BAKE_MODE_STATIC:
|
||||
dynamic_shadow_count += 1
|
||||
|
||||
if dynamic_shadow_count > 2:
|
||||
_warn(str(dynamic_shadow_count) + " lights have dynamic shadows — recommended max 2 for performance")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Light report
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _print_light_report() -> void:
|
||||
"""Print a comprehensive light report."""
|
||||
print("")
|
||||
print("--- Light Report ---")
|
||||
print(" Total lights: ", _total_lights)
|
||||
print(" DirectionalLight3D: ", _directional_lights)
|
||||
print(" OmniLight3D: ", _omni_lights)
|
||||
print(" SpotLight3D: ", _spot_lights)
|
||||
print(" Dynamic (non-baked): ", _dynamic_lights)
|
||||
print(" Baked (Static): ", _baked_lights)
|
||||
print(" Disabled: ", _disabled_lights)
|
||||
print(" Shadowed: ", _shadowed_lights)
|
||||
print("")
|
||||
|
||||
# Light detail table
|
||||
if _light_details.size() > 0:
|
||||
print(" Light Details:")
|
||||
print(" " + "-".repeat(90))
|
||||
print(" %-25s %-18s %-12s %-8s %-6s %-8s" % ["Name", "Type", "Bake Mode", "Energy", "Shadow", "Range"])
|
||||
print(" " + "-".repeat(90))
|
||||
for info in _light_details:
|
||||
var bake_mode_name := _get_bake_mode_name(info.bake_mode)
|
||||
var range_str := str(info.range) if info.has("range") and info.range > 0 else "N/A"
|
||||
var shadow_str := "Yes" if info.shadow else "No"
|
||||
print(" %-25s %-18s %-12s %-8.1f %-6s %-8s" % [
|
||||
info.name, info.type, bake_mode_name, info.energy, shadow_str, range_str
|
||||
])
|
||||
print(" " + "-".repeat(90))
|
||||
print("")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Summary
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _print_summary() -> void:
|
||||
"""Print final summary of all light checks."""
|
||||
print("==========================================")
|
||||
print(" LIGHT VALIDATION SUMMARY")
|
||||
print("==========================================")
|
||||
print(" Scene: ", _scene_path)
|
||||
print(" Total lights: ", _total_lights)
|
||||
print(" Dynamic lights: ", _dynamic_lights)
|
||||
print(" Baked (Static): ", _baked_lights)
|
||||
print(" Dynamic shadowed: ", _shadowed_lights)
|
||||
print("------------------------------------------")
|
||||
print(" Passed: ", _pass_count)
|
||||
print(" Failed: ", _fail_count)
|
||||
print(" Warnings: ", _warning_count)
|
||||
print("------------------------------------------")
|
||||
if _fail_count > 0:
|
||||
print(" RESULT: FAILED ❌")
|
||||
printerr("Validation FAILED — ", _fail_count, " light check(s) failed.")
|
||||
else:
|
||||
print(" RESULT: PASSED ✅")
|
||||
print("==========================================")
|
||||
print("")
|
||||
@@ -0,0 +1 @@
|
||||
uid://cwvi2ic1k6ubn
|
||||
@@ -0,0 +1,511 @@
|
||||
@tool
|
||||
extends SceneTree
|
||||
|
||||
# =============================================================================
|
||||
# validate_map.gd — Map Scene Structure Validator
|
||||
# =============================================================================
|
||||
# Validates that a competitive FPS map scene follows the required structure:
|
||||
# - CSG Combiner as root node
|
||||
# - Spawn groups (T Spawn / CT Spawn)
|
||||
# - Bomb sites (A / B)
|
||||
# - Buy zones (T / CT)
|
||||
# - Map bounds (invisible walls / kill plane)
|
||||
# - LightmapGI configuration
|
||||
# - Lighting setup (WorldEnvironment, directional/ambient lights)
|
||||
#
|
||||
# Usage:
|
||||
# godot --headless --script client/scripts/validate_map.gd -- res://path/to/map.tscn
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 — All checks passed
|
||||
# 1 — One or more checks failed
|
||||
# 2 — Scene file not found at the given path
|
||||
# =============================================================================
|
||||
|
||||
const EXPECTED_MIN_NODES := 15
|
||||
const EXPECTED_MAX_NODES := 500
|
||||
|
||||
# Separator used to mark user arguments after --
|
||||
const ARG_SEPARATOR := "--"
|
||||
|
||||
var _pass_count := 0
|
||||
var _fail_count := 0
|
||||
var _warning_count := 0
|
||||
var _scene_path := ""
|
||||
var _scene_instance: Node = null
|
||||
var _root_node: Node = null
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
_parse_arguments()
|
||||
|
||||
if _scene_path.is_empty():
|
||||
printerr("ERROR: No scene path provided. Usage: godot --headless --script validate_map.gd -- res://path/to/map.tscn")
|
||||
quit(2)
|
||||
return
|
||||
|
||||
print("")
|
||||
print("==========================================")
|
||||
print(" MAP SCENE STRUCTURE VALIDATOR")
|
||||
print(" Scene: ", _scene_path)
|
||||
print("==========================================")
|
||||
print("")
|
||||
|
||||
# Step 1 — Load and instantiate the scene
|
||||
var scene: PackedScene = _load_scene(_scene_path)
|
||||
if scene == null:
|
||||
printerr("ERROR: Scene not found or failed to load: ", _scene_path)
|
||||
quit(2)
|
||||
return
|
||||
|
||||
_scene_instance = _instantiate_scene(scene)
|
||||
if _scene_instance == null:
|
||||
printerr("ERROR: Failed to instantiate scene: ", _scene_path)
|
||||
quit(2)
|
||||
return
|
||||
|
||||
root.add_child(_scene_instance)
|
||||
_root_node = _scene_instance
|
||||
|
||||
# Step 2 — Run all validation checks
|
||||
_run_all_checks()
|
||||
|
||||
# Step 3 — Print summary
|
||||
_print_summary()
|
||||
|
||||
# Step 4 — Exit with appropriate code
|
||||
if _fail_count > 0:
|
||||
quit(1)
|
||||
else:
|
||||
quit(0)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Argument parsing
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _parse_arguments() -> void:
|
||||
"""
|
||||
Parse command-line arguments to extract the scene path.
|
||||
Args after '--' are user arguments: godot ... --script validate_map.gd -- res://map.tscn
|
||||
"""
|
||||
var args: PackedStringArray = OS.get_cmdline_user_args()
|
||||
if args.is_empty():
|
||||
# Fallback: look for '--' in full cmdline
|
||||
var full_args: PackedStringArray = OS.get_cmdline_args()
|
||||
var found_sep := false
|
||||
for i in range(full_args.size()):
|
||||
if full_args[i] == ARG_SEPARATOR:
|
||||
found_sep = true
|
||||
continue
|
||||
if found_sep:
|
||||
_scene_path = full_args[i].strip_edges()
|
||||
break
|
||||
else:
|
||||
# First user arg is the scene path
|
||||
_scene_path = args[0].strip_edges()
|
||||
|
||||
# Basic validation
|
||||
if _scene_path.is_empty():
|
||||
return
|
||||
|
||||
# Ensure it's a res:// path or convert relative to res://
|
||||
if not _scene_path.begins_with("res://"):
|
||||
# Could be a relative filesystem path — keep as-is, ResourceLoader handles it
|
||||
pass
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Scene loading helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _load_scene(path: String) -> PackedScene:
|
||||
"""
|
||||
Load a .tscn or .scn file and return the PackedScene, or null on failure.
|
||||
"""
|
||||
if not ResourceLoader.exists(path):
|
||||
# Try adding extension
|
||||
if not path.ends_with(".tscn") and not path.ends_with(".scn"):
|
||||
path = path + ".tscn"
|
||||
if not ResourceLoader.exists(path):
|
||||
return null
|
||||
var scene: PackedScene = ResourceLoader.load(path, "PackedScene", ResourceLoader.CACHE_MODE_REUSE)
|
||||
return scene
|
||||
|
||||
|
||||
func _instantiate_scene(scene: PackedScene) -> Node:
|
||||
"""
|
||||
Safely instantiate a packed scene, returning the root node or null.
|
||||
"""
|
||||
var instance: Node = null
|
||||
var ok := false
|
||||
instance = scene.instantiate()
|
||||
if instance != null:
|
||||
ok = true
|
||||
return instance if ok else null
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Check registration helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _pass(msg: String) -> void:
|
||||
"""Record a passed check."""
|
||||
_pass_count += 1
|
||||
print(" ✓ ", msg)
|
||||
|
||||
|
||||
func _fail(msg: String) -> void:
|
||||
"""Record a failed check."""
|
||||
_fail_count += 1
|
||||
print(" ✗ ", msg)
|
||||
|
||||
|
||||
func _warn(msg: String) -> void:
|
||||
"""Record a non-blocking warning."""
|
||||
_warning_count += 1
|
||||
print(" ⚠ ", msg)
|
||||
|
||||
|
||||
func _check(condition: bool, msg_pass: String, msg_fail: String) -> bool:
|
||||
"""Convenience: pass or fail based on condition."""
|
||||
if condition:
|
||||
_pass(msg_pass)
|
||||
else:
|
||||
_fail(msg_fail)
|
||||
return condition
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Node search helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _find_node(parent: Node, name: String) -> Node:
|
||||
"""Recursively find a node by exact name."""
|
||||
for child in parent.get_children():
|
||||
if child.name == name:
|
||||
return child
|
||||
var found := _find_node(child, name)
|
||||
if found != null:
|
||||
return found
|
||||
return null
|
||||
|
||||
|
||||
func _find_node_by_type(parent: Node, type_name: String) -> Node:
|
||||
"""Recursively find a node by its script or class name."""
|
||||
for child in parent.get_children():
|
||||
var class_str := str(child.get_class())
|
||||
if class_str == type_name:
|
||||
return child
|
||||
var found := _find_node_by_type(child, type_name)
|
||||
if found != null:
|
||||
return found
|
||||
return null
|
||||
|
||||
|
||||
func _find_nodes_by_type(parent: Node, type_name: String) -> Array[Node]:
|
||||
"""Recursively find all nodes of a given type."""
|
||||
var results: Array[Node] = []
|
||||
if str(parent.get_class()) == type_name:
|
||||
results.append(parent)
|
||||
for child in parent.get_children():
|
||||
results.append_array(_find_nodes_by_type(child, type_name))
|
||||
return results
|
||||
|
||||
|
||||
func _find_children_by_name_prefix(parent: Node, prefix: String) -> Array[Node]:
|
||||
"""Find all immediate children whose name starts with prefix."""
|
||||
var results: Array[Node] = []
|
||||
for child in parent.get_children():
|
||||
if child.name.begins_with(prefix):
|
||||
results.append(child)
|
||||
return results
|
||||
|
||||
|
||||
func _count_nodes(node: Node) -> int:
|
||||
"""Count total nodes in the subtree, including node itself."""
|
||||
var count := 1
|
||||
for child in node.get_children():
|
||||
count += _count_nodes(child)
|
||||
return count
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main validation runner
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _run_all_checks() -> void:
|
||||
"""Execute all validation categories."""
|
||||
print("--- Root Node ---")
|
||||
_check_root_node()
|
||||
|
||||
print("")
|
||||
print("--- Spawn Groups ---")
|
||||
_check_spawn_groups()
|
||||
|
||||
print("")
|
||||
print("--- Bomb Sites ---")
|
||||
_check_bomb_sites()
|
||||
|
||||
print("")
|
||||
print("--- Buy Zones ---")
|
||||
_check_buy_zones()
|
||||
|
||||
print("")
|
||||
print("--- Map Bounds ---")
|
||||
_check_map_bounds()
|
||||
|
||||
print("")
|
||||
print("--- LightmapGI Configuration ---")
|
||||
_check_lightmapgi()
|
||||
|
||||
print("")
|
||||
print("--- Lighting Setup ---")
|
||||
_check_lighting()
|
||||
|
||||
print("")
|
||||
print("--- Node Count Sanity ---")
|
||||
_check_node_sanity()
|
||||
|
||||
print("")
|
||||
print("--- CSG Combiner Hierarchy ---")
|
||||
_check_csg_hierarchy()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Individual check groups
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _check_root_node() -> void:
|
||||
"""Verify root node is a CSG Combiner or suitable root."""
|
||||
if _root_node == null:
|
||||
_fail("Root node is null")
|
||||
return
|
||||
|
||||
var class_name := _root_node.get_class()
|
||||
# Accept CSGCombiner3D as root, or any reasonable Spatial/Node3D
|
||||
var is_csg := class_name in ["CSGCombiner3D", "CSGSphere3D", "CSGBox3D", "CSGCylinder3D", "CSGTorus3D"]
|
||||
var is_node3d := class_name in ["Node3D", "Spatial"]
|
||||
|
||||
if is_csg:
|
||||
_pass("Root node is CSGCombiner3D ('" + _root_node.name + "')")
|
||||
elif is_node3d:
|
||||
_warn("Root node is Node3D ('" + _root_node.name + "') — expected CSGCombiner3D for map geometry")
|
||||
else:
|
||||
_warn("Root node is '" + class_name + "' ('" + _root_node.name + "') — expected CSGCombiner3D")
|
||||
|
||||
|
||||
func _check_spawn_groups() -> void:
|
||||
"""Verify spawn point groups exist (T Spawn, CT Spawn)."""
|
||||
var found_t_spawn := false
|
||||
var found_ct_spawn := false
|
||||
|
||||
for child in _root_node.get_children():
|
||||
var name_lower := child.name.to_lower()
|
||||
if "t_spawn" in name_lower or "tspawn" in name_lower or "terrorist" in name_lower:
|
||||
found_t_spawn = true
|
||||
_pass("T Spawn group found: '" + child.name + "'")
|
||||
if "ct_spawn" in name_lower or "ctspawn" in name_lower or "counter" in name_lower:
|
||||
found_ct_spawn = true
|
||||
_pass("CT Spawn group found: '" + child.name + "'")
|
||||
|
||||
if not found_t_spawn:
|
||||
_fail("T Spawn group not found (expected node containing 'T_Spawn' or 'Terrorist')")
|
||||
if not found_ct_spawn:
|
||||
_fail("CT Spawn group not found (expected node containing 'CT_Spawn' or 'Counter')")
|
||||
|
||||
|
||||
func _check_bomb_sites() -> void:
|
||||
"""Verify bomb site marker nodes exist (A, B)."""
|
||||
var found_a := false
|
||||
var found_b := false
|
||||
|
||||
for child in _root_node.get_children():
|
||||
var name_lower := child.name.to_lower()
|
||||
if "bombsite_a" in name_lower or "bomb_site_a" in name_lower or "site_a" in name_lower or "a_site" in name_lower:
|
||||
found_a = true
|
||||
_pass("Bomb Site A found: '" + child.name + "'")
|
||||
if "bombsite_b" in name_lower or "bomb_site_b" in name_lower or "site_b" in name_lower or "b_site" in name_lower:
|
||||
found_b = true
|
||||
_pass("Bomb Site B found: '" + child.name + "'")
|
||||
|
||||
if not found_a:
|
||||
_fail("Bomb Site A not found (expected node containing 'Bombsite_A' or 'Site_A')")
|
||||
if not found_b:
|
||||
_fail("Bomb Site B not found (expected node containing 'Bombsite_B' or 'Site_B')")
|
||||
|
||||
|
||||
func _check_buy_zones() -> void:
|
||||
"""Verify buy zone areas exist for T and CT spawns."""
|
||||
var found_t_buy := false
|
||||
var found_ct_buy := false
|
||||
|
||||
for child in _root_node.get_children():
|
||||
var name_lower := child.name.to_lower()
|
||||
if ("t_buy" in name_lower or "buy_t" in name_lower or "buyzone_t" in name_lower) and "zone" in name_lower:
|
||||
found_t_buy = true
|
||||
_pass("T Buy Zone found: '" + child.name + "'")
|
||||
if ("ct_buy" in name_lower or "buy_ct" in name_lower or "buyzone_ct" in name_lower) and "zone" in name_lower:
|
||||
found_ct_buy = true
|
||||
_pass("CT Buy Zone found: '" + child.name + "'")
|
||||
|
||||
if not found_t_buy or not found_ct_buy:
|
||||
_warn("Buy zones not found — expected Area3D nodes named 'T_BuyZone' and 'CT_BuyZone'")
|
||||
|
||||
|
||||
func _check_map_bounds() -> void:
|
||||
"""Verify map bounds exist (invisible walls, kill plane)."""
|
||||
var found_bounds := false
|
||||
var found_killplane := false
|
||||
|
||||
for child in _root_node.get_children():
|
||||
var name_lower := child.name.to_lower()
|
||||
if "bounds" in name_lower or "boundary" in name_lower or "wall" in name_lower:
|
||||
if "invisible" in name_lower or "clip" in name_lower or "world" in name_lower:
|
||||
found_bounds = true
|
||||
_pass("Map bounds found: '" + child.name + "'")
|
||||
if "kill" in name_lower or "death" in name_lower or "fall" in name_lower:
|
||||
found_killplane = true
|
||||
_pass("Kill plane / death zone found: '" + child.name + "'")
|
||||
|
||||
if not found_bounds:
|
||||
_warn("No map boundary/invisible wall nodes detected (optional but recommended)")
|
||||
if not found_killplane:
|
||||
_warn("No kill plane / death zone found (optional but recommended)")
|
||||
|
||||
|
||||
func _check_lightmapgi() -> void:
|
||||
"""Validate LightmapGI node configuration."""
|
||||
var lightmap := _find_node_by_type(_root_node, "LightmapGI")
|
||||
if lightmap == null:
|
||||
_fail("LightmapGI node not found in scene")
|
||||
return
|
||||
|
||||
_pass("LightmapGI node found: '" + lightmap.name + "'")
|
||||
|
||||
# Check quality
|
||||
if lightmap.has_method("get_quality"):
|
||||
var q := lightmap.get_quality()
|
||||
_check(q >= 1, "LightmapGI quality set (≥ Medium)", "LightmapGI quality is Low (0) — set to at least Medium")
|
||||
|
||||
# Check bounces
|
||||
var bounces: int = lightmap.get("bounces") if "bounces" in lightmap else -1
|
||||
if bounces >= 0:
|
||||
_check(bounces >= 2, "LightmapGI.bounces >= 2", "LightmapGI.bounces is " + str(bounces) + " — expected ≥ 2")
|
||||
|
||||
# Check interior
|
||||
var interior: bool = lightmap.get("interior") if "interior" in lightmap else false
|
||||
_check(interior == true, "LightmapGI.interior = true (indoor map)", "LightmapGI.interior is false — set to true for indoor scenes")
|
||||
|
||||
# Check texel scale
|
||||
var texel_scale: float = lightmap.get("texel_scale") if "texel_scale" in lightmap else 0.0
|
||||
_check(abs(texel_scale - 1.0) < 0.01, "LightmapGI.texel_scale ≈ 1.0", "LightmapGI.texel_scale is " + str(texel_scale) + " — expected 1.0")
|
||||
|
||||
# Check denoiser
|
||||
var denoiser: bool = lightmap.get("use_denoiser") if "use_denoiser" in lightmap else false
|
||||
_check(denoiser == true, "LightmapGI.use_denoiser = true", "LightmapGI.use_denoiser is false — recommended true")
|
||||
|
||||
# Check max texture size
|
||||
var max_tex: int = lightmap.get("max_texture_size") if "max_texture_size" in lightmap else 0
|
||||
if max_tex > 0:
|
||||
_check(max_tex <= 1024, "LightmapGI.max_texture_size ≤ 1024", "LightmapGI.max_texture_size is " + str(max_tex) + " — consider ≤ 1024")
|
||||
|
||||
# Check energy
|
||||
var energy: float = lightmap.get("energy") if "energy" in lightmap else 1.0
|
||||
_warn("LightmapGI energy = " + str(energy) + " (verify visually)")
|
||||
|
||||
|
||||
func _check_lighting() -> void:
|
||||
"""Verify lighting setup: WorldEnvironment, directional light, etc."""
|
||||
var world_env := _find_node_by_type(_root_node, "WorldEnvironment")
|
||||
if world_env != null:
|
||||
_pass("WorldEnvironment node found")
|
||||
# Check environment resource
|
||||
if world_env.has_method("get_environment"):
|
||||
var env := world_env.get_environment()
|
||||
if env != null:
|
||||
_pass("WorldEnvironment has Environment resource")
|
||||
else:
|
||||
_fail("WorldEnvironment has no Environment resource assigned")
|
||||
else:
|
||||
_warn("WorldEnvironment — check environment assignment manually")
|
||||
else:
|
||||
_fail("WorldEnvironment node not found — required for map lighting")
|
||||
|
||||
# Directional light (sun)
|
||||
var sun := _find_node_by_type(_root_node, "DirectionalLight3D")
|
||||
if sun != null:
|
||||
_pass("DirectionalLight3D (sun) found: '" + sun.name + "'")
|
||||
else:
|
||||
_fail("DirectionalLight3D not found — map needs a sun light")
|
||||
|
||||
# Check for too many dynamic lights
|
||||
var omni_lights := _find_nodes_by_type(_root_node, "OmniLight3D")
|
||||
var spot_lights := _find_nodes_by_type(_root_node, "SpotLight3D")
|
||||
if omni_lights.size() + spot_lights.size() > 4:
|
||||
_warn("Scene has " + str(omni_lights.size() + spot_lights.size()) + " dynamic lights — consider baking (recommended ≤ 4)")
|
||||
else:
|
||||
_pass("Dynamic light count (" + str(omni_lights.size() + spot_lights.size()) + ") within budget (≤ 4)")
|
||||
|
||||
# Reflection probes
|
||||
var ref_probes := _find_nodes_by_type(_root_node, "ReflectionProbe")
|
||||
if ref_probes.size() > 0:
|
||||
_pass("ReflectionProbe(s) found: " + str(ref_probes.size()))
|
||||
else:
|
||||
_warn("No ReflectionProbe found — consider adding for visual quality")
|
||||
|
||||
|
||||
func _check_node_sanity() -> void:
|
||||
"""Verify total node count is within expected range."""
|
||||
var total := _count_nodes(_root_node)
|
||||
if total < EXPECTED_MIN_NODES:
|
||||
_warn("Scene has only " + str(total) + " nodes — very minimal (< " + str(EXPECTED_MIN_NODES) + ")")
|
||||
elif total > EXPECTED_MAX_NODES:
|
||||
_warn("Scene has " + str(total) + " nodes — consider optimization (> " + str(EXPECTED_MAX_NODES) + ")")
|
||||
else:
|
||||
_pass("Node count " + str(total) + " within expected range [" + str(EXPECTED_MIN_NODES) + ", " + str(EXPECTED_MAX_NODES) + "]")
|
||||
|
||||
|
||||
func _check_csg_hierarchy() -> void:
|
||||
"""Check that CSG nodes are properly parented under a CSGCombiner3D."""
|
||||
var csg_nodes := _find_nodes_by_type(_root_node, "CSGCombiner3D")
|
||||
if csg_nodes.size() > 1:
|
||||
_warn("Multiple CSGCombiner3D nodes found (" + str(csg_nodes.size()) + ") — expected single root combiner")
|
||||
elif csg_nodes.size() == 0:
|
||||
_warn("No CSGCombiner3D found at root (map may use mesh instances instead)")
|
||||
|
||||
var csg_shape_nodes := 0
|
||||
csg_shape_nodes += _find_nodes_by_type(_root_node, "CSGBox3D").size()
|
||||
csg_shape_nodes += _find_nodes_by_type(_root_node, "CSGSphere3D").size()
|
||||
csg_shape_nodes += _find_nodes_by_type(_root_node, "CSGCylinder3D").size()
|
||||
csg_shape_nodes += _find_nodes_by_type(_root_node, "CSGTorus3D").size()
|
||||
csg_shape_nodes += _find_nodes_by_type(_root_node, "CSGPolygon3D").size()
|
||||
|
||||
if csg_shape_nodes > 0:
|
||||
_pass("CSG primitive nodes found: " + str(csg_shape_nodes))
|
||||
else:
|
||||
_warn("No CSG primitive nodes found — map may use imported meshes instead")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Summary
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _print_summary() -> void:
|
||||
"""Print a final summary of all checks."""
|
||||
print("")
|
||||
print("==========================================")
|
||||
print(" VALIDATION SUMMARY")
|
||||
print("==========================================")
|
||||
print(" Scene: ", _scene_path)
|
||||
print(" Passed: ", _pass_count)
|
||||
print(" Failed: ", _fail_count)
|
||||
print(" Warnings: ", _warning_count)
|
||||
print("------------------------------------------")
|
||||
if _fail_count > 0:
|
||||
print(" RESULT: FAILED ❌")
|
||||
printerr("Validation FAILED — ", _fail_count, " check(s) failed.")
|
||||
else:
|
||||
print(" RESULT: PASSED ✅")
|
||||
print("==========================================")
|
||||
print("")
|
||||
@@ -0,0 +1 @@
|
||||
uid://olpdh1gx2gca
|
||||
@@ -0,0 +1,465 @@
|
||||
@tool
|
||||
extends SceneTree
|
||||
|
||||
# =============================================================================
|
||||
# validate_polycount.gd — CSG Triangle Budget Checker
|
||||
# =============================================================================
|
||||
# Audits a Godot 4 map scene for CSG polygon / triangle budget compliance.
|
||||
# Budget limits (competitive FPS):
|
||||
# - Total faces: < 50,000
|
||||
# - Per node: < 5,000
|
||||
# - Total CSG nodes: < 200
|
||||
#
|
||||
# Usage:
|
||||
# godot --headless --script client/scripts/validate_polycount.gd -- res://path/to/map.tscn
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 — All poly budgets met
|
||||
# 1 — One or more budgets exceeded
|
||||
# 2 — Scene file not found or cannot be loaded
|
||||
# =============================================================================
|
||||
|
||||
# Budget constants (adjust for target platform)
|
||||
const MAX_TOTAL_FACES := 50_000
|
||||
const MAX_FACES_PER_NODE := 5_000
|
||||
const MAX_CSG_NODES := 200
|
||||
const MAX_MESH_INSTANCES := 150
|
||||
const WARN_TOTAL_FACES := 40_000
|
||||
const WARN_FACES_PER_NODE := 3_000
|
||||
|
||||
var _pass_count := 0
|
||||
var _fail_count := 0
|
||||
var _warning_count := 0
|
||||
var _scene_path := ""
|
||||
var _scene_instance: Node = null
|
||||
|
||||
# Accumulated stats
|
||||
var _total_faces := 0
|
||||
var _total_vertices := 0
|
||||
var _total_csg_nodes := 0
|
||||
var _total_mesh_instances := 0
|
||||
var _node_face_counts: Dictionary = {} # node_path -> face_count
|
||||
var _highest_face_node := ""
|
||||
var _highest_face_count := 0
|
||||
var _csg_node_list: Array[String] = []
|
||||
var _overbudget_nodes: Array[String] = []
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
_parse_arguments()
|
||||
|
||||
if _scene_path.is_empty():
|
||||
printerr("ERROR: No scene path provided. Usage: godot --headless --script validate_polycount.gd -- res://path/to/map.tscn")
|
||||
quit(2)
|
||||
return
|
||||
|
||||
print("")
|
||||
print("==========================================")
|
||||
print(" CSG POLYGON BUDGET VALIDATOR")
|
||||
print(" Scene: ", _scene_path)
|
||||
print("==========================================")
|
||||
print("")
|
||||
|
||||
# Budget header
|
||||
print(" Budget limits:")
|
||||
print(" Max total faces: ", _format_num(MAX_TOTAL_FACES))
|
||||
print(" Max faces per node: ", _format_num(MAX_FACES_PER_NODE))
|
||||
print(" Max CSG nodes: ", _format_num(MAX_CSG_NODES))
|
||||
print(" Max MeshInstance3D: ", _format_num(MAX_MESH_INSTANCES))
|
||||
print("")
|
||||
|
||||
var scene: PackedScene = _load_scene(_scene_path)
|
||||
if scene == null:
|
||||
printerr("ERROR: Scene not found: ", _scene_path)
|
||||
quit(2)
|
||||
return
|
||||
|
||||
_scene_instance = _instantiate_scene(scene)
|
||||
if _scene_instance == null:
|
||||
printerr("ERROR: Failed to instantiate scene: ", _scene_path)
|
||||
quit(2)
|
||||
return
|
||||
|
||||
root.add_child(_scene_instance)
|
||||
|
||||
# Perform analysis
|
||||
_analyze_node(_scene_instance, "")
|
||||
|
||||
# Run all checks
|
||||
_run_checks()
|
||||
|
||||
# Print detailed report
|
||||
_print_detailed_report()
|
||||
|
||||
# Summary
|
||||
_print_summary()
|
||||
|
||||
if _fail_count > 0:
|
||||
quit(1)
|
||||
else:
|
||||
quit(0)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Argument parsing
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _parse_arguments() -> void:
|
||||
"""Extract scene path from user arguments (after --)."""
|
||||
var args: PackedStringArray = OS.get_cmdline_user_args()
|
||||
if not args.is_empty():
|
||||
_scene_path = args[0].strip_edges()
|
||||
return
|
||||
|
||||
# Fallback: scan full cmdline for --
|
||||
var full_args: PackedStringArray = OS.get_cmdline_args()
|
||||
var found_sep := false
|
||||
for i in range(full_args.size()):
|
||||
if full_args[i] == "--":
|
||||
found_sep = true
|
||||
continue
|
||||
if found_sep:
|
||||
_scene_path = full_args[i].strip_edges()
|
||||
break
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Scene helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _load_scene(path: String) -> PackedScene:
|
||||
"""Load a .tscn/.scn file."""
|
||||
if not ResourceLoader.exists(path):
|
||||
if not path.ends_with(".tscn") and not path.ends_with(".scn"):
|
||||
path = path + ".tscn"
|
||||
if not ResourceLoader.exists(path):
|
||||
return null
|
||||
return ResourceLoader.load(path, "PackedScene", ResourceLoader.CACHE_MODE_REUSE)
|
||||
|
||||
|
||||
func _instantiate_scene(scene: PackedScene) -> Node:
|
||||
"""Instantiate a packed scene."""
|
||||
return scene.instantiate()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Check helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _pass(msg: String) -> void:
|
||||
_pass_count += 1
|
||||
print(" ✓ ", msg)
|
||||
|
||||
|
||||
func _fail(msg: String) -> void:
|
||||
_fail_count += 1
|
||||
print(" ✗ ", msg)
|
||||
|
||||
|
||||
func _warn(msg: String) -> void:
|
||||
_warning_count += 1
|
||||
print(" ⚠ ", msg)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Polygon analysis engine
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _analyze_node(node: Node, path: String) -> void:
|
||||
"""
|
||||
Recursively walk the scene tree and collect polygon statistics
|
||||
for all CSG and MeshInstance nodes.
|
||||
"""
|
||||
var current_path := path + "/" + node.name if path != "" else node.name
|
||||
|
||||
# Handle CSG nodes
|
||||
var class_name := node.get_class()
|
||||
var is_csg := class_name in [
|
||||
"CSGCombiner3D", "CSGSphere3D", "CSGBox3D", "CSGCylinder3D",
|
||||
"CSGTorus3D", "CSGPolygon3D", "CSGMesh3D"
|
||||
]
|
||||
|
||||
if is_csg:
|
||||
_total_csg_nodes += 1
|
||||
_csg_node_list.append(current_path)
|
||||
|
||||
# Try to get mesh / face count
|
||||
var face_count := _estimate_faces(node, class_name)
|
||||
var vert_count := _estimate_vertices(node, class_name)
|
||||
|
||||
if face_count > 0:
|
||||
_total_faces += face_count
|
||||
_total_vertices += vert_count
|
||||
_node_face_counts[current_path] = face_count
|
||||
|
||||
if face_count > _highest_face_count:
|
||||
_highest_face_count = face_count
|
||||
_highest_face_node = current_path
|
||||
|
||||
if face_count > MAX_FACES_PER_NODE:
|
||||
_overbudget_nodes.append(current_path)
|
||||
|
||||
# Count MeshInstance3D
|
||||
if class_name == "MeshInstance3D" or node.is_class("MeshInstance3D"):
|
||||
_total_mesh_instances += 1
|
||||
|
||||
# Recurse children
|
||||
for child in node.get_children():
|
||||
_analyze_node(child, current_path)
|
||||
|
||||
|
||||
func _estimate_faces(node: Node, class_name: String) -> int:
|
||||
"""
|
||||
Attempt to estimate the number of triangle faces for a given node.
|
||||
For CSG nodes, Godot exposes get_meshes() which returns arrays.
|
||||
For MeshInstance3D, we inspect the mesh resource.
|
||||
"""
|
||||
var faces := 0
|
||||
|
||||
# CSG nodes can provide their collision / bake mesh
|
||||
if class_name in ["CSGCombiner3D", "CSGSphere3D", "CSGBox3D", "CSGCylinder3D",
|
||||
"CSGTorus3D", "CSGPolygon3D", "CSGMesh3D"]:
|
||||
|
||||
# Try to get the bake mesh (the triangulated mesh used for lightmapping)
|
||||
if node.has_method("get_bake_mesh"):
|
||||
var bake_mesh := node.get_bake_mesh()
|
||||
if bake_mesh != null:
|
||||
faces = _count_mesh_faces(bake_mesh)
|
||||
|
||||
# If that fails, try the collision/debug mesh
|
||||
if faces == 0 and node.has_method("get_meshes"):
|
||||
var mesh_arrays := node.get_meshes()
|
||||
if mesh_arrays is Array and mesh_arrays.size() > 1:
|
||||
var mesh := mesh_arrays[1] as ArrayMesh
|
||||
if mesh != null:
|
||||
faces = _count_mesh_faces(mesh)
|
||||
|
||||
# Fallback estimate: for simple primitives
|
||||
if faces == 0:
|
||||
match class_name:
|
||||
"CSGBox3D":
|
||||
faces = 12 # 6 faces * 2 triangles
|
||||
"CSGSphere3D":
|
||||
faces = 320 # typical sphere LOD
|
||||
"CSGCylinder3D":
|
||||
faces = 128 # typical cylinder
|
||||
"CSGTorus3D":
|
||||
faces = 512 # typical torus
|
||||
"CSGPolygon3D":
|
||||
faces = 64 # default polygon
|
||||
"CSGMesh3D":
|
||||
faces = 100 # fallback
|
||||
"CSGCombiner3D":
|
||||
faces = 0 # combiner itself has no geometry
|
||||
|
||||
# MeshInstance3D: count faces from mesh resource
|
||||
if class_name == "MeshInstance3D" or node.is_class("MeshInstance3D"):
|
||||
if "mesh" in node and node.mesh != null:
|
||||
faces = _count_mesh_faces(node.mesh)
|
||||
|
||||
return faces
|
||||
|
||||
|
||||
func _estimate_vertices(node: Node, class_name: String) -> int:
|
||||
"""Estimate vertices from the node's mesh if available."""
|
||||
var verts := 0
|
||||
|
||||
if class_name in ["CSGCombiner3D", "CSGSphere3D", "CSGBox3D", "CSGCylinder3D",
|
||||
"CSGTorus3D", "CSGPolygon3D", "CSGMesh3D"]:
|
||||
if node.has_method("get_bake_mesh"):
|
||||
var bake_mesh := node.get_bake_mesh()
|
||||
if bake_mesh != null:
|
||||
verts = _count_mesh_vertices(bake_mesh)
|
||||
|
||||
if verts == 0 and (class_name == "MeshInstance3D" or node.is_class("MeshInstance3D")):
|
||||
if "mesh" in node and node.mesh != null:
|
||||
verts = _count_mesh_vertices(node.mesh)
|
||||
|
||||
return verts
|
||||
|
||||
|
||||
func _count_mesh_faces(mesh) -> int:
|
||||
"""Count triangle faces in any Mesh type."""
|
||||
if mesh == null:
|
||||
return 0
|
||||
|
||||
var count := 0
|
||||
|
||||
# ArrayMesh and similar
|
||||
if mesh.has_method("get_surface_count"):
|
||||
var surface_count := mesh.get_surface_count()
|
||||
for s in range(surface_count):
|
||||
var arrays := mesh.surface_get_arrays(s)
|
||||
if arrays is Array and arrays.size() > 0:
|
||||
var indices := arrays[Mesh.ARRAY_INDEX] as PackedInt32Array
|
||||
if indices != null and indices.size() > 0:
|
||||
count += indices.size() / 3
|
||||
else:
|
||||
# Non-indexed: count vertices / 3
|
||||
var verts := arrays[Mesh.ARRAY_VERTEX] as PackedVector3Array
|
||||
if verts != null:
|
||||
count += verts.size() / 3
|
||||
return count
|
||||
|
||||
|
||||
func _count_mesh_vertices(mesh) -> int:
|
||||
"""Count vertices in any Mesh type."""
|
||||
if mesh == null:
|
||||
return 0
|
||||
|
||||
var count := 0
|
||||
if mesh.has_method("get_surface_count"):
|
||||
var surface_count := mesh.get_surface_count()
|
||||
for s in range(surface_count):
|
||||
var arrays := mesh.surface_get_arrays(s)
|
||||
if arrays is Array and arrays.size() > 0:
|
||||
var verts := arrays[Mesh.ARRAY_VERTEX] as PackedVector3Array
|
||||
if verts != null:
|
||||
count += verts.size()
|
||||
return count
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Budget checks
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _run_checks() -> void:
|
||||
"""Run all poly budget validation checks."""
|
||||
print("--- Budget Checks ---")
|
||||
|
||||
# Total faces check
|
||||
if _total_faces < WARN_TOTAL_FACES:
|
||||
_pass("Total faces: " + _format_num(_total_faces) + " (budget: < " + _format_num(MAX_TOTAL_FACES) + ")")
|
||||
elif _total_faces < MAX_TOTAL_FACES:
|
||||
_warn("Total faces approaching limit: " + _format_num(_total_faces) + " (budget: < " + _format_num(MAX_TOTAL_FACES) + ")")
|
||||
else:
|
||||
_fail("Total faces EXCEEDED: " + _format_num(_total_faces) + " (budget: < " + _format_num(MAX_TOTAL_FACES) + ")")
|
||||
|
||||
# Per-node face check
|
||||
if _overbudget_nodes.size() == 0:
|
||||
_pass("No nodes exceed " + _format_num(MAX_FACES_PER_NODE) + " faces")
|
||||
else:
|
||||
for node_path in _overbudget_nodes:
|
||||
_fail("Node exceeds face budget: " + node_path + " (" + _format_num(_node_face_counts[node_path]) + " faces, max: " + _format_num(MAX_FACES_PER_NODE) + ")")
|
||||
|
||||
# CSG node count check
|
||||
if _total_csg_nodes < MAX_CSG_NODES:
|
||||
_pass("CSG node count: " + str(_total_csg_nodes) + " (budget: < " + str(MAX_CSG_NODES) + ")")
|
||||
else:
|
||||
_fail("CSG node count EXCEEDED: " + str(_total_csg_nodes) + " (budget: < " + str(MAX_CSG_NODES) + ")")
|
||||
|
||||
# Mesh instance count check
|
||||
if _total_mesh_instances < MAX_MESH_INSTANCES:
|
||||
_pass("MeshInstance3D count: " + str(_total_mesh_instances) + " (budget: < " + str(MAX_MESH_INSTANCES) + ")")
|
||||
else:
|
||||
_fail("MeshInstance3D count EXCEEDED: " + str(_total_mesh_instances) + " (budget: < " + str(MAX_MESH_INSTANCES) + ")")
|
||||
|
||||
# Highest face node info
|
||||
print("")
|
||||
print("--- Hot Spots ---")
|
||||
if _highest_face_count > 0:
|
||||
if _highest_face_count > WARN_FACES_PER_NODE:
|
||||
_warn("Highest face node: " + _highest_face_node + " (" + _format_num(_highest_face_count) + " faces)")
|
||||
else:
|
||||
_pass("Highest face node: " + _highest_face_node + " (" + _format_num(_highest_face_count) + " faces)")
|
||||
else:
|
||||
_warn("No face data collected (scene may use non-CSG geometry)")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Detailed report
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _print_detailed_report() -> void:
|
||||
"""Print a detailed per-node breakdown if there are few enough nodes."""
|
||||
print("")
|
||||
print("--- Detailed Breakdown ---")
|
||||
print(" Total triangles: ", _format_num(_total_faces))
|
||||
print(" Total vertices: ", _format_num(_total_vertices))
|
||||
print(" CSG nodes: ", _total_csg_nodes)
|
||||
print(" MeshInstance3D: ", _total_mesh_instances)
|
||||
print(" Nodes over budget: ", _overbudget_nodes.size())
|
||||
print("")
|
||||
|
||||
# Top 10 most expensive nodes
|
||||
var sorted_nodes: Array[Dictionary] = []
|
||||
for node_path in _node_face_counts.keys():
|
||||
sorted_nodes.append({"path": node_path, "faces": _node_face_counts[node_path]})
|
||||
sorted_nodes.sort_custom(_sort_by_faces_desc)
|
||||
|
||||
var top_n := mini(sorted_nodes.size(), 10)
|
||||
if top_n > 0:
|
||||
print(" Top " + str(top_n) + " most expensive nodes:")
|
||||
for i in range(top_n):
|
||||
var entry := sorted_nodes[i] as Dictionary
|
||||
var bar := _make_bar(entry.faces, _highest_face_count)
|
||||
print(" " + str(i + 1) + ". " + entry.path + " — " + _format_num(entry.faces) + " faces " + bar)
|
||||
print("")
|
||||
|
||||
|
||||
func _sort_by_faces_desc(a: Dictionary, b: Dictionary) -> bool:
|
||||
return a.faces > b.faces
|
||||
|
||||
|
||||
func _make_bar(value: int, max_value: int) -> String:
|
||||
"""Create a simple ASCII bar visualization."""
|
||||
if max_value == 0:
|
||||
return ""
|
||||
var ratio := float(value) / float(max_value)
|
||||
var bar_len := int(ratio * 20)
|
||||
if bar_len < 1 and value > 0:
|
||||
bar_len = 1
|
||||
var bar := ""
|
||||
for i in range(bar_len):
|
||||
bar += "█"
|
||||
return bar
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Formatting helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _format_num(n: int) -> String:
|
||||
"""Format a number with thousand separators."""
|
||||
var s := str(n)
|
||||
var result := ""
|
||||
var count := 0
|
||||
for i in range(s.length() - 1, -1, -1):
|
||||
if count > 0 and count % 3 == 0:
|
||||
result = "," + result
|
||||
result = s[i] + result
|
||||
count += 1
|
||||
return result
|
||||
|
||||
|
||||
func mini(a: int, b: int) -> int:
|
||||
return a if a < b else b
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Summary
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _print_summary() -> void:
|
||||
"""Print final summary."""
|
||||
print("==========================================")
|
||||
print(" POLYGON BUDGET SUMMARY")
|
||||
print("==========================================")
|
||||
print(" Scene: ", _scene_path)
|
||||
print(" Total faces: ", _format_num(_total_faces))
|
||||
print(" Total vertices: ", _format_num(_total_vertices))
|
||||
print(" CSG nodes: ", _total_csg_nodes)
|
||||
print(" MeshInstance3D: ", _total_mesh_instances)
|
||||
print(" Over-budget: ", _overbudget_nodes.size())
|
||||
print("------------------------------------------")
|
||||
print(" Passed: ", _pass_count)
|
||||
print(" Failed: ", _fail_count)
|
||||
print(" Warnings: ", _warning_count)
|
||||
print("------------------------------------------")
|
||||
if _fail_count > 0:
|
||||
print(" RESULT: FAILED ❌")
|
||||
printerr("Validation FAILED — ", _fail_count, " budget(s) exceeded.")
|
||||
else:
|
||||
print(" RESULT: PASSED ✅")
|
||||
print("==========================================")
|
||||
print("")
|
||||
@@ -0,0 +1 @@
|
||||
uid://bqau1kivb87fk
|
||||
@@ -0,0 +1 @@
|
||||
uid://d0go24t1lsx2j
|
||||
@@ -0,0 +1,581 @@
|
||||
@tool
|
||||
extends SceneTree
|
||||
|
||||
# =============================================================================
|
||||
# validate_textures.gd — Texture Size / Mipmaps / UV2 Checker
|
||||
# =============================================================================
|
||||
# Scans all material and texture resources used in a scene and validates:
|
||||
# - Texture dimensions ≤ 1024 (1K)
|
||||
# - Mipmaps are enabled on all textures
|
||||
# - UV2 channel is present on meshes (for lightmapping)
|
||||
# - Texture format is appropriate
|
||||
#
|
||||
# Usage:
|
||||
# godot --headless --script client/scripts/validate_textures.gd -- res://path/to/map.tscn
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 — All texture checks passed
|
||||
# 1 — One or more texture checks failed
|
||||
# 2 — Scene file not found or cannot be loaded
|
||||
# =============================================================================
|
||||
|
||||
const MAX_TEXTURE_SIZE := 1024
|
||||
const MAX_TEXTURE_SIZE_LABEL := "1K (1024x1024)"
|
||||
|
||||
var _pass_count := 0
|
||||
var _fail_count := 0
|
||||
var _warning_count := 0
|
||||
var _scene_path := ""
|
||||
var _scene_instance: Node = null
|
||||
|
||||
# Collected data
|
||||
var _textures_checked: Dictionary = {} # resource_path -> info dict
|
||||
var _overbudget_textures: Array[Dictionary] = []
|
||||
var _no_mipmap_textures: Array[Dictionary] = []
|
||||
var _meshes_without_uv2: Array[String] = []
|
||||
var _meshes_with_uv2: Array[String] = []
|
||||
var _mesh_count := 0
|
||||
var _total_textures := 0
|
||||
var _total_materials := 0
|
||||
var _texture_memory_estimate := 0 # approximate total texture memory in bytes
|
||||
|
||||
# Unique set to avoid re-checking the same resource
|
||||
var _checked_resources: Dictionary = {}
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
_parse_arguments()
|
||||
|
||||
if _scene_path.is_empty():
|
||||
printerr("ERROR: No scene path provided. Usage: godot --headless --script validate_textures.gd -- res://path/to/map.tscn")
|
||||
quit(2)
|
||||
return
|
||||
|
||||
print("")
|
||||
print("==========================================")
|
||||
print(" TEXTURE VALIDATOR")
|
||||
print(" Scene: ", _scene_path)
|
||||
print("==========================================")
|
||||
print("")
|
||||
|
||||
print(" Texture budget:")
|
||||
print(" Max dimension: ", MAX_TEXTURE_SIZE_LABEL)
|
||||
print(" Mipmaps: Required")
|
||||
print(" UV2 channel: Required for lightmapping")
|
||||
print("")
|
||||
|
||||
var scene: PackedScene = _load_scene(_scene_path)
|
||||
if scene == null:
|
||||
printerr("ERROR: Scene not found: ", _scene_path)
|
||||
quit(2)
|
||||
return
|
||||
|
||||
_scene_instance = _instantiate_scene(scene)
|
||||
if _scene_instance == null:
|
||||
printerr("ERROR: Failed to instantiate scene: ", _scene_path)
|
||||
quit(2)
|
||||
return
|
||||
|
||||
root.add_child(_scene_instance)
|
||||
|
||||
# Analyze
|
||||
_analyze_node(_scene_instance)
|
||||
|
||||
# Run checks
|
||||
_run_texture_checks()
|
||||
_run_mipmap_checks()
|
||||
_run_uv2_checks()
|
||||
_print_texture_report()
|
||||
|
||||
# Summary
|
||||
_print_summary()
|
||||
|
||||
if _fail_count > 0:
|
||||
quit(1)
|
||||
else:
|
||||
quit(0)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Argument parsing
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _parse_arguments() -> void:
|
||||
"""Extract scene path from user args (after --)."""
|
||||
var args: PackedStringArray = OS.get_cmdline_user_args()
|
||||
if not args.is_empty():
|
||||
_scene_path = args[0].strip_edges()
|
||||
return
|
||||
|
||||
var full_args: PackedStringArray = OS.get_cmdline_args()
|
||||
var found_sep := false
|
||||
for i in range(full_args.size()):
|
||||
if full_args[i] == "--":
|
||||
found_sep = true
|
||||
continue
|
||||
if found_sep:
|
||||
_scene_path = full_args[i].strip_edges()
|
||||
break
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Scene helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _load_scene(path: String) -> PackedScene:
|
||||
"""Load a .tscn/.scn file."""
|
||||
if not ResourceLoader.exists(path):
|
||||
if not path.ends_with(".tscn") and not path.ends_with(".scn"):
|
||||
path = path + ".tscn"
|
||||
if not ResourceLoader.exists(path):
|
||||
return null
|
||||
return ResourceLoader.load(path, "PackedScene", ResourceLoader.CACHE_MODE_REUSE)
|
||||
|
||||
|
||||
func _instantiate_scene(scene: PackedScene) -> Node:
|
||||
return scene.instantiate()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Check helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _pass(msg: String) -> void:
|
||||
_pass_count += 1
|
||||
print(" ✓ ", msg)
|
||||
|
||||
|
||||
func _fail(msg: String) -> void:
|
||||
_fail_count += 1
|
||||
print(" ✗ ", msg)
|
||||
|
||||
|
||||
func _warn(msg: String) -> void:
|
||||
_warning_count += 1
|
||||
print(" ⚠ ", msg)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Analysis engine
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _analyze_node(node: Node) -> void:
|
||||
"""Recursively walk scene, check materials on meshes."""
|
||||
var class_name := node.get_class()
|
||||
|
||||
# MeshInstance3D — inspect its material overrides
|
||||
if class_name == "MeshInstance3D" or node.is_class("MeshInstance3D"):
|
||||
_mesh_count += 1
|
||||
_analyze_mesh_instance(node)
|
||||
|
||||
# CSG nodes can also have materials / meshes
|
||||
if class_name in ["CSGCombiner3D", "CSGSphere3D", "CSGBox3D", "CSGCylinder3D",
|
||||
"CSGTorus3D", "CSGPolygon3D", "CSGMesh3D"]:
|
||||
_analyze_csg_node(node)
|
||||
|
||||
# GeometryInstance3D base class
|
||||
if node.is_class("GeometryInstance3D"):
|
||||
var mat_override := node.get("material_override") if "material_override" in node else null
|
||||
if mat_override != null:
|
||||
_analyze_material(mat_override)
|
||||
|
||||
for child in node.get_children():
|
||||
_analyze_node(child)
|
||||
|
||||
|
||||
func _analyze_mesh_instance(mi: Node) -> void:
|
||||
"""Inspect a MeshInstance3D node for materials and UV2."""
|
||||
# Check mesh
|
||||
if "mesh" in mi and mi.mesh != null:
|
||||
var mesh = mi.mesh
|
||||
_analyze_mesh(mesh, mi.name)
|
||||
|
||||
# Surface material overrides
|
||||
if "material_override" in mi and mi.material_override != null:
|
||||
_analyze_material(mi.material_override)
|
||||
|
||||
# Per-surface materials
|
||||
if mi.has_method("get_surface_override_material"):
|
||||
var surface_count := 0
|
||||
if "mesh" in mi and mi.mesh != null and mi.mesh.has_method("get_surface_count"):
|
||||
surface_count = mi.mesh.get_surface_count()
|
||||
for s in range(surface_count):
|
||||
var mat := mi.get_surface_override_material(s)
|
||||
if mat != null:
|
||||
_analyze_material(mat)
|
||||
|
||||
|
||||
func _analyze_csg_node(node: Node) -> void:
|
||||
"""Analyze materials on CSG nodes."""
|
||||
# CSG nodes have operation and material properties
|
||||
if "material" in node and node.material != null:
|
||||
_analyze_material(node.material)
|
||||
|
||||
# Check CSGMesh3D which references a mesh
|
||||
if node.get_class() == "CSGMesh3D" and "mesh" in node and node.mesh != null:
|
||||
_analyze_mesh(node.mesh, node.name)
|
||||
|
||||
|
||||
func _analyze_mesh(mesh, node_name: String) -> void:
|
||||
"""Analyze a mesh resource for UV2 and material slots."""
|
||||
if mesh == null:
|
||||
return
|
||||
|
||||
var mesh_id := mesh.resource_path if "resource_path" in mesh and mesh.resource_path != "" else str(mesh.get_instance_id())
|
||||
if _checked_resources.has(mesh_id):
|
||||
return
|
||||
_checked_resources[mesh_id] = true
|
||||
|
||||
# Check for UV2 channel
|
||||
var has_uv2 := false
|
||||
if mesh.has_method("get_surface_count"):
|
||||
var surface_count := mesh.get_surface_count()
|
||||
for s in range(surface_count):
|
||||
var arrays := mesh.surface_get_arrays(s)
|
||||
if arrays is Array and arrays.size() > 0:
|
||||
var uv2 := arrays[Mesh.ARRAY_TEX_UV2] as PackedVector2Array
|
||||
if uv2 != null and uv2.size() > 0:
|
||||
has_uv2 = true
|
||||
|
||||
if has_uv2:
|
||||
_meshes_with_uv2.append(node_name)
|
||||
else:
|
||||
_meshes_without_uv2.append(node_name)
|
||||
|
||||
# Check materials on the mesh
|
||||
if mesh.has_method("surface_get_material"):
|
||||
var surface_count := mesh.get_surface_count()
|
||||
for s in range(surface_count):
|
||||
var mat := mesh.surface_get_material(s)
|
||||
if mat != null:
|
||||
_analyze_material(mat)
|
||||
|
||||
|
||||
func _analyze_material(mat) -> void:
|
||||
"""Recursively analyze a material and its textures."""
|
||||
if mat == null:
|
||||
return
|
||||
|
||||
var mat_id := mat.resource_path if "resource_path" in mat and mat.resource_path != "" else str(mat.get_instance_id())
|
||||
if _checked_resources.has(mat_id):
|
||||
return
|
||||
_checked_resources[mat_id] = true
|
||||
_total_materials += 1
|
||||
|
||||
var mat_class := mat.get_class()
|
||||
|
||||
# StandardMaterial3D — check texture properties
|
||||
if mat_class == "StandardMaterial3D" or mat_class == "ORMMaterial3D":
|
||||
var tex_properties := [
|
||||
"albedo_texture", "metallic_texture", "roughness_texture",
|
||||
"normal_texture", "ambient_occlusion_texture", "emission_texture",
|
||||
"clearcoat_texture", "rim_texture", "transparency_texture",
|
||||
"heightmap_texture", "refraction_texture", "detail_albedo_texture",
|
||||
"detail_normal_texture", "detail_mask_texture", "subsurface_scattering_texture",
|
||||
"backlight_texture"
|
||||
]
|
||||
for prop in tex_properties:
|
||||
if prop in mat:
|
||||
var tex = mat.get(prop)
|
||||
if tex != null:
|
||||
_analyze_texture(tex)
|
||||
|
||||
# ShaderMaterial — inspect shader parameters for textures
|
||||
elif mat_class == "ShaderMaterial":
|
||||
if mat.has_method("get_shader_parameter_list"):
|
||||
var param_list := mat.get_shader_parameter_list()
|
||||
for param in param_list:
|
||||
var param_name := param.name if "name" in param else str(param)
|
||||
var val = mat.get_shader_parameter(param_name)
|
||||
if val != null and (val is Texture2D or val is Texture3D or val is TextureLayered):
|
||||
_analyze_texture(val)
|
||||
|
||||
# BaseMaterial3D (Godot 4 fallback)
|
||||
elif mat.is_class("BaseMaterial3D"):
|
||||
if "albedo_texture" in mat and mat.albedo_texture != null:
|
||||
_analyze_texture(mat.albedo_texture)
|
||||
if "metallic_texture" in mat and mat.metallic_texture != null:
|
||||
_analyze_texture(mat.metallic_texture)
|
||||
if "roughness_texture" in mat and mat.roughness_texture != null:
|
||||
_analyze_texture(mat.roughness_texture)
|
||||
if "normal_texture" in mat and mat.normal_texture != null:
|
||||
_analyze_texture(mat.normal_texture)
|
||||
|
||||
|
||||
func _analyze_texture(tex) -> void:
|
||||
"""Analyze a single texture resource for size, mipmaps, and format."""
|
||||
if tex == null:
|
||||
return
|
||||
|
||||
# Use resource path as unique key
|
||||
var tex_path := ""
|
||||
if "resource_path" in tex and tex.resource_path != "":
|
||||
tex_path = tex.resource_path
|
||||
else:
|
||||
tex_path = str(tex.get_instance_id())
|
||||
|
||||
if _textures_checked.has(tex_path):
|
||||
return
|
||||
|
||||
var tex_class := tex.get_class()
|
||||
var width := 0
|
||||
var height := 0
|
||||
var has_mipmaps := false
|
||||
var format := -1
|
||||
var format_name := "Unknown"
|
||||
|
||||
# Texture2D
|
||||
if tex is Texture2D:
|
||||
width = tex.get_width()
|
||||
height = tex.get_height()
|
||||
has_mipmaps = tex.has_mipmaps() if tex.has_method("has_mipmaps") else false
|
||||
if tex.has_method("get_format"):
|
||||
format = tex.get_format()
|
||||
format_name = _get_format_name(format)
|
||||
_total_textures += 1
|
||||
|
||||
# Texture3D
|
||||
elif tex is Texture3D:
|
||||
width = tex.get_width()
|
||||
height = tex.get_height()
|
||||
if tex.has_method("has_mipmaps"):
|
||||
has_mipmaps = tex.has_mipmaps()
|
||||
_total_textures += 1
|
||||
|
||||
# TextureLayered
|
||||
elif tex is TextureLayered:
|
||||
if tex.has_method("get_width"):
|
||||
width = tex.get_width()
|
||||
if tex.has_method("get_height"):
|
||||
height = tex.get_height()
|
||||
_total_textures += 1
|
||||
|
||||
var info := {
|
||||
"path": tex_path,
|
||||
"width": width,
|
||||
"height": height,
|
||||
"has_mipmaps": has_mipmaps,
|
||||
"format": format_name,
|
||||
"class": tex_class
|
||||
}
|
||||
_textures_checked[tex_path] = info
|
||||
|
||||
# Estimate memory
|
||||
var bpp := _bits_per_pixel(format)
|
||||
_texture_memory_estimate += (width * height * bpp) / 8
|
||||
|
||||
# Check over-budget
|
||||
var max_dim := maxi(width, height)
|
||||
if max_dim > MAX_TEXTURE_SIZE:
|
||||
_overbudget_textures.append(info)
|
||||
|
||||
# Check mipmaps
|
||||
if not has_mipmaps:
|
||||
_no_mipmap_textures.append(info)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Texture checks
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _run_texture_checks() -> void:
|
||||
"""Validate texture size budget."""
|
||||
print("--- Texture Size Checks ---")
|
||||
if _total_textures == 0:
|
||||
_warn("No textures found in scene (may be expected for procedural materials)")
|
||||
return
|
||||
|
||||
_pass("Total textures found: " + str(_total_textures))
|
||||
|
||||
if _overbudget_textures.size() == 0:
|
||||
_pass("All textures within " + MAX_TEXTURE_SIZE_LABEL + " limit")
|
||||
else:
|
||||
for tex_info in _overbudget_textures:
|
||||
var dim := str(tex_info.width) + "x" + str(tex_info.height)
|
||||
_fail("Texture over budget: " + _truncate_path(tex_info.path) + " (" + dim + ", max: " + MAX_TEXTURE_SIZE_LABEL + ")")
|
||||
|
||||
|
||||
func _run_mipmap_checks() -> void:
|
||||
"""Validate that all textures have mipmaps enabled."""
|
||||
print("")
|
||||
print("--- Mipmap Checks ---")
|
||||
|
||||
if _no_mipmap_textures.size() == 0:
|
||||
_pass("All textures have mipmaps enabled")
|
||||
else:
|
||||
for tex_info in _no_mipmap_textures:
|
||||
var dim := str(tex_info.width) + "x" + str(tex_info.height)
|
||||
_fail("Texture missing mipmaps: " + _truncate_path(tex_info.path) + " (" + dim + ", class: " + tex_info.class + ")")
|
||||
|
||||
|
||||
func _run_uv2_checks() -> void:
|
||||
"""Validate UV2 channel presence on meshes."""
|
||||
print("")
|
||||
print("--- UV2 Channel Checks ---")
|
||||
|
||||
if _mesh_count == 0:
|
||||
_warn("No MeshInstance3D nodes found")
|
||||
return
|
||||
|
||||
_pass("Meshes found: " + str(_mesh_count))
|
||||
|
||||
if _meshes_without_uv2.size() == 0:
|
||||
_pass("All " + str(_mesh_count) + " meshes have UV2 channel (good for lightmapping)")
|
||||
else:
|
||||
for mesh_name in _meshes_without_uv2:
|
||||
_fail("Mesh missing UV2 channel: " + mesh_name + " — lightmapping will not work correctly")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Report
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _print_texture_report() -> void:
|
||||
"""Print detailed texture report."""
|
||||
print("")
|
||||
print("--- Texture Report ---")
|
||||
print(" Total materials: ", _total_materials)
|
||||
print(" Total textures: ", _total_textures)
|
||||
print(" Over-budget textures: ", _overbudget_textures.size())
|
||||
print(" Missing mipmaps: ", _no_mipmap_textures.size())
|
||||
print(" Total meshes: ", _mesh_count)
|
||||
print(" Meshes with UV2: ", _meshes_with_uv2.size())
|
||||
print(" Meshes without UV2: ", _meshes_without_uv2.size())
|
||||
print(" Est. texture memory: ", _format_bytes(_texture_memory_estimate))
|
||||
print("")
|
||||
|
||||
# Print texture format distribution
|
||||
var format_counts: Dictionary = {}
|
||||
for tex_path in _textures_checked.keys():
|
||||
var info := _textures_checked[tex_path] as Dictionary
|
||||
var fmt := info.format
|
||||
if not format_counts.has(fmt):
|
||||
format_counts[fmt] = 0
|
||||
format_counts[fmt] += 1
|
||||
|
||||
if format_counts.size() > 0:
|
||||
print(" Texture format distribution:")
|
||||
for fmt in format_counts.keys():
|
||||
print(" " + fmt + ": " + str(format_counts[fmt]))
|
||||
print("")
|
||||
|
||||
|
||||
func _truncate_path(path: String) -> String:
|
||||
"""Truncate a resource path to fit in a single line."""
|
||||
if path.length() > 60:
|
||||
return "..." + path.substr(path.length() - 57)
|
||||
return path
|
||||
|
||||
|
||||
func _get_format_name(format_id: int) -> String:
|
||||
"""Map Godot image format enum to a human-readable name."""
|
||||
match format_id:
|
||||
0: return "L8"
|
||||
1: return "LA8"
|
||||
2: return "R8"
|
||||
3: return "RG8"
|
||||
4: return "RGB8"
|
||||
5: return "RGBA8"
|
||||
6: return "RGBA4444"
|
||||
7: return "RGBA5551"
|
||||
8: return "RF"
|
||||
9: return "RGF"
|
||||
10: return "RGBF"
|
||||
11: return "RGBAF"
|
||||
12: return "RH"
|
||||
13: return "RGH"
|
||||
14: return "RGBH"
|
||||
15: return "RGBAH"
|
||||
16: return "RGBE9995"
|
||||
17: return "DXT1 (BC1)"
|
||||
18: return "DXT3 (BC2)"
|
||||
19: return "DXT5 (BC3)"
|
||||
20: return "RGTC (BC4)"
|
||||
21: return "RGTC (BC5)"
|
||||
22: return "BPTC (BC6H)"
|
||||
23: return "BPTC (BC7)"
|
||||
24: return "ETC"
|
||||
25: return "ETC2 R11"
|
||||
26: return "ETC2 RG11"
|
||||
27: return "ETC2 RGB8"
|
||||
28: return "ETC2 RGBA8"
|
||||
29: return "PVRTC2"
|
||||
30: return "PVRTC4"
|
||||
31: return "ASTC 4x4"
|
||||
32: return "ASTC 8x8"
|
||||
33: return "Max"
|
||||
return "Unknown(" + str(format_id) + ")"
|
||||
|
||||
|
||||
func _bits_per_pixel(format_id: int) -> int:
|
||||
"""Estimate bits per pixel for a given format."""
|
||||
match format_id:
|
||||
0, 2: return 8 # L8, R8
|
||||
1, 3: return 16 # LA8, RG8
|
||||
4: return 24 # RGB8
|
||||
5, 6, 7: return 32 # RGBA8, RGBA4444, RGBA5551
|
||||
8: return 32 # RF
|
||||
9: return 64 # RGF
|
||||
10: return 96 # RGBF
|
||||
11: return 128 # RGBAF
|
||||
12: return 16 # RH (half)
|
||||
13: return 32 # RGH
|
||||
14: return 48 # RGBH
|
||||
15: return 64 # RGBAH
|
||||
16: return 32 # RGBE9995
|
||||
17, 18, 19: return 4 # DXT/BC compressed — 4bpp
|
||||
20, 21: return 4 # BC4/BC5 — 4bpp (BC5 is 8bpp but rare)
|
||||
22: return 8 # BC6H — 8bpp
|
||||
23: return 8 # BC7 — 8bpp
|
||||
24, 25, 26, 27, 28: return 4 # ETC/ETC2 — 4bpp
|
||||
29, 30: return 2 # PVRTC — 2bpp
|
||||
31: return 8 # ASTC 4x4 — 8bpp
|
||||
32: return 2 # ASTC 8x8 — 2bpp
|
||||
return 32
|
||||
|
||||
|
||||
func _format_bytes(bytes: int) -> String:
|
||||
"""Format bytes into human-readable string."""
|
||||
if bytes < 1024:
|
||||
return str(bytes) + " B"
|
||||
elif bytes < 1024 * 1024:
|
||||
return str(stepify(float(bytes) / 1024.0, 0.1)) + " KB"
|
||||
else:
|
||||
return str(stepify(float(bytes) / (1024.0 * 1024.0), 0.1)) + " MB"
|
||||
|
||||
|
||||
func maxi(a: int, b: int) -> int:
|
||||
return a if a > b else b
|
||||
|
||||
|
||||
func mini(a: int, b: int) -> int:
|
||||
return a if a < b else b
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Summary
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _print_summary() -> void:
|
||||
"""Print final summary."""
|
||||
print("==========================================")
|
||||
print(" TEXTURE VALIDATION SUMMARY")
|
||||
print("==========================================")
|
||||
print(" Scene: ", _scene_path)
|
||||
print(" Textures checked: ", _total_textures)
|
||||
print(" Materials checked: ", _total_materials)
|
||||
print(" Over-budget textures: ", _overbudget_textures.size())
|
||||
print(" Missing mipmaps: ", _no_mipmap_textures.size())
|
||||
print(" Meshes w/o UV2: ", _meshes_without_uv2.size())
|
||||
print("------------------------------------------")
|
||||
print(" Passed: ", _pass_count)
|
||||
print(" Failed: ", _fail_count)
|
||||
print(" Warnings: ", _warning_count)
|
||||
print("------------------------------------------")
|
||||
if _fail_count > 0:
|
||||
print(" RESULT: FAILED ❌")
|
||||
printerr("Validation FAILED — ", _fail_count, " texture check(s) failed.")
|
||||
else:
|
||||
print(" RESULT: PASSED ✅")
|
||||
print("==========================================")
|
||||
print("")
|
||||
@@ -0,0 +1 @@
|
||||
uid://bas55o1773n21
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 174 B |
Binary file not shown.
|
After Width: | Height: | Size: 282 B |
@@ -0,0 +1,127 @@
|
||||
[gd_scene load_steps=11 format=4 uid="uid://br6g3dtbytwpu"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://deaf6cmuofa0" path="res://client/template/scripts/Weapon_State_Machine/PickUp Weapons/Weapon_Pick_Up.gd" id="1_mpf05"]
|
||||
[ext_resource type="Script" uid="uid://bw4j1fbww4k8s" path="res://client/template/scripts/Weapon_State_Machine/WeaponSlot.gd" id="2_vl0jl"]
|
||||
[ext_resource type="Resource" uid="uid://bwrs8ensewkgc" path="res://client/template/scripts/Weapon_State_Machine/Weapon_Resources/blasterL.tres" id="3_bkkd8"]
|
||||
|
||||
[sub_resource type="Resource" id="Resource_3vvaw"]
|
||||
resource_local_to_scene = true
|
||||
script = ExtResource("2_vl0jl")
|
||||
weapon = ExtResource("3_bkkd8")
|
||||
current_ammo = 30
|
||||
reserve_ammo = 0
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_761wa"]
|
||||
resource_name = "metal"
|
||||
vertex_color_use_as_albedo = true
|
||||
albedo_color = Color(0.454902, 0.431373, 0.403922, 1)
|
||||
metallic = 1.0
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_757o3"]
|
||||
resource_name = "darkMetal"
|
||||
vertex_color_use_as_albedo = true
|
||||
albedo_color = Color(0.454902, 0.431373, 0.403922, 1)
|
||||
metallic = 1.0
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_0y2bu"]
|
||||
resource_name = "yellow"
|
||||
vertex_color_use_as_albedo = true
|
||||
albedo_color = Color(0.533333, 0.984314, 0.976471, 1)
|
||||
metallic = 1.0
|
||||
emission_enabled = true
|
||||
emission = Color(0.282353, 0.568627, 0.541176, 1)
|
||||
emission_energy_multiplier = 9.4
|
||||
|
||||
[sub_resource type="ArrayMesh" id="ArrayMesh_bjg0s"]
|
||||
_surfaces = [{
|
||||
"aabb": AABB(-0.03, -0.12, -0.075, 0.06, 0.24, 0.15),
|
||||
"format": 34359742465,
|
||||
"index_count": 168,
|
||||
"index_data": PackedByteArray("AgAAAAEAAQADAAIABgAEAAUABQAHAAYAAwABAAgACAAJAAMADAAKAAsACwANAAwADAAOAA8ADwAKAAwAEgAQABEAEQATABIAEgAJAAgACAAQABIAEQAAAAIAAgATABEACwAEAAYABgANAAsAFQAUABEAEQAWABUAEQAQABYAEAAIABYAAAARABQAFAAXAAAAFwABAAAAFwAYAAEAGAAIAAEAGAAWAAgAGgAZAAYABgAbABoABgAHABsABwAcABsABwAOABwADgAMABwADQAGABkAGQAdAA0AHQAMAA0AHQAcAAwAGQAaABQAFAAVABkAFgAYABwAHAAdABYAFQAWAB4AHgAZABUAHgAfABkAHwAgABkAIQAeABYAFgAdACEAHQAgACEAHQAZACAAGwAXABQAFAAaABsAHAAYABcAFwAbABwABwAFAA8ADwAOAAcA"),
|
||||
"name": "metal",
|
||||
"primitive": 3,
|
||||
"uv_scale": Vector4(0, 0, 0, 0),
|
||||
"vertex_count": 56,
|
||||
"vertex_data": PackedByteArray("kML1vIaWnL3rUTg9kML1vOtRuL2wyJI8CtcjvIaWnL3rUTg9CtcjvOtRuL2wyJI8CtcjPIaWnL3rUTg9CtcjPOtRuL2wyJI8kML1PIaWnL3rUTg9kML1POtRuL2wyJI8kML1vOtRuL3rUTi9CtcjvOtRuL3rUTi9CtcjPArXoz3rUTi9CtcjPArXoz3rUTg9kML1PArXoz3rUTi9kML1PArXoz3rUTg9kML1POtRuL3rUTi9CtcjPOtRuL3rUTi9kML1vArXoz3rUTi9kML1vArXoz3rUTg9CtcjvArXoz3rUTi9CtcjvArXoz3rUTg9kML1vFyPwr2amZk9kML1vK1H4T2amZk9kML1vK1H4T2amZm9kML1vJDC9b3NzMw8kML1vJDC9b2amZm9kML1PK1H4T2amZk9kML1PFyPwr2amZk9kML1PJDC9b3NzMw8kML1PJDC9b2amZm9kML1PK1H4T2amZm9CtcjvK1H4T3rUTg9CtcjPK1H4T3rUTg9CtcjPK1H4T3rUTi9CtcjvK1H4T3rUTi93kEevKKU5j3rUTi93kEePKKU5j3rUTi9vOMNvB+F6z3rUTi9vOMNPB+F6z3rUTi9a7TnO/TC7z3rUTi9a7Tnu/TC7z3rUTi9CtejuyYE8z3rUTi9CtejOyYE8z3rUTi9oZ4pu+kP9T3rUTi9oZ4pO+kP9T3rUTi9AAAAAJDC9T3rUTi9a7Tnu/TC7z3rUTg9CtejuyYE8z3rUTg9vOMNPB+F6z3rUTg9a7TnO/TC7z3rUTg93kEePKKU5j3rUTg93kEevKKU5j3rUTg9vOMNvB+F6z3rUTg9CtejOyYE8z3rUTg9oZ4pO+kP9T3rUTg9oZ4pu+kP9T3rUTg9AAAAAJDC9T3rUTg9")
|
||||
}, {
|
||||
"aabb": AABB(-0.03, -0.12, -0.075, 0.06, 0.24, 0.15),
|
||||
"format": 34359742465,
|
||||
"index_count": 18,
|
||||
"index_data": PackedByteArray("EgATAAIAAgADABIAAwAJABIABQAEAAsACwAKAAUACgAPAAUA"),
|
||||
"name": "darkMetal",
|
||||
"primitive": 3,
|
||||
"uv_scale": Vector4(0, 0, 0, 0),
|
||||
"vertex_count": 56,
|
||||
"vertex_data": PackedByteArray("kML1vIaWnL3rUTg9kML1vOtRuL2wyJI8CtcjvIaWnL3rUTg9CtcjvOtRuL2wyJI8CtcjPIaWnL3rUTg9CtcjPOtRuL2wyJI8kML1PIaWnL3rUTg9kML1POtRuL2wyJI8kML1vOtRuL3rUTi9CtcjvOtRuL3rUTi9CtcjPArXoz3rUTi9CtcjPArXoz3rUTg9kML1PArXoz3rUTi9kML1PArXoz3rUTg9kML1POtRuL3rUTi9CtcjPOtRuL3rUTi9kML1vArXoz3rUTi9kML1vArXoz3rUTg9CtcjvArXoz3rUTi9CtcjvArXoz3rUTg9kML1vFyPwr2amZk9kML1vK1H4T2amZk9kML1vK1H4T2amZm9kML1vJDC9b3NzMw8kML1vJDC9b2amZm9kML1PK1H4T2amZk9kML1PFyPwr2amZk9kML1PJDC9b3NzMw8kML1PJDC9b2amZm9kML1PK1H4T2amZm9CtcjvK1H4T3rUTg9CtcjPK1H4T3rUTg9CtcjPK1H4T3rUTi9CtcjvK1H4T3rUTi93kEevKKU5j3rUTi93kEePKKU5j3rUTi9vOMNvB+F6z3rUTi9vOMNPB+F6z3rUTi9a7TnO/TC7z3rUTi9a7Tnu/TC7z3rUTi9CtejuyYE8z3rUTi9CtejOyYE8z3rUTi9oZ4pu+kP9T3rUTi9oZ4pO+kP9T3rUTi9AAAAAJDC9T3rUTi9a7Tnu/TC7z3rUTg9CtejuyYE8z3rUTg9vOMNPB+F6z3rUTg9a7TnO/TC7z3rUTg93kEePKKU5j3rUTg93kEevKKU5j3rUTg9vOMNvB+F6z3rUTg9CtejOyYE8z3rUTg9oZ4pO+kP9T3rUTg9oZ4pu+kP9T3rUTg9AAAAAJDC9T3rUTg9")
|
||||
}, {
|
||||
"aabb": AABB(-0.03, -0.12, -0.075, 0.06, 0.24, 0.15),
|
||||
"format": 34359742465,
|
||||
"index_count": 138,
|
||||
"index_data": PackedByteArray("IgAhACAAIAAjACIAIwAkACIAIwAlACQAJQAmACQAJgAnACQAJgAoACcAJgApACgAKQAqACgAKQArACoAKwAsACoALgAtACcAJwAoAC4AJQAvADAAMAAmACUAMQAfAB4AHgAyADEAMgAvADEAMgAzAC8AMwAwAC8AMwAtADAALQA0ADAALQAuADQALgA1ADQALgA2ADUANgA3ADUAJwAtADMAMwAkACcAIgAyAB4AHgAhACIANQA3ACwALAArADUANAA1ACsAKwApADQAIwAxAC8ALwAlACMAIAAfADEAMQAjACAAMAA0ACkAKQAmADAAJAAzADIAMgAiACQANgAuACgAKAAqADYANwA2ACoAKgAsADcA"),
|
||||
"name": "yellow",
|
||||
"primitive": 3,
|
||||
"uv_scale": Vector4(0, 0, 0, 0),
|
||||
"vertex_count": 56,
|
||||
"vertex_data": PackedByteArray("kML1vIaWnL3rUTg9kML1vOtRuL2wyJI8CtcjvIaWnL3rUTg9CtcjvOtRuL2wyJI8CtcjPIaWnL3rUTg9CtcjPOtRuL2wyJI8kML1PIaWnL3rUTg9kML1POtRuL2wyJI8kML1vOtRuL3rUTi9CtcjvOtRuL3rUTi9CtcjPArXoz3rUTi9CtcjPArXoz3rUTg9kML1PArXoz3rUTi9kML1PArXoz3rUTg9kML1POtRuL3rUTi9CtcjPOtRuL3rUTi9kML1vArXoz3rUTi9kML1vArXoz3rUTg9CtcjvArXoz3rUTi9CtcjvArXoz3rUTg9kML1vFyPwr2amZk9kML1vK1H4T2amZk9kML1vK1H4T2amZm9kML1vJDC9b3NzMw8kML1vJDC9b2amZm9kML1PK1H4T2amZk9kML1PFyPwr2amZk9kML1PJDC9b3NzMw8kML1PJDC9b2amZm9kML1PK1H4T2amZm9CtcjvK1H4T3rUTg9CtcjPK1H4T3rUTg9CtcjPK1H4T3rUTi9CtcjvK1H4T3rUTi93kEevKKU5j3rUTi93kEePKKU5j3rUTi9vOMNvB+F6z3rUTi9vOMNPB+F6z3rUTi9a7TnO/TC7z3rUTi9a7Tnu/TC7z3rUTi9CtejuyYE8z3rUTi9CtejOyYE8z3rUTi9oZ4pu+kP9T3rUTi9oZ4pO+kP9T3rUTi9AAAAAJDC9T3rUTi9a7Tnu/TC7z3rUTg9CtejuyYE8z3rUTg9vOMNPB+F6z3rUTg9a7TnO/TC7z3rUTg93kEePKKU5j3rUTg93kEevKKU5j3rUTg9vOMNvB+F6z3rUTg9CtejOyYE8z3rUTg9oZ4pO+kP9T3rUTg9oZ4pu+kP9T3rUTg9AAAAAJDC9T3rUTg9")
|
||||
}]
|
||||
blend_shape_mode = 0
|
||||
|
||||
[sub_resource type="ArrayMesh" id="ArrayMesh_wf2rx"]
|
||||
resource_name = "clipA_Mesh clipA"
|
||||
_surfaces = [{
|
||||
"aabb": AABB(-0.03, -0.12, -0.075, 0.06, 0.24, 0.15),
|
||||
"attribute_data": PackedByteArray("XS6XP0T3Qj9dLpc/dxH6PyaTyT5E90I/JpPJPncR+j8mk8m+RPdCPyaTyb53Efo/XS6Xv0T3Qj9dLpe/dxH6P10ul7+US9o/XS6XvxaLRb8mk8m+lEvaPyaTyb4Wi0W/JpPJvhaLRb8mk8m+xmIxQF0ul78Wi0W/XS6Xv8ZiMUBdLpc/xmKRQCaTyT7GYpFAXS6XPyaTCcAmk8k+JpMJwF0ulz8Wi0W/XS6XP8ZiMUAmk8k+FotFvyaTyT7GYjFAJpPJvsZikUBdLpe/xmKRQCaTyb4mkwnAXS6XvyaTCcBdLpc/hlOAQCaTyT6GU4BAXS6XPyaTCcAmk8k+JpMJwCaTyb6GU4BAXS6Xv4ZTgEAmk8m+JpMJwF0ul78mkwnA9Pk8QF6vl0CLxeI/JpMJwPT5PEBUKlXA9Pk8wFQqVcCLxeK/JpMJwIvF4r/GYpFAi8XiP4ZTgEDw93s/XS63QCmXND/GYpFA9Pk8wF0ut0D0+TzAVCpVwIvF4r+GU4BA9Pk8wF6vl0Dw93u/XS63QCmXNL/GYpFA9Pk8QF0ut0CLxeI/xmKRQIvF4j8mkwnAi8XivyaTCcD0+TxAVCpVwF0ulz9er5dAXS6Xv16vl0BdLpc/VCpVwF0ul79UKlXAXS6XP10ut0BdLpe/XS63QF0ulz9UKlXAXS6Xv1QqVcBdLpe/6PP5vyaTyb7GYjFAXS6Xv/T5fEBdLpc/9Pl8QCaTyT7GYjFAJpPJPhaLRb8mk8m+FotFv10ulz/o8/m/XS6Xv+3gDkBdLpe/QIgBPV0ulz/t4A5AXS6XP0CIAT1dLpc/6PP5v10ulz/4+/0/XS6Xv+jz+b9dLpe/+Pv9PyaTyT6US9o/JpPJPhaLRb9dLpc/lEvaP10ulz8Wi0W/i8XiPyaTCcCLxeI/hlOAQIvF4r8mkwnAKZc0P8ZikUCLxeK/xmKRQIvF4r+GU4BAi8XivyaTCcAplzS/xmKRQIvF4j8mkwnAi8XiP8ZikUAmk8k+VCpVwCaTyb5UKlXA0bTCPtCvW8DRtMK+0K9bwKORrj6Iw2HAo5GuvojDYcDwiI6+cvtmwPCIjj5y+2bAJpNJPor8asAmk0m+ivxqwIOv0D3ugG3Ag6/Qve6AbcDxsKoOulxuwIvF4j+g4Mq/i8Xiv6Dgyr+LxeI/agjYv4vF4r9qCNi/i8Xiv+mZGMCLxeK/zS0fwIvF4j/pmRjAi8XiP80tH8Amk8k+VCpVwCaTyb5UKlXA0bTCPtCvW8DRtMK+0K9bwKORrj6Iw2HAo5GuvojDYcDwiI4+cvtmwPCIjr5y+2bAJpNJPor8asAmk0m+ivxqwIOv0D3ugG3Ag6/Qve6AbcAJafYNulxuwIvF4j/NLR/Ai8XiP+mZGMCLxeK/zS0fwIvF4r/pmRjAi8XiP0AVVsCLxeI/XIFPwIvF4r9AFVbAi8Xiv1yBT8CLxeK/QEXEPovF4j9ARcQ+i8Xiv2Dk+D6LxeI/YOT4PovF4r/4azW/i8XiP/hrNb+LxeK/aBwbv4vF4j9oHBu/i8Xiv0/HPMCLxeK/MltDwIvF4j9PxzzAi8XiPzJbQ8CLxeK/XIFPwIvF4r9AFVbAi8XiP1yBT8CLxeI/QBVWwIvF4r9qCNi/i8XiP2oI2L+LxeK/oODKv4vF4j+g4Mq/i8XiPzJbQ8CLxeI/T8c8wIvF4r8yW0PAi8Xiv0/HPMCLxeI/aBwbv4vF4r9oHBu/i8XiP/hrNb+LxeK/+Gs1v4vF4j9g5Pg+i8Xiv2Dk+D6LxeI/QEXEPovF4r9ARcQ+"),
|
||||
"format": 34359742487,
|
||||
"index_count": 168,
|
||||
"index_data": PackedByteArray("AgAAAAEAAQADAAIABgAEAAUABQAHAAYACgAIAAkACQALAAoADgAMAA0ADQAPAA4AEgAQABEAEQATABIAFgAUABUAFQAXABYAGgAYABkAGQAbABoAHgAcAB0AHQAfAB4AIgAgACEAIQAjACIAJgAkACUAJQAnACYAJQAoACcAKAApACcAKgAlACQAJAArACoAKwAsACoAKwAtACwALQApACwALQAnACkAMAAuAC8ALwAxADAALwAyADEAMgAzADEAMgA0ADMANAA1ADMANgAvAC4ALgA3ADYANwA1ADYANwAzADUAOgA4ADkAOQA7ADoAPgA8AD0APQA/AD4AQgBAAEEAQQBDAEIAQQBEAEMARABFAEMARgBBAEAAQABHAEYARwBFAEYARwBDAEUASgBIAEkASQBLAEoATgBMAE0ATQBPAE4AUgBQAFEAUQBTAFIA"),
|
||||
"material": SubResource("StandardMaterial3D_761wa"),
|
||||
"name": "metal",
|
||||
"primitive": 3,
|
||||
"uv_scale": Vector4(0, 0, 0, 0),
|
||||
"vertex_count": 168,
|
||||
"vertex_data": PackedByteArray("kML1vIaWnL3rUTg9kML1vOtRuL2wyJI8CtcjvIaWnL3rUTg9CtcjvOtRuL2wyJI8CtcjPIaWnL3rUTg9CtcjPOtRuL2wyJI8kML1PIaWnL3rUTg9kML1POtRuL2wyJI8kML1vOtRuL2wyJI8kML1vOtRuL3rUTi9CtcjvOtRuL2wyJI8CtcjvOtRuL3rUTi9CtcjPArXoz3rUTi9CtcjPArXoz3rUTg9kML1PArXoz3rUTi9kML1PArXoz3rUTg9kML1POtRuL3rUTi9CtcjPOtRuL3rUTi9kML1PArXoz3rUTi9CtcjPArXoz3rUTi9kML1vArXoz3rUTi9kML1vArXoz3rUTg9CtcjvArXoz3rUTi9CtcjvArXoz3rUTg9CtcjvOtRuL3rUTi9kML1vOtRuL3rUTi9CtcjvArXoz3rUTi9kML1vArXoz3rUTi9kML1vIaWnL3rUTg9CtcjvIaWnL3rUTg9kML1vArXoz3rUTg9CtcjvArXoz3rUTg9CtcjPIaWnL3rUTg9kML1PIaWnL3rUTg9CtcjPArXoz3rUTg9kML1PArXoz3rUTg9kML1vFyPwr2amZk9kML1vArXoz3rUTg9kML1vK1H4T2amZk9kML1vK1H4T2amZm9kML1vArXoz3rUTi9kML1vOtRuL3rUTi9kML1vIaWnL3rUTg9kML1vJDC9b3NzMw8kML1vOtRuL2wyJI8kML1vJDC9b2amZm9kML1PK1H4T2amZk9kML1PIaWnL3rUTg9kML1PFyPwr2amZk9kML1PJDC9b3NzMw8kML1POtRuL2wyJI8kML1PJDC9b2amZm9kML1POtRuL3rUTi9kML1PArXoz3rUTi9kML1PArXoz3rUTg9kML1PK1H4T2amZm9kML1PFyPwr2amZk9kML1vFyPwr2amZk9kML1PK1H4T2amZk9kML1vK1H4T2amZk9kML1vJDC9b2amZm9kML1PJDC9b2amZm9kML1vK1H4T2amZm9kML1PK1H4T2amZm9kML1vK1H4T2amZm9CtcjvK1H4T3rUTg9kML1vK1H4T2amZk9kML1PK1H4T2amZk9CtcjPK1H4T3rUTg9CtcjPK1H4T3rUTi9CtcjvK1H4T3rUTi9kML1PK1H4T2amZm9kML1vJDC9b3NzMw8kML1vFyPwr2amZk9kML1PJDC9b3NzMw8kML1PFyPwr2amZk9kML1vJDC9b2amZm9kML1vJDC9b3NzMw8kML1PJDC9b2amZm9kML1PJDC9b3NzMw8CtcjPOtRuL2wyJI8CtcjPOtRuL3rUTi9kML1POtRuL2wyJI8kML1POtRuL3rUTi9CtcjvArXoz3rUTg9CtcjvIaWnL3rUTg9CtcjvArXoz3rUTi9CtcjvOtRuL2wyJI8CtcjvOtRuL3rUTi9CtcjPIaWnL3rUTg9CtcjPArXoz3rUTg9CtcjPOtRuL2wyJI8CtcjPArXoz3rUTi9CtcjPOtRuL3rUTi9CtcjvK1H4T3rUTi9CtcjPK1H4T3rUTi93kEevKKU5j3rUTi93kEePKKU5j3rUTi9vOMNvB+F6z3rUTi9vOMNPB+F6z3rUTi9a7TnO/TC7z3rUTi9a7Tnu/TC7z3rUTi9CtejuyYE8z3rUTi9CtejOyYE8z3rUTi9oZ4pu+kP9T3rUTi9oZ4pO+kP9T3rUTi9AAAAAJDC9T3rUTi9a7Tnu/TC7z3rUTg9a7Tnu/TC7z3rUTi9CtejuyYE8z3rUTg9CtejuyYE8z3rUTi9vOMNPB+F6z3rUTg9a7TnO/TC7z3rUTg9vOMNPB+F6z3rUTi9a7TnO/TC7z3rUTi9CtcjPK1H4T3rUTg9CtcjvK1H4T3rUTg93kEePKKU5j3rUTg93kEevKKU5j3rUTg9vOMNPB+F6z3rUTg9vOMNvB+F6z3rUTg9a7TnO/TC7z3rUTg9a7Tnu/TC7z3rUTg9CtejOyYE8z3rUTg9CtejuyYE8z3rUTg9oZ4pO+kP9T3rUTg9oZ4pu+kP9T3rUTg9AAAAAJDC9T3rUTg9a7Tnu/TC7z3rUTg9vOMNvB+F6z3rUTg9a7Tnu/TC7z3rUTi9vOMNvB+F6z3rUTi93kEevKKU5j3rUTg9CtcjvK1H4T3rUTg93kEevKKU5j3rUTi9CtcjvK1H4T3rUTi9AAAAAJDC9T3rUTg9AAAAAJDC9T3rUTi9oZ4pO+kP9T3rUTg9oZ4pO+kP9T3rUTi9oZ4pO+kP9T3rUTg9oZ4pO+kP9T3rUTi9CtejOyYE8z3rUTg9CtejOyYE8z3rUTi93kEePKKU5j3rUTg9vOMNPB+F6z3rUTg93kEePKKU5j3rUTi9vOMNPB+F6z3rUTi9CtcjPK1H4T3rUTg93kEePKKU5j3rUTg9CtcjPK1H4T3rUTi93kEePKKU5j3rUTi9CtejOyYE8z3rUTg9CtejOyYE8z3rUTi9a7TnO/TC7z3rUTg9a7TnO/TC7z3rUTi9vOMNvB+F6z3rUTg93kEevKKU5j3rUTg9vOMNvB+F6z3rUTi93kEevKKU5j3rUTi9CtejuyYE8z3rUTg9CtejuyYE8z3rUTi9oZ4pu+kP9T3rUTg9oZ4pu+kP9T3rUTi9oZ4pu+kP9T3rUTg9oZ4pu+kP9T3rUTi9AAAAAJDC9T3rUTg9AAAAAJDC9T3rUTi9qar//wAA/7+pqv//AAD/v6mq//8AAP+/qar//wAA/7+pqv//AAD/v6mq//8AAP+/qar//wAA/7+pqv//AAD/v/9///////+//3///////7//f///////v/9///////+//38AAAAA/7//fwAAAAD/v/9/AAAAAP+//38AAAAA/7//f/9/////v/9//3////+//3//f////7//f/9/////v/9/AAAAAP+//38AAAAA/7//fwAAAAD/v/9/AAAAAP+//3//f////7//f/9/////v/9//3////+//3//f////7//////AAD/v/////8AAP+//////wAA/7//////AAD/v/////8AAP+//////wAA/7//////AAD/v/////8AAP+/AAD/f/9//78AAP9//3//vwAA/3//f/+/AAD/f/9//78AAP9//3//vwAA/3//f/+/AAD/f/9//78AAP9//3//vwAA/3//f/+/AAD/f/9//7////9//////////3/+////////f/7///////9//v///////3/+////////f/7/AID///9//v8AgP///3///wCA////f/////////9///////9//3////+//3//f////7//f/9/////v/9//3////+//////wAA/7//////AAD/v/////8AAP+//////wAA/7//f///////v/9///////+//3///////7//f////v//v/9////+//+//3///////7//f////v//v/9///////+//3+qKv///7//f6oq////v/9/qir///+//3+qKv///7//fwAAAAD/v/9/AAAAAP+//38AAAAA/7//fwAAAAD/v/9///////+//3///////7//f///////v/9///////+/AAD/fwAAAIAAAP9/AAAAgAAA/38AAACAAAD/fwAAAIAAAP9/AAAAgP///38AAACA////fwAAAID///9/AAAAgP///38AAACA////fwAAAID/////AAAAgP////8AAACA/////wAAAID/////AAAAgP////8AAACA/////wAAAID/////AAAAgP////8AAACA/////wAAAID/////AAAAgP////8AAACA/////wAAAID/////AAAAgP8//78AAACA/z//vwAAAIAlUSXRAAAAgCVRJdEAAACAJdHZrgAAAID/v/+/AAAAgCXR2a4AAACA/7//vwAAAID/f/9/AAAAgP9//38AAACA/3//fwAAAID/f/9/AAAAgP9//38AAACA/3//fwAAAID/f/9/AAAAgP9//38AAACA/3//fwAAAID/f/9/AAAAgP9//38AAACA/3//fwAAAID/f/9/AAAAgP8//78AAACA2S7ZrgAAAID/P/+/AAAAgNku2a4AAACADBsMmwAAAIDkDuOOAAAAgAwbDJsAAACA5A7jjgAAAID/f///AAAAgP9///8AAACADJvy5AAAAIAMm/LkAAAAgAyb8uQAAACADJvy5AAAAIDZriXRAAAAgNmuJdEAAACA8uQMmwAAAIAl0dmuAAAAgPLkDJsAAACAJdHZrgAAAIAa8eOOAAAAgPLkDJsAAACAGvHjjgAAAIDy5AybAAAAgNmuJdEAAACA2a4l0QAAAID/v/+/AAAAgP+//78AAACA2S7ZrgAAAIAMGwybAAAAgNku2a4AAACADBsMmwAAAIAlUSXRAAAAgCVRJdEAAACA8mTy5AAAAIDyZPLkAAAAgPJk8uQAAACA8mTy5AAAAID/f///AAAAgP9///8AAACA")
|
||||
}, {
|
||||
"aabb": AABB(-0.03, -0.12, -0.075, 0.06, 0.24, 0.15),
|
||||
"attribute_data": PackedByteArray("XS6XP0T3Qj9dLpc/dxH6PyaTyT5E90I/JpPJPncR+j8mk8m+RPdCPyaTyb53Efo/XS6Xv0T3Qj9dLpe/dxH6P10ul7+US9o/XS6XvxaLRb8mk8m+lEvaPyaTyb4Wi0W/JpPJvhaLRb8mk8m+xmIxQF0ul78Wi0W/XS6Xv8ZiMUBdLpc/xmKRQCaTyT7GYpFAXS6XPyaTCcAmk8k+JpMJwF0ulz8Wi0W/XS6XP8ZiMUAmk8k+FotFvyaTyT7GYjFAJpPJvsZikUBdLpe/xmKRQCaTyb4mkwnAXS6XvyaTCcBdLpc/hlOAQCaTyT6GU4BAXS6XPyaTCcAmk8k+JpMJwCaTyb6GU4BAXS6Xv4ZTgEAmk8m+JpMJwF0ul78mkwnA9Pk8QF6vl0CLxeI/JpMJwPT5PEBUKlXA9Pk8wFQqVcCLxeK/JpMJwIvF4r/GYpFAi8XiP4ZTgEDw93s/XS63QCmXND/GYpFA9Pk8wF0ut0D0+TzAVCpVwIvF4r+GU4BA9Pk8wF6vl0Dw93u/XS63QCmXNL/GYpFA9Pk8QF0ut0CLxeI/xmKRQIvF4j8mkwnAi8XivyaTCcD0+TxAVCpVwF0ulz9er5dAXS6Xv16vl0BdLpc/VCpVwF0ul79UKlXAXS6XP10ut0BdLpe/XS63QF0ulz9UKlXAXS6Xv1QqVcBdLpe/6PP5vyaTyb7GYjFAXS6Xv/T5fEBdLpc/9Pl8QCaTyT7GYjFAJpPJPhaLRb8mk8m+FotFv10ulz/o8/m/XS6Xv+3gDkBdLpe/QIgBPV0ulz/t4A5AXS6XP0CIAT1dLpc/6PP5v10ulz/4+/0/XS6Xv+jz+b9dLpe/+Pv9PyaTyT6US9o/JpPJPhaLRb9dLpc/lEvaP10ulz8Wi0W/i8XiPyaTCcCLxeI/hlOAQIvF4r8mkwnAKZc0P8ZikUCLxeK/xmKRQIvF4r+GU4BAi8XivyaTCcAplzS/xmKRQIvF4j8mkwnAi8XiP8ZikUAmk8k+VCpVwCaTyb5UKlXA0bTCPtCvW8DRtMK+0K9bwKORrj6Iw2HAo5GuvojDYcDwiI6+cvtmwPCIjj5y+2bAJpNJPor8asAmk0m+ivxqwIOv0D3ugG3Ag6/Qve6AbcDxsKoOulxuwIvF4j+g4Mq/i8Xiv6Dgyr+LxeI/agjYv4vF4r9qCNi/i8Xiv+mZGMCLxeK/zS0fwIvF4j/pmRjAi8XiP80tH8Amk8k+VCpVwCaTyb5UKlXA0bTCPtCvW8DRtMK+0K9bwKORrj6Iw2HAo5GuvojDYcDwiI4+cvtmwPCIjr5y+2bAJpNJPor8asAmk0m+ivxqwIOv0D3ugG3Ag6/Qve6AbcAJafYNulxuwIvF4j/NLR/Ai8XiP+mZGMCLxeK/zS0fwIvF4r/pmRjAi8XiP0AVVsCLxeI/XIFPwIvF4r9AFVbAi8Xiv1yBT8CLxeK/QEXEPovF4j9ARcQ+i8Xiv2Dk+D6LxeI/YOT4PovF4r/4azW/i8XiP/hrNb+LxeK/aBwbv4vF4j9oHBu/i8Xiv0/HPMCLxeK/MltDwIvF4j9PxzzAi8XiPzJbQ8CLxeK/XIFPwIvF4r9AFVbAi8XiP1yBT8CLxeI/QBVWwIvF4r9qCNi/i8XiP2oI2L+LxeK/oODKv4vF4j+g4Mq/i8XiPzJbQ8CLxeI/T8c8wIvF4r8yW0PAi8Xiv0/HPMCLxeI/aBwbv4vF4r9oHBu/i8XiP/hrNb+LxeK/+Gs1v4vF4j9g5Pg+i8Xiv2Dk+D6LxeI/QEXEPovF4r9ARcQ+"),
|
||||
"format": 34359742487,
|
||||
"index_count": 18,
|
||||
"index_data": PackedByteArray("VgBUAFUAVQBXAFYAVwBYAFYAWwBZAFoAWgBcAFsAXABdAFsA"),
|
||||
"material": SubResource("StandardMaterial3D_757o3"),
|
||||
"name": "darkMetal",
|
||||
"primitive": 3,
|
||||
"uv_scale": Vector4(0, 0, 0, 0),
|
||||
"vertex_count": 168,
|
||||
"vertex_data": PackedByteArray("kML1vIaWnL3rUTg9kML1vOtRuL2wyJI8CtcjvIaWnL3rUTg9CtcjvOtRuL2wyJI8CtcjPIaWnL3rUTg9CtcjPOtRuL2wyJI8kML1PIaWnL3rUTg9kML1POtRuL2wyJI8kML1vOtRuL2wyJI8kML1vOtRuL3rUTi9CtcjvOtRuL2wyJI8CtcjvOtRuL3rUTi9CtcjPArXoz3rUTi9CtcjPArXoz3rUTg9kML1PArXoz3rUTi9kML1PArXoz3rUTg9kML1POtRuL3rUTi9CtcjPOtRuL3rUTi9kML1PArXoz3rUTi9CtcjPArXoz3rUTi9kML1vArXoz3rUTi9kML1vArXoz3rUTg9CtcjvArXoz3rUTi9CtcjvArXoz3rUTg9CtcjvOtRuL3rUTi9kML1vOtRuL3rUTi9CtcjvArXoz3rUTi9kML1vArXoz3rUTi9kML1vIaWnL3rUTg9CtcjvIaWnL3rUTg9kML1vArXoz3rUTg9CtcjvArXoz3rUTg9CtcjPIaWnL3rUTg9kML1PIaWnL3rUTg9CtcjPArXoz3rUTg9kML1PArXoz3rUTg9kML1vFyPwr2amZk9kML1vArXoz3rUTg9kML1vK1H4T2amZk9kML1vK1H4T2amZm9kML1vArXoz3rUTi9kML1vOtRuL3rUTi9kML1vIaWnL3rUTg9kML1vJDC9b3NzMw8kML1vOtRuL2wyJI8kML1vJDC9b2amZm9kML1PK1H4T2amZk9kML1PIaWnL3rUTg9kML1PFyPwr2amZk9kML1PJDC9b3NzMw8kML1POtRuL2wyJI8kML1PJDC9b2amZm9kML1POtRuL3rUTi9kML1PArXoz3rUTi9kML1PArXoz3rUTg9kML1PK1H4T2amZm9kML1PFyPwr2amZk9kML1vFyPwr2amZk9kML1PK1H4T2amZk9kML1vK1H4T2amZk9kML1vJDC9b2amZm9kML1PJDC9b2amZm9kML1vK1H4T2amZm9kML1PK1H4T2amZm9kML1vK1H4T2amZm9CtcjvK1H4T3rUTg9kML1vK1H4T2amZk9kML1PK1H4T2amZk9CtcjPK1H4T3rUTg9CtcjPK1H4T3rUTi9CtcjvK1H4T3rUTi9kML1PK1H4T2amZm9kML1vJDC9b3NzMw8kML1vFyPwr2amZk9kML1PJDC9b3NzMw8kML1PFyPwr2amZk9kML1vJDC9b2amZm9kML1vJDC9b3NzMw8kML1PJDC9b2amZm9kML1PJDC9b3NzMw8CtcjPOtRuL2wyJI8CtcjPOtRuL3rUTi9kML1POtRuL2wyJI8kML1POtRuL3rUTi9CtcjvArXoz3rUTg9CtcjvIaWnL3rUTg9CtcjvArXoz3rUTi9CtcjvOtRuL2wyJI8CtcjvOtRuL3rUTi9CtcjPIaWnL3rUTg9CtcjPArXoz3rUTg9CtcjPOtRuL2wyJI8CtcjPArXoz3rUTi9CtcjPOtRuL3rUTi9CtcjvK1H4T3rUTi9CtcjPK1H4T3rUTi93kEevKKU5j3rUTi93kEePKKU5j3rUTi9vOMNvB+F6z3rUTi9vOMNPB+F6z3rUTi9a7TnO/TC7z3rUTi9a7Tnu/TC7z3rUTi9CtejuyYE8z3rUTi9CtejOyYE8z3rUTi9oZ4pu+kP9T3rUTi9oZ4pO+kP9T3rUTi9AAAAAJDC9T3rUTi9a7Tnu/TC7z3rUTg9a7Tnu/TC7z3rUTi9CtejuyYE8z3rUTg9CtejuyYE8z3rUTi9vOMNPB+F6z3rUTg9a7TnO/TC7z3rUTg9vOMNPB+F6z3rUTi9a7TnO/TC7z3rUTi9CtcjPK1H4T3rUTg9CtcjvK1H4T3rUTg93kEePKKU5j3rUTg93kEevKKU5j3rUTg9vOMNPB+F6z3rUTg9vOMNvB+F6z3rUTg9a7TnO/TC7z3rUTg9a7Tnu/TC7z3rUTg9CtejOyYE8z3rUTg9CtejuyYE8z3rUTg9oZ4pO+kP9T3rUTg9oZ4pu+kP9T3rUTg9AAAAAJDC9T3rUTg9a7Tnu/TC7z3rUTg9vOMNvB+F6z3rUTg9a7Tnu/TC7z3rUTi9vOMNvB+F6z3rUTi93kEevKKU5j3rUTg9CtcjvK1H4T3rUTg93kEevKKU5j3rUTi9CtcjvK1H4T3rUTi9AAAAAJDC9T3rUTg9AAAAAJDC9T3rUTi9oZ4pO+kP9T3rUTg9oZ4pO+kP9T3rUTi9oZ4pO+kP9T3rUTg9oZ4pO+kP9T3rUTi9CtejOyYE8z3rUTg9CtejOyYE8z3rUTi93kEePKKU5j3rUTg9vOMNPB+F6z3rUTg93kEePKKU5j3rUTi9vOMNPB+F6z3rUTi9CtcjPK1H4T3rUTg93kEePKKU5j3rUTg9CtcjPK1H4T3rUTi93kEePKKU5j3rUTi9CtejOyYE8z3rUTg9CtejOyYE8z3rUTi9a7TnO/TC7z3rUTg9a7TnO/TC7z3rUTi9vOMNvB+F6z3rUTg93kEevKKU5j3rUTg9vOMNvB+F6z3rUTi93kEevKKU5j3rUTi9CtejuyYE8z3rUTg9CtejuyYE8z3rUTi9oZ4pu+kP9T3rUTg9oZ4pu+kP9T3rUTi9oZ4pu+kP9T3rUTg9oZ4pu+kP9T3rUTi9AAAAAJDC9T3rUTg9AAAAAJDC9T3rUTi9qar//wAAAICpqv//AAAAgKmq//8AAACAqar//wAAAICpqv//AAAAgKmq//8AAACAqar//wAAAICpqv//AAAAgP9///8AAACA/3///wAAAID/f///AAAAgP9///8AAACA/38AAAAAAID/fwAAAAAAgP9/AAAAAACA/38AAAAAAID/f/9/AAAAgP9//38AAACA/3//fwAAAID/f/9/AAAAgP9/AAAAAACA/38AAAAAAID/fwAAAAAAgP9/AAAAAACA/3//fwAAAID/f/9/AAAAgP9//38AAACA/3//fwAAAID/////AAAAgP////8AAACA/////wAAAID/////AAAAgP////8AAACA/////wAAAID/////AAAAgP////8AAACAAAD/fwAAAIAAAP9/AAAAgAAA/38AAACAAAD/fwAAAIAAAP9/AAAAgAAA/38AAACAAAD/fwAAAIAAAP9/AAAAgAAA/38AAACAAAD/fwAAAID///9/AAAAgP///38AAACA////fwAAAID///9/AAAAgP///38AAACA////fwAAAID///9/AAAAgP///38AAACA////fwAAAID///9/AAAAgP9//38AAACA/3//fwAAAID/f/9/AAAAgP9//38AAACA/////wAAAID/////AAAAgP////8AAACA/////wAAAID/f///AAAAgP9///8AAACA/3///wAAAID/f///AAAAgP9///8AAACA/3///wAAAID/f///AAAAgP9///8AAACA/3+qKgAAAID/f6oqAAAAgP9/qioAAACA/3+qKgAAAID/fwAAAAAAgP9/AAAAAACA/38AAAAAAID/fwAAAAAAgP9///8AAACA/3///wAAAID/f///AAAAgP9///8AAACAAAD/f/9//78AAP9//3//vwAA/3//f/+/AAD/f/9//78AAP9//3//v////3/+////////f/////////9//v///////3//////////f///////////AAAAgP////8AAACA/////wAAAID/////AAAAgP////8AAACA/////wAAAID/////AAAAgP////8AAACA/////wAAAID/////AAAAgP////8AAACA/////wAAAID/////AAAAgP8//78AAACA/z//vwAAAIAlUSXRAAAAgCVRJdEAAACAJdHZrgAAAID/v/+/AAAAgCXR2a4AAACA/7//vwAAAID/f/9/AAAAgP9//38AAACA/3//fwAAAID/f/9/AAAAgP9//38AAACA/3//fwAAAID/f/9/AAAAgP9//38AAACA/3//fwAAAID/f/9/AAAAgP9//38AAACA/3//fwAAAID/f/9/AAAAgP8//78AAACA2S7ZrgAAAID/P/+/AAAAgNku2a4AAACADBsMmwAAAIDkDuOOAAAAgAwbDJsAAACA5A7jjgAAAID/f///AAAAgP9///8AAACADJvy5AAAAIAMm/LkAAAAgAyb8uQAAACADJvy5AAAAIDZriXRAAAAgNmuJdEAAACA8uQMmwAAAIAl0dmuAAAAgPLkDJsAAACAJdHZrgAAAIAa8eOOAAAAgPLkDJsAAACAGvHjjgAAAIDy5AybAAAAgNmuJdEAAACA2a4l0QAAAID/v/+/AAAAgP+//78AAACA2S7ZrgAAAIAMGwybAAAAgNku2a4AAACADBsMmwAAAIAlUSXRAAAAgCVRJdEAAACA8mTy5AAAAIDyZPLkAAAAgPJk8uQAAACA8mTy5AAAAID/f///AAAAgP9///8AAACA")
|
||||
}, {
|
||||
"aabb": AABB(-0.03, -0.12, -0.075, 0.06, 0.24, 0.15),
|
||||
"attribute_data": PackedByteArray("XS6XP0T3Qj9dLpc/dxH6PyaTyT5E90I/JpPJPncR+j8mk8m+RPdCPyaTyb53Efo/XS6Xv0T3Qj9dLpe/dxH6P10ul7+US9o/XS6XvxaLRb8mk8m+lEvaPyaTyb4Wi0W/JpPJvhaLRb8mk8m+xmIxQF0ul78Wi0W/XS6Xv8ZiMUBdLpc/xmKRQCaTyT7GYpFAXS6XPyaTCcAmk8k+JpMJwF0ulz8Wi0W/XS6XP8ZiMUAmk8k+FotFvyaTyT7GYjFAJpPJvsZikUBdLpe/xmKRQCaTyb4mkwnAXS6XvyaTCcBdLpc/hlOAQCaTyT6GU4BAXS6XPyaTCcAmk8k+JpMJwCaTyb6GU4BAXS6Xv4ZTgEAmk8m+JpMJwF0ul78mkwnA9Pk8QF6vl0CLxeI/JpMJwPT5PEBUKlXA9Pk8wFQqVcCLxeK/JpMJwIvF4r/GYpFAi8XiP4ZTgEDw93s/XS63QCmXND/GYpFA9Pk8wF0ut0D0+TzAVCpVwIvF4r+GU4BA9Pk8wF6vl0Dw93u/XS63QCmXNL/GYpFA9Pk8QF0ut0CLxeI/xmKRQIvF4j8mkwnAi8XivyaTCcD0+TxAVCpVwF0ulz9er5dAXS6Xv16vl0BdLpc/VCpVwF0ul79UKlXAXS6XP10ut0BdLpe/XS63QF0ulz9UKlXAXS6Xv1QqVcBdLpe/6PP5vyaTyb7GYjFAXS6Xv/T5fEBdLpc/9Pl8QCaTyT7GYjFAJpPJPhaLRb8mk8m+FotFv10ulz/o8/m/XS6Xv+3gDkBdLpe/QIgBPV0ulz/t4A5AXS6XP0CIAT1dLpc/6PP5v10ulz/4+/0/XS6Xv+jz+b9dLpe/+Pv9PyaTyT6US9o/JpPJPhaLRb9dLpc/lEvaP10ulz8Wi0W/i8XiPyaTCcCLxeI/hlOAQIvF4r8mkwnAKZc0P8ZikUCLxeK/xmKRQIvF4r+GU4BAi8XivyaTCcAplzS/xmKRQIvF4j8mkwnAi8XiP8ZikUAmk8k+VCpVwCaTyb5UKlXA0bTCPtCvW8DRtMK+0K9bwKORrj6Iw2HAo5GuvojDYcDwiI6+cvtmwPCIjj5y+2bAJpNJPor8asAmk0m+ivxqwIOv0D3ugG3Ag6/Qve6AbcDxsKoOulxuwIvF4j+g4Mq/i8Xiv6Dgyr+LxeI/agjYv4vF4r9qCNi/i8Xiv+mZGMCLxeK/zS0fwIvF4j/pmRjAi8XiP80tH8Amk8k+VCpVwCaTyb5UKlXA0bTCPtCvW8DRtMK+0K9bwKORrj6Iw2HAo5GuvojDYcDwiI4+cvtmwPCIjr5y+2bAJpNJPor8asAmk0m+ivxqwIOv0D3ugG3Ag6/Qve6AbcAJafYNulxuwIvF4j/NLR/Ai8XiP+mZGMCLxeK/zS0fwIvF4r/pmRjAi8XiP0AVVsCLxeI/XIFPwIvF4r9AFVbAi8Xiv1yBT8CLxeK/QEXEPovF4j9ARcQ+i8Xiv2Dk+D6LxeI/YOT4PovF4r/4azW/i8XiP/hrNb+LxeK/aBwbv4vF4j9oHBu/i8Xiv0/HPMCLxeK/MltDwIvF4j9PxzzAi8XiPzJbQ8CLxeK/XIFPwIvF4r9AFVbAi8XiP1yBT8CLxeI/QBVWwIvF4r9qCNi/i8XiP2oI2L+LxeK/oODKv4vF4j+g4Mq/i8XiPzJbQ8CLxeI/T8c8wIvF4r8yW0PAi8Xiv0/HPMCLxeI/aBwbv4vF4r9oHBu/i8XiP/hrNb+LxeK/+Gs1v4vF4j9g5Pg+i8Xiv2Dk+D6LxeI/QEXEPovF4r9ARcQ+"),
|
||||
"format": 34359742487,
|
||||
"index_count": 138,
|
||||
"index_data": PackedByteArray("YABeAF8AXwBhAGAAYQBiAGAAYQBjAGIAYwBkAGIAZABlAGIAZABmAGUAZABnAGYAZwBoAGYAZwBpAGgAaQBqAGgAbQBrAGwAbABuAG0AcQBvAHAAcAByAHEAdQBzAHQAdAB2AHUAdgB3AHUAdgB4AHcAeAB5AHcAeAB6AHkAegB7AHkAegB8AHsAfAB9AHsAfAB+AH0AfgB/AH0AggCAAIEAgQCDAIIAhgCEAIUAhQCHAIYAigCIAIkAiQCLAIoAjgCMAI0AjQCPAI4AkgCQAJEAkQCTAJIAlgCUAJUAlQCXAJYAmgCYAJkAmQCbAJoAngCcAJ0AnQCfAJ4AogCgAKEAoQCjAKIApgCkAKUApQCnAKYA"),
|
||||
"material": SubResource("StandardMaterial3D_0y2bu"),
|
||||
"name": "yellow",
|
||||
"primitive": 3,
|
||||
"uv_scale": Vector4(0, 0, 0, 0),
|
||||
"vertex_count": 168,
|
||||
"vertex_data": PackedByteArray("kML1vIaWnL3rUTg9kML1vOtRuL2wyJI8CtcjvIaWnL3rUTg9CtcjvOtRuL2wyJI8CtcjPIaWnL3rUTg9CtcjPOtRuL2wyJI8kML1PIaWnL3rUTg9kML1POtRuL2wyJI8kML1vOtRuL2wyJI8kML1vOtRuL3rUTi9CtcjvOtRuL2wyJI8CtcjvOtRuL3rUTi9CtcjPArXoz3rUTi9CtcjPArXoz3rUTg9kML1PArXoz3rUTi9kML1PArXoz3rUTg9kML1POtRuL3rUTi9CtcjPOtRuL3rUTi9kML1PArXoz3rUTi9CtcjPArXoz3rUTi9kML1vArXoz3rUTi9kML1vArXoz3rUTg9CtcjvArXoz3rUTi9CtcjvArXoz3rUTg9CtcjvOtRuL3rUTi9kML1vOtRuL3rUTi9CtcjvArXoz3rUTi9kML1vArXoz3rUTi9kML1vIaWnL3rUTg9CtcjvIaWnL3rUTg9kML1vArXoz3rUTg9CtcjvArXoz3rUTg9CtcjPIaWnL3rUTg9kML1PIaWnL3rUTg9CtcjPArXoz3rUTg9kML1PArXoz3rUTg9kML1vFyPwr2amZk9kML1vArXoz3rUTg9kML1vK1H4T2amZk9kML1vK1H4T2amZm9kML1vArXoz3rUTi9kML1vOtRuL3rUTi9kML1vIaWnL3rUTg9kML1vJDC9b3NzMw8kML1vOtRuL2wyJI8kML1vJDC9b2amZm9kML1PK1H4T2amZk9kML1PIaWnL3rUTg9kML1PFyPwr2amZk9kML1PJDC9b3NzMw8kML1POtRuL2wyJI8kML1PJDC9b2amZm9kML1POtRuL3rUTi9kML1PArXoz3rUTi9kML1PArXoz3rUTg9kML1PK1H4T2amZm9kML1PFyPwr2amZk9kML1vFyPwr2amZk9kML1PK1H4T2amZk9kML1vK1H4T2amZk9kML1vJDC9b2amZm9kML1PJDC9b2amZm9kML1vK1H4T2amZm9kML1PK1H4T2amZm9kML1vK1H4T2amZm9CtcjvK1H4T3rUTg9kML1vK1H4T2amZk9kML1PK1H4T2amZk9CtcjPK1H4T3rUTg9CtcjPK1H4T3rUTi9CtcjvK1H4T3rUTi9kML1PK1H4T2amZm9kML1vJDC9b3NzMw8kML1vFyPwr2amZk9kML1PJDC9b3NzMw8kML1PFyPwr2amZk9kML1vJDC9b2amZm9kML1vJDC9b3NzMw8kML1PJDC9b2amZm9kML1PJDC9b3NzMw8CtcjPOtRuL2wyJI8CtcjPOtRuL3rUTi9kML1POtRuL2wyJI8kML1POtRuL3rUTi9CtcjvArXoz3rUTg9CtcjvIaWnL3rUTg9CtcjvArXoz3rUTi9CtcjvOtRuL2wyJI8CtcjvOtRuL3rUTi9CtcjPIaWnL3rUTg9CtcjPArXoz3rUTg9CtcjPOtRuL2wyJI8CtcjPArXoz3rUTi9CtcjPOtRuL3rUTi9CtcjvK1H4T3rUTi9CtcjPK1H4T3rUTi93kEevKKU5j3rUTi93kEePKKU5j3rUTi9vOMNvB+F6z3rUTi9vOMNPB+F6z3rUTi9a7TnO/TC7z3rUTi9a7Tnu/TC7z3rUTi9CtejuyYE8z3rUTi9CtejOyYE8z3rUTi9oZ4pu+kP9T3rUTi9oZ4pO+kP9T3rUTi9AAAAAJDC9T3rUTi9a7Tnu/TC7z3rUTg9a7Tnu/TC7z3rUTi9CtejuyYE8z3rUTg9CtejuyYE8z3rUTi9vOMNPB+F6z3rUTg9a7TnO/TC7z3rUTg9vOMNPB+F6z3rUTi9a7TnO/TC7z3rUTi9CtcjPK1H4T3rUTg9CtcjvK1H4T3rUTg93kEePKKU5j3rUTg93kEevKKU5j3rUTg9vOMNPB+F6z3rUTg9vOMNvB+F6z3rUTg9a7TnO/TC7z3rUTg9a7Tnu/TC7z3rUTg9CtejOyYE8z3rUTg9CtejuyYE8z3rUTg9oZ4pO+kP9T3rUTg9oZ4pu+kP9T3rUTg9AAAAAJDC9T3rUTg9a7Tnu/TC7z3rUTg9vOMNvB+F6z3rUTg9a7Tnu/TC7z3rUTi9vOMNvB+F6z3rUTi93kEevKKU5j3rUTg9CtcjvK1H4T3rUTg93kEevKKU5j3rUTi9CtcjvK1H4T3rUTi9AAAAAJDC9T3rUTg9AAAAAJDC9T3rUTi9oZ4pO+kP9T3rUTg9oZ4pO+kP9T3rUTi9oZ4pO+kP9T3rUTg9oZ4pO+kP9T3rUTi9CtejOyYE8z3rUTg9CtejOyYE8z3rUTi93kEePKKU5j3rUTg9vOMNPB+F6z3rUTg93kEePKKU5j3rUTi9vOMNPB+F6z3rUTi9CtcjPK1H4T3rUTg93kEePKKU5j3rUTg9CtcjPK1H4T3rUTi93kEePKKU5j3rUTi9CtejOyYE8z3rUTg9CtejOyYE8z3rUTi9a7TnO/TC7z3rUTg9a7TnO/TC7z3rUTi9vOMNvB+F6z3rUTg93kEevKKU5j3rUTg9vOMNvB+F6z3rUTi93kEevKKU5j3rUTi9CtejuyYE8z3rUTg9CtejuyYE8z3rUTi9oZ4pu+kP9T3rUTg9oZ4pu+kP9T3rUTi9oZ4pu+kP9T3rUTg9oZ4pu+kP9T3rUTi9AAAAAJDC9T3rUTg9AAAAAJDC9T3rUTi9qar//wAAAICpqv//AAAAgKmq//8AAACAqar//wAAAICpqv//AAAAgKmq//8AAACAqar//wAAAICpqv//AAAAgP9///8AAACA/3///wAAAID/f///AAAAgP9///8AAACA/38AAAAAAID/fwAAAAAAgP9/AAAAAACA/38AAAAAAID/f/9/AAAAgP9//38AAACA/3//fwAAAID/f/9/AAAAgP9/AAAAAACA/38AAAAAAID/fwAAAAAAgP9/AAAAAACA/3//fwAAAID/f/9/AAAAgP9//38AAACA/3//fwAAAID/////AAAAgP////8AAACA/////wAAAID/////AAAAgP////8AAACA/////wAAAID/////AAAAgP////8AAACAAAD/fwAAAIAAAP9/AAAAgAAA/38AAACAAAD/fwAAAIAAAP9/AAAAgAAA/38AAACAAAD/fwAAAIAAAP9/AAAAgAAA/38AAACAAAD/fwAAAID///9/AAAAgP///38AAACA////fwAAAID///9/AAAAgP///38AAACA////fwAAAID///9/AAAAgP///38AAACA////fwAAAID///9/AAAAgP9//38AAACA/3//fwAAAID/f/9/AAAAgP9//38AAACA/////wAAAID/////AAAAgP////8AAACA/////wAAAID/f///AAAAgP9///8AAACA/3///wAAAID/f///AAAAgP9///8AAACA/3///wAAAID/f///AAAAgP9///8AAACA/3+qKgAAAID/f6oqAAAAgP9/qioAAACA/3+qKgAAAID/fwAAAAAAgP9/AAAAAACA/38AAAAAAID/fwAAAAAAgP9///8AAACA/3///wAAAID/f///AAAAgP9///8AAACAAAD/fwAAAIAAAP9/AAAAgAAA/38AAACAAAD/fwAAAIAAAP9/AAAAgP///38AAACA////fwAAAID///9/AAAAgP///38AAACA////fwAAAID/////AAD/v/////8AAP+//////wAA/7//////AAD/v/////8AAP+//////wAA/7//////AAD/v/////8AAP+//////wAA/7//////AAD/v/////8AAP+//////wAA/7//////AAD/v/8//7//f/+//z//v/9//78lUSXR/3//vyVRJdH/f/+/JdHZrv//////v/+//////yXR2a7//////7//v///////f/9/////v/9//3////+//3//f////7//f/9/////v/9//3////+//3//f////7//f/9/////v/9//3////+//3//f////7//f/9/////v/9//3////+//3//f////7//f/9/////v/8//7//f/+/2S7Zrv9//7//P/+//3//v9ku2a7/f/+/DBsMm/9//7/kDuOO/3//vwwbDJv/f/+/5A7jjv9//7//f/////////9/////////DJvy5P////8Mm/Lk/////wyb8uT/////DJvy5P/////ZriXR/////9muJdH/////8uQMm/////8l0dmu//////LkDJv/////JdHZrv////8a8eOO//////LkDJv/////GvHjjv/////y5Ayb/////9muJdH/////2a4l0f//////v/+///////+//7//////2S7Zrv9//78MGwyb/3//v9ku2a7/f/+/DBsMm/9//78lUSXR/3//vyVRJdH/f/+/8mTy5P9//7/yZPLk/3//v/Jk8uT/f/+/8mTy5P9//7//f////3//v/9/////f/+/")
|
||||
}]
|
||||
blend_shape_mode = 0
|
||||
shadow_mesh = SubResource("ArrayMesh_bjg0s")
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_y1cnp"]
|
||||
size = Vector3(0.0599088, 0.250772, 0.143523)
|
||||
|
||||
[node name="blasterL_Clip" type="RigidBody3D"]
|
||||
collision_layer = 8
|
||||
collision_mask = 5
|
||||
script = ExtResource("1_mpf05")
|
||||
weapon = SubResource("Resource_3vvaw")
|
||||
TYPE = "Ammo"
|
||||
|
||||
[node name="clipA2" type="MeshInstance3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1.21, 0, 0, 0, 1, 0, 0, 0)
|
||||
mesh = SubResource("ArrayMesh_wf2rx")
|
||||
skeleton = NodePath("")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
||||
shape = SubResource("BoxShape3D_y1cnp")
|
||||
@@ -0,0 +1,124 @@
|
||||
[gd_scene load_steps=11 format=4 uid="uid://dl236agqx8rjp"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://deaf6cmuofa0" path="res://client/template/scripts/Weapon_State_Machine/PickUp Weapons/Weapon_Pick_Up.gd" id="1_6xwda"]
|
||||
[ext_resource type="Script" uid="uid://bw4j1fbww4k8s" path="res://client/template/scripts/Weapon_State_Machine/WeaponSlot.gd" id="2_qrntt"]
|
||||
[ext_resource type="Resource" uid="uid://yc1f2j7pl4vr" path="res://client/template/scripts/Weapon_State_Machine/Weapon_Resources/blasterI.tres" id="3_ot8uw"]
|
||||
|
||||
[sub_resource type="Resource" id="Resource_yxf5f"]
|
||||
resource_local_to_scene = true
|
||||
script = ExtResource("2_qrntt")
|
||||
weapon = ExtResource("3_ot8uw")
|
||||
current_ammo = 30
|
||||
reserve_ammo = 0
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_n3ik4"]
|
||||
resource_name = "metal"
|
||||
vertex_color_use_as_albedo = true
|
||||
albedo_color = Color(0.904365, 0.914141, 0.944534, 1)
|
||||
metallic = 1.0
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_fil5e"]
|
||||
resource_name = "darkMetal"
|
||||
vertex_color_use_as_albedo = true
|
||||
albedo_color = Color(0.793075, 0.81829, 0.89044, 1)
|
||||
metallic = 1.0
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_bfdxb"]
|
||||
resource_name = "yellow"
|
||||
vertex_color_use_as_albedo = true
|
||||
albedo_color = Color(0.998274, 0.894448, 0.505969, 1)
|
||||
metallic = 1.0
|
||||
|
||||
[sub_resource type="ArrayMesh" id="ArrayMesh_j3k8o"]
|
||||
_surfaces = [{
|
||||
"aabb": AABB(-0.03, -0.12, -0.075, 0.06, 0.24, 0.15),
|
||||
"format": 34359742465,
|
||||
"index_count": 168,
|
||||
"index_data": PackedByteArray("AgAAAAEAAQADAAIABgAEAAUABQAHAAYAAwABAAgACAAJAAMADAAKAAsACwANAAwADAAOAA8ADwAKAAwAEgAQABEAEQATABIAEgAJAAgACAAQABIAEQAAAAIAAgATABEACwAEAAYABgANAAsAFQAUABEAEQAWABUAEQAQABYAEAAIABYAAAARABQAFAAXAAAAFwABAAAAFwAYAAEAGAAIAAEAGAAWAAgAGgAZAAYABgAbABoABgAHABsABwAcABsABwAOABwADgAMABwADQAGABkAGQAdAA0AHQAMAA0AHQAcAAwAGQAaABQAFAAVABkAFgAYABwAHAAdABYAFQAWAB4AHgAZABUAHgAfABkAHwAgABkAIQAeABYAFgAdACEAHQAgACEAHQAZACAAGwAXABQAFAAaABsAHAAYABcAFwAbABwABwAFAA8ADwAOAAcA"),
|
||||
"name": "metal",
|
||||
"primitive": 3,
|
||||
"uv_scale": Vector4(0, 0, 0, 0),
|
||||
"vertex_count": 56,
|
||||
"vertex_data": PackedByteArray("kML1vIaWnL3rUTg9kML1vOtRuL2wyJI8CtcjvIaWnL3rUTg9CtcjvOtRuL2wyJI8CtcjPIaWnL3rUTg9CtcjPOtRuL2wyJI8kML1PIaWnL3rUTg9kML1POtRuL2wyJI8kML1vOtRuL3rUTi9CtcjvOtRuL3rUTi9CtcjPArXoz3rUTi9CtcjPArXoz3rUTg9kML1PArXoz3rUTi9kML1PArXoz3rUTg9kML1POtRuL3rUTi9CtcjPOtRuL3rUTi9kML1vArXoz3rUTi9kML1vArXoz3rUTg9CtcjvArXoz3rUTi9CtcjvArXoz3rUTg9kML1vFyPwr2amZk9kML1vK1H4T2amZk9kML1vK1H4T2amZm9kML1vJDC9b3NzMw8kML1vJDC9b2amZm9kML1PK1H4T2amZk9kML1PFyPwr2amZk9kML1PJDC9b3NzMw8kML1PJDC9b2amZm9kML1PK1H4T2amZm9CtcjvK1H4T3rUTg9CtcjPK1H4T3rUTg9CtcjPK1H4T3rUTi9CtcjvK1H4T3rUTi93kEevKKU5j3rUTi93kEePKKU5j3rUTi9vOMNvB+F6z3rUTi9vOMNPB+F6z3rUTi9a7TnO/TC7z3rUTi9a7Tnu/TC7z3rUTi9CtejuyYE8z3rUTi9CtejOyYE8z3rUTi9oZ4pu+kP9T3rUTi9oZ4pO+kP9T3rUTi9AAAAAJDC9T3rUTi9a7Tnu/TC7z3rUTg9CtejuyYE8z3rUTg9vOMNPB+F6z3rUTg9a7TnO/TC7z3rUTg93kEePKKU5j3rUTg93kEevKKU5j3rUTg9vOMNvB+F6z3rUTg9CtejOyYE8z3rUTg9oZ4pO+kP9T3rUTg9oZ4pu+kP9T3rUTg9AAAAAJDC9T3rUTg9")
|
||||
}, {
|
||||
"aabb": AABB(-0.03, -0.12, -0.075, 0.06, 0.24, 0.15),
|
||||
"format": 34359742465,
|
||||
"index_count": 18,
|
||||
"index_data": PackedByteArray("EgATAAIAAgADABIAAwAJABIABQAEAAsACwAKAAUACgAPAAUA"),
|
||||
"name": "darkMetal",
|
||||
"primitive": 3,
|
||||
"uv_scale": Vector4(0, 0, 0, 0),
|
||||
"vertex_count": 56,
|
||||
"vertex_data": PackedByteArray("kML1vIaWnL3rUTg9kML1vOtRuL2wyJI8CtcjvIaWnL3rUTg9CtcjvOtRuL2wyJI8CtcjPIaWnL3rUTg9CtcjPOtRuL2wyJI8kML1PIaWnL3rUTg9kML1POtRuL2wyJI8kML1vOtRuL3rUTi9CtcjvOtRuL3rUTi9CtcjPArXoz3rUTi9CtcjPArXoz3rUTg9kML1PArXoz3rUTi9kML1PArXoz3rUTg9kML1POtRuL3rUTi9CtcjPOtRuL3rUTi9kML1vArXoz3rUTi9kML1vArXoz3rUTg9CtcjvArXoz3rUTi9CtcjvArXoz3rUTg9kML1vFyPwr2amZk9kML1vK1H4T2amZk9kML1vK1H4T2amZm9kML1vJDC9b3NzMw8kML1vJDC9b2amZm9kML1PK1H4T2amZk9kML1PFyPwr2amZk9kML1PJDC9b3NzMw8kML1PJDC9b2amZm9kML1PK1H4T2amZm9CtcjvK1H4T3rUTg9CtcjPK1H4T3rUTg9CtcjPK1H4T3rUTi9CtcjvK1H4T3rUTi93kEevKKU5j3rUTi93kEePKKU5j3rUTi9vOMNvB+F6z3rUTi9vOMNPB+F6z3rUTi9a7TnO/TC7z3rUTi9a7Tnu/TC7z3rUTi9CtejuyYE8z3rUTi9CtejOyYE8z3rUTi9oZ4pu+kP9T3rUTi9oZ4pO+kP9T3rUTi9AAAAAJDC9T3rUTi9a7Tnu/TC7z3rUTg9CtejuyYE8z3rUTg9vOMNPB+F6z3rUTg9a7TnO/TC7z3rUTg93kEePKKU5j3rUTg93kEevKKU5j3rUTg9vOMNvB+F6z3rUTg9CtejOyYE8z3rUTg9oZ4pO+kP9T3rUTg9oZ4pu+kP9T3rUTg9AAAAAJDC9T3rUTg9")
|
||||
}, {
|
||||
"aabb": AABB(-0.03, -0.12, -0.075, 0.06, 0.24, 0.15),
|
||||
"format": 34359742465,
|
||||
"index_count": 138,
|
||||
"index_data": PackedByteArray("IgAhACAAIAAjACIAIwAkACIAIwAlACQAJQAmACQAJgAnACQAJgAoACcAJgApACgAKQAqACgAKQArACoAKwAsACoALgAtACcAJwAoAC4AJQAvADAAMAAmACUAMQAfAB4AHgAyADEAMgAvADEAMgAzAC8AMwAwAC8AMwAtADAALQA0ADAALQAuADQALgA1ADQALgA2ADUANgA3ADUAJwAtADMAMwAkACcAIgAyAB4AHgAhACIANQA3ACwALAArADUANAA1ACsAKwApADQAIwAxAC8ALwAlACMAIAAfADEAMQAjACAAMAA0ACkAKQAmADAAJAAzADIAMgAiACQANgAuACgAKAAqADYANwA2ACoAKgAsADcA"),
|
||||
"name": "yellow",
|
||||
"primitive": 3,
|
||||
"uv_scale": Vector4(0, 0, 0, 0),
|
||||
"vertex_count": 56,
|
||||
"vertex_data": PackedByteArray("kML1vIaWnL3rUTg9kML1vOtRuL2wyJI8CtcjvIaWnL3rUTg9CtcjvOtRuL2wyJI8CtcjPIaWnL3rUTg9CtcjPOtRuL2wyJI8kML1PIaWnL3rUTg9kML1POtRuL2wyJI8kML1vOtRuL3rUTi9CtcjvOtRuL3rUTi9CtcjPArXoz3rUTi9CtcjPArXoz3rUTg9kML1PArXoz3rUTi9kML1PArXoz3rUTg9kML1POtRuL3rUTi9CtcjPOtRuL3rUTi9kML1vArXoz3rUTi9kML1vArXoz3rUTg9CtcjvArXoz3rUTi9CtcjvArXoz3rUTg9kML1vFyPwr2amZk9kML1vK1H4T2amZk9kML1vK1H4T2amZm9kML1vJDC9b3NzMw8kML1vJDC9b2amZm9kML1PK1H4T2amZk9kML1PFyPwr2amZk9kML1PJDC9b3NzMw8kML1PJDC9b2amZm9kML1PK1H4T2amZm9CtcjvK1H4T3rUTg9CtcjPK1H4T3rUTg9CtcjPK1H4T3rUTi9CtcjvK1H4T3rUTi93kEevKKU5j3rUTi93kEePKKU5j3rUTi9vOMNvB+F6z3rUTi9vOMNPB+F6z3rUTi9a7TnO/TC7z3rUTi9a7Tnu/TC7z3rUTi9CtejuyYE8z3rUTi9CtejOyYE8z3rUTi9oZ4pu+kP9T3rUTi9oZ4pO+kP9T3rUTi9AAAAAJDC9T3rUTi9a7Tnu/TC7z3rUTg9CtejuyYE8z3rUTg9vOMNPB+F6z3rUTg9a7TnO/TC7z3rUTg93kEePKKU5j3rUTg93kEevKKU5j3rUTg9vOMNvB+F6z3rUTg9CtejOyYE8z3rUTg9oZ4pO+kP9T3rUTg9oZ4pu+kP9T3rUTg9AAAAAJDC9T3rUTg9")
|
||||
}]
|
||||
blend_shape_mode = 0
|
||||
|
||||
[sub_resource type="ArrayMesh" id="ArrayMesh_py3xv"]
|
||||
resource_name = "clipA_Mesh clipA"
|
||||
_surfaces = [{
|
||||
"aabb": AABB(-0.03, -0.12, -0.075, 0.06, 0.24, 0.15),
|
||||
"attribute_data": PackedByteArray("XS6XP0T3Qj9dLpc/dxH6PyaTyT5E90I/JpPJPncR+j8mk8m+RPdCPyaTyb53Efo/XS6Xv0T3Qj9dLpe/dxH6P10ul7+US9o/XS6XvxaLRb8mk8m+lEvaPyaTyb4Wi0W/JpPJvhaLRb8mk8m+xmIxQF0ul78Wi0W/XS6Xv8ZiMUBdLpc/xmKRQCaTyT7GYpFAXS6XPyaTCcAmk8k+JpMJwF0ulz8Wi0W/XS6XP8ZiMUAmk8k+FotFvyaTyT7GYjFAJpPJvsZikUBdLpe/xmKRQCaTyb4mkwnAXS6XvyaTCcBdLpc/hlOAQCaTyT6GU4BAXS6XPyaTCcAmk8k+JpMJwCaTyb6GU4BAXS6Xv4ZTgEAmk8m+JpMJwF0ul78mkwnA9Pk8QF6vl0CLxeI/JpMJwPT5PEBUKlXA9Pk8wFQqVcCLxeK/JpMJwIvF4r/GYpFAi8XiP4ZTgEDw93s/XS63QCmXND/GYpFA9Pk8wF0ut0D0+TzAVCpVwIvF4r+GU4BA9Pk8wF6vl0Dw93u/XS63QCmXNL/GYpFA9Pk8QF0ut0CLxeI/xmKRQIvF4j8mkwnAi8XivyaTCcD0+TxAVCpVwF0ulz9er5dAXS6Xv16vl0BdLpc/VCpVwF0ul79UKlXAXS6XP10ut0BdLpe/XS63QF0ulz9UKlXAXS6Xv1QqVcBdLpe/6PP5vyaTyb7GYjFAXS6Xv/T5fEBdLpc/9Pl8QCaTyT7GYjFAJpPJPhaLRb8mk8m+FotFv10ulz/o8/m/XS6Xv+3gDkBdLpe/QIgBPV0ulz/t4A5AXS6XP0CIAT1dLpc/6PP5v10ulz/4+/0/XS6Xv+jz+b9dLpe/+Pv9PyaTyT6US9o/JpPJPhaLRb9dLpc/lEvaP10ulz8Wi0W/i8XiPyaTCcCLxeI/hlOAQIvF4r8mkwnAKZc0P8ZikUCLxeK/xmKRQIvF4r+GU4BAi8XivyaTCcAplzS/xmKRQIvF4j8mkwnAi8XiP8ZikUAmk8k+VCpVwCaTyb5UKlXA0bTCPtCvW8DRtMK+0K9bwKORrj6Iw2HAo5GuvojDYcDwiI6+cvtmwPCIjj5y+2bAJpNJPor8asAmk0m+ivxqwIOv0D3ugG3Ag6/Qve6AbcDxsKoOulxuwIvF4j+g4Mq/i8Xiv6Dgyr+LxeI/agjYv4vF4r9qCNi/i8Xiv+mZGMCLxeK/zS0fwIvF4j/pmRjAi8XiP80tH8Amk8k+VCpVwCaTyb5UKlXA0bTCPtCvW8DRtMK+0K9bwKORrj6Iw2HAo5GuvojDYcDwiI4+cvtmwPCIjr5y+2bAJpNJPor8asAmk0m+ivxqwIOv0D3ugG3Ag6/Qve6AbcAJafYNulxuwIvF4j/NLR/Ai8XiP+mZGMCLxeK/zS0fwIvF4r/pmRjAi8XiP0AVVsCLxeI/XIFPwIvF4r9AFVbAi8Xiv1yBT8CLxeK/QEXEPovF4j9ARcQ+i8Xiv2Dk+D6LxeI/YOT4PovF4r/4azW/i8XiP/hrNb+LxeK/aBwbv4vF4j9oHBu/i8Xiv0/HPMCLxeK/MltDwIvF4j9PxzzAi8XiPzJbQ8CLxeK/XIFPwIvF4r9AFVbAi8XiP1yBT8CLxeI/QBVWwIvF4r9qCNi/i8XiP2oI2L+LxeK/oODKv4vF4j+g4Mq/i8XiPzJbQ8CLxeI/T8c8wIvF4r8yW0PAi8Xiv0/HPMCLxeI/aBwbv4vF4r9oHBu/i8XiP/hrNb+LxeK/+Gs1v4vF4j9g5Pg+i8Xiv2Dk+D6LxeI/QEXEPovF4r9ARcQ+"),
|
||||
"format": 34359742487,
|
||||
"index_count": 168,
|
||||
"index_data": PackedByteArray("AgAAAAEAAQADAAIABgAEAAUABQAHAAYACgAIAAkACQALAAoADgAMAA0ADQAPAA4AEgAQABEAEQATABIAFgAUABUAFQAXABYAGgAYABkAGQAbABoAHgAcAB0AHQAfAB4AIgAgACEAIQAjACIAJgAkACUAJQAnACYAJQAoACcAKAApACcAKgAlACQAJAArACoAKwAsACoAKwAtACwALQApACwALQAnACkAMAAuAC8ALwAxADAALwAyADEAMgAzADEAMgA0ADMANAA1ADMANgAvAC4ALgA3ADYANwA1ADYANwAzADUAOgA4ADkAOQA7ADoAPgA8AD0APQA/AD4AQgBAAEEAQQBDAEIAQQBEAEMARABFAEMARgBBAEAAQABHAEYARwBFAEYARwBDAEUASgBIAEkASQBLAEoATgBMAE0ATQBPAE4AUgBQAFEAUQBTAFIA"),
|
||||
"material": SubResource("StandardMaterial3D_n3ik4"),
|
||||
"name": "metal",
|
||||
"primitive": 3,
|
||||
"uv_scale": Vector4(0, 0, 0, 0),
|
||||
"vertex_count": 168,
|
||||
"vertex_data": PackedByteArray("kML1vIaWnL3rUTg9kML1vOtRuL2wyJI8CtcjvIaWnL3rUTg9CtcjvOtRuL2wyJI8CtcjPIaWnL3rUTg9CtcjPOtRuL2wyJI8kML1PIaWnL3rUTg9kML1POtRuL2wyJI8kML1vOtRuL2wyJI8kML1vOtRuL3rUTi9CtcjvOtRuL2wyJI8CtcjvOtRuL3rUTi9CtcjPArXoz3rUTi9CtcjPArXoz3rUTg9kML1PArXoz3rUTi9kML1PArXoz3rUTg9kML1POtRuL3rUTi9CtcjPOtRuL3rUTi9kML1PArXoz3rUTi9CtcjPArXoz3rUTi9kML1vArXoz3rUTi9kML1vArXoz3rUTg9CtcjvArXoz3rUTi9CtcjvArXoz3rUTg9CtcjvOtRuL3rUTi9kML1vOtRuL3rUTi9CtcjvArXoz3rUTi9kML1vArXoz3rUTi9kML1vIaWnL3rUTg9CtcjvIaWnL3rUTg9kML1vArXoz3rUTg9CtcjvArXoz3rUTg9CtcjPIaWnL3rUTg9kML1PIaWnL3rUTg9CtcjPArXoz3rUTg9kML1PArXoz3rUTg9kML1vFyPwr2amZk9kML1vArXoz3rUTg9kML1vK1H4T2amZk9kML1vK1H4T2amZm9kML1vArXoz3rUTi9kML1vOtRuL3rUTi9kML1vIaWnL3rUTg9kML1vJDC9b3NzMw8kML1vOtRuL2wyJI8kML1vJDC9b2amZm9kML1PK1H4T2amZk9kML1PIaWnL3rUTg9kML1PFyPwr2amZk9kML1PJDC9b3NzMw8kML1POtRuL2wyJI8kML1PJDC9b2amZm9kML1POtRuL3rUTi9kML1PArXoz3rUTi9kML1PArXoz3rUTg9kML1PK1H4T2amZm9kML1PFyPwr2amZk9kML1vFyPwr2amZk9kML1PK1H4T2amZk9kML1vK1H4T2amZk9kML1vJDC9b2amZm9kML1PJDC9b2amZm9kML1vK1H4T2amZm9kML1PK1H4T2amZm9kML1vK1H4T2amZm9CtcjvK1H4T3rUTg9kML1vK1H4T2amZk9kML1PK1H4T2amZk9CtcjPK1H4T3rUTg9CtcjPK1H4T3rUTi9CtcjvK1H4T3rUTi9kML1PK1H4T2amZm9kML1vJDC9b3NzMw8kML1vFyPwr2amZk9kML1PJDC9b3NzMw8kML1PFyPwr2amZk9kML1vJDC9b2amZm9kML1vJDC9b3NzMw8kML1PJDC9b2amZm9kML1PJDC9b3NzMw8CtcjPOtRuL2wyJI8CtcjPOtRuL3rUTi9kML1POtRuL2wyJI8kML1POtRuL3rUTi9CtcjvArXoz3rUTg9CtcjvIaWnL3rUTg9CtcjvArXoz3rUTi9CtcjvOtRuL2wyJI8CtcjvOtRuL3rUTi9CtcjPIaWnL3rUTg9CtcjPArXoz3rUTg9CtcjPOtRuL2wyJI8CtcjPArXoz3rUTi9CtcjPOtRuL3rUTi9CtcjvK1H4T3rUTi9CtcjPK1H4T3rUTi93kEevKKU5j3rUTi93kEePKKU5j3rUTi9vOMNvB+F6z3rUTi9vOMNPB+F6z3rUTi9a7TnO/TC7z3rUTi9a7Tnu/TC7z3rUTi9CtejuyYE8z3rUTi9CtejOyYE8z3rUTi9oZ4pu+kP9T3rUTi9oZ4pO+kP9T3rUTi9AAAAAJDC9T3rUTi9a7Tnu/TC7z3rUTg9a7Tnu/TC7z3rUTi9CtejuyYE8z3rUTg9CtejuyYE8z3rUTi9vOMNPB+F6z3rUTg9a7TnO/TC7z3rUTg9vOMNPB+F6z3rUTi9a7TnO/TC7z3rUTi9CtcjPK1H4T3rUTg9CtcjvK1H4T3rUTg93kEePKKU5j3rUTg93kEevKKU5j3rUTg9vOMNPB+F6z3rUTg9vOMNvB+F6z3rUTg9a7TnO/TC7z3rUTg9a7Tnu/TC7z3rUTg9CtejOyYE8z3rUTg9CtejuyYE8z3rUTg9oZ4pO+kP9T3rUTg9oZ4pu+kP9T3rUTg9AAAAAJDC9T3rUTg9a7Tnu/TC7z3rUTg9vOMNvB+F6z3rUTg9a7Tnu/TC7z3rUTi9vOMNvB+F6z3rUTi93kEevKKU5j3rUTg9CtcjvK1H4T3rUTg93kEevKKU5j3rUTi9CtcjvK1H4T3rUTi9AAAAAJDC9T3rUTg9AAAAAJDC9T3rUTi9oZ4pO+kP9T3rUTg9oZ4pO+kP9T3rUTi9oZ4pO+kP9T3rUTg9oZ4pO+kP9T3rUTi9CtejOyYE8z3rUTg9CtejOyYE8z3rUTi93kEePKKU5j3rUTg9vOMNPB+F6z3rUTg93kEePKKU5j3rUTi9vOMNPB+F6z3rUTi9CtcjPK1H4T3rUTg93kEePKKU5j3rUTg9CtcjPK1H4T3rUTi93kEePKKU5j3rUTi9CtejOyYE8z3rUTg9CtejOyYE8z3rUTi9a7TnO/TC7z3rUTg9a7TnO/TC7z3rUTi9vOMNvB+F6z3rUTg93kEevKKU5j3rUTg9vOMNvB+F6z3rUTi93kEevKKU5j3rUTi9CtejuyYE8z3rUTg9CtejuyYE8z3rUTi9oZ4pu+kP9T3rUTg9oZ4pu+kP9T3rUTi9oZ4pu+kP9T3rUTg9oZ4pu+kP9T3rUTi9AAAAAJDC9T3rUTg9AAAAAJDC9T3rUTi9qar//wAA/7+pqv//AAD/v6mq//8AAP+/qar//wAA/7+pqv//AAD/v6mq//8AAP+/qar//wAA/7+pqv//AAD/v/9///////+//3///////7//f///////v/9///////+//38AAAAA/7//fwAAAAD/v/9/AAAAAP+//38AAAAA/7//f/9/////v/9//3////+//3//f////7//f/9/////v/9/AAAAAP+//38AAAAA/7//fwAAAAD/v/9/AAAAAP+//3//f////7//f/9/////v/9//3////+//3//f////7//////AAD/v/////8AAP+//////wAA/7//////AAD/v/////8AAP+//////wAA/7//////AAD/v/////8AAP+/AAD/f/9//78AAP9//3//vwAA/3//f/+/AAD/f/9//78AAP9//3//vwAA/3//f/+/AAD/f/9//78AAP9//3//vwAA/3//f/+/AAD/f/9//7////9//////////3/+////////f/7///////9//v///////3/+////////f/7/AID///9//v8AgP///3///wCA////f/////////9///////9//3////+//3//f////7//f/9/////v/9//3////+//////wAA/7//////AAD/v/////8AAP+//////wAA/7//f///////v/9///////+//3///////7//f////v//v/9////+//+//3///////7//f////v//v/9///////+//3+qKv///7//f6oq////v/9/qir///+//3+qKv///7//fwAAAAD/v/9/AAAAAP+//38AAAAA/7//fwAAAAD/v/9///////+//3///////7//f///////v/9///////+/AAD/fwAAAIAAAP9/AAAAgAAA/38AAACAAAD/fwAAAIAAAP9/AAAAgP///38AAACA////fwAAAID///9/AAAAgP///38AAACA////fwAAAID/////AAAAgP////8AAACA/////wAAAID/////AAAAgP////8AAACA/////wAAAID/////AAAAgP////8AAACA/////wAAAID/////AAAAgP////8AAACA/////wAAAID/////AAAAgP8//78AAACA/z//vwAAAIAlUSXRAAAAgCVRJdEAAACAJdHZrgAAAID/v/+/AAAAgCXR2a4AAACA/7//vwAAAID/f/9/AAAAgP9//38AAACA/3//fwAAAID/f/9/AAAAgP9//38AAACA/3//fwAAAID/f/9/AAAAgP9//38AAACA/3//fwAAAID/f/9/AAAAgP9//38AAACA/3//fwAAAID/f/9/AAAAgP8//78AAACA2S7ZrgAAAID/P/+/AAAAgNku2a4AAACADBsMmwAAAIDkDuOOAAAAgAwbDJsAAACA5A7jjgAAAID/f///AAAAgP9///8AAACADJvy5AAAAIAMm/LkAAAAgAyb8uQAAACADJvy5AAAAIDZriXRAAAAgNmuJdEAAACA8uQMmwAAAIAl0dmuAAAAgPLkDJsAAACAJdHZrgAAAIAa8eOOAAAAgPLkDJsAAACAGvHjjgAAAIDy5AybAAAAgNmuJdEAAACA2a4l0QAAAID/v/+/AAAAgP+//78AAACA2S7ZrgAAAIAMGwybAAAAgNku2a4AAACADBsMmwAAAIAlUSXRAAAAgCVRJdEAAACA8mTy5AAAAIDyZPLkAAAAgPJk8uQAAACA8mTy5AAAAID/f///AAAAgP9///8AAACA")
|
||||
}, {
|
||||
"aabb": AABB(-0.03, -0.12, -0.075, 0.06, 0.24, 0.15),
|
||||
"attribute_data": PackedByteArray("XS6XP0T3Qj9dLpc/dxH6PyaTyT5E90I/JpPJPncR+j8mk8m+RPdCPyaTyb53Efo/XS6Xv0T3Qj9dLpe/dxH6P10ul7+US9o/XS6XvxaLRb8mk8m+lEvaPyaTyb4Wi0W/JpPJvhaLRb8mk8m+xmIxQF0ul78Wi0W/XS6Xv8ZiMUBdLpc/xmKRQCaTyT7GYpFAXS6XPyaTCcAmk8k+JpMJwF0ulz8Wi0W/XS6XP8ZiMUAmk8k+FotFvyaTyT7GYjFAJpPJvsZikUBdLpe/xmKRQCaTyb4mkwnAXS6XvyaTCcBdLpc/hlOAQCaTyT6GU4BAXS6XPyaTCcAmk8k+JpMJwCaTyb6GU4BAXS6Xv4ZTgEAmk8m+JpMJwF0ul78mkwnA9Pk8QF6vl0CLxeI/JpMJwPT5PEBUKlXA9Pk8wFQqVcCLxeK/JpMJwIvF4r/GYpFAi8XiP4ZTgEDw93s/XS63QCmXND/GYpFA9Pk8wF0ut0D0+TzAVCpVwIvF4r+GU4BA9Pk8wF6vl0Dw93u/XS63QCmXNL/GYpFA9Pk8QF0ut0CLxeI/xmKRQIvF4j8mkwnAi8XivyaTCcD0+TxAVCpVwF0ulz9er5dAXS6Xv16vl0BdLpc/VCpVwF0ul79UKlXAXS6XP10ut0BdLpe/XS63QF0ulz9UKlXAXS6Xv1QqVcBdLpe/6PP5vyaTyb7GYjFAXS6Xv/T5fEBdLpc/9Pl8QCaTyT7GYjFAJpPJPhaLRb8mk8m+FotFv10ulz/o8/m/XS6Xv+3gDkBdLpe/QIgBPV0ulz/t4A5AXS6XP0CIAT1dLpc/6PP5v10ulz/4+/0/XS6Xv+jz+b9dLpe/+Pv9PyaTyT6US9o/JpPJPhaLRb9dLpc/lEvaP10ulz8Wi0W/i8XiPyaTCcCLxeI/hlOAQIvF4r8mkwnAKZc0P8ZikUCLxeK/xmKRQIvF4r+GU4BAi8XivyaTCcAplzS/xmKRQIvF4j8mkwnAi8XiP8ZikUAmk8k+VCpVwCaTyb5UKlXA0bTCPtCvW8DRtMK+0K9bwKORrj6Iw2HAo5GuvojDYcDwiI6+cvtmwPCIjj5y+2bAJpNJPor8asAmk0m+ivxqwIOv0D3ugG3Ag6/Qve6AbcDxsKoOulxuwIvF4j+g4Mq/i8Xiv6Dgyr+LxeI/agjYv4vF4r9qCNi/i8Xiv+mZGMCLxeK/zS0fwIvF4j/pmRjAi8XiP80tH8Amk8k+VCpVwCaTyb5UKlXA0bTCPtCvW8DRtMK+0K9bwKORrj6Iw2HAo5GuvojDYcDwiI4+cvtmwPCIjr5y+2bAJpNJPor8asAmk0m+ivxqwIOv0D3ugG3Ag6/Qve6AbcAJafYNulxuwIvF4j/NLR/Ai8XiP+mZGMCLxeK/zS0fwIvF4r/pmRjAi8XiP0AVVsCLxeI/XIFPwIvF4r9AFVbAi8Xiv1yBT8CLxeK/QEXEPovF4j9ARcQ+i8Xiv2Dk+D6LxeI/YOT4PovF4r/4azW/i8XiP/hrNb+LxeK/aBwbv4vF4j9oHBu/i8Xiv0/HPMCLxeK/MltDwIvF4j9PxzzAi8XiPzJbQ8CLxeK/XIFPwIvF4r9AFVbAi8XiP1yBT8CLxeI/QBVWwIvF4r9qCNi/i8XiP2oI2L+LxeK/oODKv4vF4j+g4Mq/i8XiPzJbQ8CLxeI/T8c8wIvF4r8yW0PAi8Xiv0/HPMCLxeI/aBwbv4vF4r9oHBu/i8XiP/hrNb+LxeK/+Gs1v4vF4j9g5Pg+i8Xiv2Dk+D6LxeI/QEXEPovF4r9ARcQ+"),
|
||||
"format": 34359742487,
|
||||
"index_count": 18,
|
||||
"index_data": PackedByteArray("VgBUAFUAVQBXAFYAVwBYAFYAWwBZAFoAWgBcAFsAXABdAFsA"),
|
||||
"material": SubResource("StandardMaterial3D_fil5e"),
|
||||
"name": "darkMetal",
|
||||
"primitive": 3,
|
||||
"uv_scale": Vector4(0, 0, 0, 0),
|
||||
"vertex_count": 168,
|
||||
"vertex_data": PackedByteArray("kML1vIaWnL3rUTg9kML1vOtRuL2wyJI8CtcjvIaWnL3rUTg9CtcjvOtRuL2wyJI8CtcjPIaWnL3rUTg9CtcjPOtRuL2wyJI8kML1PIaWnL3rUTg9kML1POtRuL2wyJI8kML1vOtRuL2wyJI8kML1vOtRuL3rUTi9CtcjvOtRuL2wyJI8CtcjvOtRuL3rUTi9CtcjPArXoz3rUTi9CtcjPArXoz3rUTg9kML1PArXoz3rUTi9kML1PArXoz3rUTg9kML1POtRuL3rUTi9CtcjPOtRuL3rUTi9kML1PArXoz3rUTi9CtcjPArXoz3rUTi9kML1vArXoz3rUTi9kML1vArXoz3rUTg9CtcjvArXoz3rUTi9CtcjvArXoz3rUTg9CtcjvOtRuL3rUTi9kML1vOtRuL3rUTi9CtcjvArXoz3rUTi9kML1vArXoz3rUTi9kML1vIaWnL3rUTg9CtcjvIaWnL3rUTg9kML1vArXoz3rUTg9CtcjvArXoz3rUTg9CtcjPIaWnL3rUTg9kML1PIaWnL3rUTg9CtcjPArXoz3rUTg9kML1PArXoz3rUTg9kML1vFyPwr2amZk9kML1vArXoz3rUTg9kML1vK1H4T2amZk9kML1vK1H4T2amZm9kML1vArXoz3rUTi9kML1vOtRuL3rUTi9kML1vIaWnL3rUTg9kML1vJDC9b3NzMw8kML1vOtRuL2wyJI8kML1vJDC9b2amZm9kML1PK1H4T2amZk9kML1PIaWnL3rUTg9kML1PFyPwr2amZk9kML1PJDC9b3NzMw8kML1POtRuL2wyJI8kML1PJDC9b2amZm9kML1POtRuL3rUTi9kML1PArXoz3rUTi9kML1PArXoz3rUTg9kML1PK1H4T2amZm9kML1PFyPwr2amZk9kML1vFyPwr2amZk9kML1PK1H4T2amZk9kML1vK1H4T2amZk9kML1vJDC9b2amZm9kML1PJDC9b2amZm9kML1vK1H4T2amZm9kML1PK1H4T2amZm9kML1vK1H4T2amZm9CtcjvK1H4T3rUTg9kML1vK1H4T2amZk9kML1PK1H4T2amZk9CtcjPK1H4T3rUTg9CtcjPK1H4T3rUTi9CtcjvK1H4T3rUTi9kML1PK1H4T2amZm9kML1vJDC9b3NzMw8kML1vFyPwr2amZk9kML1PJDC9b3NzMw8kML1PFyPwr2amZk9kML1vJDC9b2amZm9kML1vJDC9b3NzMw8kML1PJDC9b2amZm9kML1PJDC9b3NzMw8CtcjPOtRuL2wyJI8CtcjPOtRuL3rUTi9kML1POtRuL2wyJI8kML1POtRuL3rUTi9CtcjvArXoz3rUTg9CtcjvIaWnL3rUTg9CtcjvArXoz3rUTi9CtcjvOtRuL2wyJI8CtcjvOtRuL3rUTi9CtcjPIaWnL3rUTg9CtcjPArXoz3rUTg9CtcjPOtRuL2wyJI8CtcjPArXoz3rUTi9CtcjPOtRuL3rUTi9CtcjvK1H4T3rUTi9CtcjPK1H4T3rUTi93kEevKKU5j3rUTi93kEePKKU5j3rUTi9vOMNvB+F6z3rUTi9vOMNPB+F6z3rUTi9a7TnO/TC7z3rUTi9a7Tnu/TC7z3rUTi9CtejuyYE8z3rUTi9CtejOyYE8z3rUTi9oZ4pu+kP9T3rUTi9oZ4pO+kP9T3rUTi9AAAAAJDC9T3rUTi9a7Tnu/TC7z3rUTg9a7Tnu/TC7z3rUTi9CtejuyYE8z3rUTg9CtejuyYE8z3rUTi9vOMNPB+F6z3rUTg9a7TnO/TC7z3rUTg9vOMNPB+F6z3rUTi9a7TnO/TC7z3rUTi9CtcjPK1H4T3rUTg9CtcjvK1H4T3rUTg93kEePKKU5j3rUTg93kEevKKU5j3rUTg9vOMNPB+F6z3rUTg9vOMNvB+F6z3rUTg9a7TnO/TC7z3rUTg9a7Tnu/TC7z3rUTg9CtejOyYE8z3rUTg9CtejuyYE8z3rUTg9oZ4pO+kP9T3rUTg9oZ4pu+kP9T3rUTg9AAAAAJDC9T3rUTg9a7Tnu/TC7z3rUTg9vOMNvB+F6z3rUTg9a7Tnu/TC7z3rUTi9vOMNvB+F6z3rUTi93kEevKKU5j3rUTg9CtcjvK1H4T3rUTg93kEevKKU5j3rUTi9CtcjvK1H4T3rUTi9AAAAAJDC9T3rUTg9AAAAAJDC9T3rUTi9oZ4pO+kP9T3rUTg9oZ4pO+kP9T3rUTi9oZ4pO+kP9T3rUTg9oZ4pO+kP9T3rUTi9CtejOyYE8z3rUTg9CtejOyYE8z3rUTi93kEePKKU5j3rUTg9vOMNPB+F6z3rUTg93kEePKKU5j3rUTi9vOMNPB+F6z3rUTi9CtcjPK1H4T3rUTg93kEePKKU5j3rUTg9CtcjPK1H4T3rUTi93kEePKKU5j3rUTi9CtejOyYE8z3rUTg9CtejOyYE8z3rUTi9a7TnO/TC7z3rUTg9a7TnO/TC7z3rUTi9vOMNvB+F6z3rUTg93kEevKKU5j3rUTg9vOMNvB+F6z3rUTi93kEevKKU5j3rUTi9CtejuyYE8z3rUTg9CtejuyYE8z3rUTi9oZ4pu+kP9T3rUTg9oZ4pu+kP9T3rUTi9oZ4pu+kP9T3rUTg9oZ4pu+kP9T3rUTi9AAAAAJDC9T3rUTg9AAAAAJDC9T3rUTi9qar//wAAAICpqv//AAAAgKmq//8AAACAqar//wAAAICpqv//AAAAgKmq//8AAACAqar//wAAAICpqv//AAAAgP9///8AAACA/3///wAAAID/f///AAAAgP9///8AAACA/38AAAAAAID/fwAAAAAAgP9/AAAAAACA/38AAAAAAID/f/9/AAAAgP9//38AAACA/3//fwAAAID/f/9/AAAAgP9/AAAAAACA/38AAAAAAID/fwAAAAAAgP9/AAAAAACA/3//fwAAAID/f/9/AAAAgP9//38AAACA/3//fwAAAID/////AAAAgP////8AAACA/////wAAAID/////AAAAgP////8AAACA/////wAAAID/////AAAAgP////8AAACAAAD/fwAAAIAAAP9/AAAAgAAA/38AAACAAAD/fwAAAIAAAP9/AAAAgAAA/38AAACAAAD/fwAAAIAAAP9/AAAAgAAA/38AAACAAAD/fwAAAID///9/AAAAgP///38AAACA////fwAAAID///9/AAAAgP///38AAACA////fwAAAID///9/AAAAgP///38AAACA////fwAAAID///9/AAAAgP9//38AAACA/3//fwAAAID/f/9/AAAAgP9//38AAACA/////wAAAID/////AAAAgP////8AAACA/////wAAAID/f///AAAAgP9///8AAACA/3///wAAAID/f///AAAAgP9///8AAACA/3///wAAAID/f///AAAAgP9///8AAACA/3+qKgAAAID/f6oqAAAAgP9/qioAAACA/3+qKgAAAID/fwAAAAAAgP9/AAAAAACA/38AAAAAAID/fwAAAAAAgP9///8AAACA/3///wAAAID/f///AAAAgP9///8AAACAAAD/f/9//78AAP9//3//vwAA/3//f/+/AAD/f/9//78AAP9//3//v////3/+////////f/////////9//v///////3//////////f///////////AAAAgP////8AAACA/////wAAAID/////AAAAgP////8AAACA/////wAAAID/////AAAAgP////8AAACA/////wAAAID/////AAAAgP////8AAACA/////wAAAID/////AAAAgP8//78AAACA/z//vwAAAIAlUSXRAAAAgCVRJdEAAACAJdHZrgAAAID/v/+/AAAAgCXR2a4AAACA/7//vwAAAID/f/9/AAAAgP9//38AAACA/3//fwAAAID/f/9/AAAAgP9//38AAACA/3//fwAAAID/f/9/AAAAgP9//38AAACA/3//fwAAAID/f/9/AAAAgP9//38AAACA/3//fwAAAID/f/9/AAAAgP8//78AAACA2S7ZrgAAAID/P/+/AAAAgNku2a4AAACADBsMmwAAAIDkDuOOAAAAgAwbDJsAAACA5A7jjgAAAID/f///AAAAgP9///8AAACADJvy5AAAAIAMm/LkAAAAgAyb8uQAAACADJvy5AAAAIDZriXRAAAAgNmuJdEAAACA8uQMmwAAAIAl0dmuAAAAgPLkDJsAAACAJdHZrgAAAIAa8eOOAAAAgPLkDJsAAACAGvHjjgAAAIDy5AybAAAAgNmuJdEAAACA2a4l0QAAAID/v/+/AAAAgP+//78AAACA2S7ZrgAAAIAMGwybAAAAgNku2a4AAACADBsMmwAAAIAlUSXRAAAAgCVRJdEAAACA8mTy5AAAAIDyZPLkAAAAgPJk8uQAAACA8mTy5AAAAID/f///AAAAgP9///8AAACA")
|
||||
}, {
|
||||
"aabb": AABB(-0.03, -0.12, -0.075, 0.06, 0.24, 0.15),
|
||||
"attribute_data": PackedByteArray("XS6XP0T3Qj9dLpc/dxH6PyaTyT5E90I/JpPJPncR+j8mk8m+RPdCPyaTyb53Efo/XS6Xv0T3Qj9dLpe/dxH6P10ul7+US9o/XS6XvxaLRb8mk8m+lEvaPyaTyb4Wi0W/JpPJvhaLRb8mk8m+xmIxQF0ul78Wi0W/XS6Xv8ZiMUBdLpc/xmKRQCaTyT7GYpFAXS6XPyaTCcAmk8k+JpMJwF0ulz8Wi0W/XS6XP8ZiMUAmk8k+FotFvyaTyT7GYjFAJpPJvsZikUBdLpe/xmKRQCaTyb4mkwnAXS6XvyaTCcBdLpc/hlOAQCaTyT6GU4BAXS6XPyaTCcAmk8k+JpMJwCaTyb6GU4BAXS6Xv4ZTgEAmk8m+JpMJwF0ul78mkwnA9Pk8QF6vl0CLxeI/JpMJwPT5PEBUKlXA9Pk8wFQqVcCLxeK/JpMJwIvF4r/GYpFAi8XiP4ZTgEDw93s/XS63QCmXND/GYpFA9Pk8wF0ut0D0+TzAVCpVwIvF4r+GU4BA9Pk8wF6vl0Dw93u/XS63QCmXNL/GYpFA9Pk8QF0ut0CLxeI/xmKRQIvF4j8mkwnAi8XivyaTCcD0+TxAVCpVwF0ulz9er5dAXS6Xv16vl0BdLpc/VCpVwF0ul79UKlXAXS6XP10ut0BdLpe/XS63QF0ulz9UKlXAXS6Xv1QqVcBdLpe/6PP5vyaTyb7GYjFAXS6Xv/T5fEBdLpc/9Pl8QCaTyT7GYjFAJpPJPhaLRb8mk8m+FotFv10ulz/o8/m/XS6Xv+3gDkBdLpe/QIgBPV0ulz/t4A5AXS6XP0CIAT1dLpc/6PP5v10ulz/4+/0/XS6Xv+jz+b9dLpe/+Pv9PyaTyT6US9o/JpPJPhaLRb9dLpc/lEvaP10ulz8Wi0W/i8XiPyaTCcCLxeI/hlOAQIvF4r8mkwnAKZc0P8ZikUCLxeK/xmKRQIvF4r+GU4BAi8XivyaTCcAplzS/xmKRQIvF4j8mkwnAi8XiP8ZikUAmk8k+VCpVwCaTyb5UKlXA0bTCPtCvW8DRtMK+0K9bwKORrj6Iw2HAo5GuvojDYcDwiI6+cvtmwPCIjj5y+2bAJpNJPor8asAmk0m+ivxqwIOv0D3ugG3Ag6/Qve6AbcDxsKoOulxuwIvF4j+g4Mq/i8Xiv6Dgyr+LxeI/agjYv4vF4r9qCNi/i8Xiv+mZGMCLxeK/zS0fwIvF4j/pmRjAi8XiP80tH8Amk8k+VCpVwCaTyb5UKlXA0bTCPtCvW8DRtMK+0K9bwKORrj6Iw2HAo5GuvojDYcDwiI4+cvtmwPCIjr5y+2bAJpNJPor8asAmk0m+ivxqwIOv0D3ugG3Ag6/Qve6AbcAJafYNulxuwIvF4j/NLR/Ai8XiP+mZGMCLxeK/zS0fwIvF4r/pmRjAi8XiP0AVVsCLxeI/XIFPwIvF4r9AFVbAi8Xiv1yBT8CLxeK/QEXEPovF4j9ARcQ+i8Xiv2Dk+D6LxeI/YOT4PovF4r/4azW/i8XiP/hrNb+LxeK/aBwbv4vF4j9oHBu/i8Xiv0/HPMCLxeK/MltDwIvF4j9PxzzAi8XiPzJbQ8CLxeK/XIFPwIvF4r9AFVbAi8XiP1yBT8CLxeI/QBVWwIvF4r9qCNi/i8XiP2oI2L+LxeK/oODKv4vF4j+g4Mq/i8XiPzJbQ8CLxeI/T8c8wIvF4r8yW0PAi8Xiv0/HPMCLxeI/aBwbv4vF4r9oHBu/i8XiP/hrNb+LxeK/+Gs1v4vF4j9g5Pg+i8Xiv2Dk+D6LxeI/QEXEPovF4r9ARcQ+"),
|
||||
"format": 34359742487,
|
||||
"index_count": 138,
|
||||
"index_data": PackedByteArray("YABeAF8AXwBhAGAAYQBiAGAAYQBjAGIAYwBkAGIAZABlAGIAZABmAGUAZABnAGYAZwBoAGYAZwBpAGgAaQBqAGgAbQBrAGwAbABuAG0AcQBvAHAAcAByAHEAdQBzAHQAdAB2AHUAdgB3AHUAdgB4AHcAeAB5AHcAeAB6AHkAegB7AHkAegB8AHsAfAB9AHsAfAB+AH0AfgB/AH0AggCAAIEAgQCDAIIAhgCEAIUAhQCHAIYAigCIAIkAiQCLAIoAjgCMAI0AjQCPAI4AkgCQAJEAkQCTAJIAlgCUAJUAlQCXAJYAmgCYAJkAmQCbAJoAngCcAJ0AnQCfAJ4AogCgAKEAoQCjAKIApgCkAKUApQCnAKYA"),
|
||||
"material": SubResource("StandardMaterial3D_bfdxb"),
|
||||
"name": "yellow",
|
||||
"primitive": 3,
|
||||
"uv_scale": Vector4(0, 0, 0, 0),
|
||||
"vertex_count": 168,
|
||||
"vertex_data": PackedByteArray("kML1vIaWnL3rUTg9kML1vOtRuL2wyJI8CtcjvIaWnL3rUTg9CtcjvOtRuL2wyJI8CtcjPIaWnL3rUTg9CtcjPOtRuL2wyJI8kML1PIaWnL3rUTg9kML1POtRuL2wyJI8kML1vOtRuL2wyJI8kML1vOtRuL3rUTi9CtcjvOtRuL2wyJI8CtcjvOtRuL3rUTi9CtcjPArXoz3rUTi9CtcjPArXoz3rUTg9kML1PArXoz3rUTi9kML1PArXoz3rUTg9kML1POtRuL3rUTi9CtcjPOtRuL3rUTi9kML1PArXoz3rUTi9CtcjPArXoz3rUTi9kML1vArXoz3rUTi9kML1vArXoz3rUTg9CtcjvArXoz3rUTi9CtcjvArXoz3rUTg9CtcjvOtRuL3rUTi9kML1vOtRuL3rUTi9CtcjvArXoz3rUTi9kML1vArXoz3rUTi9kML1vIaWnL3rUTg9CtcjvIaWnL3rUTg9kML1vArXoz3rUTg9CtcjvArXoz3rUTg9CtcjPIaWnL3rUTg9kML1PIaWnL3rUTg9CtcjPArXoz3rUTg9kML1PArXoz3rUTg9kML1vFyPwr2amZk9kML1vArXoz3rUTg9kML1vK1H4T2amZk9kML1vK1H4T2amZm9kML1vArXoz3rUTi9kML1vOtRuL3rUTi9kML1vIaWnL3rUTg9kML1vJDC9b3NzMw8kML1vOtRuL2wyJI8kML1vJDC9b2amZm9kML1PK1H4T2amZk9kML1PIaWnL3rUTg9kML1PFyPwr2amZk9kML1PJDC9b3NzMw8kML1POtRuL2wyJI8kML1PJDC9b2amZm9kML1POtRuL3rUTi9kML1PArXoz3rUTi9kML1PArXoz3rUTg9kML1PK1H4T2amZm9kML1PFyPwr2amZk9kML1vFyPwr2amZk9kML1PK1H4T2amZk9kML1vK1H4T2amZk9kML1vJDC9b2amZm9kML1PJDC9b2amZm9kML1vK1H4T2amZm9kML1PK1H4T2amZm9kML1vK1H4T2amZm9CtcjvK1H4T3rUTg9kML1vK1H4T2amZk9kML1PK1H4T2amZk9CtcjPK1H4T3rUTg9CtcjPK1H4T3rUTi9CtcjvK1H4T3rUTi9kML1PK1H4T2amZm9kML1vJDC9b3NzMw8kML1vFyPwr2amZk9kML1PJDC9b3NzMw8kML1PFyPwr2amZk9kML1vJDC9b2amZm9kML1vJDC9b3NzMw8kML1PJDC9b2amZm9kML1PJDC9b3NzMw8CtcjPOtRuL2wyJI8CtcjPOtRuL3rUTi9kML1POtRuL2wyJI8kML1POtRuL3rUTi9CtcjvArXoz3rUTg9CtcjvIaWnL3rUTg9CtcjvArXoz3rUTi9CtcjvOtRuL2wyJI8CtcjvOtRuL3rUTi9CtcjPIaWnL3rUTg9CtcjPArXoz3rUTg9CtcjPOtRuL2wyJI8CtcjPArXoz3rUTi9CtcjPOtRuL3rUTi9CtcjvK1H4T3rUTi9CtcjPK1H4T3rUTi93kEevKKU5j3rUTi93kEePKKU5j3rUTi9vOMNvB+F6z3rUTi9vOMNPB+F6z3rUTi9a7TnO/TC7z3rUTi9a7Tnu/TC7z3rUTi9CtejuyYE8z3rUTi9CtejOyYE8z3rUTi9oZ4pu+kP9T3rUTi9oZ4pO+kP9T3rUTi9AAAAAJDC9T3rUTi9a7Tnu/TC7z3rUTg9a7Tnu/TC7z3rUTi9CtejuyYE8z3rUTg9CtejuyYE8z3rUTi9vOMNPB+F6z3rUTg9a7TnO/TC7z3rUTg9vOMNPB+F6z3rUTi9a7TnO/TC7z3rUTi9CtcjPK1H4T3rUTg9CtcjvK1H4T3rUTg93kEePKKU5j3rUTg93kEevKKU5j3rUTg9vOMNPB+F6z3rUTg9vOMNvB+F6z3rUTg9a7TnO/TC7z3rUTg9a7Tnu/TC7z3rUTg9CtejOyYE8z3rUTg9CtejuyYE8z3rUTg9oZ4pO+kP9T3rUTg9oZ4pu+kP9T3rUTg9AAAAAJDC9T3rUTg9a7Tnu/TC7z3rUTg9vOMNvB+F6z3rUTg9a7Tnu/TC7z3rUTi9vOMNvB+F6z3rUTi93kEevKKU5j3rUTg9CtcjvK1H4T3rUTg93kEevKKU5j3rUTi9CtcjvK1H4T3rUTi9AAAAAJDC9T3rUTg9AAAAAJDC9T3rUTi9oZ4pO+kP9T3rUTg9oZ4pO+kP9T3rUTi9oZ4pO+kP9T3rUTg9oZ4pO+kP9T3rUTi9CtejOyYE8z3rUTg9CtejOyYE8z3rUTi93kEePKKU5j3rUTg9vOMNPB+F6z3rUTg93kEePKKU5j3rUTi9vOMNPB+F6z3rUTi9CtcjPK1H4T3rUTg93kEePKKU5j3rUTg9CtcjPK1H4T3rUTi93kEePKKU5j3rUTi9CtejOyYE8z3rUTg9CtejOyYE8z3rUTi9a7TnO/TC7z3rUTg9a7TnO/TC7z3rUTi9vOMNvB+F6z3rUTg93kEevKKU5j3rUTg9vOMNvB+F6z3rUTi93kEevKKU5j3rUTi9CtejuyYE8z3rUTg9CtejuyYE8z3rUTi9oZ4pu+kP9T3rUTg9oZ4pu+kP9T3rUTi9oZ4pu+kP9T3rUTg9oZ4pu+kP9T3rUTi9AAAAAJDC9T3rUTg9AAAAAJDC9T3rUTi9qar//wAAAICpqv//AAAAgKmq//8AAACAqar//wAAAICpqv//AAAAgKmq//8AAACAqar//wAAAICpqv//AAAAgP9///8AAACA/3///wAAAID/f///AAAAgP9///8AAACA/38AAAAAAID/fwAAAAAAgP9/AAAAAACA/38AAAAAAID/f/9/AAAAgP9//38AAACA/3//fwAAAID/f/9/AAAAgP9/AAAAAACA/38AAAAAAID/fwAAAAAAgP9/AAAAAACA/3//fwAAAID/f/9/AAAAgP9//38AAACA/3//fwAAAID/////AAAAgP////8AAACA/////wAAAID/////AAAAgP////8AAACA/////wAAAID/////AAAAgP////8AAACAAAD/fwAAAIAAAP9/AAAAgAAA/38AAACAAAD/fwAAAIAAAP9/AAAAgAAA/38AAACAAAD/fwAAAIAAAP9/AAAAgAAA/38AAACAAAD/fwAAAID///9/AAAAgP///38AAACA////fwAAAID///9/AAAAgP///38AAACA////fwAAAID///9/AAAAgP///38AAACA////fwAAAID///9/AAAAgP9//38AAACA/3//fwAAAID/f/9/AAAAgP9//38AAACA/////wAAAID/////AAAAgP////8AAACA/////wAAAID/f///AAAAgP9///8AAACA/3///wAAAID/f///AAAAgP9///8AAACA/3///wAAAID/f///AAAAgP9///8AAACA/3+qKgAAAID/f6oqAAAAgP9/qioAAACA/3+qKgAAAID/fwAAAAAAgP9/AAAAAACA/38AAAAAAID/fwAAAAAAgP9///8AAACA/3///wAAAID/f///AAAAgP9///8AAACAAAD/fwAAAIAAAP9/AAAAgAAA/38AAACAAAD/fwAAAIAAAP9/AAAAgP///38AAACA////fwAAAID///9/AAAAgP///38AAACA////fwAAAID/////AAD/v/////8AAP+//////wAA/7//////AAD/v/////8AAP+//////wAA/7//////AAD/v/////8AAP+//////wAA/7//////AAD/v/////8AAP+//////wAA/7//////AAD/v/8//7//f/+//z//v/9//78lUSXR/3//vyVRJdH/f/+/JdHZrv//////v/+//////yXR2a7//////7//v///////f/9/////v/9//3////+//3//f////7//f/9/////v/9//3////+//3//f////7//f/9/////v/9//3////+//3//f////7//f/9/////v/9//3////+//3//f////7//f/9/////v/8//7//f/+/2S7Zrv9//7//P/+//3//v9ku2a7/f/+/DBsMm/9//7/kDuOO/3//vwwbDJv/f/+/5A7jjv9//7//f/////////9/////////DJvy5P////8Mm/Lk/////wyb8uT/////DJvy5P/////ZriXR/////9muJdH/////8uQMm/////8l0dmu//////LkDJv/////JdHZrv////8a8eOO//////LkDJv/////GvHjjv/////y5Ayb/////9muJdH/////2a4l0f//////v/+///////+//7//////2S7Zrv9//78MGwyb/3//v9ku2a7/f/+/DBsMm/9//78lUSXR/3//vyVRJdH/f/+/8mTy5P9//7/yZPLk/3//v/Jk8uT/f/+/8mTy5P9//7//f////3//v/9/////f/+/")
|
||||
}]
|
||||
blend_shape_mode = 0
|
||||
shadow_mesh = SubResource("ArrayMesh_j3k8o")
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_y1cnp"]
|
||||
size = Vector3(0.0599088, 0.407185, 0.143523)
|
||||
|
||||
[node name="blasterI_Clip" type="RigidBody3D"]
|
||||
collision_layer = 8
|
||||
collision_mask = 5
|
||||
script = ExtResource("1_6xwda")
|
||||
weapon = SubResource("Resource_yxf5f")
|
||||
TYPE = "Ammo"
|
||||
|
||||
[node name="clipA2" type="MeshInstance3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1.936, 0, 0, 0, 0.99, 0, 0, 0)
|
||||
mesh = SubResource("ArrayMesh_py3xv")
|
||||
skeleton = NodePath("")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
||||
shape = SubResource("BoxShape3D_y1cnp")
|
||||
@@ -0,0 +1,122 @@
|
||||
[gd_scene load_steps=11 format=4 uid="uid://cgt2lt4ultlaq"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://deaf6cmuofa0" path="res://client/template/scripts/Weapon_State_Machine/PickUp Weapons/Weapon_Pick_Up.gd" id="1_qqhy0"]
|
||||
[ext_resource type="Script" uid="uid://bw4j1fbww4k8s" path="res://client/template/scripts/Weapon_State_Machine/WeaponSlot.gd" id="2_ybq5e"]
|
||||
[ext_resource type="Resource" uid="uid://cd8msvjysh2uv" path="res://client/template/scripts/Weapon_State_Machine/Weapon_Resources/blasterM.tres" id="3_parqs"]
|
||||
|
||||
[sub_resource type="Resource" id="Resource_jnsso"]
|
||||
resource_local_to_scene = true
|
||||
script = ExtResource("2_ybq5e")
|
||||
weapon = ExtResource("3_parqs")
|
||||
current_ammo = 9
|
||||
reserve_ammo = 0
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_0eh6a"]
|
||||
resource_name = "metal"
|
||||
albedo_color = Color(1, 0.894118, 0.505882, 1)
|
||||
metallic = 1.0
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_8qvhc"]
|
||||
resource_name = "darkMetal"
|
||||
albedo_color = Color(0.793075, 0.81829, 0.89044, 1)
|
||||
metallic = 1.0
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_mihsn"]
|
||||
resource_name = "yellow"
|
||||
albedo_color = Color(0.998274, 0.894448, 0.505969, 1)
|
||||
metallic = 1.0
|
||||
|
||||
[sub_resource type="ArrayMesh" id="ArrayMesh_68l5q"]
|
||||
_surfaces = [{
|
||||
"aabb": AABB(-0.03, -0.12, -0.075, 0.06, 0.24, 0.15),
|
||||
"format": 34896613377,
|
||||
"index_count": 168,
|
||||
"index_data": PackedByteArray("AgAAAAEAAQADAAIABgAEAAUABQAHAAYAAwABAAgACAAJAAMADAAKAAsACwANAAwADAAOAA8ADwAKAAwAEgAQABEAEQATABIAEgAJAAgACAAQABIAEQAAAAIAAgATABEACwAEAAYABgANAAsAFQAUABEAEQAWABUAEQAQABYAEAAIABYAAAARABQAFAAXAAAAFwABAAAAFwAYAAEAGAAIAAEAGAAWAAgAGgAZAAYABgAbABoABgAHABsABwAcABsABwAOABwADgAMABwADQAGABkAGQAdAA0AHQAMAA0AHQAcAAwAGQAaABQAFAAVABkAFgAYABwAHAAdABYAFQAWAB4AHgAZABUAHgAfABkAHwAgABkAIQAeABYAFgAdACEAHQAgACEAHQAZACAAGwAXABQAFAAaABsAHAAYABcAFwAbABwABwAFAA8ADwAOAAcA"),
|
||||
"name": "metal",
|
||||
"primitive": 3,
|
||||
"uv_scale": Vector4(0, 0, 0, 0),
|
||||
"vertex_count": 56,
|
||||
"vertex_data": PackedByteArray("AABxLszMAAAAAP8fk54AAFVVcS7MzAAAVVX/H5OeAACpqnEuzMwAAKmq/x+TngAA//9xLszMAAD///8fk54AAAAA/x8zMwAAVVX/HzMzAACpqlTVMzMAAKmqVNXMzAAA//9U1TMzAAD//1TVzMwAAP///x8zMwAAqar/HzMzAAAAAFTVMzMAAAAAVNXMzAAAVVVU1TMzAABVVVTVzMwAAAAAqhr//wAAAABU9f//AAAAAFT1AAAAAAAAAACpqgAAAAAAAAAAAAD//1T1//8AAP//qhr//wAA//8AAKmqAAD//wAAAAAAAP//VPUAAAAAVVVU9czMAACpqlT1zMwAAKmqVPUzMwAAVVVU9TMzAADJVhf4MzMAADWpF/gzMwAADFup+jMzAADypKn6MzMAACqe3/wzMwAA1GHf/DMzAACqapH+MzMAAFSVkf4zMwAA9HSh/zMzAAAKi6H/MzMAAP9///8zMwAA1GHf/MzMAACqapH+zMwAAPKkqfrMzAAAKp7f/MzMAAA1qRf4zMwAAMlWF/jMzAAADFup+szMAABUlZH+zMwAAAqLof/MzAAA9HSh/8zMAAD/f///zMwAAA==")
|
||||
}, {
|
||||
"aabb": AABB(-0.03, -0.12, -0.075, 0.06, 0.24, 0.15),
|
||||
"format": 34896613377,
|
||||
"index_count": 18,
|
||||
"index_data": PackedByteArray("EgATAAIAAgADABIAAwAJABIABQAEAAsACwAKAAUACgAPAAUA"),
|
||||
"name": "darkMetal",
|
||||
"primitive": 3,
|
||||
"uv_scale": Vector4(0, 0, 0, 0),
|
||||
"vertex_count": 56,
|
||||
"vertex_data": PackedByteArray("AABxLszMAAAAAP8fk54AAFVVcS7MzAAAVVX/H5OeAACpqnEuzMwAAKmq/x+TngAA//9xLszMAAD///8fk54AAAAA/x8zMwAAVVX/HzMzAACpqlTVMzMAAKmqVNXMzAAA//9U1TMzAAD//1TVzMwAAP///x8zMwAAqar/HzMzAAAAAFTVMzMAAAAAVNXMzAAAVVVU1TMzAABVVVTVzMwAAAAAqhr//wAAAABU9f//AAAAAFT1AAAAAAAAAACpqgAAAAAAAAAAAAD//1T1//8AAP//qhr//wAA//8AAKmqAAD//wAAAAAAAP//VPUAAAAAVVVU9czMAACpqlT1zMwAAKmqVPUzMwAAVVVU9TMzAADJVhf4MzMAADWpF/gzMwAADFup+jMzAADypKn6MzMAACqe3/wzMwAA1GHf/DMzAACqapH+MzMAAFSVkf4zMwAA9HSh/zMzAAAKi6H/MzMAAP9///8zMwAA1GHf/MzMAACqapH+zMwAAPKkqfrMzAAAKp7f/MzMAAA1qRf4zMwAAMlWF/jMzAAADFup+szMAABUlZH+zMwAAAqLof/MzAAA9HSh/8zMAAD/f///zMwAAA==")
|
||||
}, {
|
||||
"aabb": AABB(-0.03, -0.12, -0.075, 0.06, 0.24, 0.15),
|
||||
"format": 34896613377,
|
||||
"index_count": 138,
|
||||
"index_data": PackedByteArray("IgAhACAAIAAjACIAIwAkACIAIwAlACQAJQAmACQAJgAnACQAJgAoACcAJgApACgAKQAqACgAKQArACoAKwAsACoALgAtACcAJwAoAC4AJQAvADAAMAAmACUAMQAfAB4AHgAyADEAMgAvADEAMgAzAC8AMwAwAC8AMwAtADAALQA0ADAALQAuADQALgA1ADQALgA2ADUANgA3ADUAJwAtADMAMwAkACcAIgAyAB4AHgAhACIANQA3ACwALAArADUANAA1ACsAKwApADQAIwAxAC8ALwAlACMAIAAfADEAMQAjACAAMAA0ACkAKQAmADAAJAAzADIAMgAiACQANgAuACgAKAAqADYANwA2ACoAKgAsADcA"),
|
||||
"name": "yellow",
|
||||
"primitive": 3,
|
||||
"uv_scale": Vector4(0, 0, 0, 0),
|
||||
"vertex_count": 56,
|
||||
"vertex_data": PackedByteArray("AABxLszMAAAAAP8fk54AAFVVcS7MzAAAVVX/H5OeAACpqnEuzMwAAKmq/x+TngAA//9xLszMAAD///8fk54AAAAA/x8zMwAAVVX/HzMzAACpqlTVMzMAAKmqVNXMzAAA//9U1TMzAAD//1TVzMwAAP///x8zMwAAqar/HzMzAAAAAFTVMzMAAAAAVNXMzAAAVVVU1TMzAABVVVTVzMwAAAAAqhr//wAAAABU9f//AAAAAFT1AAAAAAAAAACpqgAAAAAAAAAAAAD//1T1//8AAP//qhr//wAA//8AAKmqAAD//wAAAAAAAP//VPUAAAAAVVVU9czMAACpqlT1zMwAAKmqVPUzMwAAVVVU9TMzAADJVhf4MzMAADWpF/gzMwAADFup+jMzAADypKn6MzMAACqe3/wzMwAA1GHf/DMzAACqapH+MzMAAFSVkf4zMwAA9HSh/zMzAAAKi6H/MzMAAP9///8zMwAA1GHf/MzMAACqapH+zMwAAPKkqfrMzAAAKp7f/MzMAAA1qRf4zMwAAMlWF/jMzAAADFup+szMAABUlZH+zMwAAAqLof/MzAAA9HSh/8zMAAD/f///zMwAAA==")
|
||||
}]
|
||||
blend_shape_mode = 0
|
||||
|
||||
[sub_resource type="ArrayMesh" id="ArrayMesh_5ckjg"]
|
||||
resource_name = "clipA_Mesh clipA"
|
||||
_surfaces = [{
|
||||
"aabb": AABB(-0.03, -0.12, -0.075, 0.06, 0.24, 0.15),
|
||||
"attribute_data": PackedByteArray("MrMGkTKzrqsQkQaREJGuq+5uBpHubq6rzEwGkcxMrqvMTCGmzEy+bu5uIabubr5u7m6+bu5u+L3MTL5uzEz4vTKzluUQkZblMrPuTxCR7k8ys75uMrP4vRCRvm4Qkfi97m6W5cxMluXubu5PzEzuTzKzqtkQkarZMrPuTxCR7k/ubqrZzEyq2e5u7k/MTO5P///86cvM7k///4U1AACFNTMz7k8zM5bly8yq2aqq//+TnpblAAD//wAAhTUzM6rZAAD86VRV//9rYZbl/////8vMluXLzO5PMzPuT///hTUys/zpzEz86TKzhTXMTIU1MrP//8xM//8ys4U1zEyFNcxMVVTubvi9zExh2DKzYdgQkfi9EJG+bu5uvm4ys1VUzEzqscxMtIAys+qxMrO0gDKzVVQys12szExVVMxMXawQkSGmEJG+bjKzIaYys75uy8zuT8vMqtkzM+5Pk56W5TMzluUzM6rZMzPuT2thluXLzO5Py8yW5RCRhTXuboU1e5A+M4NvPjPHjh8xN3EfMe5zTC8QjEwvh4jmLXd35i1qhAUtlHsFLf9/uCzLzI5cMzOOXMvMQlozM0JaMzOuSjMzYkjLzK5Ky8xiSBCRhTXuboU1e5A+M4NvPjPHjh8xN3EfMRCMTC/uc0wvh4jmLXd35i1qhAUtlHsFLf9/uCzLzGJIy8yuSjMzYkgzM65Ky8wzNcvMgDczMzM1MzOANzMzkYjLzJGIMzPeisvM3oozMyZwy8wmcDMzc3LLzHNyMzMLPjMzvjvLzAs+y8y+OzMzgDczMzM1y8yAN8vMMzUzM0Jay8xCWjMzjlzLzI5cy8y+O8vMCz4zM747MzMLPsvMc3IzM3Nyy8wmcDMzJnDLzN6KMzPeisvMkYgzM5GI"),
|
||||
"format": 34896613399,
|
||||
"index_count": 168,
|
||||
"index_data": PackedByteArray("AgAAAAEAAQADAAIABgAEAAUABQAHAAYACgAIAAkACQALAAoADgAMAA0ADQAPAA4AEgAQABEAEQATABIAFgAUABUAFQAXABYAGgAYABkAGQAbABoAHgAcAB0AHQAfAB4AIgAgACEAIQAjACIAJgAkACUAJQAnACYAJQAoACcAKAApACcAKgAlACQAJAArACoAKwAsACoAKwAtACwALQApACwALQAnACkAMAAuAC8ALwAxADAALwAyADEAMgAzADEAMgA0ADMANAA1ADMANgAvAC4ALgA3ADYANwA1ADYANwAzADUAOgA4ADkAOQA7ADoAPgA8AD0APQA/AD4AQgBAAEEAQQBDAEIAQQBEAEMARABFAEMARgBBAEAAQABHAEYARwBFAEYARwBDAEUASgBIAEkASQBLAEoATgBMAE0ATQBPAE4AUgBQAFEAUQBTAFIA"),
|
||||
"material": SubResource("StandardMaterial3D_0eh6a"),
|
||||
"name": "metal",
|
||||
"primitive": 3,
|
||||
"uv_scale": Vector4(5.90551, 11.4488, 0, 0),
|
||||
"vertex_count": 168,
|
||||
"vertex_data": PackedByteArray("AABxLszM//8AAP8fk57//1VVcS7MzP//VVX/H5Oe//+pqnEuzMz//6mq/x+Tnv////9xLszM//////8fk57//wAA/x+Tnv+/AAD/HzMz/79VVf8fk57/v1VV/x8zM/+/qapU1TMz//+pqlTVzMz/////VNUzM/////9U1czM//////8fMzP/f6mq/x8zM/9///9U1TMz/3+pqlTVMzP/fwAAVNUzM///AABU1czM//9VVVTVMzP//1VVVNXMzP//VVX/HzMz/38AAP8fMzP/f1VVVNUzM/9/AABU1TMz/38AAHEuzMz//1VVcS7MzP//AABU1czM//9VVVTVzMz//6mqcS7MzP////9xLszM//+pqlTVzMz/////VNXMzP//AACqGv///78AAFTVzMz/vwAAVPX///+/AABU9QAA/78AAFTVMzP/vwAA/x8zM/+/AABxLszM/78AAAAAqar/vwAA/x+Tnv+/AAAAAAAA/7///1T1////v///cS7MzP+///+qGv///7///wAAqar/v////x+Tnv+///8AAAAA/7////8fMzP/v///VNUzM/+///9U1czM/7///1T1AAD/v///qhr///9/AACqGv///3///1T1////fwAAVPX///9/AAAAAAAA/////wAAAAD//wAAVPUAAP////9U9QAA//8AAFT1AAD/v1VVVPXMzP+/AABU9f///7///1T1////v6mqVPXMzP+/qapU9TMz/79VVVT1MzP/v///VPUAAP+/AAAAAKmqG60AAKoa//8brf//AACpqhut//+qGv//G60AAAAAAAD//wAAAACpqv////8AAAAA/////wAAqar//6mq/x+Tnv+/qar/HzMz/7////8fk57/v////x8zM/+/VVVU1czMVNVVVXEuzMxU1VVVVNUzM1TVVVX/H5OeVNVVVf8fMzNU1amqcS7MzFTVqapU1czMVNWpqv8fk55U1amqVNUzM1TVqar/HzMzVNVVVVT1MzP//6mqVPUzM///yVYX+DMz//81qRf4MzP//wxbqfozM///8qSp+jMz//8qnt/8MzP//9Rh3/wzM///qmqR/jMz//9UlZH+MzP///R0of8zM///Couh/zMz////f///MzP//9Rh3/zMzFTV1GHf/DMzVNWqapH+zMxU1apqkf4zM1TV8qSp+szMVNUqnt/8zMxU1fKkqfozM1TVKp7f/DMzVNWpqlT1zMz//1VVVPXMzP//NakX+MzM///JVhf4zMz///KkqfrMzP//DFup+szM//8qnt/8zMz//9Rh3/zMzP//VJWR/szM//+qapH+zMz//wqLof/MzP//9HSh/8zM////f///zMz//9Rh3/zMzFTVDFup+szMVNXUYd/8MzNU1QxbqfozM1TVyVYX+MzMVNVVVVT1zMxU1clWF/gzM1TVVVVU9TMzVNX/f///zMxU1f9///8zM1TVCouh/8zMVNUKi6H/MzNU1QqLof/MzFTVCouh/zMzVNVUlZH+zMxU1VSVkf4zM1TVNakX+MzMVNXypKn6zMxU1TWpF/gzM1TV8qSp+jMzVNWpqlT1zMxU1TWpF/jMzFTVqapU9TMzVNU1qRf4MzNU1VSVkf7MzFTVVJWR/jMzVNUqnt/8zMxU1Sqe3/wzM1TVDFup+szMVNXJVhf4zMxU1QxbqfozM1TVyVYX+DMzVNWqapH+zMxU1apqkf4zM1TV9HSh/8zMVNX0dKH/MzNU1fR0of/MzFTV9HSh/zMzVNX/f///zMxU1f9///8zM1TV/38az/9/Gs//fxrP/38az/9/Gs//fxrP/38az/9/Gs////9/////f////3////9//3//P/9//z//f/8//3//P/9/////f////3////9/////f/8//3//P/9//z//f/8//3////9/////f////3////9/////f////3////9/////f////3////9/////f////3/+//9/////f////3////9/////f/7//3/+//9//v//f/7//3/+//9/AAD/fwAA/38AAP9/AAD/fwAA/38AAP9/AAD/fwAA/38AAP9/AAD/f////3////9/////f////3////9/////f////3///////3/+//9/////f////3/+//9/////f/7//3////9/AAD/fwAA/38AAP9/AAD/f/9//z//f/8//3//P/9//z////9/////f////3////9//3////9/////f////3////9/////fwAA/38AAP9/AAD/fwAA/38AAP9/////f////3////9/////f////3////9/////f////3////9/////f////3////9/////v/+//7//vyXR2a4l0dmu2a7ZLv+//z/Zrtku/7//P/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//v/+/2a4l0f+//7/ZriXRDJvy5OOOGvEMm/Lk444a8f///3////9/8uTyZPLk8mTy5PJk8uTyZCXRJVEl0SVRDJsMG9mu2S4Mmwwb2a7ZLuOO5A4Mmwwb447kDgybDBsl0SVRJdElUf+//z//v/8/2a4l0Qyb8uTZriXRDJvy5CXR2a4l0dmu8uQMm/LkDJvy5Ayb8uQMm////3////9/")
|
||||
}, {
|
||||
"aabb": AABB(-0.03, -0.12, -0.075, 0.06, 0.24, 0.15),
|
||||
"attribute_data": PackedByteArray("MrMGkTKzrqsQkQaREJGuq+5uBpHubq6rzEwGkcxMrqvMTCGmzEy+bu5uIabubr5u7m6+bu5u+L3MTL5uzEz4vTKzluUQkZblMrPuTxCR7k8ys75uMrP4vRCRvm4Qkfi97m6W5cxMluXubu5PzEzuTzKzqtkQkarZMrPuTxCR7k/ubqrZzEyq2e5u7k/MTO5P///86cvM7k///4U1AACFNTMz7k8zM5bly8yq2aqq//+TnpblAAD//wAAhTUzM6rZAAD86VRV//9rYZbl/////8vMluXLzO5PMzPuT///hTUys/zpzEz86TKzhTXMTIU1MrP//8xM//8ys4U1zEyFNcxMVVTubvi9zExh2DKzYdgQkfi9EJG+bu5uvm4ys1VUzEzqscxMtIAys+qxMrO0gDKzVVQys12szExVVMxMXawQkSGmEJG+bjKzIaYys75uy8zuT8vMqtkzM+5Pk56W5TMzluUzM6rZMzPuT2thluXLzO5Py8yW5RCRhTXuboU1e5A+M4NvPjPHjh8xN3EfMe5zTC8QjEwvh4jmLXd35i1qhAUtlHsFLf9/uCzLzI5cMzOOXMvMQlozM0JaMzOuSjMzYkjLzK5Ky8xiSBCRhTXuboU1e5A+M4NvPjPHjh8xN3EfMRCMTC/uc0wvh4jmLXd35i1qhAUtlHsFLf9/uCzLzGJIy8yuSjMzYkgzM65Ky8wzNcvMgDczMzM1MzOANzMzkYjLzJGIMzPeisvM3oozMyZwy8wmcDMzc3LLzHNyMzMLPjMzvjvLzAs+y8y+OzMzgDczMzM1y8yAN8vMMzUzM0Jay8xCWjMzjlzLzI5cy8y+O8vMCz4zM747MzMLPsvMc3IzM3Nyy8wmcDMzJnDLzN6KMzPeisvMkYgzM5GI"),
|
||||
"format": 34896613399,
|
||||
"index_count": 18,
|
||||
"index_data": PackedByteArray("VgBUAFUAVQBXAFYAVwBYAFYAWwBZAFoAWgBcAFsAXABdAFsA"),
|
||||
"material": SubResource("StandardMaterial3D_8qvhc"),
|
||||
"name": "darkMetal",
|
||||
"primitive": 3,
|
||||
"uv_scale": Vector4(5.90551, 11.4488, 0, 0),
|
||||
"vertex_count": 168,
|
||||
"vertex_data": PackedByteArray("AABxLszM9eAAAP8fk5714FVVcS7MzPXgVVX/H5Oe9eCpqnEuzMz14Kmq/x+TnvXg//9xLszM9eD///8fk5714AAA/x+TnlTVAAD/HzMzVNVVVf8fk55U1VVV/x8zM1TVqapU1TMzVNWpqlTVzMxU1f//VNUzM1TV//9U1czMVNX///8fMzP//6mq/x8zM/////9U1TMz//+pqlTVMzP//wAAVNUzM1TVAABU1czMVNVVVVTVMzNU1VVVVNXMzFTVVVX/HzMz//8AAP8fMzP//1VVVNUzM///AABU1TMz//8AAHEuzMz//1VVcS7MzP//AABU1czM//9VVVTVzMz//6mqcS7MzP////9xLszM//+pqlTVzMz/////VNXMzP//AACqGv//VNUAAFTVzMxU1QAAVPX//1TVAABU9QAAVNUAAFTVMzNU1QAA/x8zM1TVAABxLszMVNUAAAAAqapU1QAA/x+TnlTVAAAAAAAAVNX//1T1//9U1f//cS7MzFTV//+qGv//VNX//wAAqapU1f///x+TnlTV//8AAAAAVNX///8fMzNU1f//VNUzM1TV//9U1czMVNX//1T1AABU1f//qhr/////AACqGv///////1T1/////wAAVPX/////AAAAAAAA/////wAAAAD//wAAVPUAAP////9U9QAA//8AAFT1AABU1VVVVPXMzFTVAABU9f//VNX//1T1//9U1amqVPXMzFTVqapU9TMzVNVVVVT1MzNU1f//VPUAAFTVAAAAAKmqaMsAAKoa//9oy///AACpqmjL//+qGv//aMsAAAAAAABU1QAAAACpqlTV//8AAAAAVNX//wAAqapU1amq/x+TnlTVqar/HzMzVNX///8fk55U1f///x8zM1TVVVVU1czM/79VVXEuzMz/v1VVVNUzM/+/VVX/H5Oe/79VVf8fMzP/v6mqcS7MzP+/qapU1czM/7+pqv8fk57/v6mqVNUzM/+/qar/HzMz/79VVVT1MzP//6mqVPUzM///yVYX+DMz//81qRf4MzP//wxbqfozM///8qSp+jMz//8qnt/8MzP//9Rh3/wzM///qmqR/jMz//9UlZH+MzP///R0of8zM///Couh/zMz////f///MzP//9Rh3/zMzFTV1GHf/DMzVNWqapH+zMxU1apqkf4zM1TV8qSp+szMVNUqnt/8zMxU1fKkqfozM1TVKp7f/DMzVNWpqlT1zMz//1VVVPXMzP//NakX+MzM///JVhf4zMz///KkqfrMzP//DFup+szM//8qnt/8zMz//9Rh3/zMzP//VJWR/szM//+qapH+zMz//wqLof/MzP//9HSh/8zM////f///zMz//9Rh3/zMzFTVDFup+szMVNXUYd/8MzNU1QxbqfozM1TVyVYX+MzMVNVVVVT1zMxU1clWF/gzM1TVVVVU9TMzVNX/f///zMxU1f9///8zM1TVCouh/8zMVNUKi6H/MzNU1QqLof/MzFTVCouh/zMzVNVUlZH+zMxU1VSVkf4zM1TVNakX+MzMVNXypKn6zMxU1TWpF/gzM1TV8qSp+jMzVNWpqlT1zMxU1TWpF/jMzFTVqapU9TMzVNU1qRf4MzNU1VSVkf7MzFTVVJWR/jMzVNUqnt/8zMxU1Sqe3/wzM1TVDFup+szMVNXJVhf4zMxU1QxbqfozM1TVyVYX+DMzVNWqapH+zMxU1apqkf4zM1TV9HSh/8zMVNX0dKH/MzNU1fR0of/MzFTV9HSh/zMzVNX/f///zMxU1f9///8zM1TV////f////3////9/////f////3////9/////f////3////9/////f////3////9/AAD/fwAA/38AAP9/AAD/f/9//3//f/9//3//f/9//38AAP9/AAD/fwAA/38AAP9//3//f/9//3//f/9//3//f/9/////f////3////9/////f////3////9/////f////3////9/////f////3////9/////f////3////9/////f////3////9/AAD/fwAA/38AAP9/AAD/fwAA/38AAP9/AAD/fwAA/38AAP9/AAD/f/9//3//f/9//3//f/9//3////9/////f////3///////3////9/////f////3////9/////f////3////9/AAD/fwAA/38AAP9/AAD/fwAA/38AAP9/AAD/fwAA/3////9/////f////3////9//3////9//v//f////3/+//9/////fwAA/38AAP9/AAD/fwAA/38AAP9/////f////3////9/////f////3////9/////f////3////9/////f////3////9/////v/+//7//vyXR2a4l0dmu2a7ZLv+//z/Zrtku/7//P/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//v/+/2a4l0f+//7/ZriXRDJvy5OOOGvEMm/Lk444a8f///3////9/8uTyZPLk8mTy5PJk8uTyZCXRJVEl0SVRDJsMG9mu2S4Mmwwb2a7ZLuOO5A4Mmwwb447kDgybDBsl0SVRJdElUf+//z//v/8/2a4l0Qyb8uTZriXRDJvy5CXR2a4l0dmu8uQMm/LkDJvy5Ayb8uQMm////3////9/")
|
||||
}, {
|
||||
"aabb": AABB(-0.03, -0.12, -0.075, 0.06, 0.24, 0.15),
|
||||
"attribute_data": PackedByteArray("MrMGkTKzrqsQkQaREJGuq+5uBpHubq6rzEwGkcxMrqvMTCGmzEy+bu5uIabubr5u7m6+bu5u+L3MTL5uzEz4vTKzluUQkZblMrPuTxCR7k8ys75uMrP4vRCRvm4Qkfi97m6W5cxMluXubu5PzEzuTzKzqtkQkarZMrPuTxCR7k/ubqrZzEyq2e5u7k/MTO5P///86cvM7k///4U1AACFNTMz7k8zM5bly8yq2aqq//+TnpblAAD//wAAhTUzM6rZAAD86VRV//9rYZbl/////8vMluXLzO5PMzPuT///hTUys/zpzEz86TKzhTXMTIU1MrP//8xM//8ys4U1zEyFNcxMVVTubvi9zExh2DKzYdgQkfi9EJG+bu5uvm4ys1VUzEzqscxMtIAys+qxMrO0gDKzVVQys12szExVVMxMXawQkSGmEJG+bjKzIaYys75uy8zuT8vMqtkzM+5Pk56W5TMzluUzM6rZMzPuT2thluXLzO5Py8yW5RCRhTXuboU1e5A+M4NvPjPHjh8xN3EfMe5zTC8QjEwvh4jmLXd35i1qhAUtlHsFLf9/uCzLzI5cMzOOXMvMQlozM0JaMzOuSjMzYkjLzK5Ky8xiSBCRhTXuboU1e5A+M4NvPjPHjh8xN3EfMRCMTC/uc0wvh4jmLXd35i1qhAUtlHsFLf9/uCzLzGJIy8yuSjMzYkgzM65Ky8wzNcvMgDczMzM1MzOANzMzkYjLzJGIMzPeisvM3oozMyZwy8wmcDMzc3LLzHNyMzMLPjMzvjvLzAs+y8y+OzMzgDczMzM1y8yAN8vMMzUzM0Jay8xCWjMzjlzLzI5cy8y+O8vMCz4zM747MzMLPsvMc3IzM3Nyy8wmcDMzJnDLzN6KMzPeisvMkYgzM5GI"),
|
||||
"format": 34896613399,
|
||||
"index_count": 138,
|
||||
"index_data": PackedByteArray("YABeAF8AXwBhAGAAYQBiAGAAYQBjAGIAYwBkAGIAZABlAGIAZABmAGUAZABnAGYAZwBoAGYAZwBpAGgAaQBqAGgAbQBrAGwAbABuAG0AcQBvAHAAcAByAHEAdQBzAHQAdAB2AHUAdgB3AHUAdgB4AHcAeAB5AHcAeAB6AHkAegB7AHkAegB8AHsAfAB9AHsAfAB+AH0AfgB/AH0AggCAAIEAgQCDAIIAhgCEAIUAhQCHAIYAigCIAIkAiQCLAIoAjgCMAI0AjQCPAI4AkgCQAJEAkQCTAJIAlgCUAJUAlQCXAJYAmgCYAJkAmQCbAJoAngCcAJ0AnQCfAJ4AogCgAKEAoQCjAKIApgCkAKUApQCnAKYA"),
|
||||
"material": SubResource("StandardMaterial3D_mihsn"),
|
||||
"name": "yellow",
|
||||
"primitive": 3,
|
||||
"uv_scale": Vector4(5.90551, 11.4488, 0, 0),
|
||||
"vertex_count": 168,
|
||||
"vertex_data": PackedByteArray("AABxLszM9eAAAP8fk5714FVVcS7MzPXgVVX/H5Oe9eCpqnEuzMz14Kmq/x+TnvXg//9xLszM9eD///8fk5714AAA/x+TnlTVAAD/HzMzVNVVVf8fk55U1VVV/x8zM1TVqapU1TMzVNWpqlTVzMxU1f//VNUzM1TV//9U1czMVNX///8fMzP//6mq/x8zM/////9U1TMz//+pqlTVMzP//wAAVNUzM1TVAABU1czMVNVVVVTVMzNU1VVVVNXMzFTVVVX/HzMz//8AAP8fMzP//1VVVNUzM///AABU1TMz//8AAHEuzMz//1VVcS7MzP//AABU1czM//9VVVTVzMz//6mqcS7MzP////9xLszM//+pqlTVzMz/////VNXMzP//AACqGv//VNUAAFTVzMxU1QAAVPX//1TVAABU9QAAVNUAAFTVMzNU1QAA/x8zM1TVAABxLszMVNUAAAAAqapU1QAA/x+TnlTVAAAAAAAAVNX//1T1//9U1f//cS7MzFTV//+qGv//VNX//wAAqapU1f///x+TnlTV//8AAAAAVNX///8fMzNU1f//VNUzM1TV//9U1czMVNX//1T1AABU1f//qhr/////AACqGv///////1T1/////wAAVPX/////AAAAAAAA/////wAAAAD//wAAVPUAAP////9U9QAA//8AAFT1AABU1VVVVPXMzFTVAABU9f//VNX//1T1//9U1amqVPXMzFTVqapU9TMzVNVVVVT1MzNU1f//VPUAAFTVAAAAAKmqaMsAAKoa//9oy///AACpqmjL//+qGv//aMsAAAAAAABU1QAAAACpqlTV//8AAAAAVNX//wAAqapU1amq/x+TnlTVqar/HzMzVNX///8fk55U1f///x8zM1TVVVVU1czMVNVVVXEuzMxU1VVVVNUzM1TVVVX/H5OeVNVVVf8fMzNU1amqcS7MzFTVqapU1czMVNWpqv8fk55U1amqVNUzM1TVqar/HzMzVNVVVVT1MzP//6mqVPUzM///yVYX+DMz//81qRf4MzP//wxbqfozM///8qSp+jMz//8qnt/8MzP//9Rh3/wzM///qmqR/jMz//9UlZH+MzP///R0of8zM///Couh/zMz////f///MzP//9Rh3/zMzPzF1GHf/DMz/MWqapH+zMxKyqpqkf4zM0rK8qSp+szMusIqnt/8zMz8xfKkqfozM7rCKp7f/DMz/MWpqlT1zMz/f1VVVPXMzP9/NakX+MzM/3/JVhf4zMz/f/KkqfrMzP9/DFup+szM/38qnt/8zMz/f9Rh3/zMzP9/VJWR/szM/3+qapH+zMz/fwqLof/MzP9/9HSh/8zM/3//f///zMz/f9Rh3/zMzPzFDFup+szMusLUYd/8MzP8xQxbqfozM7rCyVYX+MzMsMBVVVT1zMwrwMlWF/gzM7DAVVVU9TMzK8D/f///zMxU1f9///8zM1TVCouh/8zMd88Ki6H/MzN3zwqLof/MzHfPCouh/zMzd89UlZH+zMxKylSVkf4zM0rKNakX+MzMsMDypKn6zMy6wjWpF/gzM7DA8qSp+jMzusKpqlT1zMwrwDWpF/jMzLDAqapU9TMzK8A1qRf4MzOwwFSVkf7MzErKVJWR/jMzSsoqnt/8zMz8xSqe3/wzM/zFDFup+szMusLJVhf4zMywwAxbqfozM7rCyVYX+DMzsMCqapH+zMxKyqpqkf4zM0rK9HSh/8zMd8/0dKH/MzN3z/R0of/MzHfP9HSh/zMzd8//f///zMxU1f9///8zM1TV////f////3////9/////f////3////9/////f////3////9/////f////3////9/AAD/fwAA/38AAP9/AAD/f/9//3//f/9//3//f/9//38AAP9/AAD/fwAA/38AAP9//3//f/9//3//f/9//3//f/9/////f////3////9/////f////3////9/////f////3////9/////f////3////9/////f////3////9/////f////3////9/AAD/fwAA/38AAP9/AAD/fwAA/38AAP9/AAD/fwAA/38AAP9/AAD/f/9//3//f/9//3//f/9//3////9/////f////3///////3////9/////f////3////9/////f////3////9/AAD/fwAA/38AAP9/AAD/fwAA/38AAP9/AAD/fwAA/3////9/////f////3////9//3////9/////f////3////9/////fwAA/38AAP9/AAD/fwAA/38AAP9/////f////3////9/////f////3////9/////f////3////9/////f////3////9////+nADG/pwAxkuiZrtLoma7qKxUFv25/xyorFQW/bn/HP9/////f////3////9/////f////3////9/////f////3////9/////f////3////9////+nADGVJZV0/6cAMZUllXTVo1R5WqHKfFWjVHlaocp8VTVqipU1aoqf82/Jn/NvyZ/zb8mf82/JpfETCKXxEwirZpWDaisVBatmlYNqKxUFtWOagetmlYN1Y5qB62aVg2XxEwil8RMIv25/xz9uf8cVJZV01aNUeVUllXTVo1R5UuiZrtLoma7v6Z/sr+mf7K/pn+yv6Z/sqqqqqqqqqqq")
|
||||
}]
|
||||
blend_shape_mode = 0
|
||||
shadow_mesh = SubResource("ArrayMesh_68l5q")
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_g84k4"]
|
||||
size = Vector3(0.166504, 0.159378, 0.162319)
|
||||
|
||||
[node name="blasterM_clip" type="RigidBody3D"]
|
||||
collision_layer = 8
|
||||
collision_mask = 5
|
||||
script = ExtResource("1_qqhy0")
|
||||
weapon = SubResource("Resource_jnsso")
|
||||
TYPE = "Ammo"
|
||||
|
||||
[node name="clipA2" type="MeshInstance3D" parent="."]
|
||||
transform = Transform3D(2.76, 0, 0, 0, -0.695, 9.66022e-08, 0, -6.07588e-08, -1.105, 0, 0, 0)
|
||||
mesh = SubResource("ArrayMesh_5ckjg")
|
||||
skeleton = NodePath("")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 5.53727e-05, 0.00519562, 0.00165367)
|
||||
shape = SubResource("BoxShape3D_g84k4")
|
||||
@@ -0,0 +1,123 @@
|
||||
[gd_scene load_steps=11 format=4 uid="uid://cjxvrad2c6vh8"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://deaf6cmuofa0" path="res://client/template/scripts/Weapon_State_Machine/PickUp Weapons/Weapon_Pick_Up.gd" id="1_d1wy0"]
|
||||
[ext_resource type="Script" uid="uid://bw4j1fbww4k8s" path="res://client/template/scripts/Weapon_State_Machine/WeaponSlot.gd" id="2_tnv4c"]
|
||||
[ext_resource type="Resource" uid="uid://c1jg0ifn7yvve" path="res://client/template/scripts/Weapon_State_Machine/Weapon_Resources/blasterN.tres" id="3_x72lj"]
|
||||
|
||||
[sub_resource type="Resource" id="Resource_8ete7"]
|
||||
resource_local_to_scene = true
|
||||
script = ExtResource("2_tnv4c")
|
||||
weapon = ExtResource("3_x72lj")
|
||||
current_ammo = 5
|
||||
reserve_ammo = 0
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_n3ik4"]
|
||||
resource_name = "metal"
|
||||
vertex_color_use_as_albedo = true
|
||||
albedo_color = Color(0.904365, 0.914141, 0.944534, 1)
|
||||
metallic = 1.0
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_fil5e"]
|
||||
resource_name = "darkMetal"
|
||||
vertex_color_use_as_albedo = true
|
||||
albedo_color = Color(0.793075, 0.81829, 0.89044, 1)
|
||||
metallic = 1.0
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_bfdxb"]
|
||||
resource_name = "yellow"
|
||||
vertex_color_use_as_albedo = true
|
||||
albedo_color = Color(0.998274, 0.894448, 0.505969, 1)
|
||||
metallic = 1.0
|
||||
|
||||
[sub_resource type="ArrayMesh" id="ArrayMesh_j3k8o"]
|
||||
_surfaces = [{
|
||||
"aabb": AABB(-0.03, -0.12, -0.075, 0.06, 0.24, 0.15),
|
||||
"format": 34359742465,
|
||||
"index_count": 168,
|
||||
"index_data": PackedByteArray("AgAAAAEAAQADAAIABgAEAAUABQAHAAYAAwABAAgACAAJAAMADAAKAAsACwANAAwADAAOAA8ADwAKAAwAEgAQABEAEQATABIAEgAJAAgACAAQABIAEQAAAAIAAgATABEACwAEAAYABgANAAsAFQAUABEAEQAWABUAEQAQABYAEAAIABYAAAARABQAFAAXAAAAFwABAAAAFwAYAAEAGAAIAAEAGAAWAAgAGgAZAAYABgAbABoABgAHABsABwAcABsABwAOABwADgAMABwADQAGABkAGQAdAA0AHQAMAA0AHQAcAAwAGQAaABQAFAAVABkAFgAYABwAHAAdABYAFQAWAB4AHgAZABUAHgAfABkAHwAgABkAIQAeABYAFgAdACEAHQAgACEAHQAZACAAGwAXABQAFAAaABsAHAAYABcAFwAbABwABwAFAA8ADwAOAAcA"),
|
||||
"name": "metal",
|
||||
"primitive": 3,
|
||||
"uv_scale": Vector4(0, 0, 0, 0),
|
||||
"vertex_count": 56,
|
||||
"vertex_data": PackedByteArray("kML1vIaWnL3rUTg9kML1vOtRuL2wyJI8CtcjvIaWnL3rUTg9CtcjvOtRuL2wyJI8CtcjPIaWnL3rUTg9CtcjPOtRuL2wyJI8kML1PIaWnL3rUTg9kML1POtRuL2wyJI8kML1vOtRuL3rUTi9CtcjvOtRuL3rUTi9CtcjPArXoz3rUTi9CtcjPArXoz3rUTg9kML1PArXoz3rUTi9kML1PArXoz3rUTg9kML1POtRuL3rUTi9CtcjPOtRuL3rUTi9kML1vArXoz3rUTi9kML1vArXoz3rUTg9CtcjvArXoz3rUTi9CtcjvArXoz3rUTg9kML1vFyPwr2amZk9kML1vK1H4T2amZk9kML1vK1H4T2amZm9kML1vJDC9b3NzMw8kML1vJDC9b2amZm9kML1PK1H4T2amZk9kML1PFyPwr2amZk9kML1PJDC9b3NzMw8kML1PJDC9b2amZm9kML1PK1H4T2amZm9CtcjvK1H4T3rUTg9CtcjPK1H4T3rUTg9CtcjPK1H4T3rUTi9CtcjvK1H4T3rUTi93kEevKKU5j3rUTi93kEePKKU5j3rUTi9vOMNvB+F6z3rUTi9vOMNPB+F6z3rUTi9a7TnO/TC7z3rUTi9a7Tnu/TC7z3rUTi9CtejuyYE8z3rUTi9CtejOyYE8z3rUTi9oZ4pu+kP9T3rUTi9oZ4pO+kP9T3rUTi9AAAAAJDC9T3rUTi9a7Tnu/TC7z3rUTg9CtejuyYE8z3rUTg9vOMNPB+F6z3rUTg9a7TnO/TC7z3rUTg93kEePKKU5j3rUTg93kEevKKU5j3rUTg9vOMNvB+F6z3rUTg9CtejOyYE8z3rUTg9oZ4pO+kP9T3rUTg9oZ4pu+kP9T3rUTg9AAAAAJDC9T3rUTg9")
|
||||
}, {
|
||||
"aabb": AABB(-0.03, -0.12, -0.075, 0.06, 0.24, 0.15),
|
||||
"format": 34359742465,
|
||||
"index_count": 18,
|
||||
"index_data": PackedByteArray("EgATAAIAAgADABIAAwAJABIABQAEAAsACwAKAAUACgAPAAUA"),
|
||||
"name": "darkMetal",
|
||||
"primitive": 3,
|
||||
"uv_scale": Vector4(0, 0, 0, 0),
|
||||
"vertex_count": 56,
|
||||
"vertex_data": PackedByteArray("kML1vIaWnL3rUTg9kML1vOtRuL2wyJI8CtcjvIaWnL3rUTg9CtcjvOtRuL2wyJI8CtcjPIaWnL3rUTg9CtcjPOtRuL2wyJI8kML1PIaWnL3rUTg9kML1POtRuL2wyJI8kML1vOtRuL3rUTi9CtcjvOtRuL3rUTi9CtcjPArXoz3rUTi9CtcjPArXoz3rUTg9kML1PArXoz3rUTi9kML1PArXoz3rUTg9kML1POtRuL3rUTi9CtcjPOtRuL3rUTi9kML1vArXoz3rUTi9kML1vArXoz3rUTg9CtcjvArXoz3rUTi9CtcjvArXoz3rUTg9kML1vFyPwr2amZk9kML1vK1H4T2amZk9kML1vK1H4T2amZm9kML1vJDC9b3NzMw8kML1vJDC9b2amZm9kML1PK1H4T2amZk9kML1PFyPwr2amZk9kML1PJDC9b3NzMw8kML1PJDC9b2amZm9kML1PK1H4T2amZm9CtcjvK1H4T3rUTg9CtcjPK1H4T3rUTg9CtcjPK1H4T3rUTi9CtcjvK1H4T3rUTi93kEevKKU5j3rUTi93kEePKKU5j3rUTi9vOMNvB+F6z3rUTi9vOMNPB+F6z3rUTi9a7TnO/TC7z3rUTi9a7Tnu/TC7z3rUTi9CtejuyYE8z3rUTi9CtejOyYE8z3rUTi9oZ4pu+kP9T3rUTi9oZ4pO+kP9T3rUTi9AAAAAJDC9T3rUTi9a7Tnu/TC7z3rUTg9CtejuyYE8z3rUTg9vOMNPB+F6z3rUTg9a7TnO/TC7z3rUTg93kEePKKU5j3rUTg93kEevKKU5j3rUTg9vOMNvB+F6z3rUTg9CtejOyYE8z3rUTg9oZ4pO+kP9T3rUTg9oZ4pu+kP9T3rUTg9AAAAAJDC9T3rUTg9")
|
||||
}, {
|
||||
"aabb": AABB(-0.03, -0.12, -0.075, 0.06, 0.24, 0.15),
|
||||
"format": 34359742465,
|
||||
"index_count": 138,
|
||||
"index_data": PackedByteArray("IgAhACAAIAAjACIAIwAkACIAIwAlACQAJQAmACQAJgAnACQAJgAoACcAJgApACgAKQAqACgAKQArACoAKwAsACoALgAtACcAJwAoAC4AJQAvADAAMAAmACUAMQAfAB4AHgAyADEAMgAvADEAMgAzAC8AMwAwAC8AMwAtADAALQA0ADAALQAuADQALgA1ADQALgA2ADUANgA3ADUAJwAtADMAMwAkACcAIgAyAB4AHgAhACIANQA3ACwALAArADUANAA1ACsAKwApADQAIwAxAC8ALwAlACMAIAAfADEAMQAjACAAMAA0ACkAKQAmADAAJAAzADIAMgAiACQANgAuACgAKAAqADYANwA2ACoAKgAsADcA"),
|
||||
"name": "yellow",
|
||||
"primitive": 3,
|
||||
"uv_scale": Vector4(0, 0, 0, 0),
|
||||
"vertex_count": 56,
|
||||
"vertex_data": PackedByteArray("kML1vIaWnL3rUTg9kML1vOtRuL2wyJI8CtcjvIaWnL3rUTg9CtcjvOtRuL2wyJI8CtcjPIaWnL3rUTg9CtcjPOtRuL2wyJI8kML1PIaWnL3rUTg9kML1POtRuL2wyJI8kML1vOtRuL3rUTi9CtcjvOtRuL3rUTi9CtcjPArXoz3rUTi9CtcjPArXoz3rUTg9kML1PArXoz3rUTi9kML1PArXoz3rUTg9kML1POtRuL3rUTi9CtcjPOtRuL3rUTi9kML1vArXoz3rUTi9kML1vArXoz3rUTg9CtcjvArXoz3rUTi9CtcjvArXoz3rUTg9kML1vFyPwr2amZk9kML1vK1H4T2amZk9kML1vK1H4T2amZm9kML1vJDC9b3NzMw8kML1vJDC9b2amZm9kML1PK1H4T2amZk9kML1PFyPwr2amZk9kML1PJDC9b3NzMw8kML1PJDC9b2amZm9kML1PK1H4T2amZm9CtcjvK1H4T3rUTg9CtcjPK1H4T3rUTg9CtcjPK1H4T3rUTi9CtcjvK1H4T3rUTi93kEevKKU5j3rUTi93kEePKKU5j3rUTi9vOMNvB+F6z3rUTi9vOMNPB+F6z3rUTi9a7TnO/TC7z3rUTi9a7Tnu/TC7z3rUTi9CtejuyYE8z3rUTi9CtejOyYE8z3rUTi9oZ4pu+kP9T3rUTi9oZ4pO+kP9T3rUTi9AAAAAJDC9T3rUTi9a7Tnu/TC7z3rUTg9CtejuyYE8z3rUTg9vOMNPB+F6z3rUTg9a7TnO/TC7z3rUTg93kEePKKU5j3rUTg93kEevKKU5j3rUTg9vOMNvB+F6z3rUTg9CtejOyYE8z3rUTg9oZ4pO+kP9T3rUTg9oZ4pu+kP9T3rUTg9AAAAAJDC9T3rUTg9")
|
||||
}]
|
||||
blend_shape_mode = 0
|
||||
|
||||
[sub_resource type="ArrayMesh" id="ArrayMesh_py3xv"]
|
||||
resource_name = "clipA_Mesh clipA"
|
||||
_surfaces = [{
|
||||
"aabb": AABB(-0.03, -0.12, -0.075, 0.06, 0.24, 0.15),
|
||||
"attribute_data": PackedByteArray("XS6XP0T3Qj9dLpc/dxH6PyaTyT5E90I/JpPJPncR+j8mk8m+RPdCPyaTyb53Efo/XS6Xv0T3Qj9dLpe/dxH6P10ul7+US9o/XS6XvxaLRb8mk8m+lEvaPyaTyb4Wi0W/JpPJvhaLRb8mk8m+xmIxQF0ul78Wi0W/XS6Xv8ZiMUBdLpc/xmKRQCaTyT7GYpFAXS6XPyaTCcAmk8k+JpMJwF0ulz8Wi0W/XS6XP8ZiMUAmk8k+FotFvyaTyT7GYjFAJpPJvsZikUBdLpe/xmKRQCaTyb4mkwnAXS6XvyaTCcBdLpc/hlOAQCaTyT6GU4BAXS6XPyaTCcAmk8k+JpMJwCaTyb6GU4BAXS6Xv4ZTgEAmk8m+JpMJwF0ul78mkwnA9Pk8QF6vl0CLxeI/JpMJwPT5PEBUKlXA9Pk8wFQqVcCLxeK/JpMJwIvF4r/GYpFAi8XiP4ZTgEDw93s/XS63QCmXND/GYpFA9Pk8wF0ut0D0+TzAVCpVwIvF4r+GU4BA9Pk8wF6vl0Dw93u/XS63QCmXNL/GYpFA9Pk8QF0ut0CLxeI/xmKRQIvF4j8mkwnAi8XivyaTCcD0+TxAVCpVwF0ulz9er5dAXS6Xv16vl0BdLpc/VCpVwF0ul79UKlXAXS6XP10ut0BdLpe/XS63QF0ulz9UKlXAXS6Xv1QqVcBdLpe/6PP5vyaTyb7GYjFAXS6Xv/T5fEBdLpc/9Pl8QCaTyT7GYjFAJpPJPhaLRb8mk8m+FotFv10ulz/o8/m/XS6Xv+3gDkBdLpe/QIgBPV0ulz/t4A5AXS6XP0CIAT1dLpc/6PP5v10ulz/4+/0/XS6Xv+jz+b9dLpe/+Pv9PyaTyT6US9o/JpPJPhaLRb9dLpc/lEvaP10ulz8Wi0W/i8XiPyaTCcCLxeI/hlOAQIvF4r8mkwnAKZc0P8ZikUCLxeK/xmKRQIvF4r+GU4BAi8XivyaTCcAplzS/xmKRQIvF4j8mkwnAi8XiP8ZikUAmk8k+VCpVwCaTyb5UKlXA0bTCPtCvW8DRtMK+0K9bwKORrj6Iw2HAo5GuvojDYcDwiI6+cvtmwPCIjj5y+2bAJpNJPor8asAmk0m+ivxqwIOv0D3ugG3Ag6/Qve6AbcDxsKoOulxuwIvF4j+g4Mq/i8Xiv6Dgyr+LxeI/agjYv4vF4r9qCNi/i8Xiv+mZGMCLxeK/zS0fwIvF4j/pmRjAi8XiP80tH8Amk8k+VCpVwCaTyb5UKlXA0bTCPtCvW8DRtMK+0K9bwKORrj6Iw2HAo5GuvojDYcDwiI4+cvtmwPCIjr5y+2bAJpNJPor8asAmk0m+ivxqwIOv0D3ugG3Ag6/Qve6AbcAJafYNulxuwIvF4j/NLR/Ai8XiP+mZGMCLxeK/zS0fwIvF4r/pmRjAi8XiP0AVVsCLxeI/XIFPwIvF4r9AFVbAi8Xiv1yBT8CLxeK/QEXEPovF4j9ARcQ+i8Xiv2Dk+D6LxeI/YOT4PovF4r/4azW/i8XiP/hrNb+LxeK/aBwbv4vF4j9oHBu/i8Xiv0/HPMCLxeK/MltDwIvF4j9PxzzAi8XiPzJbQ8CLxeK/XIFPwIvF4r9AFVbAi8XiP1yBT8CLxeI/QBVWwIvF4r9qCNi/i8XiP2oI2L+LxeK/oODKv4vF4j+g4Mq/i8XiPzJbQ8CLxeI/T8c8wIvF4r8yW0PAi8Xiv0/HPMCLxeI/aBwbv4vF4r9oHBu/i8XiP/hrNb+LxeK/+Gs1v4vF4j9g5Pg+i8Xiv2Dk+D6LxeI/QEXEPovF4r9ARcQ+"),
|
||||
"format": 34359742487,
|
||||
"index_count": 168,
|
||||
"index_data": PackedByteArray("AgAAAAEAAQADAAIABgAEAAUABQAHAAYACgAIAAkACQALAAoADgAMAA0ADQAPAA4AEgAQABEAEQATABIAFgAUABUAFQAXABYAGgAYABkAGQAbABoAHgAcAB0AHQAfAB4AIgAgACEAIQAjACIAJgAkACUAJQAnACYAJQAoACcAKAApACcAKgAlACQAJAArACoAKwAsACoAKwAtACwALQApACwALQAnACkAMAAuAC8ALwAxADAALwAyADEAMgAzADEAMgA0ADMANAA1ADMANgAvAC4ALgA3ADYANwA1ADYANwAzADUAOgA4ADkAOQA7ADoAPgA8AD0APQA/AD4AQgBAAEEAQQBDAEIAQQBEAEMARABFAEMARgBBAEAAQABHAEYARwBFAEYARwBDAEUASgBIAEkASQBLAEoATgBMAE0ATQBPAE4AUgBQAFEAUQBTAFIA"),
|
||||
"material": SubResource("StandardMaterial3D_n3ik4"),
|
||||
"name": "metal",
|
||||
"primitive": 3,
|
||||
"uv_scale": Vector4(0, 0, 0, 0),
|
||||
"vertex_count": 168,
|
||||
"vertex_data": PackedByteArray("kML1vIaWnL3rUTg9kML1vOtRuL2wyJI8CtcjvIaWnL3rUTg9CtcjvOtRuL2wyJI8CtcjPIaWnL3rUTg9CtcjPOtRuL2wyJI8kML1PIaWnL3rUTg9kML1POtRuL2wyJI8kML1vOtRuL2wyJI8kML1vOtRuL3rUTi9CtcjvOtRuL2wyJI8CtcjvOtRuL3rUTi9CtcjPArXoz3rUTi9CtcjPArXoz3rUTg9kML1PArXoz3rUTi9kML1PArXoz3rUTg9kML1POtRuL3rUTi9CtcjPOtRuL3rUTi9kML1PArXoz3rUTi9CtcjPArXoz3rUTi9kML1vArXoz3rUTi9kML1vArXoz3rUTg9CtcjvArXoz3rUTi9CtcjvArXoz3rUTg9CtcjvOtRuL3rUTi9kML1vOtRuL3rUTi9CtcjvArXoz3rUTi9kML1vArXoz3rUTi9kML1vIaWnL3rUTg9CtcjvIaWnL3rUTg9kML1vArXoz3rUTg9CtcjvArXoz3rUTg9CtcjPIaWnL3rUTg9kML1PIaWnL3rUTg9CtcjPArXoz3rUTg9kML1PArXoz3rUTg9kML1vFyPwr2amZk9kML1vArXoz3rUTg9kML1vK1H4T2amZk9kML1vK1H4T2amZm9kML1vArXoz3rUTi9kML1vOtRuL3rUTi9kML1vIaWnL3rUTg9kML1vJDC9b3NzMw8kML1vOtRuL2wyJI8kML1vJDC9b2amZm9kML1PK1H4T2amZk9kML1PIaWnL3rUTg9kML1PFyPwr2amZk9kML1PJDC9b3NzMw8kML1POtRuL2wyJI8kML1PJDC9b2amZm9kML1POtRuL3rUTi9kML1PArXoz3rUTi9kML1PArXoz3rUTg9kML1PK1H4T2amZm9kML1PFyPwr2amZk9kML1vFyPwr2amZk9kML1PK1H4T2amZk9kML1vK1H4T2amZk9kML1vJDC9b2amZm9kML1PJDC9b2amZm9kML1vK1H4T2amZm9kML1PK1H4T2amZm9kML1vK1H4T2amZm9CtcjvK1H4T3rUTg9kML1vK1H4T2amZk9kML1PK1H4T2amZk9CtcjPK1H4T3rUTg9CtcjPK1H4T3rUTi9CtcjvK1H4T3rUTi9kML1PK1H4T2amZm9kML1vJDC9b3NzMw8kML1vFyPwr2amZk9kML1PJDC9b3NzMw8kML1PFyPwr2amZk9kML1vJDC9b2amZm9kML1vJDC9b3NzMw8kML1PJDC9b2amZm9kML1PJDC9b3NzMw8CtcjPOtRuL2wyJI8CtcjPOtRuL3rUTi9kML1POtRuL2wyJI8kML1POtRuL3rUTi9CtcjvArXoz3rUTg9CtcjvIaWnL3rUTg9CtcjvArXoz3rUTi9CtcjvOtRuL2wyJI8CtcjvOtRuL3rUTi9CtcjPIaWnL3rUTg9CtcjPArXoz3rUTg9CtcjPOtRuL2wyJI8CtcjPArXoz3rUTi9CtcjPOtRuL3rUTi9CtcjvK1H4T3rUTi9CtcjPK1H4T3rUTi93kEevKKU5j3rUTi93kEePKKU5j3rUTi9vOMNvB+F6z3rUTi9vOMNPB+F6z3rUTi9a7TnO/TC7z3rUTi9a7Tnu/TC7z3rUTi9CtejuyYE8z3rUTi9CtejOyYE8z3rUTi9oZ4pu+kP9T3rUTi9oZ4pO+kP9T3rUTi9AAAAAJDC9T3rUTi9a7Tnu/TC7z3rUTg9a7Tnu/TC7z3rUTi9CtejuyYE8z3rUTg9CtejuyYE8z3rUTi9vOMNPB+F6z3rUTg9a7TnO/TC7z3rUTg9vOMNPB+F6z3rUTi9a7TnO/TC7z3rUTi9CtcjPK1H4T3rUTg9CtcjvK1H4T3rUTg93kEePKKU5j3rUTg93kEevKKU5j3rUTg9vOMNPB+F6z3rUTg9vOMNvB+F6z3rUTg9a7TnO/TC7z3rUTg9a7Tnu/TC7z3rUTg9CtejOyYE8z3rUTg9CtejuyYE8z3rUTg9oZ4pO+kP9T3rUTg9oZ4pu+kP9T3rUTg9AAAAAJDC9T3rUTg9a7Tnu/TC7z3rUTg9vOMNvB+F6z3rUTg9a7Tnu/TC7z3rUTi9vOMNvB+F6z3rUTi93kEevKKU5j3rUTg9CtcjvK1H4T3rUTg93kEevKKU5j3rUTi9CtcjvK1H4T3rUTi9AAAAAJDC9T3rUTg9AAAAAJDC9T3rUTi9oZ4pO+kP9T3rUTg9oZ4pO+kP9T3rUTi9oZ4pO+kP9T3rUTg9oZ4pO+kP9T3rUTi9CtejOyYE8z3rUTg9CtejOyYE8z3rUTi93kEePKKU5j3rUTg9vOMNPB+F6z3rUTg93kEePKKU5j3rUTi9vOMNPB+F6z3rUTi9CtcjPK1H4T3rUTg93kEePKKU5j3rUTg9CtcjPK1H4T3rUTi93kEePKKU5j3rUTi9CtejOyYE8z3rUTg9CtejOyYE8z3rUTi9a7TnO/TC7z3rUTg9a7TnO/TC7z3rUTi9vOMNvB+F6z3rUTg93kEevKKU5j3rUTg9vOMNvB+F6z3rUTi93kEevKKU5j3rUTi9CtejuyYE8z3rUTg9CtejuyYE8z3rUTi9oZ4pu+kP9T3rUTg9oZ4pu+kP9T3rUTi9oZ4pu+kP9T3rUTg9oZ4pu+kP9T3rUTi9AAAAAJDC9T3rUTg9AAAAAJDC9T3rUTi9qar//wAA/7+pqv//AAD/v6mq//8AAP+/qar//wAA/7+pqv//AAD/v6mq//8AAP+/qar//wAA/7+pqv//AAD/v/9///////+//3///////7//f///////v/9///////+//38AAAAA/7//fwAAAAD/v/9/AAAAAP+//38AAAAA/7//f/9/////v/9//3////+//3//f////7//f/9/////v/9/AAAAAP+//38AAAAA/7//fwAAAAD/v/9/AAAAAP+//3//f////7//f/9/////v/9//3////+//3//f////7//////AAD/v/////8AAP+//////wAA/7//////AAD/v/////8AAP+//////wAA/7//////AAD/v/////8AAP+/AAD/f/9//78AAP9//3//vwAA/3//f/+/AAD/f/9//78AAP9//3//vwAA/3//f/+/AAD/f/9//78AAP9//3//vwAA/3//f/+/AAD/f/9//7////9//////////3/+////////f/7///////9//v///////3/+////////f/7/AID///9//v8AgP///3///wCA////f/////////9///////9//3////+//3//f////7//f/9/////v/9//3////+//////wAA/7//////AAD/v/////8AAP+//////wAA/7//f///////v/9///////+//3///////7//f////v//v/9////+//+//3///////7//f////v//v/9///////+//3+qKv///7//f6oq////v/9/qir///+//3+qKv///7//fwAAAAD/v/9/AAAAAP+//38AAAAA/7//fwAAAAD/v/9///////+//3///////7//f///////v/9///////+/AAD/fwAAAIAAAP9/AAAAgAAA/38AAACAAAD/fwAAAIAAAP9/AAAAgP///38AAACA////fwAAAID///9/AAAAgP///38AAACA////fwAAAID/////AAAAgP////8AAACA/////wAAAID/////AAAAgP////8AAACA/////wAAAID/////AAAAgP////8AAACA/////wAAAID/////AAAAgP////8AAACA/////wAAAID/////AAAAgP8//78AAACA/z//vwAAAIAlUSXRAAAAgCVRJdEAAACAJdHZrgAAAID/v/+/AAAAgCXR2a4AAACA/7//vwAAAID/f/9/AAAAgP9//38AAACA/3//fwAAAID/f/9/AAAAgP9//38AAACA/3//fwAAAID/f/9/AAAAgP9//38AAACA/3//fwAAAID/f/9/AAAAgP9//38AAACA/3//fwAAAID/f/9/AAAAgP8//78AAACA2S7ZrgAAAID/P/+/AAAAgNku2a4AAACADBsMmwAAAIDkDuOOAAAAgAwbDJsAAACA5A7jjgAAAID/f///AAAAgP9///8AAACADJvy5AAAAIAMm/LkAAAAgAyb8uQAAACADJvy5AAAAIDZriXRAAAAgNmuJdEAAACA8uQMmwAAAIAl0dmuAAAAgPLkDJsAAACAJdHZrgAAAIAa8eOOAAAAgPLkDJsAAACAGvHjjgAAAIDy5AybAAAAgNmuJdEAAACA2a4l0QAAAID/v/+/AAAAgP+//78AAACA2S7ZrgAAAIAMGwybAAAAgNku2a4AAACADBsMmwAAAIAlUSXRAAAAgCVRJdEAAACA8mTy5AAAAIDyZPLkAAAAgPJk8uQAAACA8mTy5AAAAID/f///AAAAgP9///8AAACA")
|
||||
}, {
|
||||
"aabb": AABB(-0.03, -0.12, -0.075, 0.06, 0.24, 0.15),
|
||||
"attribute_data": PackedByteArray("XS6XP0T3Qj9dLpc/dxH6PyaTyT5E90I/JpPJPncR+j8mk8m+RPdCPyaTyb53Efo/XS6Xv0T3Qj9dLpe/dxH6P10ul7+US9o/XS6XvxaLRb8mk8m+lEvaPyaTyb4Wi0W/JpPJvhaLRb8mk8m+xmIxQF0ul78Wi0W/XS6Xv8ZiMUBdLpc/xmKRQCaTyT7GYpFAXS6XPyaTCcAmk8k+JpMJwF0ulz8Wi0W/XS6XP8ZiMUAmk8k+FotFvyaTyT7GYjFAJpPJvsZikUBdLpe/xmKRQCaTyb4mkwnAXS6XvyaTCcBdLpc/hlOAQCaTyT6GU4BAXS6XPyaTCcAmk8k+JpMJwCaTyb6GU4BAXS6Xv4ZTgEAmk8m+JpMJwF0ul78mkwnA9Pk8QF6vl0CLxeI/JpMJwPT5PEBUKlXA9Pk8wFQqVcCLxeK/JpMJwIvF4r/GYpFAi8XiP4ZTgEDw93s/XS63QCmXND/GYpFA9Pk8wF0ut0D0+TzAVCpVwIvF4r+GU4BA9Pk8wF6vl0Dw93u/XS63QCmXNL/GYpFA9Pk8QF0ut0CLxeI/xmKRQIvF4j8mkwnAi8XivyaTCcD0+TxAVCpVwF0ulz9er5dAXS6Xv16vl0BdLpc/VCpVwF0ul79UKlXAXS6XP10ut0BdLpe/XS63QF0ulz9UKlXAXS6Xv1QqVcBdLpe/6PP5vyaTyb7GYjFAXS6Xv/T5fEBdLpc/9Pl8QCaTyT7GYjFAJpPJPhaLRb8mk8m+FotFv10ulz/o8/m/XS6Xv+3gDkBdLpe/QIgBPV0ulz/t4A5AXS6XP0CIAT1dLpc/6PP5v10ulz/4+/0/XS6Xv+jz+b9dLpe/+Pv9PyaTyT6US9o/JpPJPhaLRb9dLpc/lEvaP10ulz8Wi0W/i8XiPyaTCcCLxeI/hlOAQIvF4r8mkwnAKZc0P8ZikUCLxeK/xmKRQIvF4r+GU4BAi8XivyaTCcAplzS/xmKRQIvF4j8mkwnAi8XiP8ZikUAmk8k+VCpVwCaTyb5UKlXA0bTCPtCvW8DRtMK+0K9bwKORrj6Iw2HAo5GuvojDYcDwiI6+cvtmwPCIjj5y+2bAJpNJPor8asAmk0m+ivxqwIOv0D3ugG3Ag6/Qve6AbcDxsKoOulxuwIvF4j+g4Mq/i8Xiv6Dgyr+LxeI/agjYv4vF4r9qCNi/i8Xiv+mZGMCLxeK/zS0fwIvF4j/pmRjAi8XiP80tH8Amk8k+VCpVwCaTyb5UKlXA0bTCPtCvW8DRtMK+0K9bwKORrj6Iw2HAo5GuvojDYcDwiI4+cvtmwPCIjr5y+2bAJpNJPor8asAmk0m+ivxqwIOv0D3ugG3Ag6/Qve6AbcAJafYNulxuwIvF4j/NLR/Ai8XiP+mZGMCLxeK/zS0fwIvF4r/pmRjAi8XiP0AVVsCLxeI/XIFPwIvF4r9AFVbAi8Xiv1yBT8CLxeK/QEXEPovF4j9ARcQ+i8Xiv2Dk+D6LxeI/YOT4PovF4r/4azW/i8XiP/hrNb+LxeK/aBwbv4vF4j9oHBu/i8Xiv0/HPMCLxeK/MltDwIvF4j9PxzzAi8XiPzJbQ8CLxeK/XIFPwIvF4r9AFVbAi8XiP1yBT8CLxeI/QBVWwIvF4r9qCNi/i8XiP2oI2L+LxeK/oODKv4vF4j+g4Mq/i8XiPzJbQ8CLxeI/T8c8wIvF4r8yW0PAi8Xiv0/HPMCLxeI/aBwbv4vF4r9oHBu/i8XiP/hrNb+LxeK/+Gs1v4vF4j9g5Pg+i8Xiv2Dk+D6LxeI/QEXEPovF4r9ARcQ+"),
|
||||
"format": 34359742487,
|
||||
"index_count": 18,
|
||||
"index_data": PackedByteArray("VgBUAFUAVQBXAFYAVwBYAFYAWwBZAFoAWgBcAFsAXABdAFsA"),
|
||||
"material": SubResource("StandardMaterial3D_fil5e"),
|
||||
"name": "darkMetal",
|
||||
"primitive": 3,
|
||||
"uv_scale": Vector4(0, 0, 0, 0),
|
||||
"vertex_count": 168,
|
||||
"vertex_data": PackedByteArray("kML1vIaWnL3rUTg9kML1vOtRuL2wyJI8CtcjvIaWnL3rUTg9CtcjvOtRuL2wyJI8CtcjPIaWnL3rUTg9CtcjPOtRuL2wyJI8kML1PIaWnL3rUTg9kML1POtRuL2wyJI8kML1vOtRuL2wyJI8kML1vOtRuL3rUTi9CtcjvOtRuL2wyJI8CtcjvOtRuL3rUTi9CtcjPArXoz3rUTi9CtcjPArXoz3rUTg9kML1PArXoz3rUTi9kML1PArXoz3rUTg9kML1POtRuL3rUTi9CtcjPOtRuL3rUTi9kML1PArXoz3rUTi9CtcjPArXoz3rUTi9kML1vArXoz3rUTi9kML1vArXoz3rUTg9CtcjvArXoz3rUTi9CtcjvArXoz3rUTg9CtcjvOtRuL3rUTi9kML1vOtRuL3rUTi9CtcjvArXoz3rUTi9kML1vArXoz3rUTi9kML1vIaWnL3rUTg9CtcjvIaWnL3rUTg9kML1vArXoz3rUTg9CtcjvArXoz3rUTg9CtcjPIaWnL3rUTg9kML1PIaWnL3rUTg9CtcjPArXoz3rUTg9kML1PArXoz3rUTg9kML1vFyPwr2amZk9kML1vArXoz3rUTg9kML1vK1H4T2amZk9kML1vK1H4T2amZm9kML1vArXoz3rUTi9kML1vOtRuL3rUTi9kML1vIaWnL3rUTg9kML1vJDC9b3NzMw8kML1vOtRuL2wyJI8kML1vJDC9b2amZm9kML1PK1H4T2amZk9kML1PIaWnL3rUTg9kML1PFyPwr2amZk9kML1PJDC9b3NzMw8kML1POtRuL2wyJI8kML1PJDC9b2amZm9kML1POtRuL3rUTi9kML1PArXoz3rUTi9kML1PArXoz3rUTg9kML1PK1H4T2amZm9kML1PFyPwr2amZk9kML1vFyPwr2amZk9kML1PK1H4T2amZk9kML1vK1H4T2amZk9kML1vJDC9b2amZm9kML1PJDC9b2amZm9kML1vK1H4T2amZm9kML1PK1H4T2amZm9kML1vK1H4T2amZm9CtcjvK1H4T3rUTg9kML1vK1H4T2amZk9kML1PK1H4T2amZk9CtcjPK1H4T3rUTg9CtcjPK1H4T3rUTi9CtcjvK1H4T3rUTi9kML1PK1H4T2amZm9kML1vJDC9b3NzMw8kML1vFyPwr2amZk9kML1PJDC9b3NzMw8kML1PFyPwr2amZk9kML1vJDC9b2amZm9kML1vJDC9b3NzMw8kML1PJDC9b2amZm9kML1PJDC9b3NzMw8CtcjPOtRuL2wyJI8CtcjPOtRuL3rUTi9kML1POtRuL2wyJI8kML1POtRuL3rUTi9CtcjvArXoz3rUTg9CtcjvIaWnL3rUTg9CtcjvArXoz3rUTi9CtcjvOtRuL2wyJI8CtcjvOtRuL3rUTi9CtcjPIaWnL3rUTg9CtcjPArXoz3rUTg9CtcjPOtRuL2wyJI8CtcjPArXoz3rUTi9CtcjPOtRuL3rUTi9CtcjvK1H4T3rUTi9CtcjPK1H4T3rUTi93kEevKKU5j3rUTi93kEePKKU5j3rUTi9vOMNvB+F6z3rUTi9vOMNPB+F6z3rUTi9a7TnO/TC7z3rUTi9a7Tnu/TC7z3rUTi9CtejuyYE8z3rUTi9CtejOyYE8z3rUTi9oZ4pu+kP9T3rUTi9oZ4pO+kP9T3rUTi9AAAAAJDC9T3rUTi9a7Tnu/TC7z3rUTg9a7Tnu/TC7z3rUTi9CtejuyYE8z3rUTg9CtejuyYE8z3rUTi9vOMNPB+F6z3rUTg9a7TnO/TC7z3rUTg9vOMNPB+F6z3rUTi9a7TnO/TC7z3rUTi9CtcjPK1H4T3rUTg9CtcjvK1H4T3rUTg93kEePKKU5j3rUTg93kEevKKU5j3rUTg9vOMNPB+F6z3rUTg9vOMNvB+F6z3rUTg9a7TnO/TC7z3rUTg9a7Tnu/TC7z3rUTg9CtejOyYE8z3rUTg9CtejuyYE8z3rUTg9oZ4pO+kP9T3rUTg9oZ4pu+kP9T3rUTg9AAAAAJDC9T3rUTg9a7Tnu/TC7z3rUTg9vOMNvB+F6z3rUTg9a7Tnu/TC7z3rUTi9vOMNvB+F6z3rUTi93kEevKKU5j3rUTg9CtcjvK1H4T3rUTg93kEevKKU5j3rUTi9CtcjvK1H4T3rUTi9AAAAAJDC9T3rUTg9AAAAAJDC9T3rUTi9oZ4pO+kP9T3rUTg9oZ4pO+kP9T3rUTi9oZ4pO+kP9T3rUTg9oZ4pO+kP9T3rUTi9CtejOyYE8z3rUTg9CtejOyYE8z3rUTi93kEePKKU5j3rUTg9vOMNPB+F6z3rUTg93kEePKKU5j3rUTi9vOMNPB+F6z3rUTi9CtcjPK1H4T3rUTg93kEePKKU5j3rUTg9CtcjPK1H4T3rUTi93kEePKKU5j3rUTi9CtejOyYE8z3rUTg9CtejOyYE8z3rUTi9a7TnO/TC7z3rUTg9a7TnO/TC7z3rUTi9vOMNvB+F6z3rUTg93kEevKKU5j3rUTg9vOMNvB+F6z3rUTi93kEevKKU5j3rUTi9CtejuyYE8z3rUTg9CtejuyYE8z3rUTi9oZ4pu+kP9T3rUTg9oZ4pu+kP9T3rUTi9oZ4pu+kP9T3rUTg9oZ4pu+kP9T3rUTi9AAAAAJDC9T3rUTg9AAAAAJDC9T3rUTi9qar//wAAAICpqv//AAAAgKmq//8AAACAqar//wAAAICpqv//AAAAgKmq//8AAACAqar//wAAAICpqv//AAAAgP9///8AAACA/3///wAAAID/f///AAAAgP9///8AAACA/38AAAAAAID/fwAAAAAAgP9/AAAAAACA/38AAAAAAID/f/9/AAAAgP9//38AAACA/3//fwAAAID/f/9/AAAAgP9/AAAAAACA/38AAAAAAID/fwAAAAAAgP9/AAAAAACA/3//fwAAAID/f/9/AAAAgP9//38AAACA/3//fwAAAID/////AAAAgP////8AAACA/////wAAAID/////AAAAgP////8AAACA/////wAAAID/////AAAAgP////8AAACAAAD/fwAAAIAAAP9/AAAAgAAA/38AAACAAAD/fwAAAIAAAP9/AAAAgAAA/38AAACAAAD/fwAAAIAAAP9/AAAAgAAA/38AAACAAAD/fwAAAID///9/AAAAgP///38AAACA////fwAAAID///9/AAAAgP///38AAACA////fwAAAID///9/AAAAgP///38AAACA////fwAAAID///9/AAAAgP9//38AAACA/3//fwAAAID/f/9/AAAAgP9//38AAACA/////wAAAID/////AAAAgP////8AAACA/////wAAAID/f///AAAAgP9///8AAACA/3///wAAAID/f///AAAAgP9///8AAACA/3///wAAAID/f///AAAAgP9///8AAACA/3+qKgAAAID/f6oqAAAAgP9/qioAAACA/3+qKgAAAID/fwAAAAAAgP9/AAAAAACA/38AAAAAAID/fwAAAAAAgP9///8AAACA/3///wAAAID/f///AAAAgP9///8AAACAAAD/f/9//78AAP9//3//vwAA/3//f/+/AAD/f/9//78AAP9//3//v////3/+////////f/////////9//v///////3//////////f///////////AAAAgP////8AAACA/////wAAAID/////AAAAgP////8AAACA/////wAAAID/////AAAAgP////8AAACA/////wAAAID/////AAAAgP////8AAACA/////wAAAID/////AAAAgP8//78AAACA/z//vwAAAIAlUSXRAAAAgCVRJdEAAACAJdHZrgAAAID/v/+/AAAAgCXR2a4AAACA/7//vwAAAID/f/9/AAAAgP9//38AAACA/3//fwAAAID/f/9/AAAAgP9//38AAACA/3//fwAAAID/f/9/AAAAgP9//38AAACA/3//fwAAAID/f/9/AAAAgP9//38AAACA/3//fwAAAID/f/9/AAAAgP8//78AAACA2S7ZrgAAAID/P/+/AAAAgNku2a4AAACADBsMmwAAAIDkDuOOAAAAgAwbDJsAAACA5A7jjgAAAID/f///AAAAgP9///8AAACADJvy5AAAAIAMm/LkAAAAgAyb8uQAAACADJvy5AAAAIDZriXRAAAAgNmuJdEAAACA8uQMmwAAAIAl0dmuAAAAgPLkDJsAAACAJdHZrgAAAIAa8eOOAAAAgPLkDJsAAACAGvHjjgAAAIDy5AybAAAAgNmuJdEAAACA2a4l0QAAAID/v/+/AAAAgP+//78AAACA2S7ZrgAAAIAMGwybAAAAgNku2a4AAACADBsMmwAAAIAlUSXRAAAAgCVRJdEAAACA8mTy5AAAAIDyZPLkAAAAgPJk8uQAAACA8mTy5AAAAID/f///AAAAgP9///8AAACA")
|
||||
}, {
|
||||
"aabb": AABB(-0.03, -0.12, -0.075, 0.06, 0.24, 0.15),
|
||||
"attribute_data": PackedByteArray("XS6XP0T3Qj9dLpc/dxH6PyaTyT5E90I/JpPJPncR+j8mk8m+RPdCPyaTyb53Efo/XS6Xv0T3Qj9dLpe/dxH6P10ul7+US9o/XS6XvxaLRb8mk8m+lEvaPyaTyb4Wi0W/JpPJvhaLRb8mk8m+xmIxQF0ul78Wi0W/XS6Xv8ZiMUBdLpc/xmKRQCaTyT7GYpFAXS6XPyaTCcAmk8k+JpMJwF0ulz8Wi0W/XS6XP8ZiMUAmk8k+FotFvyaTyT7GYjFAJpPJvsZikUBdLpe/xmKRQCaTyb4mkwnAXS6XvyaTCcBdLpc/hlOAQCaTyT6GU4BAXS6XPyaTCcAmk8k+JpMJwCaTyb6GU4BAXS6Xv4ZTgEAmk8m+JpMJwF0ul78mkwnA9Pk8QF6vl0CLxeI/JpMJwPT5PEBUKlXA9Pk8wFQqVcCLxeK/JpMJwIvF4r/GYpFAi8XiP4ZTgEDw93s/XS63QCmXND/GYpFA9Pk8wF0ut0D0+TzAVCpVwIvF4r+GU4BA9Pk8wF6vl0Dw93u/XS63QCmXNL/GYpFA9Pk8QF0ut0CLxeI/xmKRQIvF4j8mkwnAi8XivyaTCcD0+TxAVCpVwF0ulz9er5dAXS6Xv16vl0BdLpc/VCpVwF0ul79UKlXAXS6XP10ut0BdLpe/XS63QF0ulz9UKlXAXS6Xv1QqVcBdLpe/6PP5vyaTyb7GYjFAXS6Xv/T5fEBdLpc/9Pl8QCaTyT7GYjFAJpPJPhaLRb8mk8m+FotFv10ulz/o8/m/XS6Xv+3gDkBdLpe/QIgBPV0ulz/t4A5AXS6XP0CIAT1dLpc/6PP5v10ulz/4+/0/XS6Xv+jz+b9dLpe/+Pv9PyaTyT6US9o/JpPJPhaLRb9dLpc/lEvaP10ulz8Wi0W/i8XiPyaTCcCLxeI/hlOAQIvF4r8mkwnAKZc0P8ZikUCLxeK/xmKRQIvF4r+GU4BAi8XivyaTCcAplzS/xmKRQIvF4j8mkwnAi8XiP8ZikUAmk8k+VCpVwCaTyb5UKlXA0bTCPtCvW8DRtMK+0K9bwKORrj6Iw2HAo5GuvojDYcDwiI6+cvtmwPCIjj5y+2bAJpNJPor8asAmk0m+ivxqwIOv0D3ugG3Ag6/Qve6AbcDxsKoOulxuwIvF4j+g4Mq/i8Xiv6Dgyr+LxeI/agjYv4vF4r9qCNi/i8Xiv+mZGMCLxeK/zS0fwIvF4j/pmRjAi8XiP80tH8Amk8k+VCpVwCaTyb5UKlXA0bTCPtCvW8DRtMK+0K9bwKORrj6Iw2HAo5GuvojDYcDwiI4+cvtmwPCIjr5y+2bAJpNJPor8asAmk0m+ivxqwIOv0D3ugG3Ag6/Qve6AbcAJafYNulxuwIvF4j/NLR/Ai8XiP+mZGMCLxeK/zS0fwIvF4r/pmRjAi8XiP0AVVsCLxeI/XIFPwIvF4r9AFVbAi8Xiv1yBT8CLxeK/QEXEPovF4j9ARcQ+i8Xiv2Dk+D6LxeI/YOT4PovF4r/4azW/i8XiP/hrNb+LxeK/aBwbv4vF4j9oHBu/i8Xiv0/HPMCLxeK/MltDwIvF4j9PxzzAi8XiPzJbQ8CLxeK/XIFPwIvF4r9AFVbAi8XiP1yBT8CLxeI/QBVWwIvF4r9qCNi/i8XiP2oI2L+LxeK/oODKv4vF4j+g4Mq/i8XiPzJbQ8CLxeI/T8c8wIvF4r8yW0PAi8Xiv0/HPMCLxeI/aBwbv4vF4r9oHBu/i8XiP/hrNb+LxeK/+Gs1v4vF4j9g5Pg+i8Xiv2Dk+D6LxeI/QEXEPovF4r9ARcQ+"),
|
||||
"format": 34359742487,
|
||||
"index_count": 138,
|
||||
"index_data": PackedByteArray("YABeAF8AXwBhAGAAYQBiAGAAYQBjAGIAYwBkAGIAZABlAGIAZABmAGUAZABnAGYAZwBoAGYAZwBpAGgAaQBqAGgAbQBrAGwAbABuAG0AcQBvAHAAcAByAHEAdQBzAHQAdAB2AHUAdgB3AHUAdgB4AHcAeAB5AHcAeAB6AHkAegB7AHkAegB8AHsAfAB9AHsAfAB+AH0AfgB/AH0AggCAAIEAgQCDAIIAhgCEAIUAhQCHAIYAigCIAIkAiQCLAIoAjgCMAI0AjQCPAI4AkgCQAJEAkQCTAJIAlgCUAJUAlQCXAJYAmgCYAJkAmQCbAJoAngCcAJ0AnQCfAJ4AogCgAKEAoQCjAKIApgCkAKUApQCnAKYA"),
|
||||
"material": SubResource("StandardMaterial3D_bfdxb"),
|
||||
"name": "yellow",
|
||||
"primitive": 3,
|
||||
"uv_scale": Vector4(0, 0, 0, 0),
|
||||
"vertex_count": 168,
|
||||
"vertex_data": PackedByteArray("kML1vIaWnL3rUTg9kML1vOtRuL2wyJI8CtcjvIaWnL3rUTg9CtcjvOtRuL2wyJI8CtcjPIaWnL3rUTg9CtcjPOtRuL2wyJI8kML1PIaWnL3rUTg9kML1POtRuL2wyJI8kML1vOtRuL2wyJI8kML1vOtRuL3rUTi9CtcjvOtRuL2wyJI8CtcjvOtRuL3rUTi9CtcjPArXoz3rUTi9CtcjPArXoz3rUTg9kML1PArXoz3rUTi9kML1PArXoz3rUTg9kML1POtRuL3rUTi9CtcjPOtRuL3rUTi9kML1PArXoz3rUTi9CtcjPArXoz3rUTi9kML1vArXoz3rUTi9kML1vArXoz3rUTg9CtcjvArXoz3rUTi9CtcjvArXoz3rUTg9CtcjvOtRuL3rUTi9kML1vOtRuL3rUTi9CtcjvArXoz3rUTi9kML1vArXoz3rUTi9kML1vIaWnL3rUTg9CtcjvIaWnL3rUTg9kML1vArXoz3rUTg9CtcjvArXoz3rUTg9CtcjPIaWnL3rUTg9kML1PIaWnL3rUTg9CtcjPArXoz3rUTg9kML1PArXoz3rUTg9kML1vFyPwr2amZk9kML1vArXoz3rUTg9kML1vK1H4T2amZk9kML1vK1H4T2amZm9kML1vArXoz3rUTi9kML1vOtRuL3rUTi9kML1vIaWnL3rUTg9kML1vJDC9b3NzMw8kML1vOtRuL2wyJI8kML1vJDC9b2amZm9kML1PK1H4T2amZk9kML1PIaWnL3rUTg9kML1PFyPwr2amZk9kML1PJDC9b3NzMw8kML1POtRuL2wyJI8kML1PJDC9b2amZm9kML1POtRuL3rUTi9kML1PArXoz3rUTi9kML1PArXoz3rUTg9kML1PK1H4T2amZm9kML1PFyPwr2amZk9kML1vFyPwr2amZk9kML1PK1H4T2amZk9kML1vK1H4T2amZk9kML1vJDC9b2amZm9kML1PJDC9b2amZm9kML1vK1H4T2amZm9kML1PK1H4T2amZm9kML1vK1H4T2amZm9CtcjvK1H4T3rUTg9kML1vK1H4T2amZk9kML1PK1H4T2amZk9CtcjPK1H4T3rUTg9CtcjPK1H4T3rUTi9CtcjvK1H4T3rUTi9kML1PK1H4T2amZm9kML1vJDC9b3NzMw8kML1vFyPwr2amZk9kML1PJDC9b3NzMw8kML1PFyPwr2amZk9kML1vJDC9b2amZm9kML1vJDC9b3NzMw8kML1PJDC9b2amZm9kML1PJDC9b3NzMw8CtcjPOtRuL2wyJI8CtcjPOtRuL3rUTi9kML1POtRuL2wyJI8kML1POtRuL3rUTi9CtcjvArXoz3rUTg9CtcjvIaWnL3rUTg9CtcjvArXoz3rUTi9CtcjvOtRuL2wyJI8CtcjvOtRuL3rUTi9CtcjPIaWnL3rUTg9CtcjPArXoz3rUTg9CtcjPOtRuL2wyJI8CtcjPArXoz3rUTi9CtcjPOtRuL3rUTi9CtcjvK1H4T3rUTi9CtcjPK1H4T3rUTi93kEevKKU5j3rUTi93kEePKKU5j3rUTi9vOMNvB+F6z3rUTi9vOMNPB+F6z3rUTi9a7TnO/TC7z3rUTi9a7Tnu/TC7z3rUTi9CtejuyYE8z3rUTi9CtejOyYE8z3rUTi9oZ4pu+kP9T3rUTi9oZ4pO+kP9T3rUTi9AAAAAJDC9T3rUTi9a7Tnu/TC7z3rUTg9a7Tnu/TC7z3rUTi9CtejuyYE8z3rUTg9CtejuyYE8z3rUTi9vOMNPB+F6z3rUTg9a7TnO/TC7z3rUTg9vOMNPB+F6z3rUTi9a7TnO/TC7z3rUTi9CtcjPK1H4T3rUTg9CtcjvK1H4T3rUTg93kEePKKU5j3rUTg93kEevKKU5j3rUTg9vOMNPB+F6z3rUTg9vOMNvB+F6z3rUTg9a7TnO/TC7z3rUTg9a7Tnu/TC7z3rUTg9CtejOyYE8z3rUTg9CtejuyYE8z3rUTg9oZ4pO+kP9T3rUTg9oZ4pu+kP9T3rUTg9AAAAAJDC9T3rUTg9a7Tnu/TC7z3rUTg9vOMNvB+F6z3rUTg9a7Tnu/TC7z3rUTi9vOMNvB+F6z3rUTi93kEevKKU5j3rUTg9CtcjvK1H4T3rUTg93kEevKKU5j3rUTi9CtcjvK1H4T3rUTi9AAAAAJDC9T3rUTg9AAAAAJDC9T3rUTi9oZ4pO+kP9T3rUTg9oZ4pO+kP9T3rUTi9oZ4pO+kP9T3rUTg9oZ4pO+kP9T3rUTi9CtejOyYE8z3rUTg9CtejOyYE8z3rUTi93kEePKKU5j3rUTg9vOMNPB+F6z3rUTg93kEePKKU5j3rUTi9vOMNPB+F6z3rUTi9CtcjPK1H4T3rUTg93kEePKKU5j3rUTg9CtcjPK1H4T3rUTi93kEePKKU5j3rUTi9CtejOyYE8z3rUTg9CtejOyYE8z3rUTi9a7TnO/TC7z3rUTg9a7TnO/TC7z3rUTi9vOMNvB+F6z3rUTg93kEevKKU5j3rUTg9vOMNvB+F6z3rUTi93kEevKKU5j3rUTi9CtejuyYE8z3rUTg9CtejuyYE8z3rUTi9oZ4pu+kP9T3rUTg9oZ4pu+kP9T3rUTi9oZ4pu+kP9T3rUTg9oZ4pu+kP9T3rUTi9AAAAAJDC9T3rUTg9AAAAAJDC9T3rUTi9qar//wAAAICpqv//AAAAgKmq//8AAACAqar//wAAAICpqv//AAAAgKmq//8AAACAqar//wAAAICpqv//AAAAgP9///8AAACA/3///wAAAID/f///AAAAgP9///8AAACA/38AAAAAAID/fwAAAAAAgP9/AAAAAACA/38AAAAAAID/f/9/AAAAgP9//38AAACA/3//fwAAAID/f/9/AAAAgP9/AAAAAACA/38AAAAAAID/fwAAAAAAgP9/AAAAAACA/3//fwAAAID/f/9/AAAAgP9//38AAACA/3//fwAAAID/////AAAAgP////8AAACA/////wAAAID/////AAAAgP////8AAACA/////wAAAID/////AAAAgP////8AAACAAAD/fwAAAIAAAP9/AAAAgAAA/38AAACAAAD/fwAAAIAAAP9/AAAAgAAA/38AAACAAAD/fwAAAIAAAP9/AAAAgAAA/38AAACAAAD/fwAAAID///9/AAAAgP///38AAACA////fwAAAID///9/AAAAgP///38AAACA////fwAAAID///9/AAAAgP///38AAACA////fwAAAID///9/AAAAgP9//38AAACA/3//fwAAAID/f/9/AAAAgP9//38AAACA/////wAAAID/////AAAAgP////8AAACA/////wAAAID/f///AAAAgP9///8AAACA/3///wAAAID/f///AAAAgP9///8AAACA/3///wAAAID/f///AAAAgP9///8AAACA/3+qKgAAAID/f6oqAAAAgP9/qioAAACA/3+qKgAAAID/fwAAAAAAgP9/AAAAAACA/38AAAAAAID/fwAAAAAAgP9///8AAACA/3///wAAAID/f///AAAAgP9///8AAACAAAD/fwAAAIAAAP9/AAAAgAAA/38AAACAAAD/fwAAAIAAAP9/AAAAgP///38AAACA////fwAAAID///9/AAAAgP///38AAACA////fwAAAID/////AAD/v/////8AAP+//////wAA/7//////AAD/v/////8AAP+//////wAA/7//////AAD/v/////8AAP+//////wAA/7//////AAD/v/////8AAP+//////wAA/7//////AAD/v/8//7//f/+//z//v/9//78lUSXR/3//vyVRJdH/f/+/JdHZrv//////v/+//////yXR2a7//////7//v///////f/9/////v/9//3////+//3//f////7//f/9/////v/9//3////+//3//f////7//f/9/////v/9//3////+//3//f////7//f/9/////v/9//3////+//3//f////7//f/9/////v/8//7//f/+/2S7Zrv9//7//P/+//3//v9ku2a7/f/+/DBsMm/9//7/kDuOO/3//vwwbDJv/f/+/5A7jjv9//7//f/////////9/////////DJvy5P////8Mm/Lk/////wyb8uT/////DJvy5P/////ZriXR/////9muJdH/////8uQMm/////8l0dmu//////LkDJv/////JdHZrv////8a8eOO//////LkDJv/////GvHjjv/////y5Ayb/////9muJdH/////2a4l0f//////v/+///////+//7//////2S7Zrv9//78MGwyb/3//v9ku2a7/f/+/DBsMm/9//78lUSXR/3//vyVRJdH/f/+/8mTy5P9//7/yZPLk/3//v/Jk8uT/f/+/8mTy5P9//7//f////3//v/9/////f/+/")
|
||||
}]
|
||||
blend_shape_mode = 0
|
||||
shadow_mesh = SubResource("ArrayMesh_j3k8o")
|
||||
|
||||
[sub_resource type="ConvexPolygonShape3D" id="ConvexPolygonShape3D_p04et"]
|
||||
points = PackedVector3Array(0.03, 0.109992, 0.075, 0.00258418, 0.119648, 0.0449883, 0, 0.12, 0.0449883, -0.03, 0.109992, 0.075, 0.03, -0.0950039, 0.075, 0.03, 0.109992, -0.075, 0.00258418, 0.119648, -0.0450029, 0, 0.12, -0.0450029, -0.00259006, 0.119648, 0.0449883, -0.03, 0.109992, -0.075, -0.03, -0.0950039, 0.075, 0.03, -0.12, 0.0249902, 0.03, -0.12, -0.075, -0.00259006, 0.119648, -0.0450029, -0.03, -0.12, -0.075, -0.03, -0.12, 0.0249902)
|
||||
|
||||
[node name="blasterN_Clip" type="RigidBody3D"]
|
||||
collision_layer = 8
|
||||
collision_mask = 5
|
||||
script = ExtResource("1_d1wy0")
|
||||
weapon = SubResource("Resource_8ete7")
|
||||
TYPE = "Ammo"
|
||||
|
||||
[node name="clipA2" type="MeshInstance3D" parent="."]
|
||||
mesh = SubResource("ArrayMesh_py3xv")
|
||||
skeleton = NodePath("")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
||||
shape = SubResource("ConvexPolygonShape3D_p04et")
|
||||
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://dc48o4niy1mok"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://b26eegme16mfw" path="res://client/template/scripts/Projectiles/Projectile.gd" id="1_epqi3"]
|
||||
|
||||
[node name="Projectile" type="Node3D"]
|
||||
script = ExtResource("1_epqi3")
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://bpxa3je174q43"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://b26eegme16mfw" path="res://client/template/scripts/Projectiles/Projectile.gd" id="1_won33"]
|
||||
[ext_resource type="PackedScene" uid="uid://dxlkgbr1p580" path="res://client/template/Spawnable_Objects/bullet.tscn" id="2_sq5da"]
|
||||
|
||||
[node name="Projectile" type="Node3D"]
|
||||
script = ExtResource("1_won33")
|
||||
Projectile_Type = "Rigidbody_Projectile"
|
||||
Projectile_Velocity = 20
|
||||
Rigid_Body_Projectile = ExtResource("2_sq5da")
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 990 B |
@@ -0,0 +1,14 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://6rfh1ejt4m6m"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://bfn8eu00l4btq" path="res://client/template/scripts/Projectiles/melee_weapon_projectile.gd" id="1_qlmbe"]
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_q86ac"]
|
||||
|
||||
[node name="MeleeProjectile" type="Node3D"]
|
||||
script = ExtResource("1_qlmbe")
|
||||
Projectile_Type = "over_ride"
|
||||
|
||||
[node name="MeleeHitbox" type="ShapeCast3D" parent="."]
|
||||
shape = SubResource("BoxShape3D_q86ac")
|
||||
target_position = Vector3(0, 0, -1)
|
||||
collision_mask = 37
|
||||
@@ -0,0 +1,22 @@
|
||||
[gd_scene load_steps=4 format=3 uid="uid://ddfiivxgnakti"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://biivm72fp7af8" path="res://client/template/scripts/Projectiles/rocket_launcher_projectile.gd" id="1_dkmt1"]
|
||||
[ext_resource type="PackedScene" uid="uid://cqhlfy6ccomm3" path="res://client/template/Spawnable_Objects/rocket.tscn" id="2_u24w0"]
|
||||
|
||||
[sub_resource type="SphereShape3D" id="SphereShape3D_wqjlt"]
|
||||
radius = 5.0
|
||||
|
||||
[node name="Projectile" type="Node3D" node_paths=PackedStringArray("explosion")]
|
||||
script = ExtResource("1_dkmt1")
|
||||
explosion = NodePath("ExplosionRadias")
|
||||
Projectile_Type = "Rigidbody_Projectile"
|
||||
Projectile_Velocity = 20
|
||||
Expirey_Time = 20
|
||||
Rigid_Body_Projectile = ExtResource("2_u24w0")
|
||||
pass_through = true
|
||||
|
||||
[node name="ExplosionRadias" type="ShapeCast3D" parent="."]
|
||||
top_level = true
|
||||
shape = SubResource("SphereShape3D_wqjlt")
|
||||
target_position = Vector3(0, 0, 0)
|
||||
collision_mask = 37
|
||||
@@ -0,0 +1,17 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://brvfyahytwd8m"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dnh4a4vkl0cl1" path="res://client/template/scripts/Projectiles/shotgun_projectile.gd" id="1_idx2q"]
|
||||
|
||||
[sub_resource type="Curve2D" id="Curve2D_2lhxk"]
|
||||
_data = {
|
||||
"points": PackedVector2Array(0, 0, 0, 0, 7, 3, 0, 0, 0, 0, -30, -11, 0, 0, 0, 0, -8, -18, 0, 0, 0, 0, 8, -9, -2.50282, -11.0124, 2.50282, 11.0124, 19, 13, 0, 0, 0, 0, -21, 1, 0, 0, 0, 0, 22, 22, 0, 0, 0, 0, 12, 21, 0, 0, 0, 0, -33, 4)
|
||||
}
|
||||
point_count = 9
|
||||
|
||||
[node name="shotgun_projectile" type="Node3D"]
|
||||
script = ExtResource("1_idx2q")
|
||||
Randomness = 2.941
|
||||
Split_Damage = true
|
||||
|
||||
[node name="shotgun_pattern" type="Path2D" parent="."]
|
||||
curve = SubResource("Curve2D_2lhxk")
|
||||
@@ -0,0 +1,41 @@
|
||||
extends Node
|
||||
class_name Spray_Profile
|
||||
|
||||
@export_category("Random_Spray")
|
||||
@export var Spray_Noise: FastNoiseLite
|
||||
@export_range(0.0,10.0) var Random_Spray_Multiplier: float = 1.0
|
||||
@export var Max_Limit: int = 30
|
||||
@export var Updwards_Lock: bool = false
|
||||
@export var True_Random: bool = true
|
||||
|
||||
@export_category("Path_Spray")
|
||||
@export var Spray_Path: Path2D
|
||||
@export_range(0.0,10.0) var Path_Spray_Multiplier: float = 1.0
|
||||
@export_range(0.0,1.0) var Spray_Mix: float = 1.0
|
||||
|
||||
var Random_Spray: Vector2 = Vector2.ZERO
|
||||
var Spray_Vector: Vector2 = Vector2.ZERO
|
||||
|
||||
func Get_Spray(count: int,_max_count: int)->Vector2:
|
||||
if Spray_Noise:
|
||||
if True_Random:
|
||||
Spray_Noise.set_seed(randi())
|
||||
|
||||
var x = Spray_Noise.get_noise_1d(count)*min(count,Max_Limit)
|
||||
var y = Spray_Noise.get_noise_2d(_max_count,count)*min(count,Max_Limit)
|
||||
|
||||
if Updwards_Lock:
|
||||
y = -abs(y)
|
||||
|
||||
Random_Spray = Vector2(x,y)*Random_Spray_Multiplier
|
||||
|
||||
if Spray_Path:
|
||||
var Path_Points: Curve2D = Spray_Path.get_curve()
|
||||
var Point_Count: int = Path_Points.get_point_count()
|
||||
count = min(count-1,Point_Count-1)
|
||||
|
||||
Spray_Vector = Path_Points.get_point_position(count)*Path_Spray_Multiplier
|
||||
|
||||
|
||||
var Spray = Spray_Vector + (Random_Spray*Spray_Mix)
|
||||
return Spray
|
||||
@@ -0,0 +1 @@
|
||||
uid://dvcpsronqmkmr
|
||||
@@ -0,0 +1,20 @@
|
||||
[gd_scene load_steps=4 format=3 uid="uid://ciu0xkaw0ioef"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dvcpsronqmkmr" path="res://client/template/Spawnable_Objects/SprayProfiles/spray_profile.gd" id="1_60v4d"]
|
||||
|
||||
[sub_resource type="FastNoiseLite" id="FastNoiseLite_4msk4"]
|
||||
|
||||
[sub_resource type="Curve2D" id="Curve2D_0vouc"]
|
||||
_data = {
|
||||
"points": PackedVector2Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, -2, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 1, -23, 0, 0, 0, 0, 2, -35, 0, 0, 0, 0, -4, -50, 0, 0, 0, 0, -7, -62, 0, 0, 0, 0, -12, -69, 0, 0, 0, 0, -6, -76, 0, 0, 0, 0, 12, -74, 0, 0, 0, 0, 9, -86, 0, 0, 0, 0, 16, -81, 0, 0, 0, 0, 21, -76, 0, 0, 0, 0, 19, -83, 0, 0, 0, 0, 23, -84, 0, 0, 0, 0, 36, -82, 0, 0, 0, 0, 35, -79, 0, 0, 0, 0, 28, -81, 0, 0, 0, 0, 23, -81, 0, 0, 0, 0, 5, -88, 0, 0, 0, 0, 2, -90, 0, 0, 0, 0, -6, -88, 0, 0, 0, 0, -10, -91, 0, 0, 0, 0, -12, -91, 0, 0, 0, 0, -13, -89, 0, 0, 0, 0, -13, -85, 0, 0, 0, 0, -15, -85, 0, 0, 0, 0, -16, -88, 0, 0, 0, 0, -19, -90, 0, 0, 0, 0, -20, -85)
|
||||
}
|
||||
point_count = 30
|
||||
|
||||
[node name="Spray_Profile" type="Node" node_paths=PackedStringArray("Spray_Path")]
|
||||
script = ExtResource("1_60v4d")
|
||||
Spray_Noise = SubResource("FastNoiseLite_4msk4")
|
||||
Spray_Path = NodePath("Path2D")
|
||||
Path_Spray_Multiplier = 0.58
|
||||
|
||||
[node name="Path2D" type="Path2D" parent="."]
|
||||
curve = SubResource("Curve2D_0vouc")
|
||||
@@ -0,0 +1,19 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://3mb12hvruajh"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dvcpsronqmkmr" path="res://client/template/Spawnable_Objects/SprayProfiles/spray_profile.gd" id="1_35g6m"]
|
||||
|
||||
[sub_resource type="FastNoiseLite" id="FastNoiseLite_4msk4"]
|
||||
seed = 145
|
||||
frequency = 0.307
|
||||
cellular_jitter = -1.115
|
||||
cellular_return_type = 0
|
||||
domain_warp_type = 1
|
||||
domain_warp_amplitude = 30.075
|
||||
domain_warp_frequency = -0.835
|
||||
domain_warp_fractal_type = 2
|
||||
domain_warp_fractal_octaves = 1
|
||||
|
||||
[node name="Spray_Profile" type="Node"]
|
||||
script = ExtResource("1_35g6m")
|
||||
Spray_Noise = SubResource("FastNoiseLite_4msk4")
|
||||
Random_Spray_Multiplier = 2.371
|
||||
@@ -0,0 +1,20 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://ceuaamuau8pl8"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dvcpsronqmkmr" path="res://client/template/Spawnable_Objects/SprayProfiles/spray_profile.gd" id="1_hmoot"]
|
||||
|
||||
[sub_resource type="FastNoiseLite" id="FastNoiseLite_4msk4"]
|
||||
seed = 145
|
||||
frequency = 0.307
|
||||
cellular_jitter = -1.115
|
||||
cellular_return_type = 0
|
||||
domain_warp_type = 1
|
||||
domain_warp_amplitude = 30.075
|
||||
domain_warp_frequency = -0.835
|
||||
domain_warp_fractal_type = 2
|
||||
domain_warp_fractal_octaves = 1
|
||||
|
||||
[node name="Spray_Profile" type="Node"]
|
||||
script = ExtResource("1_hmoot")
|
||||
Spray_Noise = SubResource("FastNoiseLite_4msk4")
|
||||
Random_Spray_Multiplier = 10.0
|
||||
Updwards_Lock = true
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,65 @@
|
||||
[gd_scene load_steps=5 format=4 uid="uid://dxlkgbr1p580"]
|
||||
|
||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_bix2d"]
|
||||
radius = 0.099605
|
||||
height = 0.419677
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_x0fj0"]
|
||||
resource_name = "yellow"
|
||||
vertex_color_use_as_albedo = true
|
||||
albedo_color = Color(0.0627451, 0.639216, 0.576471, 1)
|
||||
metallic = 1.0
|
||||
emission_enabled = true
|
||||
emission = Color(0, 0.960784, 0.960784, 1)
|
||||
emission_energy_multiplier = 1.78
|
||||
|
||||
[sub_resource type="ArrayMesh" id="ArrayMesh_ly6dy"]
|
||||
_surfaces = [{
|
||||
"aabb": AABB(-0.01, -0.025, -0.01, 0.02, 0.05, 0.02),
|
||||
"format": 34359742465,
|
||||
"index_count": 276,
|
||||
"index_data": PackedByteArray("AgAAAAEAAQADAAIAAQAEAAMABAAFAAMABAAGAAUABgAHAAUABgAIAAcACAAJAAcACAAKAAkACgALAAkACgAMAAsADAANAAsADAAOAA0ADgAPAA0ADwAQAA0ADwARABAADwASABEAEgATABEAEgAUABMAFAAVABMAFAAWABUAFgAXABUAGQAYAAYABgAEABkAFgAUABoAGgAbABYAHAASAA8ADwAdABwAHgAFAAcABwAfAB4AIAAOAAwADAAhACAAEQATACIAIgAjABEAIQAMAAoACgAkACEAJwAlACYAJgAZACcAJgAoABkAKAAYABkAKAAeABgAHgApABgAHgAfACkAHwAkACkAHwAqACQAKgAhACQAKgArACEAKwAgACEAKwAsACAALAAdACAALAAtAB0ALQAcAB0ALQAjABwAIwAaABwAIwAiABoAIgAbABoAIgAuABsALgAvABsAKwALAA0ADQAsACsALAANABAAEAAtACwAHgAoAAMAAwAFAB4AJgAlAAAAAAACACYAJAAKAAgACAApACQAHwAHAAkACQAqAB8AJQAnAAEAAQAAACUALQAQABEAEQAjAC0AFAASABwAHAAaABQAFwAWABsAGwAvABcAEwAVAC4ALgAiABMAHQAPAA4ADgAgAB0AFQAXAC8ALwAuABUAJwAZAAQABAABACcAKQAIAAYABgAYACkAKAAmAAIAAgADACgAKgAJAAsACwArACoA"),
|
||||
"name": "yellow",
|
||||
"primitive": 3,
|
||||
"uv_scale": Vector4(0, 0, 0, 0),
|
||||
"vertex_count": 48,
|
||||
"vertex_data": PackedByteArray("CtcjvM3MzLwAAACA3kEevM3MzLyhnik73kEevM3MzLyhnim7vOMNvM3MzLwK16O7vOMNvM3MzLwK16M7a7Tnu83MzLxrtOe7a7Tnu83MzLxrtOc7Cteju83MzLy84w28Cteju83MzLy84w08oZ4pu83MzLzeQR68oZ4pu83MzLzeQR48AAAAAM3MzLwK1yO8AAAAAM3MzLwK1yM8oZ4pO83MzLzeQR68oZ4pO83MzLzeQR48CtejO83MzLy84w08CtejO83MzLy84w28a7TnO83MzLxrtOe7a7TnO83MzLxrtOc7vOMNPM3MzLwK16O7vOMNPM3MzLwK16M73kEePM3MzLyhnim73kEePM3MzLyhnik7CtcjPM3MzLwAAACAa7Tnu83MzDxrtOc7vOMNvM3MzDwK16M7vOMNPM3MzDwK16M73kEePM3MzDyhnik7a7TnO83MzDxrtOc7CtejO83MzDy84w08a7Tnu83MzDxrtOe7Cteju83MzDy84w28oZ4pO83MzDzeQR48AAAAAM3MzDwK1yM8vOMNPM3MzDwK16O7a7TnO83MzDxrtOe7oZ4pu83MzDzeQR48CtcjvM3MzDwAAACA3kEevM3MzDyhnim73kEevM3MzDyhnik7vOMNvM3MzDwK16O7Cteju83MzDy84w08oZ4pu83MzDzeQR68AAAAAM3MzDwK1yO8oZ4pO83MzDzeQR68CtejO83MzDy84w283kEePM3MzDyhnim7CtcjPM3MzDwAAACA")
|
||||
}]
|
||||
blend_shape_mode = 0
|
||||
|
||||
[sub_resource type="ArrayMesh" id="ArrayMesh_sqgwv"]
|
||||
resource_name = "foamBulletA_Mesh foamBulletA"
|
||||
_surfaces = [{
|
||||
"aabb": AABB(-0.01, -0.025, -0.01, 0.02, 0.05, 0.02),
|
||||
"attribute_data": PackedByteArray("JpPJPgAAgD/RtMI++AqNP9G0wj4Q6mU/o5GuPjabTT+jka4+ZTKZP/CIjj6Iuzg/8IiOPjyioz8mk0k+LrcoPyaTST5ppKs/g6/QPZilHj+Dr9A9NK2wPwAAAABtNhs/AAAAAMpksj+Dr9C9mKUeP4Ov0L00rbA/JpNJvmmkqz8mk0m+LrcoP/CIjr6Iuzg/8IiOvjyioz+jka6+NptNP6ORrr5lMpk/0bTCvhDqZT/RtMK++AqNPyaTyb4AAIA/gHxSPQACgTyAfFI9+Pv9P4B8Ur0AAoE8gHxSvfj7/T+AfFK9+Pv9P4B8Ur0AAoE8gHxSPfj7/T+AfFI9AAKBPIB8Uj34+/0/gHxSvfj7/T+AfFI9AAKBPIB8Ur0AAoE8gHxSPfj7/T+AfFK9+Pv9P4B8Uj0AAoE8gHxSvQACgTyAfFI9+Pv9P4B8Ur34+/0/gHxSPQACgTyAfFK9AAKBPIB8Ur34+/0/gHxSvQACgTyAfFI9+Pv9P4B8Uj0AAoE8gHxSPfj7/T+AfFK9+Pv9P4B8Uj0AAoE8gHxSvQACgTwmk8m+AACAP9G0wr4Q6mU/0bTCvvgKjT+jka6+ZTKZP6ORrr42m00/8IiOvjyioz/wiI6+iLs4PyaTSb5ppKs/JpNJvi63KD+Dr9C9NK2wP4Ov0L2YpR4/AAAAAMpksj8AAAAAbTYbP4Ov0D00rbA/g6/QPZilHj8mk0k+aaSrPyaTST4utyg/8IiOPjyioz/wiI4+iLs4P6ORrj5lMpk/o5GuPjabTT/RtMI++AqNP9G0wj4Q6mU/JpPJPgAAgD+AfFI9+Pv9P4B8Ur34+/0/gHxSPQACgTyAfFK9AAKBPIB8Uj34+/0/gHxSvfj7/T+AfFI9AAKBPIB8Ur0AAoE8gHxSPQACgTyAfFI9+Pv9P4B8Ur0AAoE8gHxSvfj7/T+AfFI9AAKBPIB8Uj34+/0/gHxSvQACgTyAfFK9+Pv9P4B8Uj34+/0/gHxSvfj7/T+AfFI9AAKBPIB8Ur0AAoE8gHxSPfj7/T+AfFK9+Pv9P4B8Uj0AAoE8gHxSvQACgTyAfFI9AAKBPIB8Uj34+/0/gHxSvQACgTyAfFK9+Pv9P4B8Uj34+/0/gHxSvfj7/T+AfFI9AAKBPIB8Ur0AAoE8gHxSvfj7/T+AfFK9AAKBPIB8Uj34+/0/gHxSPQACgTyAfFK9+Pv9P4B8Ur0AAoE8gHxSPfj7/T+AfFI9AAKBPIB8Ur34+/0/gHxSvQACgTyAfFI9+Pv9P4B8Uj0AAoE8gHxSPfj7/T+AfFK9+Pv9P4B8Uj0AAoE8gHxSvQACgTyAfFK9+Pv9P4B8Ur0AAoE8gHxSPfj7/T+AfFI9AAKBPIB8Uj0AAoE8gHxSPfj7/T+AfFK9AAKBPIB8Ur34+/0/gHxSPfj7/T+AfFK9+Pv9P4B8Uj0AAoE8gHxSvQACgTyAfFI9AAKBPIB8Uj34+/0/gHxSvQACgTyAfFK9+Pv9P4B8Uj34+/0/gHxSvfj7/T+AfFI9AAKBPIB8Ur0AAoE8"),
|
||||
"format": 34359742487,
|
||||
"index_count": 276,
|
||||
"index_data": PackedByteArray("AgAAAAEAAQADAAIAAQAEAAMABAAFAAMABAAGAAUABgAHAAUABgAIAAcACAAJAAcACAAKAAkACgALAAkACgAMAAsADAANAAsADAAOAA0ADgAPAA0ADwAQAA0ADwARABAADwASABEAEgATABEAEgAUABMAFAAVABMAFAAWABUAFgAXABUAGgAYABkAGQAbABoAHgAcAB0AHQAfAB4AIgAgACEAIQAjACIAJgAkACUAJQAnACYAKgAoACkAKQArACoALgAsAC0ALQAvAC4AMgAwADEAMQAzADIANgA0ADUANQA3ADYANQA4ADcAOAA5ADcAOAA6ADkAOgA7ADkAOgA8ADsAPAA9ADsAPAA+AD0APgA/AD0APgBAAD8AQABBAD8AQABCAEEAQgBDAEEAQgBEAEMARABFAEMARABGAEUARgBHAEUARgBIAEcASABJAEcASABKAEkASgBLAEkATgBMAE0ATQBPAE4AUgBQAFEAUQBTAFIAVgBUAFUAVQBXAFYAWgBYAFkAWQBbAFoAXgBcAF0AXQBfAF4AYgBgAGEAYQBjAGIAZgBkAGUAZQBnAGYAagBoAGkAaQBrAGoAbgBsAG0AbQBvAG4AcgBwAHEAcQBzAHIAdgB0AHUAdQB3AHYAegB4AHkAeQB7AHoAfgB8AH0AfQB/AH4AggCAAIEAgQCDAIIAhgCEAIUAhQCHAIYAigCIAIkAiQCLAIoAjgCMAI0AjQCPAI4A"),
|
||||
"material": SubResource("StandardMaterial3D_x0fj0"),
|
||||
"name": "yellow",
|
||||
"primitive": 3,
|
||||
"uv_scale": Vector4(0, 0, 0, 0),
|
||||
"vertex_count": 144,
|
||||
"vertex_data": PackedByteArray("CtcjvM3MzLwAAACA3kEevM3MzLyhnik73kEevM3MzLyhnim7vOMNvM3MzLwK16O7vOMNvM3MzLwK16M7a7Tnu83MzLxrtOe7a7Tnu83MzLxrtOc7Cteju83MzLy84w28Cteju83MzLy84w08oZ4pu83MzLzeQR68oZ4pu83MzLzeQR48AAAAAM3MzLwK1yO8AAAAAM3MzLwK1yM8oZ4pO83MzLzeQR68oZ4pO83MzLzeQR48CtejO83MzLy84w08CtejO83MzLy84w28a7TnO83MzLxrtOe7a7TnO83MzLxrtOc7vOMNPM3MzLwK16O7vOMNPM3MzLwK16M73kEePM3MzLyhnim73kEePM3MzLyhnik7CtcjPM3MzLwAAACAa7Tnu83MzDxrtOc7a7Tnu83MzLxrtOc7vOMNvM3MzDwK16M7vOMNvM3MzLwK16M7vOMNPM3MzLwK16M7vOMNPM3MzDwK16M73kEePM3MzLyhnik73kEePM3MzDyhnik7a7TnO83MzLxrtOc7CtejO83MzLy84w08a7TnO83MzDxrtOc7CtejO83MzDy84w08a7Tnu83MzLxrtOe7Cteju83MzLy84w28a7Tnu83MzDxrtOe7Cteju83MzDy84w28oZ4pO83MzLzeQR48AAAAAM3MzLwK1yM8oZ4pO83MzDzeQR48AAAAAM3MzDwK1yM8vOMNPM3MzLwK16O7vOMNPM3MzDwK16O7a7TnO83MzLxrtOe7a7TnO83MzDxrtOe7AAAAAM3MzLwK1yM8oZ4pu83MzLzeQR48AAAAAM3MzDwK1yM8oZ4pu83MzDzeQR48CtcjvM3MzDwAAACA3kEevM3MzDyhnim73kEevM3MzDyhnik7vOMNvM3MzDwK16M7vOMNvM3MzDwK16O7a7Tnu83MzDxrtOc7a7Tnu83MzDxrtOe7Cteju83MzDy84w08Cteju83MzDy84w28oZ4pu83MzDzeQR48oZ4pu83MzDzeQR68AAAAAM3MzDwK1yM8AAAAAM3MzDwK1yO8oZ4pO83MzDzeQR48oZ4pO83MzDzeQR68CtejO83MzDy84w08CtejO83MzDy84w28a7TnO83MzDxrtOc7a7TnO83MzDxrtOe7vOMNPM3MzDwK16M7vOMNPM3MzDwK16O73kEePM3MzDyhnik73kEePM3MzDyhnim7CtcjPM3MzDwAAACAAAAAAM3MzLwK1yO8oZ4pO83MzLzeQR68AAAAAM3MzDwK1yO8oZ4pO83MzDzeQR68oZ4pO83MzLzeQR68CtejO83MzLy84w28oZ4pO83MzDzeQR68CtejO83MzDy84w28vOMNvM3MzDwK16O7vOMNvM3MzLwK16O7a7Tnu83MzDxrtOe7a7Tnu83MzLxrtOe7CtcjvM3MzDwAAACACtcjvM3MzLwAAACA3kEevM3MzDyhnim73kEevM3MzLyhnim7oZ4pu83MzLzeQR48Cteju83MzLy84w08oZ4pu83MzDzeQR48Cteju83MzDy84w08Cteju83MzLy84w28oZ4pu83MzLzeQR68Cteju83MzDy84w28oZ4pu83MzDzeQR683kEevM3MzDyhnik73kEevM3MzLyhnik7CtcjvM3MzDwAAACACtcjvM3MzLwAAACACtejO83MzLy84w28a7TnO83MzLxrtOe7CtejO83MzDy84w28a7TnO83MzDxrtOe7a7TnO83MzLxrtOc7a7TnO83MzDxrtOc7vOMNPM3MzLwK16M7vOMNPM3MzDwK16M73kEePM3MzLyhnik73kEePM3MzDyhnik7CtcjPM3MzLwAAACACtcjPM3MzDwAAACA3kEePM3MzLyhnim73kEePM3MzDyhnim7vOMNPM3MzLwK16O7vOMNPM3MzDwK16O7CtejO83MzLy84w08oZ4pO83MzLzeQR48CtejO83MzDy84w08oZ4pO83MzDzeQR48CtcjPM3MzLwAAACACtcjPM3MzDwAAACA3kEePM3MzLyhnim73kEePM3MzDyhnim7vOMNvM3MzDwK16M7vOMNvM3MzLwK16M73kEevM3MzDyhnik73kEevM3MzLyhnik7Cteju83MzLy84w08a7Tnu83MzLxrtOc7Cteju83MzDy84w08a7Tnu83MzDxrtOc73kEevM3MzDyhnim73kEevM3MzLyhnim7vOMNvM3MzDwK16O7vOMNvM3MzLwK16O7oZ4pu83MzLzeQR68AAAAAM3MzLwK1yO8oZ4pu83MzDzeQR68AAAAAM3MzDwK1yO8/38AAAAA/7//fwAAAAD/v/9/AAAAAP+//38AAAAA/7//fwAAAAD/v/9/AAAAAP+//38AAAAA/7//fwAAAAD/v/9/AAAAAP+//38AAAAA/7//fwAAAAD/v/9/AAAAAP+//38AAAAA/7//fwAAAAD/v/9/AAAAAP+//38AAAAA/7//fwAAAAD/v/9/AAAAAP+//38AAAAA/7//fwAAAAD/v/9/AAAAAP+//38AAAAA/7//fwAAAAD/v/9/AAAAAP+//z//f/+//7//P/9//7//v9ku/3/Zrv+/2S7/f9mu/78l0f9///+S6CXR/3///5Lo8uT/f///ePLy5P9///948v+//3/////f2a7/f///bNf/v/9/////39mu/3///2zXAAD/v/8//78AACXR2S7/vwAA/7//P/+/AAAl0dku/78Mm/9///+Fzf9//3////+/DJv/f///hc3/f/9/////v///2a4AAJLo///ZrgAAkuj///+/AAD/3////78AAP/f/3//f////7/yZP9/8uT/v/9//3////+/8mT/f/Lk/7//f///////v/9////+//+//3///////7//f///////v/9///////+//3///////7//f////v//v/9////+//+//3///////7//f///////v/9///////+//3///////7//f///////v/9////+//+//3////7//7//f///////v/9////+//+//3////7//7//f///////v/9///////+//3////7//7//f////v//v/9///////+//3///////7//////AAD/v///8uQAAIXN/////wAA/7////LkAACFzf//8uQAAIXN//8l0QAAbNf///LkAACFzf//JdEAAGzXAADZriVR/78AANmuJVH/vwAA/7//P/+/AAD/v/8//78AAP9//3//vwAA/3//f/+/AAAMm/Jk/78AAAyb8mT/v/Jk/3/y5P+/JVH/fyXR/7/yZP9/8uT/vyVR/38l0f+/AAAl0dku/78AAPLkDBv/vwAAJdHZLv+/AADy5Awb/78MG/9/DJv/vwwb/38Mm/+/AAD/f/9//78AAP9//3//v///JdEAAGzX////vwAA/9///yXRAABs1////78AAP/f/7//f////9//v/9/////3yXR/3///5LoJdH/f///kujy5P9///948vLk/3///3jy////f/////////9/////////DJsAAHjy//8MmwAAePL//9muAACS6P//2a4AAJLo2a7/f///bNcMm/9///+Fzdmu/3///2zXDJv/f///hc3///9//////////3////////8MmwAAePL//wybAAB48tku/3/Zrv+/2S7/f9mu/78MG/9/DJv/vwwb/38Mm/+/JVH/fyXR/7//P/9//7//vyVR/38l0f+//z//f/+//78AAAyb8mT/vwAADJvyZP+/AADZriVR/78AANmuJVH/vwAA8uQMG/+//////wAA/78AAPLkDBv/v/////8AAP+/")
|
||||
}]
|
||||
blend_shape_mode = 0
|
||||
shadow_mesh = SubResource("ArrayMesh_ly6dy")
|
||||
|
||||
[node name="bullet" type="RigidBody3D"]
|
||||
collision_layer = 16
|
||||
collision_mask = 37
|
||||
gravity_scale = 0.0
|
||||
continuous_cd = true
|
||||
contact_monitor = true
|
||||
max_contacts_reported = 5
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, -4.37114e-08, -1, 0, 1, -4.37114e-08, 0, 0, 0)
|
||||
shape = SubResource("CapsuleShape3D_bix2d")
|
||||
|
||||
[node name="foamBulletA2" type="MeshInstance3D" parent="."]
|
||||
transform = Transform3D(5, 0, 0, 0, -2.18557e-07, -5, 0, 5, -2.18557e-07, 0, 0, 0)
|
||||
mesh = SubResource("ArrayMesh_sqgwv")
|
||||
skeleton = NodePath("")
|
||||
|
||||
[connection signal="body_entered" from="." to="." method="_on_body_entered"]
|
||||
@@ -0,0 +1,15 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://dguqnsdowqt3w"]
|
||||
|
||||
[ext_resource type="Texture2D" uid="uid://clfvflhvcccy1" path="res://client/template/Spawnable_Objects/Projectiles_To_Load/debug_sprite.png" id="1_smdbx"]
|
||||
[ext_resource type="Script" uid="uid://dkt6w5wvebi72" path="res://client/template/scripts/Weapon_State_Machine/hit_debug.gd" id="2_gvqnn"]
|
||||
|
||||
[node name="Node3D" type="Sprite3D"]
|
||||
transform = Transform3D(0.1, 0, 0, 0, 0.1, 0, 0, 0, 0.1, 0, 0, 0)
|
||||
billboard = 2
|
||||
texture = ExtResource("1_smdbx")
|
||||
script = ExtResource("2_gvqnn")
|
||||
|
||||
[node name="Timer" type="Timer" parent="."]
|
||||
autostart = true
|
||||
|
||||
[connection signal="timeout" from="Timer" to="." method="_on_timer_timeout"]
|
||||
@@ -0,0 +1,17 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://bj15ogfi1535l"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dkt6w5wvebi72" path="res://client/template/scripts/Weapon_State_Machine/hit_debug.gd" id="2_w2l74"]
|
||||
|
||||
[sub_resource type="CompressedTexture2D" id="CompressedTexture2D_kr237"]
|
||||
load_path = "res://.godot/imported/crosshair046.png-adf558f9d87ef20104f2ce22346ed1b7.s3tc.ctex"
|
||||
|
||||
[node name="Node3D" type="Sprite3D"]
|
||||
modulate = Color(0, 0, 1, 1)
|
||||
billboard = 2
|
||||
texture = SubResource("CompressedTexture2D_kr237")
|
||||
script = ExtResource("2_w2l74")
|
||||
|
||||
[node name="Timer" type="Timer" parent="."]
|
||||
autostart = true
|
||||
|
||||
[connection signal="timeout" from="Timer" to="." method="_on_timer_timeout"]
|
||||
@@ -0,0 +1,87 @@
|
||||
[gd_scene load_steps=6 format=4 uid="uid://cqhlfy6ccomm3"]
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_030fb"]
|
||||
resource_name = "yellow"
|
||||
albedo_color = Color(0.996078, 0, 0.219608, 1)
|
||||
metallic = 1.0
|
||||
emission_enabled = true
|
||||
emission = Color(1, 0, 0, 1)
|
||||
emission_energy_multiplier = 19.35
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_urtjv"]
|
||||
resource_name = "pink"
|
||||
albedo_color = Color(0.996078, 0.219608, 0.411765, 1)
|
||||
metallic = 1.0
|
||||
|
||||
[sub_resource type="ArrayMesh" id="ArrayMesh_iqbgl"]
|
||||
_surfaces = [{
|
||||
"aabb": AABB(-0.01, -0.03, -0.01, 0.02, 0.06, 0.02),
|
||||
"format": 34896613377,
|
||||
"index_count": 66,
|
||||
"index_data": PackedByteArray("AgAAAAEAAQADAAIAAQAAAAQABAAFAAEABQAEAAYABgAHAAUACAAHAAYABgAJAAgADAAKAAsACwANAAwADgAJAAYABgAKAA4ABgAEAAoABAALAAoABAAAAAsAAAACAAsADwAIAAkACQAOAA8ADwAOAAoACgAMAA8ACwACAAMAAwANAAsA"),
|
||||
"name": "yellow",
|
||||
"primitive": 3,
|
||||
"uv_scale": Vector4(0, 0, 0, 0),
|
||||
"vertex_count": 24,
|
||||
"vertex_data": PackedByteArray("/98AAP/fAAD/31TV/98AAP//AAD/fwAA//9U1f9/AAD/fwAA//8AAP9/VNX//wAA/x8AAP/fAAD/H1TV/98AAAAAVNX/fwAAAAAAAP9/AAD/fwAAAAAAAP/fAAD/HwAA/39U1QAAAAD/31TV/x8AAP8fAAD/HwAA/x9U1f8fAAD/H////38AAP83////NwAA/zf////HAAD/f////98AAP9/////HwAA/8f////HAAD/x////zcAAP/f////fwAA")
|
||||
}, {
|
||||
"aabb": AABB(-0.01, -0.03, -0.01, 0.02, 0.06, 0.02),
|
||||
"format": 34896613377,
|
||||
"index_count": 66,
|
||||
"index_data": PackedByteArray("EgAQABEAEQATABIAEQAUABMAFAAVABMAFAAWABUAFgAXABUAFAAMAA0ADQAWABQAEQAPAAwADAAUABEAEwAFAAcABwASABMAEQAQAAgACAAPABEAEgAHAAgACAAQABIAFQABAAUABQATABUAAwABABUAFQAXAAMADQADABcAFwAWAA0A"),
|
||||
"name": "pink",
|
||||
"primitive": 3,
|
||||
"uv_scale": Vector4(0, 0, 0, 0),
|
||||
"vertex_count": 24,
|
||||
"vertex_data": PackedByteArray("/98AAP/fAAD/31TV/98AAP//AAD/fwAA//9U1f9/AAD/fwAA//8AAP9/VNX//wAA/x8AAP/fAAD/H1TV/98AAAAAVNX/fwAAAAAAAP9/AAD/fwAAAAAAAP/fAAD/HwAA/39U1QAAAAD/31TV/x8AAP8fAAD/HwAA/x9U1f8fAAD/H////38AAP83////NwAA/zf////HAAD/f////98AAP9/////HwAA/8f////HAAD/x////zcAAP/f////fwAA")
|
||||
}]
|
||||
blend_shape_mode = 0
|
||||
|
||||
[sub_resource type="ArrayMesh" id="ArrayMesh_mt61o"]
|
||||
resource_name = "foamBulletB-low_Mesh foamBulletB-low"
|
||||
_surfaces = [{
|
||||
"aabb": AABB(-0.01, -0.03, -0.01, 0.02, 0.06, 0.02),
|
||||
"attribute_data": PackedByteArray("SEP//0hDeYx5qP//eah5jLa8//+FV///trx5jIVXeYx5qP//SEP//3moeYxIQ3mMtrx5jLa8//+FV3mMhVf//3mo//9IQ///eah5jEhDeYz//666/98CzP/fWqn/f5Sj/3/J0f8fWqn/HwLMAACuunmoeYx5qP//SEN5jEhD//+2vP//hVf//7a8eYyFV3mMhVf//4VXeYy2vP//trx5jP8frrr/N6+t/zetx/9/Asz/f1qp/8etx//Hr63/3666eajHkkhDx5Jangh7dlIIe7a8x5KFV8eSiK0Ie6RhCHt5qMeSSEPHklqeCHt2Ugh7Wp4Ie3mox5J2Ugh7SEPHkra8x5KFV8eSiK0Ie6RhCHu2vMeShVfHkoitCHukYQh7SEPHknZSCHt5qMeSWp4Ie4VXx5KkYQh7trzHkoitCHs="),
|
||||
"format": 34896613399,
|
||||
"index_count": 66,
|
||||
"index_data": PackedByteArray("AgAAAAEAAQADAAIABgAEAAUABQAHAAYACgAIAAkACQALAAoADgAMAA0ADQAPAA4AEgAQABEAEQATABIAFgAUABUAFQAXABYAFQAYABcAGAAZABcAGAAaABkAGgAbABkAHgAcAB0AHQAfAB4AIgAgACEAIQAjACIAJgAkACUAJQAnACYA"),
|
||||
"material": SubResource("StandardMaterial3D_030fb"),
|
||||
"name": "yellow",
|
||||
"primitive": 3,
|
||||
"uv_scale": Vector4(0.787402, 4.3622, 0, 0),
|
||||
"vertex_count": 80,
|
||||
"vertex_data": PackedByteArray("/98AAP/f/5//31TV/9//n///AAD/f/+///9U1f9//7//3wAA/9//n/9/AAD//wCA/99U1f/f/5//f1TV//8AgP9/AAD//wCA/x8AAP/f/5//f1TV//8AgP8fVNX/3/+f/x9U1f/f/5//HwAA/9//nwAAVNX/f/+/AAAAAP9//7//fwAAAAD////fAAD/H//f/39U1QAA////31TV/x//3wAAAAD/f////x8AAP/f////HwAA/x////9/AAAAAP///38AAP//////3wAA/x/////fAAD/3/////8AAP9///8AAFTV/3//vwAAAAD/f/+//x9U1f8f/9//HwAA/x//3/8fAAD/H//f/38AAAAA////H1TV/x//3/9/VNUAAP////8AAP9//7///1TV/3//v//fAAD/H//f/99U1f8f/9//H////39U1f83////N1TV/zf////HVNX/f////99U1f9/////H1TV/8f////HVNX/x////zdU1f/f////f1TV/39U1QAA8vj/31TV/x/C6P9/////H/L4/8f///83wuj/H1TV/x/C6P9/VNUAAPL4/zf///83wuj/f////x/y+P9/VNX//5rA/x9U1f/feMb/f////9+awP83////x3jG/x////9/VNUAAFTV/39U1f83////N8Lo/x9U1f8fwuj/H1TV/994xgAAVNX/f1TV/zf////HeMb/H////39U1f/fVNX/33jG/39U1f//msD/x////8d4xv9/////35rA/99U1f/feMb/x////8d4xv//VNX/f1TV/9////9/VNX//1TV/39U1f/f////f1TV/99U1f8fwuj/x////zfC6P9/AAD/fwAA/38AAP9/AAD/fwAA/3////9/AAD/f////3////9/////f////3////9/////f////3////9/////f////38AAP9/////fwAA/3//P/9//z//f/8//3//P/9//z//f/8//3//P/9//z//f////3////9/////f////3////9/////f////3////9/AAD/fwAA/38AAP9/AAD///9/////f////3////9/////f////3////9/////f////3/oougi////f+ii6CLoohbd////f+iiFt3///9/////f+iiFt3///9/6KIW3ZmZZeaZmWXm6KIW3eiiFt3oohbdmZll5uiiFt2ZmWXm6KLoIv///3/oougi////f+ii6CLoougimZmZGZmZmRmZmZkZmZmZGeii6CLoougi")
|
||||
}, {
|
||||
"aabb": AABB(-0.01, -0.03, -0.01, 0.02, 0.06, 0.02),
|
||||
"attribute_data": PackedByteArray("SEP//0hDeYx5qP//eah5jLa8//+FV///trx5jIVXeYx5qP//SEP//3moeYxIQ3mMtrx5jLa8//+FV3mMhVf//3mo//9IQ///eah5jEhDeYz//666/98CzP/fWqn/f5Sj/3/J0f8fWqn/HwLMAACuunmoeYx5qP//SEN5jEhD//+2vP//hVf//7a8eYyFV3mMhVf//4VXeYy2vP//trx5jP8frrr/N6+t/zetx/9/Asz/f1qp/8etx//Hr63/3666eajHkkhDx5Jangh7dlIIe7a8x5KFV8eSiK0Ie6RhCHt5qMeSSEPHklqeCHt2Ugh7Wp4Ie3mox5J2Ugh7SEPHkra8x5KFV8eSiK0Ie6RhCHu2vMeShVfHkoitCHukYQh7SEPHknZSCHt5qMeSWp4Ie4VXx5KkYQh7trzHkoitCHs="),
|
||||
"format": 34896613399,
|
||||
"index_count": 66,
|
||||
"index_data": PackedByteArray("KgAoACkAKQArACoAKQAsACsALAAtACsALAAuAC0ALgAvAC0AMgAwADEAMQAzADIANgA0ADUANQA3ADYAOgA4ADkAOQA7ADoAPgA8AD0APQA/AD4AQgBAAEEAQQBDAEIARgBEAEUARQBHAEYASgBIAEkASQBLAEoATgBMAE0ATQBPAE4A"),
|
||||
"material": SubResource("StandardMaterial3D_urtjv"),
|
||||
"name": "pink",
|
||||
"primitive": 3,
|
||||
"uv_scale": Vector4(0.787402, 4.3622, 0, 0),
|
||||
"vertex_count": 80,
|
||||
"vertex_data": PackedByteArray("/98AAP/f/MX/31TV/9/8xf//AAD/f1TV//9U1f9/VNX/3wAA/9/8xf9/AAD//////99U1f/f/MX/f1TV//////9/AAD//////x8AAP/f/MX/f1TV//////8fVNX/3/zF/x9U1f/f/MX/HwAA/9/8xQAAVNX/f1TVAAAAAP9/VNX/fwAAAAD////fAAD/H6rp/39U1QAA////31TV/x+q6QAAAAD/f1TV/x8AAP/fVNX/HwAA/x9U1f9/AAAAAFTV/38AAP//VNX/3wAA/x9U1f/fAAD/31TV//8AAP9/VNUAAFTV/39U1QAAAAD/f1TV/x9U1f8fqun/HwAA/x+q6f8fAAD/H6rp/38AAAAA////H1TV/x+q6f9/VNUAAP////8AAP9/VNX//1TV/39U1f/fAAD/H6rp/99U1f8fqun/H////3//v/83////N/+//zf////H/7//f////9//v/9/////H/+//8f////H/7//x////zf/v//f////f/+//39U1QAAmP//31TV/x8E4f9/////H5j//8f///83BOH/H1TV/x8E4f9/VNUAAJj//zf///83BOH/f////x+Y//9/VNX//4CK/x9U1f/fWKH/f////9+Aiv83////x1ih/x////9/EcEAAFTV/38Rwf83////N63f/x9U1f8frd//H1TV/9+cogAAVNX/f0XA/zf////HnKL/H////39FwP/fVNX/31ih/39U1f//gIr/x////8dYof9/////34CK/99U1f/fnKL/x////8ecov//VNX/f0XA/9////9/RcD//1TV/38Rwf/f////fxHB/99U1f8frd//x////zet3/9/AAD/fwAA/38AAP9/AAD/fwAA/3//f/9/AAD/f/9//3//f/9/////f/9//3////9/////f////3////9/////f////38AAP9/////fwAAAAD/fwAA/38AAP9/AAD/fwAA/38AAP9/AAD/fwAA/3//f////3////9/////f////3////9/////f////3////9/AAD/fwAA/38AAP9/AAD+//9/////f/7//3////9/////f////3/+//9//v//f/uRcATFbRMB+5FwBMVtEwHsfsXtcIQD7ux+xe1whAPujd3Ggz6l8tyN3caDPqXy3HaIzuZ2iM7mYIyz6GCMs+iklPLT2pCb5qSU8tPakJvmDKM/JTj8jV0Moz8lOPyNXQuspBQLrKQUY5nbEGOZ2xAvmXcIL5l3CEqXYAxKl2AM")
|
||||
}]
|
||||
blend_shape_mode = 0
|
||||
shadow_mesh = SubResource("ArrayMesh_iqbgl")
|
||||
|
||||
[sub_resource type="CylinderShape3D" id="CylinderShape3D_r5ds2"]
|
||||
height = 0.202453
|
||||
radius = 0.0673677
|
||||
|
||||
[node name="RigidBody3D" type="RigidBody3D"]
|
||||
gravity_scale = 0.0
|
||||
continuous_cd = true
|
||||
contact_monitor = true
|
||||
max_contacts_reported = 5
|
||||
|
||||
[node name="foamBulletB-low" type="MeshInstance3D" parent="."]
|
||||
transform = Transform3D(10, 0, 0, 0, -4.37114e-07, 10, 0, -10, -4.37114e-07, 0, 0, 0)
|
||||
mesh = SubResource("ArrayMesh_mt61o")
|
||||
skeleton = NodePath("")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, -4.37114e-08, -1, 0, 1, -4.37114e-08, 0, 0, 0)
|
||||
shape = SubResource("CylinderShape3D_r5ds2")
|
||||
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
BIN
Binary file not shown.
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user