34 Commits

Author SHA1 Message Date
shawn db477b4c48 Fix simulation_server_stub Entity class conflict + ServerConfig load timing
- Renamed inner class Entity→StubEntity (Godot has native Entity class)
- Added await ServerConfig.config_loaded before reading map_list in server_main.gd
- Config loads via call_deferred, so _ready() must wait for signal
2026-07-02 10:24:32 -04:00
shawn 4a5264c5b0 Fix headless class_name dependencies
- Replaced TeamData.Team type hints with int in all scripts
- Added explicit preloads for headless mode class resolution
- Created stub LagCompensation and DamageProcessor scripts
- Fixed PluginManager, SpawnManager, EconomyManager, BuyZone,
  RoundManager, BuyMenuHandler, BombObjective class_name references
- Updated server config to match ServerConfig.gd format

Gray screen root cause: server scripts failed to parse in headless
mode due to Godot 4 class_name loading order (Resource after Node),
leaving server_main.gd non-functional — accepted connections but
never spawned players.
2026-07-02 10:09:28 -04:00
shawn ad48f38ca5 feat: integrate ChaffGames FPS template as local player controller
Replaces the overhead box-mesh view with a full FPS character:

- mouse look, WASD movement, jump (Space), crouch (C), sprint (Shift), lean (Q/E)

- 6 weapons (LMB shoot, R reload, scroll switch, F melee, G drop)

- Crosshair HUD, ammo counter, sprint bar

- Client sends position to server at 20Hz for multiplayer visibility

- Server broadcasts positions to all peers

- Remote players shown as boxes (placeholder)

source: https://godotengine.org/asset-library/asset/2652 (MIT license)
2026-07-02 00:26:45 -04:00
shawn f0c95dcfd2 fix: create local player on client so WASD movement works
client_main.gd: when server spawns our player, create a player node with authority

- Camera follows local player from 3/4 overhead view

- player.gd WASD input sends position to server via RPC

- Server validates and broadcasts to all peers
2026-07-02 00:15:08 -04:00
shawn d060ac449d fix: suppress 3 benign startup warnings on Windows client
- Create missing server/plugins/ dir with .gdignore (stops PluginManager warning)

- Create config/default_server_config.cfg (stops ServerConfig error)

- Reorder light look_at after add_child (stops 'not in tree' warning)

- Switch rendering_method from opengl3 to forward_plus (RTX 2080 Vulkan works fine; gray screen was server-scene issue not OpenGL)
2026-07-02 00:05:27 -04:00
shawn 87060b0b71 fix: move sub_resource tags before nodes in test_range.tscn (export parse fix) 2026-07-02 00:02:25 -04:00
shawn 638e109d8b fix: client_main.tscn overrides server_host to 68.202.6.107 2026-07-01 22:21:29 -04:00
shawn 4c0e334575 fix: lazy load scenes in entry.gd (no preload of server in client mode) 2026-07-01 22:04:41 -04:00
shawn e270f34150 fix: client_main defaults to remote server (68.202.6.107) 2026-07-01 22:04:03 -04:00
shawn 20ca43f928 fix: add smart entry scene (client vs server via headless detection) 2026-07-01 21:44:14 -04:00
shawn aae57c63bd fix: TeamManager signal type hints — use int instead of TeamData.Team (export parse order issue) 2026-07-01 21:41:36 -04:00
shawn 0994a55486 chore: clean up duplicate .gdextension files (keep only gdextension/simulation.gdextension) 2026-07-01 21:38:46 -04:00
shawn 82216bfa64 fix: Windows client compile errors — get_world_3d on Node, array type mismatch, null plugin guard, maps format 2026-07-01 21:38:07 -04:00
shawn d5098c61e1 fix: switch to OpenGL 3 renderer for Windows compatibility (gray screen fix) 2026-07-01 21:16:26 -04:00
shawn 194aad8f83 t_p2_bomb: Bomb/defuse objective system for search & destroy rounds
Adds:
- bomb_objective.gd: server-authoritative bomb state machine (IDLE→PLANTED→EXPLODED/DEFUSED)
  - plant_bomb(player_id, pos) - T-side only, LIVE phase, inside bombsite
  - defuse_bomb(player_id) - CT-side only, near bomb, 5s timer
  - cancel_defuse(player_id) - if defuser moves or is killed
  - bomb_explode() - configurable radius damage (10m lethal, 15m half)
  - 40s bomb timer (configurable)
  - Full query API: is_bomb_planted(), get_bomb_position(), get_bomb_timer()
  - Signals: bomb_planted, bomb_defused, bomb_exploded, defuse_started, defuse_cancelled
  - Connected to RoundManager via GameServer for round-end outcomes

- bomb_carrier.gd: client-side bomb carrier UI
  - Bomb status/defuse progress UI updates
  - Direction indicator to planted bomb
  - "Hold E to defuse" prompt for CT near bomb
  - "Hold E to plant" prompt for T with bomb in bombsite

- test_range.tscn: BombsiteA (south-west, -20,0,-5) and BombsiteB (north-east, 15,0,20)
  - 8x4m Area3D zones with BoxShape3D collision
  - PlantPosition Marker3D children
  - Group 'bomb_sites' for server discovery

- game_server.gd: BombObjective integration in _ready()
  - Creates and wires BombObjective to RoundManager, TeamManager, DamageProcessor
  - Registers bomb sites from scene tree
  - Connects bomb_exploded/bomb_defused → RoundManager.end_round()
  - Connects round_ended → bomb.reset()
2026-07-01 20:39:17 -04:00
shawn 222dcaebb3 t_p4_anticheat: add basic server-side anti-cheat controller
- Rate limiting: flags players exceeding 130Hz input RPC rate
- Speed hacks: validates position deltas against max speed * 1.5
- Aimbot snap: flags look-direction changes >45° in one tick
- Suspicion level tracking (0.0-1.0) with decay over time
- Escalating thresholds: 0.3=warn(log), 0.6=kick(signal), 0.8=ban(signal, 1h temp)
- Configurable max speed via set_max_speed()
- Ban handler Callable for caller-defined disconnect logic
- Design: generous tolerances, near-zero false positives, Phase-4 basic hardening
2026-07-01 20:37:20 -04:00
shawn 0aa29d33e4 fix: GDExtension C++ build — fix enum bitwise cast, add .gdextension entry file 2026-07-01 20:34:15 -04:00
shawn 6a08487c4c t_p2_buy: add buy menu and economy system
Creates the full buy/economy subsystem for a tactical FPS:

- EconomyManager (server/scripts/economy/economy_manager.gd): per-player
  money tracking with starting 00, kill/round/objective rewards, spending
  validation, escalating loss bonuses, and money_changed signal.

- BuyMenuHandler (server/scripts/economy/buy_menu_handler.gd): server-side
  RPC validation of purchase requests — checks weapon validity, buy-zone
  membership (scans BuyZone nodes in scene tree), affordability, then
  deducts money and grants weapon via WeaponServer.give_weapon().

- BuyMenu (client/characters/weapon/buy_menu.gd): client-side Control UI
  with styled panel, money display, weapon grid (name/cost/stats), numeric
  key (1-4) and mouse-click purchase, server-driven affordability, and
  auto-close after purchase.

- GameServer integration: creates EconomyManager + BuyMenuHandler in
  _ready(), registers players for economy on spawn, awards kill rewards
  via DamageProcessor.player_killed signal, and unregisters on despawn.

Economy rules: win=+,250, loss=,900 (+00/streak, cap ,900),
kill=+00, bomb_plant/defuse=+00. Weapon costs: pistol=00,
smg=,200, rifle=,700, shotgun=,900.
2026-07-01 20:31:25 -04:00
shawn f20d532add t_p1_round: Add round/match lifecycle system (RoundManager)
- New server/scripts/round/round_manager.gd with:
  - RoundPhase enum (WARMUP, PREP, LIVE, POST)
  - Round life cycle: warmup -> prep (15s) -> live (105s) -> post (8s)
  - Team elimination detection using entity->peer->team mappings
  - Time-expired round ending (default: CT wins)
  - Economy hooks: win money (250), loss money (900 escalating to 900), kill bonus (00)
  - All requested signals: round_phase_changed, round_started, round_ended, match_point, warmup_ended, round_economy_data

- Updated GameServer:
  - Creates and wires RoundManager in _ready()
  - Connects DamageProcessor.player_killed -> round tracking + elimination detection
  - Added peer_to_entity mapping for backwards entity lookup
  - Added RCON command handler (_on_rcon_command) for start_match/end_round

- Updated RCON command handler:
  - Added start_match and end_round to dispatch list and help text
2026-07-01 20:30:01 -04:00
shawn 705b068ed2 t_p1_hitscan: Add lag-compensated hitscan weapon system
NEW: server/scripts/combat/lag_compensation.gd
  - 128-entry ring buffer of player positions per server tick
  - rewind_and_raycast(tick, origin, direction, range, exclude):
    saves current positions, rewinds to tick-time positions,
    performs PhysicsDirectSpaceState3D.intersect_ray(), restores
  - Falls back to normal raycast if history missing for tick
  - shot_processed signal

NEW: scripts/combat/damage_processor.gd
  - Processes WeaponServer hit results: applies damage, tracks kills
  - Optional distance-based damage falloff
  - player_damaged / player_killed signals
  - Health and kill-count queries

MODIFY: server/scripts/weapons/weapon_server.gd
  - fire() now takes tick parameter: fire(tick, player_id, ...)
  - _perform_hitscan() uses LagCompensation.rewind_and_raycast()
    when lag_compensation reference is set and tick >= 0
  - Non-compensated fallback path preserved

MODIFY: server/scripts/game_server.gd
  - Adds _current_tick counter, incremented each simulation tick
  - Creates WeaponServer + LagCompensation + DamageProcessor as children
  - record_tick() called before each tick's simulation
  - register/unregister_player_node() for player lifecycle
  - _on_player_killed() relays kill events via player_damaged signal

MODIFY: scripts/network/server_main.gd
  - _spawn_player() registers node with GameServer.register_player_node()
  - _despawn_player() unregisters via unregister_player_node()
2026-07-01 20:26:18 -04:00
shawn 3465922be4 t_p6_browser: add workshop map browser with voting (3 files)
- workshop_browser.gd: scans server maps dir for .pck/.tscn files
- map_browser_rpc.gd: client-server RPC bridge for map list/change
- map_vote.gd: simple >50% majority vote with 30s timeout

File scope:
  server/scripts/browser/workshop_browser.gd (NEW)
  scripts/network/map_browser_rpc.gd (NEW)
  server/scripts/browser/map_vote.gd (NEW)
2026-07-01 20:24:23 -04:00
shawn 46ff83325f t_p2: Add team, spawn, and buy-zone system
- team_data.gd: Team enum (SPECTATOR/CT/Terrorist), constants for names,
  colors, starting money, and spawn groups; TeamData resource class
- team_manager.gd: Player-to-team assignment, auto-balancing, score
  tracking; round-independent (persists across rounds)
- spawn_manager.gd: Scans scene for Marker3D nodes in spawn_points_ct
  and spawn_points_t groups; selects valid spawns (farthest from enemies
  with proximity check, fallback to first available)
- buy_zone.gd: Area3D-based trigger zone with team filtering, player
  enter/exit tracking and signals
- test_range.tscn: Added 2 CT spawn markers and 2 T spawn markers with
  appropriate groups
2026-07-01 20:24:16 -04:00
shawn f6d69545c9 feat: implement client-side prediction and reconciliation system
Add client-side prediction (t_p1_pred) with:

- scripts/network/snapshot.gd — lightweight snapshot resource with
  to_dict/from_dict serialization for RPC. Stores position (Vector3),
  rotation (Quaternion), velocity (Vector3), grounded (bool).

- scripts/network/client_prediction.gd — prediction/reconciliation
  controller. 64-entry ring buffer of local snapshots, sends inputs to
  server each physics tick (128Hz, ENet channel 0), detects misprediction
  on server state arrival, rewinds and re-applies unconfirmed inputs.
  Also supports remote player interpolation.

- scripts/network/network_manager.gd — new RPC endpoints for client
  prediction: send_client_input (client->server, ch 0) and
  send_server_state (server->client, ch 1). New signals for routing.

- client/characters/character/fps_character_controller.gd — prediction
  hooks in _physics_process: on_before_tick() captures pre-input
  snapshot, on_after_tick() sends input to server. Client prediction
  path uses local movement (instant feedback) instead of reading from
  SimulationServer entity.

Architecture:
  Each tick: client applies input → predicts new state locally
  → sends input to server → server returns authoritative state
  → client compares and reconciles if mismatch.
2026-07-01 20:21:16 -04:00
shawn 2452aba0d7 feat: implement weapon set system (t_p2_weapons)
- WeaponData resource class with configurable weapon properties and
  pre-configured constants for rifle, pistol, shotgun, and SMG
- WeaponDefinitions singleton/static registry with WEAPONS dictionary
- WeaponServer node for server-authoritative weapon logic including
  fire rate cooldowns, ammo tracking, reload state machine, and
  multi-pellet hit-scan raycasting
- Extended WeaponManager with weapon inventory array, weapon switching,
  per-weapon state persistence, fire_animation_triggered signal
2026-07-01 20:16:22 -04:00
shawn 3c3251fa7a feat: add t_p1_greybox — greybox test map scene
Creates test_range.tscn under client/assets/maps/test_range/ as a simple
open-area test map for validating movement, shooting, and networking.

Layout:
- 80×80 open area with grey floor (CSGBox3D, 12 tris)
- Low platform (center, 1 unit high, gold)
- Medium platform (east, 2 units high, gold)
- High platform (north-west, 3 units high, red)
- Ramp (south, tilted ~20°, leads ground to low platform top)
- Enclosure/room (east, 12×12 interior, 4-unit walls, south doorway)
- SpawnPoint (Marker3D) with group 'spawn_points'
- DirectionalLight3D (sun) + WorldEnvironment
- All geometry uses Solid-color StandardMaterial3D placeholders
- Collision enabled on all CSGBox3D shapes
- ~132 tris total (well under 500 budget)
2026-07-01 20:15:34 -04:00
shawn d02b112d99 Phase 7: test server deployment + Windows export
- Fix: export_presets.cfg — platform='Windows Desktop' (correct name)
- Fix: server_main.gd — formatting bug on lag-comp string
- Fix: game_server.gd — auto-detect GDExtension, fall back to GDScript stub
- Add: server/scripts/simulation_server_stub.gd — lightweight SimulationServer in GDScript
- Add: docs/playtest-guide.md — connection instructions, control bindings, server commands
- Add: systemd user service — tactical-shooter-server (enabled, running)
- Add: server config at ~/tactical-shooter-server/server_config.cfg
- Build: Windows 109MB PE32+ client (build/tactical-shooter-windows-x86_64/)
- Build: Linux server binary (78MB) — running on oplabs:34197
- Temporarily disabled GDExtension (needs C++ build fix in entity.cpp enum casting)
2026-07-01 20:02:05 -04:00
shawn 2582cb1b0d Phase 7: Windows export preset fix + code fixes
- Fix: export_presets.cfg — platform='Windows Desktop' (not 'Windows'), Windows as [preset.0]
- Fix: plugin_manager.gd — removed class_name (autoload conflict), fixed multiline % formatting
- Disable GDExtension temporarily for clean export
- Add build/ to .gitignore (binary artifacts)
- Build artifacts: tactical-shooter.exe (109MB Windows PE32+), windows.zip, server binary (78MB)
2026-07-01 19:47:12 -04:00
shawn 9ea98aa7b8 Phase 7: Windows client export, preset fix, code fixes
- Fix: export_presets.cfg — platform='Windows Desktop' (not 'Windows'), Windows as [preset.0]
- Fix: plugin_manager.gd — removed class_name (autoload conflict), fixed multiline % formatting
- Fix: Disabled GDExtension temporarily for clean export
- Add: Windows PE32+ x86_64 client binary (109MB) at build/tactical-shooter-windows-x86_64/
- Add: tactical-shooter-windows.zip (portable zip package)
- Build: Linux server binary (78MB) rebuilt
2026-07-01 19:47:12 -04:00
shawn e385eae0f5 feat: performance budget profiling setup
- docs/performance-budget.md: 60fps/2GB VRAM budget with CPU, GPU,
  memory, and draw-call targets (P95<16.6ms, P99<50ms)
- scripts/profiling/: SceneTree + Node profilers, test scenes
- scripts/generate_occluders.gd: auto-generate OccluderInstance3D
  from CSG wall pieces (128^3 voxel occlusion culling)
- scripts/convert_csg_to_mesh.gd: CSG->StaticMesh baker with LOD
  slots (LOD1@15m, LOD2@30m)
- project.godot: occlusion culling + LOD settings enabled
- modular scene UV2 data from lightmapping pass
- rcon_command_handler.gd conflict resolved (kept upstream plugin cmd)
- Fixed profiler_node.gd warmup frame bug (60 frames, not 3)
- Fixed convert_csg_to_mesh.gd LOD API (add_lod + distance)
2026-07-01 19:01:03 -04:00
shawn 34507f9043 docs: community mapmaking documentation (7-file SDK guide)
- docs/mapmaking/00-index.md — SDK overview with workflow diagram
- docs/mapmaking/01-getting-started.md — template setup & first map
- docs/mapmaking/02-building-geometry.md — CSG guide & prefab reference
- docs/mapmaking/03-lighting-and-env.md — LightmapGI baking guide
- docs/mapmaking/04-validation.md — validator CLI & CI/CD usage
- docs/mapmaking/05-packaging-and-shipping.md — .pck pipeline
- docs/mapmaking/06-faq-and-troubleshooting.md — 30+ common issues
- README.md — add mapmaking SDK link to features list
2026-07-01 18:47:06 -04:00
shawn 159c554a86 t_p6_api: build plugin scripting system
- Add PluginBase (plugin_base.gd) — abstract base class with 12 virtual hooks
- Add PluginManifest (plugin_manifest.gd) — custom Resource for plugin metadata
- Add PluginManager (plugin_manager.gd) — autoload singleton: directory scan,
  hook dispatch, lifecycle (load/unload/reload/rescan), cvar integration
- Add hello_world example plugin with all hooks demonstrating usage
- Register PluginManager autoload in project.godot
- Extend rcon_command_handler.gd with 'plugin' subcommand (list/load/unload/
  reload/info/rescan) delegating to PluginManager
2026-07-01 18:36:23 -04:00
shawn ffa72a8f24 t_p5_validator: add 5 Godot 4 map validator scripts
- validate_map.gd — scene structure validator (CSG combiner root, spawn groups,
  bomb sites, buy zones, map bounds, LightmapGI config, lighting setup)
- validate_polycount.gd — CSG triangle budget checker
  (<50K faces, <5K per node, <200 CSG nodes)
- validate_textures.gd — texture size ≤1K, mipmaps, UV2 checker
- validate_lights.gd — dynamic lights ≤4, bake mode validator
- bake_lightmaps.gd — LightmapGI config checker
  (quality, bounces, texel_scale, interior, denoiser)

All scripts: @tool SceneTree, accept scene path via -- separator,
exit 0=pass / 1=fail / 2=scene-not-found. 443-610 lines each.
2026-07-01 18:35:52 -04:00
shawn 589b90d886 fix: FPS character controller review fixes
- Camera: Make view bob additive to crouch eye height (was overwriting position.y)
- Camera: Sustain sprint FOV kick while sprinting (was recovering immediately)
- Camera: Remove unused _was_sprinting variable
- Controller: Use just_pressed for jump (was sending every frame while held)
- Controller: Direct input dict assignment instead of merge() to avoid alloc
- Controller: Add clearer comments on server-authoritative readback mode
2026-07-01 18:35:16 -04:00
shawn e9dc05983c Save workspace artifacts: character-controller, gdextension scaffold, network scripts, map-pipeline, server config
Includes code from task workspaces that was never pushed:
- client/characters/ — fps_character_controller.gd (400 lines), fps_camera.gd, input_handler.gd
- gdextension/simulation/ — C++ GDExtension scaffold: entity, movement, hit detection, simulation server, state serializer, bitstream (14 files)
- scripts/network/ — ENet-based network_manager.gd, server/client_main.gd, player.gd
- scripts/map_packaging/ — PCK pipeline: pack_map.gd, map_downloader.gd, map_registry_server.py, README
- scripts/config/ — server_config.gd (480 lines)
- scenes/ — client_main, server_main, player, test_range
- project.godot, export_presets.cfg
2026-07-01 18:30:44 -04:00
257 changed files with 382647 additions and 24 deletions
+2
View File
@@ -27,3 +27,5 @@ server/build/
# Secrets (operator must create per deployment) # Secrets (operator must create per deployment)
server/data/rcon_password.cfg server/data/rcon_password.cfg
build/
build/
+2 -1
View File
@@ -5,6 +5,7 @@ A competitive, round-based tactical FPS built in Godot 4.
- 128-tick authoritative dedicated servers - 128-tick authoritative dedicated servers
- GDExtension (C++) simulation core - GDExtension (C++) simulation core
- Community-hostable, self-hosted server browser - 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 [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] [gd_scene load_steps=2 format=3]
[ext_resource type="Material" path="res://assets/materials/accent_team_blue.tres" id="1"] [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) size = Vector3(1.28, 0.64, 0.08)
material = ExtResource("1") material = ExtResource("1")
use_collision = true 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] [gd_scene load_steps=2 format=3]
[ext_resource type="Material" path="res://assets/materials/accent_team_red.tres" id="1"] [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) size = Vector3(1.28, 0.64, 0.08)
material = ExtResource("1") material = ExtResource("1")
use_collision = true use_collision = true
visibility_range_end = 20.0
visibility_range_end_margin = 2.0
+160
View File
@@ -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
+3 -1
View File
@@ -1,7 +1,9 @@
[gd_scene load_steps=2 format=3] [gd_scene load_steps=2 format=3]
[ext_resource type="Material" path="res://assets/materials/metal_structural_01.tres" id="1"] [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) size = Vector3(3.84, 0.32, 0.32)
material = ExtResource("1") material = ExtResource("1")
use_collision = true 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] [gd_scene load_steps=2 format=3]
[ext_resource type="Material" path="res://assets/materials/floor_concrete_01.tres" id="1"] [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) size = Vector3(2.56, 0.08, 2.56)
material = ExtResource("1") material = ExtResource("1")
use_collision = true 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] [gd_scene load_steps=2 format=3]
[ext_resource type="Material" path="res://assets/materials/floor_tile_01.tres" id="1"] [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) size = Vector3(2.56, 0.08, 2.56)
material = ExtResource("1") material = ExtResource("1")
use_collision = true 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)
+3 -1
View File
@@ -1,7 +1,9 @@
[gd_scene load_steps=2 format=3] [gd_scene load_steps=2 format=3]
[ext_resource type="Material" path="res://assets/materials/wall_concrete_02.tres" id="1"] [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) size = Vector3(0.64, 2.56, 0.64)
material = ExtResource("1") material = ExtResource("1")
use_collision = true 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] [gd_scene load_steps=2 format=3]
[ext_resource type="Material" path="res://assets/materials/wall_concrete_01.tres" id="1"] [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 use_collision = false
visibility_range_end = 50.0
visibility_range_end_margin = 5.0
[node name="WallA" type="CSGBox3D" parent="WallCorner01"] [node name="WallA" type="CSGBox3D" parent="WallCorner01"]
operation = 0 operation = 0
size = Vector3(2.56, 2.56, 0.16) size = Vector3(2.56, 2.56, 0.16)
@@ -1,8 +1,10 @@
[gd_scene load_steps=2 format=3] [gd_scene load_steps=2 format=3]
[ext_resource type="Material" path="res://assets/materials/wall_concrete_01.tres" id="1"] [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 use_collision = false
visibility_range_end = 40.0
visibility_range_end_margin = 5.0
[node name="MainWall" type="CSGBox3D" parent="WallDoorway01"] [node name="MainWall" type="CSGBox3D" parent="WallDoorway01"]
operation = 0 operation = 0
size = Vector3(2.56, 2.56, 0.16) size = Vector3(2.56, 2.56, 0.16)
@@ -1,7 +1,9 @@
[gd_scene load_steps=2 format=3] [gd_scene load_steps=2 format=3]
[ext_resource type="Material" path="res://assets/materials/wall_concrete_01.tres" id="1"] [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) size = Vector3(0.32, 2.56, 0.16)
material = ExtResource("1") material = ExtResource("1")
use_collision = true 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] [gd_scene load_steps=2 format=3]
[ext_resource type="Material" path="res://assets/materials/wall_concrete_01.tres" id="1"] [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) size = Vector3(2.56, 2.56, 0.16)
material = ExtResource("1") material = ExtResource("1")
use_collision = true 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] [gd_scene load_steps=2 format=3]
[ext_resource type="Material" path="res://assets/materials/wall_concrete_01.tres" id="1"] [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 use_collision = false
visibility_range_end = 40.0
visibility_range_end_margin = 5.0
[node name="MainWall" type="CSGBox3D" parent="WallWindow01"] [node name="MainWall" type="CSGBox3D" parent="WallWindow01"]
operation = 0 operation = 0
size = Vector3(2.56, 2.56, 0.16) size = Vector3(2.56, 2.56, 0.16)
+119
View File
@@ -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.10.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
+220
View File
@@ -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
+59
View File
@@ -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
+445
View File
@@ -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()
+1
View File
@@ -0,0 +1 @@
uid://cdsyn134gsca8
+348
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
uid://q8nutj781fyf
+610
View File
@@ -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.54.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"
+1
View File
@@ -0,0 +1 @@
uid://d0mbp6gs2mwlr
+198
View File
@@ -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
+138
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
uid://pauclwpn265a
+170
View File
@@ -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
+147
View File
@@ -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")
+21
View File
@@ -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
+14
View File
@@ -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")
-6
View File
@@ -1,6 +0,0 @@
extends Node
func _ready():
print("Hello from Godot headless script!")
print("Testing: ", Engine.get_version_info())
get_tree().quit(0)
+1
View File
@@ -0,0 +1 @@
uid://dbtpbhja3nk60
+443
View File
@@ -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("")
+1
View File
@@ -0,0 +1 @@
uid://cwvi2ic1k6ubn
+511
View File
@@ -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("")
+1
View File
@@ -0,0 +1 @@
uid://olpdh1gx2gca
+465
View File
@@ -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("")
+1
View File
@@ -0,0 +1 @@
uid://bqau1kivb87fk
+1
View File
@@ -0,0 +1 @@
uid://d0go24t1lsx2j
+581
View File
@@ -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("")
+1
View File
@@ -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")
@@ -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")
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
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