diff --git a/.gitignore b/.gitignore
index 73161f4..a8a84c5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,9 +1,12 @@
-# Compiled genesis binary
-/bin/genesis
+# Compiled binaries
+/bin/
# C build objects
/obj/
+# Generated maps
+/demo/maps/
+
# Godot GDExtension build artifacts
/godot/build/
/demo/addons/brogue_gen/
diff --git a/Makefile b/Makefile
index 8a028a0..f70ff96 100644
--- a/Makefile
+++ b/Makefile
@@ -2,11 +2,35 @@ CC := cc
CFLAGS := -std=c99 -Wall -Wpedantic -Werror=implicit -Wmissing-prototypes -O2 -g -Isrc
LDFLAGS :=
-GEN_SRC := $(wildcard src/gen/*.c)
-GEN_OBJ := $(GEN_SRC:src/%.c=obj/%.o)
+GEN_SRC := $(wildcard src/gen/*.c)
+GEN_OBJ := $(GEN_SRC:src/%.c=obj/%.o)
-.PHONY: all clean genesis test stress godot godot-clean
-all: genesis
+BLOBBER_SRC := $(wildcard src/blobber/*.c)
+BLOBBER_OBJ := $(BLOBBER_SRC:src/%.c=obj/%.o)
+
+MESH_SRC := $(wildcard src/mesh/*.c)
+MESH_OBJ := $(MESH_SRC:src/%.c=obj/%.o)
+
+.PHONY: all clean genesis genesis3d test stress godot godot-clean map tb-sync
+all: genesis genesis3d
+
+# --- TrenchBroom round-trip helpers -----------------------------------------
+# Override on the command line: `make map SEED=99 DEPTH=30 LEVELS=4`.
+GENERATOR ?= brogue
+SEED ?= 42
+DEPTH ?= 20
+LEVELS ?= 1
+MAP_OUT ?= $(abspath demo/maps/generated.map)
+TB_GAME_DIR ?= $(HOME)/.TrenchBroom/games/brogue-genesis
+
+map:
+ @mkdir -p $(dir $(MAP_OUT))
+ godot --headless --path demo --script scripts/export_map.gd -- \
+ --generator $(GENERATOR) --seed $(SEED) --depth $(DEPTH) \
+ --levels $(LEVELS) --out $(MAP_OUT)
+
+tb-sync:
+ godot --headless --path demo --script scripts/export_tb_config.gd -- $(TB_GAME_DIR)
test: bin/genesis
@bash test/run_tests.sh
@@ -20,12 +44,17 @@ godot:
godot-clean:
@rm -rf godot/build godot/godot-cpp/bin demo/addons/brogue_gen
-genesis: bin/genesis
+genesis: bin/genesis
+genesis3d: bin/genesis3d
bin/genesis: $(GEN_OBJ) obj/genesis_main.o obj/json_emit.o
@mkdir -p bin
$(CC) $(LDFLAGS) -o $@ $^ -lm
+bin/genesis3d: $(GEN_OBJ) $(BLOBBER_OBJ) $(MESH_OBJ) obj/genesis3d_main.o
+ @mkdir -p bin
+ $(CC) $(LDFLAGS) -o $@ $^ -lm
+
obj/%.o: src/%.c
@mkdir -p $(dir $@)
$(CC) $(CFLAGS) -c $< -o $@
@@ -34,6 +63,10 @@ obj/genesis_main.o: src/genesis_main.c
@mkdir -p $(dir $@)
$(CC) $(CFLAGS) -c $< -o $@
+obj/genesis3d_main.o: src/genesis3d_main.c
+ @mkdir -p $(dir $@)
+ $(CC) $(CFLAGS) -c $< -o $@
+
obj/json_emit.o: src/json_emit.c src/json_emit.h
@mkdir -p $(dir $@)
$(CC) $(CFLAGS) -c $< -o $@
diff --git a/demo/actor.gd b/demo/actor.gd
new file mode 100644
index 0000000..67e954f
--- /dev/null
+++ b/demo/actor.gd
@@ -0,0 +1 @@
+extends Node3D
diff --git a/demo/actor.gd.uid b/demo/actor.gd.uid
new file mode 100644
index 0000000..890e25f
--- /dev/null
+++ b/demo/actor.gd.uid
@@ -0,0 +1 @@
+uid://lcahyros52n4
diff --git a/demo/addons/func_godot/LICENSE b/demo/addons/func_godot/LICENSE
new file mode 100644
index 0000000..a136146
--- /dev/null
+++ b/demo/addons/func_godot/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 func-godot
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/demo/addons/func_godot/fgd/cull_interior_faces.tres b/demo/addons/func_godot/fgd/cull_interior_faces.tres
new file mode 100644
index 0000000..63baa31
--- /dev/null
+++ b/demo/addons/func_godot/fgd/cull_interior_faces.tres
@@ -0,0 +1,15 @@
+[gd_resource type="Resource" script_class="FuncGodotFGDBaseClass" load_steps=2 format=3 uid="uid://bcdsueg5pysfq"]
+
+[ext_resource type="Script" uid="uid://ck575aqs1sbrb" path="res://addons/func_godot/src/fgd/func_godot_fgd_base_class.gd" id="1_21jph"]
+
+[resource]
+script = ExtResource("1_21jph")
+classname = "CullInteriorFaces"
+description = "Cull interior faces option for SolidClass Geometry"
+class_properties = Dictionary[String, Variant]({
+"_cull_interior_faces": false
+})
+class_property_descriptions = Dictionary[String, Variant]({
+"_cull_interior_faces": "If true, cull interior faces with matching vertices or faces that are flush within a larger face. Note: This has a performance impact that scales with how many brushes are in the brush entity."
+})
+metadata/_custom_type_script = "uid://cgkrrgcimlr8y"
diff --git a/demo/addons/func_godot/fgd/func_detail.tres b/demo/addons/func_godot/fgd/func_detail.tres
new file mode 100644
index 0000000..14ffcaa
--- /dev/null
+++ b/demo/addons/func_godot/fgd/func_detail.tres
@@ -0,0 +1,18 @@
+[gd_resource type="Resource" script_class="FuncGodotFGDSolidClass" load_steps=5 format=3 uid="uid://cxy7jnh6d7msn"]
+
+[ext_resource type="Script" uid="uid://5cow84q03m6a" path="res://addons/func_godot/src/fgd/func_godot_fgd_solid_class.gd" id="1_0fsmp"]
+[ext_resource type="Resource" uid="uid://nayxb8n7see2" path="res://addons/func_godot/fgd/phong_base.tres" id="1_c3bns"]
+[ext_resource type="Resource" uid="uid://doo4ly322b4jc" path="res://addons/func_godot/fgd/vertex_merge_distance_base.tres" id="2_c03gr"]
+[ext_resource type="Resource" uid="uid://bcdsueg5pysfq" path="res://addons/func_godot/fgd/cull_interior_faces.tres" id="3_wuxhx"]
+
+[resource]
+script = ExtResource("1_0fsmp")
+collision_shape_type = 2
+collision_mask = 0
+classname = "func_detail"
+description = "Static collidable geometry. Builds a StaticBody3D with a MeshInstance3D and a single concave CollisionShape3D. Does not occlude other VisualInstance3D nodes."
+base_classes = Array[Resource]([ExtResource("1_c3bns"), ExtResource("2_c03gr"), ExtResource("3_wuxhx")])
+meta_properties = Dictionary[String, Variant]({
+"color": Color(0.8, 0.8, 0.8, 1)
+})
+node_class = "StaticBody3D"
diff --git a/demo/addons/func_godot/fgd/func_detail_illusionary.tres b/demo/addons/func_godot/fgd/func_detail_illusionary.tres
new file mode 100644
index 0000000..25d9e0f
--- /dev/null
+++ b/demo/addons/func_godot/fgd/func_detail_illusionary.tres
@@ -0,0 +1,17 @@
+[gd_resource type="Resource" script_class="FuncGodotFGDSolidClass" load_steps=5 format=3 uid="uid://ch3e0dix85uhb"]
+
+[ext_resource type="Resource" uid="uid://nayxb8n7see2" path="res://addons/func_godot/fgd/phong_base.tres" id="1_ar63x"]
+[ext_resource type="Resource" uid="uid://doo4ly322b4jc" path="res://addons/func_godot/fgd/vertex_merge_distance_base.tres" id="2_j7vgq"]
+[ext_resource type="Script" uid="uid://5cow84q03m6a" path="res://addons/func_godot/src/fgd/func_godot_fgd_solid_class.gd" id="2_lhb87"]
+[ext_resource type="Resource" uid="uid://bcdsueg5pysfq" path="res://addons/func_godot/fgd/cull_interior_faces.tres" id="3_1mhrv"]
+
+[resource]
+script = ExtResource("2_lhb87")
+collision_shape_type = 0
+classname = "func_detail_illusionary"
+description = "Static geometry with no collision. Builds a Node3D with a MeshInstance3D. Does not occlude other VisualInstance3D nodes."
+base_classes = Array[Resource]([ExtResource("1_ar63x"), ExtResource("2_j7vgq"), ExtResource("3_1mhrv")])
+meta_properties = Dictionary[String, Variant]({
+"color": Color(0.8, 0.8, 0.8, 1)
+})
+node_class = "Node3D"
diff --git a/demo/addons/func_godot/fgd/func_geo.tres b/demo/addons/func_godot/fgd/func_geo.tres
new file mode 100644
index 0000000..e2c3d73
--- /dev/null
+++ b/demo/addons/func_godot/fgd/func_geo.tres
@@ -0,0 +1,19 @@
+[gd_resource type="Resource" script_class="FuncGodotFGDSolidClass" load_steps=5 format=3 uid="uid://b70vf4t5dc70t"]
+
+[ext_resource type="Resource" uid="uid://nayxb8n7see2" path="res://addons/func_godot/fgd/phong_base.tres" id="1_5mwee"]
+[ext_resource type="Script" uid="uid://5cow84q03m6a" path="res://addons/func_godot/src/fgd/func_godot_fgd_solid_class.gd" id="2_8o081"]
+[ext_resource type="Resource" uid="uid://doo4ly322b4jc" path="res://addons/func_godot/fgd/vertex_merge_distance_base.tres" id="2_bp8pb"]
+[ext_resource type="Resource" uid="uid://bcdsueg5pysfq" path="res://addons/func_godot/fgd/cull_interior_faces.tres" id="3_xnsya"]
+
+[resource]
+script = ExtResource("2_8o081")
+build_occlusion = true
+collision_shape_type = 2
+collision_mask = 0
+classname = "func_geo"
+description = "Static collidable geometry. Builds a StaticBody3D with a MeshInstance3D, a single concave CollisionShape3D, and an OccluderInstance3D."
+base_classes = Array[Resource]([ExtResource("1_5mwee"), ExtResource("2_bp8pb"), ExtResource("3_xnsya")])
+meta_properties = Dictionary[String, Variant]({
+"color": Color(0.8, 0.8, 0.8, 1)
+})
+node_class = "StaticBody3D"
diff --git a/demo/addons/func_godot/fgd/func_godot_fgd.tres b/demo/addons/func_godot/fgd/func_godot_fgd.tres
new file mode 100644
index 0000000..0f36d00
--- /dev/null
+++ b/demo/addons/func_godot/fgd/func_godot_fgd.tres
@@ -0,0 +1,15 @@
+[gd_resource type="Resource" script_class="FuncGodotFGDFile" load_steps=10 format=3 uid="uid://crgpdahjaj"]
+
+[ext_resource type="Script" uid="uid://drlmgulwbjwqu" path="res://addons/func_godot/src/fgd/func_godot_fgd_file.gd" id="1_axt3h"]
+[ext_resource type="Resource" uid="uid://nayxb8n7see2" path="res://addons/func_godot/fgd/phong_base.tres" id="1_ehab8"]
+[ext_resource type="Resource" uid="uid://doo4ly322b4jc" path="res://addons/func_godot/fgd/vertex_merge_distance_base.tres" id="2_7jebp"]
+[ext_resource type="Resource" uid="uid://bdji3873bg32h" path="res://addons/func_godot/fgd/worldspawn.tres" id="2_ri2rx"]
+[ext_resource type="Resource" uid="uid://b70vf4t5dc70t" path="res://addons/func_godot/fgd/func_geo.tres" id="3_7jigp"]
+[ext_resource type="Resource" uid="uid://cxy7jnh6d7msn" path="res://addons/func_godot/fgd/func_detail.tres" id="3_fqfww"]
+[ext_resource type="Resource" uid="uid://bcdsueg5pysfq" path="res://addons/func_godot/fgd/cull_interior_faces.tres" id="3_h5cmk"]
+[ext_resource type="Resource" uid="uid://dg5x44cc7flew" path="res://addons/func_godot/fgd/func_illusionary.tres" id="4_c4ucw"]
+[ext_resource type="Resource" uid="uid://ch3e0dix85uhb" path="res://addons/func_godot/fgd/func_detail_illusionary.tres" id="5_b2q3p"]
+
+[resource]
+script = ExtResource("1_axt3h")
+entity_definitions = Array[Resource]([ExtResource("1_ehab8"), ExtResource("2_7jebp"), ExtResource("3_h5cmk"), ExtResource("2_ri2rx"), ExtResource("3_7jigp"), ExtResource("3_fqfww"), ExtResource("5_b2q3p"), ExtResource("4_c4ucw")])
diff --git a/demo/addons/func_godot/fgd/func_illusionary.tres b/demo/addons/func_godot/fgd/func_illusionary.tres
new file mode 100644
index 0000000..3850780
--- /dev/null
+++ b/demo/addons/func_godot/fgd/func_illusionary.tres
@@ -0,0 +1,18 @@
+[gd_resource type="Resource" script_class="FuncGodotFGDSolidClass" load_steps=5 format=3 uid="uid://dg5x44cc7flew"]
+
+[ext_resource type="Resource" uid="uid://nayxb8n7see2" path="res://addons/func_godot/fgd/phong_base.tres" id="1_kv0mq"]
+[ext_resource type="Resource" uid="uid://doo4ly322b4jc" path="res://addons/func_godot/fgd/vertex_merge_distance_base.tres" id="2_hovr4"]
+[ext_resource type="Script" uid="uid://5cow84q03m6a" path="res://addons/func_godot/src/fgd/func_godot_fgd_solid_class.gd" id="2_uffhi"]
+[ext_resource type="Resource" uid="uid://bcdsueg5pysfq" path="res://addons/func_godot/fgd/cull_interior_faces.tres" id="3_woywv"]
+
+[resource]
+script = ExtResource("2_uffhi")
+build_occlusion = true
+collision_shape_type = 0
+classname = "func_illusionary"
+description = "Static geometry with no collision. Builds a Node3D with a MeshInstance3D and an Occluder3D to aid in render culling of other VisualInstance3D nodes."
+base_classes = Array[Resource]([ExtResource("1_kv0mq"), ExtResource("2_hovr4"), ExtResource("3_woywv")])
+meta_properties = Dictionary[String, Variant]({
+"color": Color(0.8, 0.8, 0.8, 1)
+})
+node_class = "Node3D"
diff --git a/demo/addons/func_godot/fgd/phong_base.tres b/demo/addons/func_godot/fgd/phong_base.tres
new file mode 100644
index 0000000..365813d
--- /dev/null
+++ b/demo/addons/func_godot/fgd/phong_base.tres
@@ -0,0 +1,19 @@
+[gd_resource type="Resource" script_class="FuncGodotFGDBaseClass" load_steps=2 format=3 uid="uid://nayxb8n7see2"]
+
+[ext_resource type="Script" uid="uid://ck575aqs1sbrb" path="res://addons/func_godot/src/fgd/func_godot_fgd_base_class.gd" id="1_04y3n"]
+
+[resource]
+script = ExtResource("1_04y3n")
+classname = "Phong"
+description = "Phong shading options for SolidClass geometry."
+class_properties = Dictionary[String, Variant]({
+"_phong": {
+"Disabled": 0,
+"Smooth shading": 1
+},
+"_phong_angle": 89.0
+})
+class_property_descriptions = Dictionary[String, Variant]({
+"_phong": ["Phong shading", 0],
+"_phong_angle": "Phong smoothing angle"
+})
diff --git a/demo/addons/func_godot/fgd/vertex_merge_distance_base.tres b/demo/addons/func_godot/fgd/vertex_merge_distance_base.tres
new file mode 100644
index 0000000..6ef62d5
--- /dev/null
+++ b/demo/addons/func_godot/fgd/vertex_merge_distance_base.tres
@@ -0,0 +1,15 @@
+[gd_resource type="Resource" script_class="FuncGodotFGDBaseClass" load_steps=2 format=3 uid="uid://doo4ly322b4jc"]
+
+[ext_resource type="Script" uid="uid://ck575aqs1sbrb" path="res://addons/func_godot/src/fgd/func_godot_fgd_base_class.gd" id="1_h3atm"]
+
+[resource]
+script = ExtResource("1_h3atm")
+classname = "VertexMergeDistance"
+description = "Adjustable value to snap vertices to on map build. This can reduce instances of seams between polygons."
+class_properties = Dictionary[String, Variant]({
+"_vertex_merge_distance": 0.03125
+})
+class_property_descriptions = Dictionary[String, Variant]({
+"_vertex_merge_distance": "Adjustable value to snap vertices to on map build. This can reduce instances of seams between polygons."
+})
+metadata/_custom_type_script = "uid://ck575aqs1sbrb"
diff --git a/demo/addons/func_godot/fgd/worldspawn.tres b/demo/addons/func_godot/fgd/worldspawn.tres
new file mode 100644
index 0000000..e773109
--- /dev/null
+++ b/demo/addons/func_godot/fgd/worldspawn.tres
@@ -0,0 +1,18 @@
+[gd_resource type="Resource" script_class="FuncGodotFGDSolidClass" load_steps=4 format=3 uid="uid://bdji3873bg32h"]
+
+[ext_resource type="Script" uid="uid://5cow84q03m6a" path="res://addons/func_godot/src/fgd/func_godot_fgd_solid_class.gd" id="1_62t8m"]
+[ext_resource type="Resource" uid="uid://doo4ly322b4jc" path="res://addons/func_godot/fgd/vertex_merge_distance_base.tres" id="1_h1046"]
+[ext_resource type="Resource" uid="uid://bcdsueg5pysfq" path="res://addons/func_godot/fgd/cull_interior_faces.tres" id="2_ky6lr"]
+
+[resource]
+script = ExtResource("1_62t8m")
+spawn_type = 0
+origin_type = 1
+collision_mask = 0
+classname = "worldspawn"
+description = "Default static world geometry. Builds a StaticBody3D with a single MeshInstance3D and a single convex CollisionShape3D shape."
+base_classes = Array[Resource]([ExtResource("1_h1046"), ExtResource("2_ky6lr")])
+meta_properties = Dictionary[String, Variant]({
+"color": Color(0.8, 0.8, 0.8, 1)
+})
+node_class = "StaticBody3D"
diff --git a/demo/addons/func_godot/func_godot_default_map_settings.tres b/demo/addons/func_godot/func_godot_default_map_settings.tres
new file mode 100644
index 0000000..bf9ad02
--- /dev/null
+++ b/demo/addons/func_godot/func_godot_default_map_settings.tres
@@ -0,0 +1,9 @@
+[gd_resource type="Resource" script_class="FuncGodotMapSettings" load_steps=5 format=3 uid="uid://bkhxcqsquw1yg"]
+
+[ext_resource type="Material" uid="uid://cvex6toty8yn7" path="res://addons/func_godot/textures/default_material.tres" id="1_8l5wm"]
+[ext_resource type="Script" uid="uid://38q6k0ctahjn" path="res://addons/func_godot/src/map/func_godot_map_settings.gd" id="1_dlf23"]
+[ext_resource type="Resource" uid="uid://crgpdahjaj" path="res://addons/func_godot/fgd/func_godot_fgd.tres" id="2_hf4oi"]
+[ext_resource type="Script" uid="uid://cij36hpqc46c" path="res://addons/func_godot/src/import/quake_wad_file.gd" id="4_576s4"]
+
+[resource]
+script = ExtResource("1_dlf23")
diff --git a/demo/addons/func_godot/func_godot_local_config.tres b/demo/addons/func_godot/func_godot_local_config.tres
new file mode 100644
index 0000000..482e00e
--- /dev/null
+++ b/demo/addons/func_godot/func_godot_local_config.tres
@@ -0,0 +1,6 @@
+[gd_resource type="Resource" script_class="FuncGodotLocalConfig" load_steps=2 format=3 uid="uid://bqjt7nyekxgog"]
+
+[ext_resource type="Script" uid="uid://xsjnhahhyein" path="res://addons/func_godot/src/util/func_godot_local_config.gd" id="1_g8kqj"]
+
+[resource]
+script = ExtResource("1_g8kqj")
diff --git a/demo/addons/func_godot/game_config/netradiant_custom/func_godot_netradiant_custom_gamepack_config.tres b/demo/addons/func_godot/game_config/netradiant_custom/func_godot_netradiant_custom_gamepack_config.tres
new file mode 100644
index 0000000..cfc3c5f
--- /dev/null
+++ b/demo/addons/func_godot/game_config/netradiant_custom/func_godot_netradiant_custom_gamepack_config.tres
@@ -0,0 +1,13 @@
+[gd_resource type="Resource" script_class="NetRadiantCustomGamePackConfig" load_steps=6 format=3 uid="uid://cv1k2e85fo2ax"]
+
+[ext_resource type="Resource" uid="uid://crgpdahjaj" path="res://addons/func_godot/fgd/func_godot_fgd.tres" id="1_gct4v"]
+[ext_resource type="Script" uid="uid://dfhj3me2g5j0l" path="res://addons/func_godot/src/netradiant_custom/netradiant_custom_gamepack_config.gd" id="2_en8ro"]
+[ext_resource type="Resource" uid="uid://f5erfnvbg6b7" path="res://addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_clip.tres" id="2_w7psh"]
+[ext_resource type="Resource" uid="uid://cfhg30jclb4lw" path="res://addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_skip.tres" id="3_6gpk8"]
+[ext_resource type="Resource" uid="uid://bpnj14oaufdpt" path="res://addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_origin.tres" id="4_8rl60"]
+
+[resource]
+script = ExtResource("2_en8ro")
+model_types = PackedStringArray("glb", "gltf", "obj")
+sound_types = PackedStringArray("wav", "ogg")
+texture_types = PackedStringArray("png", "jpg", "jpeg", "bmp", "tga")
diff --git a/demo/addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_clip.tres b/demo/addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_clip.tres
new file mode 100644
index 0000000..de8d99e
--- /dev/null
+++ b/demo/addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_clip.tres
@@ -0,0 +1,7 @@
+[gd_resource type="Resource" script_class="NetRadiantCustomShader" load_steps=2 format=3 uid="uid://f5erfnvbg6b7"]
+
+[ext_resource type="Script" uid="uid://dn86acprv4e86" path="res://addons/func_godot/src/netradiant_custom/netradiant_custom_shader.gd" id="1_cuylw"]
+
+[resource]
+script = ExtResource("1_cuylw")
+texture_path = "textures/clip"
diff --git a/demo/addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_origin.tres b/demo/addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_origin.tres
new file mode 100644
index 0000000..243bb53
--- /dev/null
+++ b/demo/addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_origin.tres
@@ -0,0 +1,7 @@
+[gd_resource type="Resource" script_class="NetRadiantCustomShader" load_steps=2 format=3 uid="uid://bpnj14oaufdpt"]
+
+[ext_resource type="Script" uid="uid://dn86acprv4e86" path="res://addons/func_godot/src/netradiant_custom/netradiant_custom_shader.gd" id="1_ah2cp"]
+
+[resource]
+script = ExtResource("1_ah2cp")
+texture_path = "textures/origin"
diff --git a/demo/addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_skip.tres b/demo/addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_skip.tres
new file mode 100644
index 0000000..769b5c8
--- /dev/null
+++ b/demo/addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_skip.tres
@@ -0,0 +1,7 @@
+[gd_resource type="Resource" script_class="NetRadiantCustomShader" load_steps=2 format=3 uid="uid://cfhg30jclb4lw"]
+
+[ext_resource type="Script" uid="uid://dn86acprv4e86" path="res://addons/func_godot/src/netradiant_custom/netradiant_custom_shader.gd" id="1_4ja6h"]
+
+[resource]
+script = ExtResource("1_4ja6h")
+texture_path = "textures/skip"
diff --git a/demo/addons/func_godot/game_config/trenchbroom/func_godot_tb_game_config.tres b/demo/addons/func_godot/game_config/trenchbroom/func_godot_tb_game_config.tres
new file mode 100644
index 0000000..31019ce
--- /dev/null
+++ b/demo/addons/func_godot/game_config/trenchbroom/func_godot_tb_game_config.tres
@@ -0,0 +1,11 @@
+[gd_resource type="Resource" script_class="TrenchBroomGameConfig" format=3 uid="uid://b44ah5b2000wa"]
+
+[ext_resource type="Resource" uid="uid://crgpdahjaj" path="res://addons/func_godot/fgd/func_godot_fgd.tres" id="1_8u1vq"]
+[ext_resource type="Resource" uid="uid://b4xhdj0e16lop" path="res://addons/func_godot/game_config/trenchbroom/tb_face_tag_clip.tres" id="1_rsp20"]
+[ext_resource type="Resource" uid="uid://ca7377sfgj074" path="res://addons/func_godot/game_config/trenchbroom/tb_face_tag_skip.tres" id="2_166i2"]
+[ext_resource type="Script" uid="uid://cx44c4vnq8bt5" path="res://addons/func_godot/src/trenchbroom/trenchbroom_game_config.gd" id="2_ns6ah"]
+[ext_resource type="Resource" uid="uid://bkjxc54mmdhbo" path="res://addons/func_godot/game_config/trenchbroom/tb_face_tag_origin.tres" id="3_stisi"]
+[ext_resource type="Texture2D" uid="uid://decwujsyhj0qy" path="res://addons/func_godot/icon32.png" id="6_tex5j"]
+
+[resource]
+script = ExtResource("2_ns6ah")
diff --git a/demo/addons/func_godot/game_config/trenchbroom/tb_brush_tag_func.tres b/demo/addons/func_godot/game_config/trenchbroom/tb_brush_tag_func.tres
new file mode 100644
index 0000000..0899a17
--- /dev/null
+++ b/demo/addons/func_godot/game_config/trenchbroom/tb_brush_tag_func.tres
@@ -0,0 +1,10 @@
+[gd_resource type="Resource" script_class="TrenchBroomTag" load_steps=2 format=3 uid="uid://37iduqf7tpxq"]
+
+[ext_resource type="Script" uid="uid://b66qdknwqpfup" path="res://addons/func_godot/src/trenchbroom/trenchbroom_tag.gd" id="1_rn13a"]
+
+[resource]
+script = ExtResource("1_rn13a")
+tag_name = "Func"
+tag_attributes = Array[String]([])
+tag_match_type = 1
+tag_pattern = "func*"
diff --git a/demo/addons/func_godot/game_config/trenchbroom/tb_brush_tag_trigger.tres b/demo/addons/func_godot/game_config/trenchbroom/tb_brush_tag_trigger.tres
new file mode 100644
index 0000000..5028a57
--- /dev/null
+++ b/demo/addons/func_godot/game_config/trenchbroom/tb_brush_tag_trigger.tres
@@ -0,0 +1,10 @@
+[gd_resource type="Resource" script_class="TrenchBroomTag" load_steps=2 format=3 uid="uid://co2sb1ng7cw4i"]
+
+[ext_resource type="Script" uid="uid://b66qdknwqpfup" path="res://addons/func_godot/src/trenchbroom/trenchbroom_tag.gd" id="1_msqpk"]
+
+[resource]
+script = ExtResource("1_msqpk")
+tag_name = "Trigger"
+tag_match_type = 1
+tag_pattern = "trigger*"
+texture_name = "trigger"
diff --git a/demo/addons/func_godot/game_config/trenchbroom/tb_face_tag_clip.tres b/demo/addons/func_godot/game_config/trenchbroom/tb_face_tag_clip.tres
new file mode 100644
index 0000000..c00270a
--- /dev/null
+++ b/demo/addons/func_godot/game_config/trenchbroom/tb_face_tag_clip.tres
@@ -0,0 +1,8 @@
+[gd_resource type="Resource" script_class="TrenchBroomTag" load_steps=2 format=3 uid="uid://b4xhdj0e16lop"]
+
+[ext_resource type="Script" uid="uid://b66qdknwqpfup" path="res://addons/func_godot/src/trenchbroom/trenchbroom_tag.gd" id="1_7td58"]
+
+[resource]
+script = ExtResource("1_7td58")
+tag_name = "Clip"
+tag_pattern = "clip"
diff --git a/demo/addons/func_godot/game_config/trenchbroom/tb_face_tag_origin.tres b/demo/addons/func_godot/game_config/trenchbroom/tb_face_tag_origin.tres
new file mode 100644
index 0000000..84f4da9
--- /dev/null
+++ b/demo/addons/func_godot/game_config/trenchbroom/tb_face_tag_origin.tres
@@ -0,0 +1,8 @@
+[gd_resource type="Resource" script_class="TrenchBroomTag" load_steps=2 format=3 uid="uid://bkjxc54mmdhbo"]
+
+[ext_resource type="Script" uid="uid://b66qdknwqpfup" path="res://addons/func_godot/src/trenchbroom/trenchbroom_tag.gd" id="1_enkfc"]
+
+[resource]
+script = ExtResource("1_enkfc")
+tag_name = "Origin"
+tag_pattern = "origin"
diff --git a/demo/addons/func_godot/game_config/trenchbroom/tb_face_tag_skip.tres b/demo/addons/func_godot/game_config/trenchbroom/tb_face_tag_skip.tres
new file mode 100644
index 0000000..e44627d
--- /dev/null
+++ b/demo/addons/func_godot/game_config/trenchbroom/tb_face_tag_skip.tres
@@ -0,0 +1,8 @@
+[gd_resource type="Resource" script_class="TrenchBroomTag" load_steps=2 format=3 uid="uid://ca7377sfgj074"]
+
+[ext_resource type="Script" uid="uid://b66qdknwqpfup" path="res://addons/func_godot/src/trenchbroom/trenchbroom_tag.gd" id="1_2teqe"]
+
+[resource]
+script = ExtResource("1_2teqe")
+tag_name = "Skip"
+tag_pattern = "skip"
diff --git a/demo/addons/func_godot/icon.png b/demo/addons/func_godot/icon.png
new file mode 100644
index 0000000..9376a44
Binary files /dev/null and b/demo/addons/func_godot/icon.png differ
diff --git a/demo/addons/func_godot/icon.svg b/demo/addons/func_godot/icon.svg
new file mode 100644
index 0000000..de26a4d
--- /dev/null
+++ b/demo/addons/func_godot/icon.svg
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/demo/addons/func_godot/icon32.png b/demo/addons/func_godot/icon32.png
new file mode 100644
index 0000000..9376a44
Binary files /dev/null and b/demo/addons/func_godot/icon32.png differ
diff --git a/demo/addons/func_godot/icons/icon_godambler.svg b/demo/addons/func_godot/icons/icon_godambler.svg
new file mode 100644
index 0000000..39b832f
--- /dev/null
+++ b/demo/addons/func_godot/icons/icon_godambler.svg
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/demo/addons/func_godot/icons/icon_godambler3d.svg b/demo/addons/func_godot/icons/icon_godambler3d.svg
new file mode 100644
index 0000000..c40b606
--- /dev/null
+++ b/demo/addons/func_godot/icons/icon_godambler3d.svg
@@ -0,0 +1,34 @@
+
+
+
+
diff --git a/demo/addons/func_godot/icons/icon_godot_ranger.svg b/demo/addons/func_godot/icons/icon_godot_ranger.svg
new file mode 100644
index 0000000..80a3aec
--- /dev/null
+++ b/demo/addons/func_godot/icons/icon_godot_ranger.svg
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/demo/addons/func_godot/icons/icon_godot_ranger3d.svg b/demo/addons/func_godot/icons/icon_godot_ranger3d.svg
new file mode 100644
index 0000000..5c5aee0
--- /dev/null
+++ b/demo/addons/func_godot/icons/icon_godot_ranger3d.svg
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/demo/addons/func_godot/icons/icon_quake_file.svg b/demo/addons/func_godot/icons/icon_quake_file.svg
new file mode 100644
index 0000000..67383b2
--- /dev/null
+++ b/demo/addons/func_godot/icons/icon_quake_file.svg
@@ -0,0 +1,85 @@
+
+
+
+
diff --git a/demo/addons/func_godot/icons/icon_slipgate.svg b/demo/addons/func_godot/icons/icon_slipgate.svg
new file mode 100644
index 0000000..f78ac7b
--- /dev/null
+++ b/demo/addons/func_godot/icons/icon_slipgate.svg
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/demo/addons/func_godot/icons/icon_slipgate3d.svg b/demo/addons/func_godot/icons/icon_slipgate3d.svg
new file mode 100644
index 0000000..e8c25be
--- /dev/null
+++ b/demo/addons/func_godot/icons/icon_slipgate3d.svg
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/demo/addons/func_godot/palette.lmp b/demo/addons/func_godot/palette.lmp
new file mode 100644
index 0000000..7eefda1
Binary files /dev/null and b/demo/addons/func_godot/palette.lmp differ
diff --git a/demo/addons/func_godot/plugin.cfg b/demo/addons/func_godot/plugin.cfg
new file mode 100644
index 0000000..38b77fd
--- /dev/null
+++ b/demo/addons/func_godot/plugin.cfg
@@ -0,0 +1,7 @@
+[plugin]
+
+name="FuncGodot"
+description="Quake .map and Half-Life .vmf file support for Godot."
+author="Josh Palmer, Hannah Crawford, Emberlynn Bland, Tim Maccabe, Vera Lux, func_godot Community"
+version="2025.12"
+script="src/func_godot_plugin.gd"
diff --git a/demo/addons/func_godot/src/core/data.gd b/demo/addons/func_godot/src/core/data.gd
new file mode 100644
index 0000000..b3edf24
--- /dev/null
+++ b/demo/addons/func_godot/src/core/data.gd
@@ -0,0 +1,196 @@
+@icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
+class_name FuncGodotData
+## Container that holds various data structs to be used in the [FuncGodotMap] build process.
+##
+## FuncGodot utilizes multiple custom data structs to hold information parsed from the map file
+## and read and modified by the other core build classes.
+## All data structs extend from [RefCounted], therefore all data is passed by reference.
+## [br][br]
+## [FuncGodotData.FaceData][br]
+## [FuncGodotData.BrushData][br]
+## [FuncGodotData.PatchData][br]
+## [FuncGodotData.GroupData][br]
+## [FuncGodotData.EntityData][br]
+
+## Data struct representing both a single map plane and a mesh face. Generated during parsing by plane definitions in the map file,
+## it is further modified and utilized during the geo generation stage to create the final entity meshes.
+class FaceData extends RefCounted:
+ ## Vertex array for the face. Only populated in combination with other faces, as a result of planar intersections.
+ var vertices: PackedVector3Array = []
+ ## Index array for the face. Used in ArrayMesh creation.
+ var indices: PackedInt32Array = []
+ ## Vertex normal array for the face.
+ ## By default, set to the planar normal, which results in flat shading. May be modified to adjust shading.
+ var normals: PackedVector3Array = []
+ ## Tangent data for the face.
+ var tangents: PackedFloat32Array = []
+ ## Local path to the texture without the extension, relative to the FuncGodotMap node's settings' base texture directory.
+ var texture: String
+ ## UV transform data generated during the parsing stage. Used for both Standard and Valve 220 UV formats,
+ ## though rotation is not applied to the transform when using Valve 220.
+ var uv: Transform2D
+ ## Raw vector data provided by the Valve 220 format during parsing. It is used to calculate rotations.
+ ## The presence of this data determines how face UVs and tangents are calculated.
+ var uv_axes: PackedVector3Array = []
+ ## Raw plane data parsed from the map file using the id Tech coordinate system.
+ var plane: Plane
+
+ ## Returns the average position of all vertices in the face. Only valid when the face has at least one vertex.
+ func get_centroid() -> Vector3:
+ return FuncGodotUtil.op_vec3_avg(vertices)
+
+ ## Returns an arbitrary coplanar direction to use for winding the face.
+ ## Only valid when the face has at least two vertices.
+ func get_basis() -> Vector3:
+ if vertices.size() < 2:
+ push_error("Cannot get winding basis without at least 2 vertices!")
+ return Vector3.ZERO
+ return (vertices[1] - vertices[0]).normalized()
+
+ ## Prepares the face for OpenGL triangle winding order.
+ ## Sorts the vertex array in-place by angle from the centroid.
+ func wind() -> void:
+ var centroid: Vector3 = get_centroid()
+ var u_axis: Vector3 = get_basis()
+ var v_axis: Vector3 = u_axis.cross(plane.normal).normalized()
+ var cmp_winding_angle: Callable = (
+ func(a: Vector3, b: Vector3) -> bool:
+ var dir_a: Vector3 = a - centroid
+ var dir_b: Vector3 = b - centroid
+ var angle_a: float = atan2(dir_a.dot(v_axis), dir_a.dot(u_axis))
+ var angle_b: float = atan2(dir_b.dot(v_axis), dir_b.dot(u_axis))
+ return angle_a < angle_b
+ )
+
+ var _vertices: Array[Vector3]
+ _vertices.assign(vertices)
+ _vertices.sort_custom(cmp_winding_angle)
+ vertices = _vertices
+
+ ## Repopulate the [member indices] array to create a triangle fan.
+ ## The face must be properly wound for the resulting indices to be valid.
+ func index_vertices() -> void:
+ var tri_count: int = vertices.size() - 2
+ indices.resize(tri_count * 3)
+ var index: int = 0
+ for i in tri_count:
+ indices[index] = 0
+ indices[index + 1] = i + 1
+ indices[index + 2] = i + 2
+ index += 3
+
+## Data struct representing a single map format brush. It is largely meant as a container for [FuncGodotData.FaceData] data.
+class BrushData extends RefCounted:
+ ## Raw plane data parsed from the map file using the id Tech coordinate system.
+ var planes: Array[Plane]
+ ## Collection of [FuncGodotData.FaceData].
+ var faces: Array[FaceData]
+ ## [code]true[/code] if this brush is completely covered in the [i]Origin[/i] texture defined in [FuncGodotMapSettings].
+ ## Determined during [FuncGodotParser] and utilized during [FuncGodotGeometryGenerator].
+ var origin: bool = false
+
+## Data struct representing a patch def entity.
+class PatchData extends RefCounted:
+ ## Local path to the texture without the extension, relative to the FuncGodotMap node's settings' base texture directory.
+ var texture: String
+ var size: PackedInt32Array
+ var points: PackedVector3Array
+ var uvs: PackedVector2Array
+
+## Data struct representing a TrenchBroom Group, TrenchBroom Layer, or Valve VisGroup.
+## Generated during the parsing stage and utilized during both parsing and entity assembly stages.
+class GroupData extends RefCounted:
+ enum GroupType { GROUP, LAYER, }
+ ## Defines whether the group is a Group or a Layer. Currently only determines the name of the group.
+ var type: GroupType = GroupType.GROUP
+ ## Group ID retrieved from the map file. Utilized during the parsing and entity assembly stages to determine
+ ## which entities belong to which groups as well as which groups are children of other groups.
+ var id: int
+ ## Generated during the parsing stage using the format of type_id_name, eg: group_2_Arkham.
+ var name: String
+ ## ID of the parent group data, used to determine which group data is this group's parent.
+ var parent_id: int = -1
+ ## Pointer to another group data that this group is a child of.
+ var parent: GroupData = null
+ ## Pointer to generated Node3D representing this group in the SceneTree.
+ var node: Node3D = null
+ ## If true, erases all entities assigned to this group and then the group itself at the end of the parsing stage, preventing those entities from being generated into nodes.
+ ## Can be set in TrenchBroom on layers using the "omit layer" option.
+ var omit: bool = false
+
+## Data struct representing a map format entity.
+class EntityData extends RefCounted:
+ ## All of the entity's key value pairs from the map file, retrieved during parsing.
+ ## The func_godot_properties dictionary generated at the end of entity assembly is derived from this.
+ var properties: Dictionary[String, Variant] = {}
+ ## The entity's brush data collected during the parsing stage. If the entity's FGD resource cannot be found,
+ ## the presence of a single brush determines this entity to be a Solid Entity.
+ var brushes: Array[BrushData] = []
+ ## The entity's patch def data collected during the parsing stage. If the entity's FGD resource cannot be found,
+ ## the presence of a single patch def determines this entity to be a Solid Entity.
+ var patches: Array[PatchData] = []
+ ## Pointer to the group data this entity belongs to.
+ var group: GroupData = null
+ ## The entity's FGD resource, determined by matching the classname properties of each.
+ ## This can only be a [FuncGodotFGDSolidClass], [FuncGodotFGDPointClass], or [FuncGodotFGDModelPointClass].
+ var definition: FuncGodotFGDEntityClass = null
+ ## Mesh resource generated during the geometry generation stage and applied during the entity assembly stage.
+ var mesh: ArrayMesh = null
+ ## MeshInstance3D node generated during the entity assembly stage.
+ var mesh_instance: MeshInstance3D = null
+ ## Optional mesh metadata compiled during the geometry generation stage, used to determine face information from collision.
+ var mesh_metadata: Dictionary = {}
+ ## A collection of collision shape resources generated during the geometry generation stage and applied during the entity assembly stage.
+ var shapes: Array[Shape3D] = []
+ ## A collection of [CollisionShape3D] nodes generated during the entity assembly stage. Each node corresponds to a shape in the [member shapes] array.
+ var collision_shapes: Array[CollisionShape3D] = []
+ ## [OccluderInstance3D] node generated during the entity assembly stage using the [member mesh] resource.
+ var occluder_instance: OccluderInstance3D = null
+ ## True global position of the entity's generated node that the mesh's vertices are offset by during the geometry generation stage.
+ var origin: Vector3 = Vector3.ZERO
+
+ ## Checks the entity's FGD resource definition, returning whether the Solid Class has a [MeshInstance3D] built for it.
+ func is_visual() -> bool:
+ return (definition
+ and definition is FuncGodotFGDSolidClass
+ and definition.build_visuals)
+
+ func is_gi_enabled() -> bool:
+ return (definition
+ and definition is FuncGodotFGDSolidClass
+ and definition.global_illumination_mode
+ )
+
+ ## Checks the entity's FGD resource definition, returning whether the Solid Class CollisionShapeType is set to Convex.
+ func is_collision_convex() -> bool:
+ return (definition
+ and definition is FuncGodotFGDSolidClass
+ and definition.collision_shape_type == FuncGodotFGDSolidClass.CollisionShapeType.CONVEX
+ )
+
+ ## Checks the entity's FGD resource definition, returning whether the Solid Class CollisionShapeType is set to Concave.
+ func is_collision_concave() -> bool:
+ return (definition
+ and definition is FuncGodotFGDSolidClass
+ and definition.collision_shape_type == FuncGodotFGDSolidClass.CollisionShapeType.CONCAVE
+ )
+
+ ## Determines if the entity's mesh should be processed for normal smoothing.
+ ## The smoothing property can be retrieved from [member FuncGodotMapSettings.entity_smoothing_property].
+ func is_smooth_shaded(smoothing_property: String = "_phong") -> bool:
+ return properties.get(smoothing_property, 0)
+
+ ## Retrieves the entity's smoothing angle to determine if the face should be smoothed.
+ ## The smoothing angle property can be retrieved from [member FuncGodotMapSettings.entity_smoothing_angle_property].
+ func get_smoothing_angle(smoothing_angle_property: String = "_phong_angle") -> float:
+ return properties.get(smoothing_angle_property, 89.0)
+
+class VertexGroupData:
+ ## Faces this vertex appears in.
+ var faces: Array[FaceData]
+ ## Index within the associated face for this vertex.
+ var face_indices: PackedInt32Array
+
+class ParseData:
+ var entities: Array[EntityData] = []
+ var groups: Array[GroupData] = []
diff --git a/demo/addons/func_godot/src/core/data.gd.uid b/demo/addons/func_godot/src/core/data.gd.uid
new file mode 100644
index 0000000..e10beda
--- /dev/null
+++ b/demo/addons/func_godot/src/core/data.gd.uid
@@ -0,0 +1 @@
+uid://cqye8dehq4c7q
diff --git a/demo/addons/func_godot/src/core/entity_assembler.gd b/demo/addons/func_godot/src/core/entity_assembler.gd
new file mode 100644
index 0000000..ad1e7cc
--- /dev/null
+++ b/demo/addons/func_godot/src/core/entity_assembler.gd
@@ -0,0 +1,357 @@
+@icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
+class_name FuncGodotEntityAssembler extends RefCounted
+## Entity assembly class that is instantiated by a [FuncGodotMap] node.
+
+const _SIGNATURE: String = "[ENT]"
+
+# Namespacing
+const _GroupData := FuncGodotData.GroupData
+const _EntityData := FuncGodotData.EntityData
+
+# Class members
+## [FuncGodotMapSettings] provided by the [FuncGodotMap] during the build process.
+var map_settings: FuncGodotMapSettings = null
+## [enum FuncGodotMap.BuildFlags] that may affect the build process provided by the [FuncGodotMap].
+var build_flags: int = 0
+
+# Signals
+## Emitted when a step in the entity assembly process is completed.
+## It is connected to [method FuncGodotUtil.print_profile_info] method if [member FuncGodotMap.build_flags] SHOW_PROFILE_INFO flag is set.
+signal declare_step(step: String)
+
+func _init(settings: FuncGodotMapSettings) -> void:
+ map_settings = settings
+
+## Attempts to retrieve a [Script] via class name, to allow for [GDScript] class instantiation.
+static func get_script_by_class_name(name_of_class: String) -> Script:
+ if ResourceLoader.exists(name_of_class, "Script"):
+ return load(name_of_class) as Script
+ for global_class in ProjectSettings.get_global_class_list():
+ var found_name_of_class : String = global_class["class"]
+ var found_path : String = global_class["path"]
+ if found_name_of_class == name_of_class:
+ return load(found_path) as Script
+ return null
+
+## Generates a [Node3D] for a group's [SceneTree] representation and links the new [Node3D] to that group.
+func generate_group_node(group_data: _GroupData) -> Node3D:
+ var group_node := Node3D.new()
+ group_node.name = group_data.name
+ group_data.node = group_node
+ return group_node
+
+## Generates and assembles a new [Node] based upon processed [FuncGodotData.EntityData]. Depending upon provided data,
+## additional [MeshInstance3D], [CollisionShape3D], and [OccluderInstance3D] nodes may also be generated.
+func generate_solid_entity_node(node: Node, node_name: String, data: _EntityData, definition: FuncGodotFGDSolidClass) -> Node:
+ if definition.spawn_type == FuncGodotFGDSolidClass.SpawnType.MERGE_WORLDSPAWN:
+ return null
+
+ if definition.node_class != "":
+ if ClassDB.class_exists(definition.node_class):
+ node = ClassDB.instantiate(definition.node_class)
+ else:
+ var script: Script = get_script_by_class_name(definition.node_class)
+ if script is Script:
+ node = script.new()
+ else:
+ node = Node3D.new()
+
+ node.name = node_name
+ node_name = node_name.trim_suffix(definition.classname).trim_suffix("_")
+ var properties: Dictionary[String, Variant] = data.properties
+
+ # Mesh Instance generation
+ if data.mesh:
+ var mesh_instance := MeshInstance3D.new()
+ mesh_instance.name = node_name + "_mesh_instance"
+ mesh_instance.mesh = data.mesh
+ mesh_instance.gi_mode = GeometryInstance3D.GI_MODE_DISABLED
+ if definition.global_illumination_mode:
+ mesh_instance.gi_mode = definition.global_illumination_mode
+ mesh_instance.cast_shadow = definition.shadow_casting_setting
+ mesh_instance.layers = definition.render_layers
+ node.add_child(mesh_instance)
+ data.mesh_instance = mesh_instance
+
+ # Occluder generation
+ if definition.build_occlusion and data.mesh:
+ var verts: PackedVector3Array = []
+ var indices: PackedInt32Array = []
+ var index: int = 0
+ for surf_idx in range(data.mesh.get_surface_count()):
+ var vert_count: int = verts.size()
+ var surf_array: Array = data.mesh.surface_get_arrays(surf_idx)
+ verts.append_array(surf_array[Mesh.ARRAY_VERTEX])
+ indices.resize(indices.size() + surf_array[Mesh.ARRAY_INDEX].size())
+ for new_index in surf_array[Mesh.ARRAY_INDEX]:
+ indices[index] = (new_index + vert_count)
+ index += 1
+
+ var occluder := ArrayOccluder3D.new()
+ occluder.set_arrays(verts, indices)
+ var occluder_instance := OccluderInstance3D.new()
+ occluder_instance.name = node_name + "_occluder_instance"
+ occluder_instance.occluder = occluder
+ node.add_child(occluder_instance)
+ data.occluder_instance = occluder_instance
+
+ # NOTE: Currently occuring in EntityAssembler until the appropriate method in GeometryGenerator is resolved
+ # For now, smooth entire mesh, then unwrap for lightmap if needed
+ if not (build_flags & FuncGodotMap.BuildFlags.DISABLE_SMOOTHING) and data.is_smooth_shaded(map_settings.entity_smoothing_property):
+ mesh_instance.mesh = FuncGodotUtil.smooth_mesh_by_angle(data.mesh, data.get_smoothing_angle(map_settings.entity_smoothing_angle_property))
+
+ if data.is_gi_enabled() and (build_flags & FuncGodotMap.BuildFlags.UNWRAP_UV2):
+ mesh_instance.mesh.lightmap_unwrap(
+ Transform3D.IDENTITY,
+ map_settings.uv_unwrap_texel_size * map_settings.scale_factor
+ )
+
+ # Collision generation
+ if data.shapes.size() and node is CollisionObject3D:
+ node.collision_layer = definition.collision_layer
+ node.collision_mask = definition.collision_mask
+ node.collision_priority = definition.collision_priority
+
+ var shape_to_face_array : Array[PackedInt32Array] = []
+ if data.mesh_metadata.has('shape_to_face_array'):
+ shape_to_face_array = data.mesh_metadata['shape_to_face_array']
+ data.mesh_metadata.erase('shape_to_face_array')
+
+ # Generate CollisionShape3D nodes and apply shapes
+ var face_index_metadata : Dictionary[String, PackedInt32Array] = {}
+ for i in data.shapes.size():
+ var shape := data.shapes[i]
+ var collision_shape := CollisionShape3D.new()
+ if definition.collision_shape_type == FuncGodotFGDSolidClass.CollisionShapeType.CONCAVE:
+ collision_shape.name = node_name + "_collision_shape"
+ else:
+ collision_shape.name = node_name + "_brush_%s_collision_shape" % i
+ collision_shape.shape = shape
+ collision_shape.shape.margin = definition.collision_shape_margin
+ collision_shape.owner = node.owner
+ node.add_child(collision_shape)
+ data.collision_shapes.append(collision_shape)
+ if shape_to_face_array.size() > i:
+ face_index_metadata[collision_shape.name] = shape_to_face_array[i]
+
+ if definition.add_collision_shape_to_face_indices_metadata:
+ data.mesh_metadata['collision_shape_to_face_indices_map'] = face_index_metadata
+
+ if "position" in node:
+ if node.position is Vector3:
+ node.position = FuncGodotUtil.id_to_opengl(data.origin)
+ elif node.position is Vector2:
+ node.position = Vector2(data.origin.z, -data.origin.y) * map_settings.inverse_scale_factor
+
+ if not data.mesh_metadata.is_empty():
+ node.set_meta("func_godot_mesh_data", data.mesh_metadata)
+
+ return node
+
+## Generates and assembles a new [Node] or [PackedScene] based upon processed [FuncGodotData.EntityData].
+func generate_point_entity_node(node: Node, node_name: String, properties: Dictionary, definition: FuncGodotFGDPointClass) -> Node:
+ var classname: String = properties["classname"]
+
+ if definition.scene_file:
+ var flag: PackedScene.GenEditState = PackedScene.GEN_EDIT_STATE_DISABLED
+ if Engine.is_editor_hint():
+ flag = PackedScene.GEN_EDIT_STATE_INSTANCE
+ node = definition.scene_file.instantiate(flag)
+ elif definition.node_class != "":
+ if ClassDB.class_exists(definition.node_class):
+ node = ClassDB.instantiate(definition.node_class)
+ else:
+ var script: Script = get_script_by_class_name(definition.node_class)
+ if script is Script:
+ node = script.new()
+ if not node:
+ node = Node3D.new()
+
+ node.name = node_name
+
+ if "rotation_degrees" in node and definition.apply_rotation_on_map_build:
+ var angles := Vector3.ZERO
+ if "angles" in properties or "mangle" in properties:
+ var key := "angles" if "angles" in properties else "mangle"
+ var angles_raw = properties[key]
+ if angles_raw is String:
+ angles_raw = angles_raw.split_floats(' ')
+ if angles_raw.size() < 3:
+ push_error("Invalid vector format for \"" + key + "\" in entity \"" + classname + "\"")
+ angles_raw = null
+ if angles_raw:
+ angles = Vector3(-angles_raw[0], angles_raw[1], -angles_raw[2])
+ if key == "mangle":
+ if definition.classname.begins_with("light"):
+ angles = Vector3(angles_raw[1], angles_raw[0], -angles_raw[2])
+ elif definition.classname == "info_intermission":
+ angles = Vector3(angles_raw[0], angles_raw[1], -angles_raw[2])
+ elif "angle" in properties:
+ var angle = properties["angle"]
+ if not angle is float:
+ angle = float(angle)
+ if is_equal_approx(angle, -1):
+ angles.x = 90
+ elif is_equal_approx(angle, -2):
+ angles.x = -90
+ else:
+ angles.y += angle
+ angles.y += 180
+ node.rotation_degrees = angles
+
+ if "scale" in node and definition.apply_scale_on_map_build:
+ if "scale" in properties:
+ var scale_prop: Variant = properties["scale"]
+ if typeof(scale_prop) == TYPE_STRING:
+ var scale_arr: PackedStringArray = (scale_prop as String).split(" ")
+ match scale_arr.size():
+ 1: scale_prop = scale_arr[0].to_float()
+ 3: scale_prop = Vector3(scale_arr[1].to_float(), scale_arr[2].to_float(), scale_arr[0].to_float())
+ 2: scale_prop = Vector2(scale_arr[0].to_float(), scale_arr[0].to_float())
+ if typeof(scale_prop) == TYPE_FLOAT or typeof(scale_prop) == TYPE_INT:
+ node.scale *= scale_prop as float
+ elif node.scale is Vector3:
+ if typeof(scale_prop) == TYPE_VECTOR3 or typeof(scale_prop) == TYPE_VECTOR3I:
+ node.scale *= scale_prop as Vector3
+ elif node.scale is Vector2:
+ if typeof(scale_prop) == TYPE_VECTOR2 or typeof(scale_prop) == TYPE_VECTOR2I:
+ node.scale *= scale_prop as Vector2
+
+ if "origin" in properties:
+ var origin_vec: Vector3 = Vector3.ZERO
+ var origin_prop = properties['origin']
+ if origin_prop is Vector3:
+ origin_vec = Vector3(origin_prop.y, origin_prop.z, origin_prop.x)
+ elif origin_prop is String:
+ var origin_comps: PackedFloat64Array = properties['origin'].split_floats(' ')
+ if origin_comps.size() > 2:
+ origin_vec = Vector3(origin_comps[1], origin_comps[2], origin_comps[0])
+ else:
+ push_error("Invalid vector format for \"origin\" in " + node_name)
+ else:
+ push_error("Invalid vector format for \"origin\" in " + node_name)
+
+ if "position" in node:
+ if node.position is Vector3:
+ node.position = origin_vec * map_settings.scale_factor
+ elif node.position is Vector2:
+ node.position = Vector2(origin_vec.z, -origin_vec.y)
+
+ return node
+
+## Converts the [String] values of the entity data's [code]properties[/code] [Dictionary] to various [Variant] formats
+## based upon the [FuncGodotFGDEntity]'s class properties, then attempts to send those properties to a [code]func_godot_properties[/code] [Dictionary]
+## and an [code]_func_godot_apply_properties(properties: Dictionary)[/code] method on the node. A deferred call to [code]_func_godot_build_complete()[/code] is also made.
+func apply_entity_properties(node: Node, data: _EntityData) -> void:
+ var properties: Dictionary[String, Variant] = data.properties
+
+ if data.definition:
+ var def := data.definition
+ if def.auto_apply_to_matching_node_properties:
+ for property in properties:
+ if property == 'scale' and def is FuncGodotFGDPointClass and def.apply_scale_on_map_build:
+ # scale has already been applied
+ continue
+ if property in node:
+ if typeof(node.get(property)) == typeof(properties[property]):
+ node.set(property, properties[property])
+ else:
+ push_error("Entity %s property \'%s\' type mismatch with matching generated node property." % [node.name, property])
+
+ if "func_godot_properties" in node:
+ node.func_godot_properties = properties
+
+ if node.has_method("_func_godot_apply_properties"):
+ node.call("_func_godot_apply_properties", properties)
+
+ if node.has_method("_func_godot_build_complete"):
+ node.call_deferred("_func_godot_build_complete")
+
+## Generate a [Node] from [FuncGodotData.EntityData]. The returned node value can be [code]null[/code],
+## in the case of [FuncGodotFGDSolidClass] entities with no [FuncGodotData.BrushData] entries.
+func generate_entity_node(entity_data: _EntityData, entity_index: int) -> Node:
+ var node: Node = null
+ var node_name: String = "entity_%s" % entity_index
+ var properties: Dictionary[String, Variant] = entity_data.properties
+ var entity_def: FuncGodotFGDEntityClass = entity_data.definition
+
+ if "classname" in entity_data.properties:
+ var classname: String = properties["classname"]
+
+ node_name += "_" + properties["classname"]
+
+ var name_prop: String
+ if entity_def.name_property in properties:
+ name_prop = str(properties[entity_def.name_property])
+ elif map_settings.entity_name_property in properties:
+ name_prop = str(properties[map_settings.entity_name_property])
+ if not name_prop.is_empty():
+ node_name = "entity_" + name_prop
+
+ if entity_def is FuncGodotFGDSolidClass:
+ node = generate_solid_entity_node(node, node_name, entity_data, entity_def)
+ elif entity_def is FuncGodotFGDPointClass:
+ node = generate_point_entity_node(node, node_name, properties, entity_def)
+
+ if node:
+ if entity_def.script_class:
+ node.set_script(entity_def.script_class)
+
+ var node_groups: Array[String] = map_settings.entity_node_groups.duplicate()
+ node_groups.append_array(entity_def.node_groups)
+ for node_group in node_groups:
+ if node_group.is_empty():
+ continue
+ node.add_to_group(node_group, true)
+
+ return node
+
+## Main entity assembly process called by [FuncGodotMap]. Generates and sorts group nodes in the [SceneTree] first,
+## then generates and assembles [Node]s based upon the provided [FuncGodotData.EntityData] and adds them to the [SceneTree].
+func build(map_node: FuncGodotMap, entities: Array[_EntityData], groups: Array[_GroupData]) -> void:
+ var scene_root := map_node.get_tree().edited_scene_root if map_node.is_inside_tree() else map_node
+ build_flags = map_node.build_flags
+
+ if map_settings.use_groups_hierarchy:
+ declare_step.emit("Generating %s groups" % groups.size())
+ # Generate group nodes
+ for group in groups:
+ group.node = generate_group_node(group)
+ # Sort hierarchy and add them to the map
+ for group in groups:
+ if group.parent_id < 0:
+ map_node.add_child(group.node)
+ group.node.owner = scene_root
+ else:
+ for parent in groups:
+ if group.parent_id == parent.id:
+ parent.node.add_child(group.node)
+ group.node.owner = scene_root
+ declare_step.emit("Groups generation and sorting complete")
+
+ declare_step.emit("Assembling %s entities" % entities.size())
+ var entity_node: Node = null
+ for entity_index in entities.size():
+ var entity_data : _EntityData = entities[entity_index]
+ entity_node = generate_entity_node(entity_data, entity_index)
+ if entity_node:
+ if not map_settings.use_groups_hierarchy or not entity_data.group:
+ map_node.add_child(entity_node)
+ if entity_index == 0:
+ map_node.move_child(entity_node, 0)
+ elif map_settings.use_groups_hierarchy:
+ for group in groups:
+ if entity_data.group.id == group.id:
+ group.node.add_child(entity_node)
+
+ entity_node.owner = scene_root
+ if entity_data.mesh_instance:
+ entity_data.mesh_instance.owner = scene_root
+ for shape in entity_data.collision_shapes:
+ if shape:
+ shape.owner = scene_root
+ if entity_data.occluder_instance:
+ entity_data.occluder_instance.owner = scene_root
+
+ apply_entity_properties(entity_node, entity_data)
+ declare_step.emit("Entity assembly and property application complete")
diff --git a/demo/addons/func_godot/src/core/entity_assembler.gd.uid b/demo/addons/func_godot/src/core/entity_assembler.gd.uid
new file mode 100644
index 0000000..7bec2d0
--- /dev/null
+++ b/demo/addons/func_godot/src/core/entity_assembler.gd.uid
@@ -0,0 +1 @@
+uid://dh73tfvwp7kr6
diff --git a/demo/addons/func_godot/src/core/geometry_generator.gd b/demo/addons/func_godot/src/core/geometry_generator.gd
new file mode 100644
index 0000000..c84210f
--- /dev/null
+++ b/demo/addons/func_godot/src/core/geometry_generator.gd
@@ -0,0 +1,635 @@
+@icon("res://addons/func_godot/icons/icon_slipgate.svg")
+class_name FuncGodotGeometryGenerator extends RefCounted
+## Geometry generation class that is instantiated by a [FuncGodotMap] node.
+
+const _SIGNATURE: String = "[GEO]"
+
+# Namespacing
+const _VERTEX_EPSILON := FuncGodotUtil._VERTEX_EPSILON
+const _VERTEX_EPSILON2 := _VERTEX_EPSILON * _VERTEX_EPSILON
+
+const _OriginType := FuncGodotFGDSolidClass.OriginType
+
+const _GroupData := FuncGodotData.GroupData
+const _EntityData := FuncGodotData.EntityData
+const _BrushData := FuncGodotData.BrushData
+const _PatchData := FuncGodotData.PatchData
+const _FaceData := FuncGodotData.FaceData
+const _VertexGroupData := FuncGodotData.VertexGroupData
+
+# Class members
+var map_settings: FuncGodotMapSettings = null
+var hyperplane_size: float = 512.0
+var entity_data: Array[_EntityData]
+var texture_materials: Dictionary[String, Material]
+var texture_sizes: Dictionary[String, Vector2]
+
+# Signals
+
+## Emitted when beginning a new step of the generation process.
+signal declare_step(step: String)
+
+func _init(settings: FuncGodotMapSettings = null, hplane_size: float = 512.0) -> void:
+ map_settings = settings
+ hyperplane_size = hplane_size
+
+#region TOOLS
+func is_skip(face: _FaceData) -> bool:
+ return FuncGodotUtil.is_skip(face.texture, map_settings)
+
+func is_clip(face: _FaceData) -> bool:
+ return FuncGodotUtil.is_clip(face.texture, map_settings)
+
+func is_origin(face: _FaceData) -> bool:
+ return FuncGodotUtil.is_origin(face.texture, map_settings)
+
+#endregion
+
+#region PATCHES
+func sample_bezier_curve(controls: Array[Vector3], t: float) -> Vector3:
+ var points: Array[Vector3] = controls.duplicate()
+ for i in controls.size():
+ for j in controls.size() - 1 - i:
+ points[j] = points[j].lerp(points[j + 1], t)
+ return points[0]
+
+func sample_bezier_surface(controls: Array[Vector3], width: int, height: int, u: float, v: float) -> Vector3:
+ var curve: Array[Vector3] = []
+ for x in range(width):
+ var col: Array[Vector3] = []
+ for y in range(height):
+ var idx := y * width + x
+ col.append(controls[idx])
+ curve.append(sample_bezier_curve(col, v))
+ return sample_bezier_curve(curve, u)
+
+# Generate patch triangle indices
+func get_triangle_indices(width: int, height: int) -> Array[int]:
+ var indices: Array[int] = []
+ if width < 2 or height < 2:
+ return indices
+
+ for row in range(height - 1):
+ for col in range(width - 1):
+ ## First triangle of the square; top left, top right, bottom left
+ indices.append(col + row * width)
+ indices.append((col + 1) + row * width)
+ indices.append(col + (row + 1) * width)
+
+ ## Second triangle of the square; top right, bottom right, bottom left
+ indices.append((col + 1) + row * width)
+ indices.append((col + 1) + (row + 1) * width)
+ indices.append(col + (row + 1) * width)
+ return indices
+
+func create_patch_mesh(data: Array[_PatchData], mesh: Mesh):
+ return
+
+#endregion
+
+#region BRUSHES
+func generate_base_winding(plane: Plane) -> PackedVector3Array:
+ var up := Vector3.UP
+ if abs(plane.normal.dot(up)) > 0.9:
+ up = Vector3.RIGHT
+
+ var right: Vector3 = plane.normal.cross(up).normalized()
+ var forward: Vector3 = right.cross(plane.normal).normalized()
+ var centroid: Vector3 = plane.get_center()
+
+ # construct oversized square on the plane to clip against
+ var winding := PackedVector3Array()
+ var h: float = hyperplane_size
+ winding.append(centroid + (right * h) + (forward * h))
+ winding.append(centroid + (right * -h) + (forward * h))
+ winding.append(centroid + (right * -h) + (forward * -h))
+ winding.append(centroid + (right * h) + (forward * -h))
+ return winding
+
+func generate_face_vertices(brush: _BrushData, face_index: int, vertex_merge_distance: float = 0.0) -> PackedVector3Array:
+ var plane: Plane = brush.faces[face_index].plane
+
+ # Generate initial square polygon to clip other planes against
+ var winding: PackedVector3Array = generate_base_winding(plane)
+
+ for other_face_index in brush.faces.size():
+ if other_face_index == face_index:
+ continue
+
+ # NOTE: This may need to be recentered to the origin, then moved back to the correct face position
+ # This problem may arise from floating point inaccuracy, given a large enough initial brush
+ winding = Geometry3D.clip_polygon(winding, brush.faces[other_face_index].plane)
+ if winding.is_empty():
+ break
+
+ # Perform rounding and merge adjacent vertices that are equivalent
+ if vertex_merge_distance > 0:
+ var merged_winding : PackedVector3Array = PackedVector3Array()
+ var prev_vtx : Vector3 = winding[0].snappedf(vertex_merge_distance)
+ merged_winding.append(prev_vtx)
+ for i in range(1, winding.size()):
+ var cur_vtx : Vector3 = winding[i].snappedf(vertex_merge_distance)
+ if prev_vtx != cur_vtx:
+ merged_winding.append(cur_vtx)
+ prev_vtx = cur_vtx
+ winding = merged_winding
+
+ return winding
+
+func generate_brush_vertices(entity_index: int, brush_index: int) -> void:
+ var entity: _EntityData = entity_data[entity_index]
+ var brush: _BrushData = entity.brushes[brush_index]
+ var vertex_merge_distance: float = entity.properties.get(map_settings.vertex_merge_distance_property, 0.0) as float
+
+ for face_index in brush.faces.size():
+ var face: _FaceData = brush.faces[face_index]
+ face.vertices = generate_face_vertices(brush, face_index, vertex_merge_distance)
+
+ face.normals.resize(face.vertices.size())
+ face.normals.fill(face.plane.normal)
+
+ var tangent: PackedFloat32Array = FuncGodotUtil.get_face_tangent(face)
+
+ # convert into OpenGL coordinates
+ for i in face.vertices.size():
+ face.tangents.append(tangent[1]) # Y
+ face.tangents.append(tangent[2]) # Z
+ face.tangents.append(tangent[0]) # X
+ face.tangents.append(tangent[3]) # W
+ return
+
+func generate_entity_vertices(entity_index: int) -> void:
+ var entity: _EntityData = entity_data[entity_index]
+ for brush_index in entity.brushes.size():
+ generate_brush_vertices(entity_index, brush_index)
+
+func determine_entity_origins(entity_index: int) -> void:
+ var entity: _EntityData = entity_data[entity_index]
+ var origin_type := _OriginType.BRUSH
+
+ if entity.definition is not FuncGodotFGDSolidClass:
+ if entity.brushes.is_empty():
+ return
+ else:
+ origin_type = entity.definition.origin_type
+
+ if entity_index == 0:
+ entity.origin = Vector3.ZERO
+ return
+
+ var entity_mins: Vector3 = Vector3.INF
+ var entity_maxs: Vector3 = Vector3.INF
+ var origin_mins: Vector3 = Vector3.INF
+ var origin_maxs: Vector3 = -Vector3.INF
+
+ for brush in entity.brushes:
+ for face in brush.faces:
+ for vertex in face.vertices:
+ if entity_mins != Vector3.INF:
+ entity_mins = entity_mins.min(vertex)
+ else:
+ entity_mins = vertex
+ if entity_maxs != Vector3.INF:
+ entity_maxs = entity_maxs.max(vertex)
+ else:
+ entity_maxs = vertex
+
+ if brush.origin:
+ if origin_mins != Vector3.INF:
+ origin_mins = origin_mins.min(vertex)
+ else:
+ origin_mins = vertex
+ if origin_maxs != Vector3.INF:
+ origin_maxs = origin_maxs.max(vertex)
+ else:
+ origin_maxs = vertex
+
+ # Default origin type is BOUNDS_CENTER
+ if entity_maxs != Vector3.INF and entity_mins != Vector3.INF:
+ entity.origin = entity_maxs - ((entity_maxs - entity_mins) * 0.5)
+
+ if origin_type != _OriginType.BOUNDS_CENTER and entity.brushes.size() > 0:
+ match origin_type:
+ _OriginType.ABSOLUTE, _OriginType.RELATIVE:
+ if "origin" in entity.properties:
+ var origin_comps: PackedFloat64Array = entity.properties["origin"].split_floats(" ")
+ if origin_comps.size() > 2:
+ if entity.origin_type == _OriginType.ABSOLUTE:
+ entity.origin = Vector3(origin_comps[0], origin_comps[1], origin_comps[2]) * map_settings.scale_factor
+ else: # _OriginType.RELATIVE
+ entity.origin += Vector3(origin_comps[0], origin_comps[1], origin_comps[2]) * map_settings.scale_factor
+
+ _OriginType.BRUSH:
+ if origin_mins != Vector3.INF:
+ entity.origin = origin_maxs - ((origin_maxs - origin_mins) * 0.5)
+
+ _OriginType.BOUNDS_MINS:
+ entity.origin = entity_mins
+
+ _OriginType.BOUNDS_MAXS:
+ entity.origin = entity_maxs
+
+ _OriginType.AVERAGED:
+ entity.origin = Vector3.ZERO
+ var vertices: PackedVector3Array
+ for brush in entity.brushes:
+ for face in brush.faces:
+ vertices.append_array(face.vertices)
+ entity.origin = FuncGodotUtil.op_vec3_avg(vertices)
+
+func wind_entity_faces(entity_index: int) -> void:
+ var entity: _EntityData = entity_data[entity_index]
+ for brush in entity.brushes:
+ for face in brush.faces:
+ # Faces should already be wound from the new generation process, but this should be tested further first.
+ face.wind()
+ face.index_vertices()
+
+func smooth_entity_vertices(entity_index: int) -> void:
+ var entity: _EntityData = entity_data[entity_index]
+ if not entity.is_smooth_shaded(map_settings.entity_smoothing_property):
+ return
+
+ var smoothing_angle: float = deg_to_rad(entity.get_smoothing_angle(map_settings.entity_smoothing_angle_property))
+ var vertex_map: Dictionary[Vector3, _VertexGroupData] = {}
+
+ # Group vertices by position and build map. NOTE: Vector3 keys can suffer from floating point precision.
+ # However, the vertex position should have already been snapped to _VERTEX_EPSILON.
+ for brush in entity.brushes:
+ for face in brush.faces:
+ for i in face.vertices.size():
+ var pos := face.vertices[i].snappedf(_VERTEX_EPSILON)
+
+ if not vertex_map.has(pos):
+ vertex_map[pos] = _VertexGroupData.new()
+
+ var data := vertex_map[pos]
+ data.faces.append(face)
+ data.face_indices.append(i)
+
+ var smoothed_normals: PackedVector3Array
+
+ for vertex_group in vertex_map.values():
+ if vertex_group.faces.size() <= 1:
+ continue
+
+ # Collect final normals in a temporary arrays
+ # These cannot be applied until all original normals have been checked.
+ smoothed_normals = []
+
+ for i in vertex_group.faces.size():
+ var this_face: _FaceData = vertex_group.faces[i]
+ var this_index: int = vertex_group.face_indices[i]
+ var this_normal: Vector3 = this_face.normals[this_index]
+ var average_normal: Vector3 = this_normal
+
+ for j in vertex_group.faces.size():
+ # Skip this face
+ if i == j:
+ continue
+
+ var other_face: _FaceData = vertex_group.faces[j]
+ var other_index: int = vertex_group.face_indices[j]
+ var other_normal: Vector3 = other_face.normals[other_index]
+
+ if this_normal.angle_to(other_normal) <= smoothing_angle:
+ average_normal += other_normal
+
+ # Store the averaged normal
+ smoothed_normals.append(average_normal.normalized())
+
+ # Apply smoothed normals back to face data
+ for i in vertex_group.faces.size():
+ var face: _FaceData = vertex_group.faces[i]
+ var index: int = vertex_group.face_indices[i]
+ face.normals[index] = smoothed_normals[i]
+ return
+
+#endregion
+
+func generate_entity_surfaces(entity_index: int) -> void:
+ var entity: _EntityData = entity_data[entity_index]
+
+ # Don't build for non-solid classes or solids without any brushes.
+ if not entity or entity.brushes.is_empty():
+ return
+
+ var def: FuncGodotFGDSolidClass
+ if entity.definition is not FuncGodotFGDSolidClass:
+ def = FuncGodotFGDSolidClass.new()
+ else:
+ def = entity.definition
+
+ var op_entity_ogl_xf: Callable = func(v: Vector3) -> Vector3:
+ return (FuncGodotUtil.id_to_opengl(v - entity.origin))
+
+ # Surface groupings
+ var surfaces: Dictionary[String, Array] = {}
+
+ # Metadata
+ var current_metadata_index: int = 0
+ var texture_names_metadata: Array[StringName] = []
+ var textures_metadata: PackedInt32Array = []
+ var vertices_metadata: PackedVector3Array = []
+ var normals_metadata: PackedVector3Array = []
+ var positions_metadata: PackedVector3Array = []
+ var shape_to_face_metadata: Array[PackedInt32Array] = []
+ var face_index_metadata_map: Dictionary[_FaceData, PackedInt32Array] = {}
+
+ # Arrange faces by surface texture
+ for brush in entity.brushes:
+ for face in brush.faces:
+ if is_skip(face) or is_origin(face):
+ continue
+
+ if not surfaces.has(face.texture):
+ surfaces[face.texture] = []
+ surfaces[face.texture].append(face)
+
+ # Cache order for consistency when rebuilding
+ var textures: Array[String] = surfaces.keys()
+
+ # Output mesh data
+ var mesh := ArrayMesh.new()
+ var mesh_arrays: Array[Array] = []
+ var build_concave: bool = entity.is_collision_concave()
+ var concave_vertices: PackedVector3Array
+
+ # Iteration variables
+ var arrays: Array
+ var faces: Array
+
+ # MULTISURFACE SCOPE BEGIN
+ for texture_name in textures:
+ # SURFACE SCOPE BEGIN
+ faces = surfaces[texture_name]
+
+ # Get texture index for metadata
+ var tex_index: int = texture_names_metadata.size()
+ if def.add_textures_metadata:
+ texture_names_metadata.append(texture_name)
+
+ # Prepare new array
+ arrays = Array()
+ arrays.resize(ArrayMesh.ARRAY_MAX)
+ arrays[Mesh.ARRAY_VERTEX] = PackedVector3Array()
+ arrays[Mesh.ARRAY_NORMAL] = PackedVector3Array()
+ arrays[Mesh.ARRAY_TANGENT] = PackedFloat32Array()
+ arrays[Mesh.ARRAY_TEX_UV] = PackedVector2Array()
+ arrays[Mesh.ARRAY_INDEX] = PackedInt32Array()
+
+ # Begin fresh index offset for this subarray
+ var index_offset: int = 0
+
+ for face: _FaceData in faces:
+ # FACE SCOPE BEGIN
+
+ # Reject invalid faces
+ if face.vertices.size() < 3 or is_skip(face) or is_origin(face):
+ continue
+
+ #region Reject interior faces only if desired
+ if entity.properties.get(map_settings.cull_interior_faces_property, false):
+ var remove_face := false
+ for face2: _FaceData in faces:
+ if face == face2:
+ continue
+ # Are the planes aligned?
+ if !face2.plane.has_point(face.plane.get_center()):
+ continue
+ # Opposite planes
+ if !(face.plane.normal*-1.0).is_equal_approx(face2.plane.normal):
+ continue;
+
+ # Check for faces that share all their vertices.
+ var all_verts_in_face := true
+ for vert in face.vertices:
+ if !face2.vertices.has(vert):
+ all_verts_in_face = false
+ break;
+ if all_verts_in_face:
+ remove_face = true
+ break
+
+ # Check if all vertices of Face1 intersect with any triangle of face 2
+ # If they do, then Face 1 is entirely overlapped on Face 2 and we can remove Face 1
+ var all_verts_in_face2 := true
+ for vert in face.vertices:
+ var vert_in_any_tri := false
+ var from := vert - face2.plane.normal*0.001
+ var to := face2.plane.normal*0.001
+
+ # Loop over all triangles in face 2 and see if the vert intersects any of them
+ for i in ((face2.indices.size()/3)):
+ var intersect = Geometry3D.ray_intersects_triangle(
+ from,
+ to,
+ face2.vertices[face2.indices[i*3]],
+ face2.vertices[face2.indices[i*3 + 1]],
+ face2.vertices[face2.indices[i*3 + 2]]
+ )
+ if !intersect:
+ continue
+ if intersect:
+ vert_in_any_tri = true
+ break;
+ # This vert didn't show up any triangle, can't remove this face
+ if !vert_in_any_tri:
+ all_verts_in_face2 = false
+ break
+ # All verts of face 1 are in face 2, so we can safely remove that face
+ if all_verts_in_face2:
+ remove_face = true
+ break;
+ if remove_face:
+ continue;
+ #endregion
+
+ # Create trimesh points regardless of texture
+ if build_concave:
+ var tris: PackedVector3Array
+ tris.resize(face.indices.size())
+
+ # Add triangles from face indices directly
+ # TODO: This can possibly be merged with the below loop in a clever way
+ for i in face.indices.size():
+ tris[i] = op_entity_ogl_xf.call(face.vertices[face.indices[i]])
+
+ concave_vertices.append_array(tris)
+
+ # Do not generate visuals for clip textures
+ if is_clip(face):
+ continue
+
+ # Handle metadata for this face
+ # Add metadata per triangle rather than per face to keep consistent metadata
+ var num_tris = face.indices.size() / 3
+ if def.add_textures_metadata:
+ var tex_array: Array[int] = []
+ tex_array.resize(num_tris)
+ tex_array.fill(tex_index)
+ textures_metadata.append_array(tex_array)
+ if def.add_face_normal_metadata:
+ var normal_array: Array[Vector3] = []
+ normal_array.resize(num_tris)
+ normal_array.fill(FuncGodotUtil.id_to_opengl(face.plane.normal))
+ normals_metadata.append_array(normal_array)
+ if def.add_face_position_metadata:
+ for i in num_tris:
+ var triangle_indices: Array[int] = []
+ var triangle_vertices: Array[Vector3] = []
+ triangle_indices.assign(face.indices.slice(i * 3, i * 3 + 3))
+ triangle_vertices.assign(triangle_indices.map(func(idx : int) -> Vector3: return face.vertices[idx]))
+ var position := FuncGodotUtil.op_vec3_avg(triangle_vertices)
+ positions_metadata.append(op_entity_ogl_xf.call(position))
+ if def.add_vertex_metadata:
+ for i in face.indices:
+ vertices_metadata.append(op_entity_ogl_xf.call(face.vertices[i]))
+ if def.add_collision_shape_to_face_indices_metadata:
+ face_index_metadata_map[face] = PackedInt32Array(range(current_metadata_index, current_metadata_index + num_tris))
+ current_metadata_index += num_tris
+
+ # Append face data to surface array
+ for i in face.vertices.size():
+ # TODO: Mesh metadata may be generated here.
+ var v: Vector3 = face.vertices[i]
+ arrays[ArrayMesh.ARRAY_VERTEX].append(op_entity_ogl_xf.call(v))
+ arrays[ArrayMesh.ARRAY_NORMAL].append(FuncGodotUtil.id_to_opengl(face.normals[i]))
+ var tx_sz: Vector2 = texture_sizes.get(face.texture, Vector2.ONE * map_settings.inverse_scale_factor)
+ arrays[ArrayMesh.ARRAY_TEX_UV].append(FuncGodotUtil.get_face_vertex_uv(v, face, tx_sz))
+
+ for j in 4:
+ arrays[ArrayMesh.ARRAY_TANGENT].append(face.tangents[(i * 4) + j])
+
+ # Create offset indices for the visual mesh
+ var op_shift_index: Callable = (func(a: int) -> int: return a + index_offset)
+ arrays[ArrayMesh.ARRAY_INDEX].append_array(Array(face.indices).map(op_shift_index))
+
+ index_offset += face.vertices.size()
+
+ # FACE SCOPE END
+
+ if FuncGodotUtil.filter_face(texture_name, map_settings):
+ continue
+
+ mesh_arrays.append(arrays)
+
+ # SURFACE SCOPE END
+
+ # MULTISURFACE SCOPE END
+ textures.erase(map_settings.clip_texture)
+
+ if def.build_visuals:
+ # Build mesh
+ for array_index in mesh_arrays.size():
+ mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, mesh_arrays[array_index])
+ mesh.surface_set_name(array_index, textures[array_index])
+ mesh.surface_set_material(array_index, texture_materials[textures[array_index]])
+
+ # Apply mesh metadata
+ if def.add_textures_metadata:
+ entity.mesh_metadata["texture_names"] = texture_names_metadata
+ entity.mesh_metadata["textures"] = textures_metadata
+ if def.add_vertex_metadata:
+ entity.mesh_metadata["vertices"] = vertices_metadata
+ if def.add_face_normal_metadata:
+ entity.mesh_metadata["normals"] = normals_metadata
+ if def.add_face_position_metadata:
+ entity.mesh_metadata["positions"] = positions_metadata
+
+ entity.mesh = mesh
+
+ # Clear up unusued memory
+ arrays = []
+ surfaces = {}
+
+ if entity.is_collision_convex():
+ var sh: ConvexPolygonShape3D
+ for b in entity.brushes:
+ if b.planes.is_empty() or b.origin:
+ continue
+
+ var points := Array(Geometry3D.compute_convex_mesh_points(b.planes)).map(op_entity_ogl_xf)
+ if points.is_empty():
+ continue
+
+ sh = ConvexPolygonShape3D.new()
+ sh.points = points
+ entity.shapes.append(sh)
+
+ if def.add_collision_shape_to_face_indices_metadata:
+ # convex collision has one shape per brush, so collect the
+ # indices for this brush's faces
+ var face_indices_array : PackedInt32Array = []
+ for face in b.faces:
+ if face_index_metadata_map.has(face):
+ face_indices_array.append_array(face_index_metadata_map[face])
+ shape_to_face_metadata.append(face_indices_array)
+
+ elif build_concave and concave_vertices.size():
+ var sh := ConcavePolygonShape3D.new()
+ sh.set_faces(concave_vertices)
+ entity.shapes.append(sh)
+
+ if def.add_collision_shape_to_face_indices_metadata:
+ # for concave collision the shape will always represent every face
+ # in the entity, so just add every face here
+ var face_indices_array : PackedInt32Array = []
+ for fm in face_index_metadata_map.values():
+ face_indices_array.append_array(fm)
+ shape_to_face_metadata.append(face_indices_array)
+
+ if def.add_collision_shape_to_face_indices_metadata:
+ # this metadata will be mapped to the actual shape node names during entity assembly
+ entity.mesh_metadata["shape_to_face_array"] = shape_to_face_metadata
+
+func unwrap_uv2s(entity_index: int, texel_size: float) -> void:
+ var entity: _EntityData = entity_data[entity_index]
+ # NOTE: This skips smoothed meshes as they need to be unwrapped after smoothing.
+ # Ideally smoothing will be performed here in GeoGen before this process.
+ # For now, since it occurs in EntityAssembler, skip it.
+ if entity.mesh and entity.is_gi_enabled() and not entity.is_smooth_shaded(map_settings.entity_smoothing_property):
+ entity.mesh.lightmap_unwrap(Transform3D.IDENTITY, texel_size)
+
+# Main build process
+func build(build_flags: int, entities: Array[_EntityData]) -> Error:
+ var entity_count: int = entities.size()
+ declare_step.emit("Preparing %s %s" % [entity_count, "entity" if entity_count == 1 else "entities"])
+ entity_data = entities
+
+ declare_step.emit("Gathering materials")
+ var texture_map: Array[Dictionary] = FuncGodotUtil.build_texture_map(entity_data, map_settings)
+ texture_materials = texture_map[0]
+ texture_sizes = texture_map[1]
+
+ var task_id: int
+ declare_step.emit("Generating brush vertices")
+ task_id = WorkerThreadPool.add_group_task(generate_entity_vertices, entity_count, -1, false, "Generate Brush Vertices")
+ WorkerThreadPool.wait_for_group_task_completion(task_id)
+
+ declare_step.emit("Determining solid entity origins")
+ task_id = WorkerThreadPool.add_group_task(determine_entity_origins, entity_count, -1, false, "Determine Entity Origins")
+ WorkerThreadPool.wait_for_group_task_completion(task_id)
+
+ declare_step.emit("Winding faces")
+ task_id = WorkerThreadPool.add_group_task(wind_entity_faces, entity_count, -1, false, "Wind Brush Faces")
+ WorkerThreadPool.wait_for_group_task_completion(task_id)
+
+ # TODO: Reimplement after solving issues
+ #if not (build_flags & FuncGodotMap.BuildFlags.DISABLE_SMOOTHING):
+ # declare_step.emit("Smoothing entity faces")
+ # task_id = WorkerThreadPool.add_group_task(smooth_entity_vertices, entity_count, -1, false, "Smooth Entities")
+ # WorkerThreadPool.wait_for_group_task_completion(task_id)
+
+ declare_step.emit("Generating surfaces")
+ task_id = WorkerThreadPool.add_group_task(generate_entity_surfaces, entity_count, -1, false, "Generate Surfaces")
+ WorkerThreadPool.wait_for_group_task_completion(task_id)
+
+ if build_flags & FuncGodotMap.BuildFlags.UNWRAP_UV2:
+ declare_step.emit("Unwrapping UV2s")
+ var texel_size: float = map_settings.uv_unwrap_texel_size * map_settings.scale_factor
+ for entity_index in entity_count:
+ unwrap_uv2s(entity_index, texel_size)
+
+ declare_step.emit("Geometry generation complete")
+ return OK
diff --git a/demo/addons/func_godot/src/core/geometry_generator.gd.uid b/demo/addons/func_godot/src/core/geometry_generator.gd.uid
new file mode 100644
index 0000000..02d12c4
--- /dev/null
+++ b/demo/addons/func_godot/src/core/geometry_generator.gd.uid
@@ -0,0 +1 @@
+uid://b1yg28xbyno7v
diff --git a/demo/addons/func_godot/src/core/parser.gd b/demo/addons/func_godot/src/core/parser.gd
new file mode 100644
index 0000000..dc2db4a
--- /dev/null
+++ b/demo/addons/func_godot/src/core/parser.gd
@@ -0,0 +1,592 @@
+@icon("res://addons/func_godot/icons/icon_godambler.svg")
+class_name FuncGodotParser extends RefCounted
+## MAP and VMF parser class that is instantiated by a [FuncGodotMap] node during the build process.
+##
+## @tutorial(Quake Wiki Map Format Article): https://quakewiki.org/wiki/Quake_Map_Format
+## @tutorial(Valve Developer Wiki VMF Article): https://developer.valvesoftware.com/wiki/VMF_(Valve_Map_Format)
+
+const _SIGNATURE: String = "[PRS]"
+
+const _GroupData := FuncGodotData.GroupData
+const _EntityData := FuncGodotData.EntityData
+const _BrushData := FuncGodotData.BrushData
+const _PatchData := FuncGodotData.PatchData
+const _FaceData := FuncGodotData.FaceData
+const _ParseData := FuncGodotData.ParseData
+
+## Emitted when a step in the parsing process is completed.
+## It is connected to [method FuncGodotUtil.print_profile_info] method if [member FuncGodotMap.build_flags] SHOW_PROFILE_INFO flag is set.
+signal declare_step(step: String)
+
+## Parses the map file, generating entity and group data and sub-data, then returns the generated data as an array of arrays.
+## The first array is Array[FuncGodotData.EntityData], while the second array is Array[FuncGodotData.GroupData].
+func parse_map_data(map_file: String, map_settings: FuncGodotMapSettings) -> _ParseData:
+ var map_data: PackedStringArray = []
+ var parse_data := _ParseData.new()
+ declare_step.emit("Loading map file %s" % map_file)
+
+ # Retrieve real path if needed
+ if map_file.begins_with("uid://"):
+ var uid := ResourceUID.text_to_id(map_file)
+ if not ResourceUID.has_id(uid):
+ printerr("Error: failed to retrieve path for UID (%s)" % map_file)
+ return parse_data
+ map_file = ResourceUID.get_id_path(uid)
+
+ # Open the map file
+ var file: FileAccess = FileAccess.open(map_file, FileAccess.READ)
+ if not file:
+ file = FileAccess.open(map_file + ".import", FileAccess.READ)
+ if file:
+ map_file += ".import"
+ else:
+ printerr("Error: Failed to open map file (" + map_file + ")")
+ return parse_data
+
+ # Packed map file resources need to be accessed differently in exported projects.
+ if map_file.ends_with(".import"):
+ while not file.eof_reached():
+ var line: String = file.get_line()
+ if line.begins_with("path"):
+ file.close()
+ line = line.replace("path=", "")
+ line = line.replace('"', '')
+ var data: String = (load(line) as QuakeMapFile).map_data
+ if data.is_empty():
+ printerr("Error: Failed to open map file (" + line + ")")
+ return parse_data
+ map_data = data.split("\n")
+ break
+ else:
+ while not file.eof_reached():
+ map_data.append(file.get_line())
+
+ # Determine map type and parse data
+ if map_file.to_lower().contains(".map"):
+ declare_step.emit("Parsing as Quake MAP")
+ parse_data = _parse_quake_map(map_data, map_settings, parse_data)
+ elif map_file.to_lower().contains(".vmf"):
+ declare_step.emit("Parsing as Source VMF")
+ parse_data = _parse_vmf(map_data, map_settings, parse_data)
+
+ # Determine group hierarchy
+ declare_step.emit("Determining groups hierarchy")
+ var groups_data: Array[_GroupData] = parse_data.groups
+ for g in groups_data:
+ if g.parent_id != -1:
+ for p in groups_data:
+ if p.id == g.parent_id:
+ g.parent = p
+ break
+
+ var entities_data: Array[_EntityData] = parse_data.entities
+ var entity_defs: Dictionary[String, FuncGodotFGDEntityClass] = map_settings.entity_fgd.get_entity_definitions()
+ var missing_defs: PackedStringArray = []
+
+ var default_point_class := FuncGodotFGDPointClass.new()
+ default_point_class.node_class = "Marker3D"
+
+ var default_solid_class := FuncGodotFGDSolidClass.new()
+ default_solid_class.spawn_type = FuncGodotFGDSolidClass.SpawnType.ENTITY
+ default_solid_class.build_occlusion = false
+ default_solid_class.collision_shape_type = FuncGodotFGDSolidClass.CollisionShapeType.NONE
+ default_solid_class.origin_type = FuncGodotFGDSolidClass.OriginType.BRUSH
+
+ declare_step.emit("Checking entity omission, definition status, and property types")
+
+ # Cache retrieved class property defaults. Format is Dictionary[Classname, Properties].
+ var prop_defaults_cache: Dictionary[String, Dictionary] = {}
+ var prop_descriptions_cache: Dictionary[String, Dictionary] = {}
+
+ for i in range(entities_data.size() - 1, -1, -1):
+ var entity: _EntityData = entities_data[i]
+
+ # Delete entities from omitted groups
+ if entity.group != null and entity.group.omit == true:
+ entities_data.remove_at(i)
+ continue
+
+ # Provide entity definition to entity data. This gets used in both
+ # geo generation and entity assembly.
+ if "classname" in entity.properties:
+ var classname: String = entity.properties["classname"]
+ if classname in entity_defs:
+ entity.definition = entity_defs[classname]
+ if not entity.definition is FuncGodotFGDSolidClass and not entity.definition is FuncGodotFGDPointClass:
+ if missing_defs.find(classname) < 0:
+ push_error("Invalid entity definition for \"" + classname + "\". Entity definition must be Solid Class or Point Class.")
+ missing_defs.append(classname)
+ entity.definition = null
+ elif missing_defs.find(classname) < 0:
+ push_error("No entity definition found for \"" + classname + "\"")
+ missing_defs.append(classname)
+
+ # Make sure we have a default definition to build entities from
+ # This will make sure nothing goes wrong in the build processes
+ if not entity.definition:
+ if entity.brushes.is_empty():
+ entity.definition = default_point_class
+ else:
+ entity.definition = default_solid_class
+
+ # Convert the string values of the entity's properties Dictionary to various
+ # Variant formats based on the entity definition's class property defaults.
+ var def := entity.definition
+ var properties: Dictionary = entity.properties
+ for property in properties:
+ var prop_string = entity.properties[property]
+ if property in def.class_properties:
+ var prop_default: Variant = def.class_properties[property]
+
+ match typeof(prop_default):
+ TYPE_INT:
+ properties[property] = prop_string.to_int()
+ TYPE_FLOAT:
+ properties[property] = prop_string.to_float()
+ TYPE_BOOL:
+ properties[property] = bool(prop_string.to_int())
+ TYPE_VECTOR3:
+ var prop_comps: PackedFloat64Array = prop_string.split_floats(" ")
+ if prop_comps.size() > 2:
+ properties[property] = Vector3(prop_comps[0], prop_comps[1], prop_comps[2])
+ else:
+ push_error("Invalid Vector3 format for \'" + property + "\' in entity \'" + def.classname + "\': " + prop_string)
+ properties[property] = prop_default
+ TYPE_VECTOR3I:
+ var prop_vec: Vector3i = prop_default
+ var prop_comps: PackedStringArray = prop_string.split(" ")
+ if prop_comps.size() > 2:
+ for v in 3:
+ prop_vec[v] = prop_comps[v].to_int()
+ else:
+ push_error("Invalid Vector3i format for \'" + property + "\' in entity \'" + def.classname + "\': " + prop_string)
+ properties[property] = prop_vec
+ TYPE_COLOR:
+ var prop_color: Color = prop_default
+ var prop_comps: PackedStringArray = prop_string.split(" ")
+ if prop_comps.size() > 2:
+ prop_color.r8 = prop_comps[0].to_int()
+ prop_color.g8 = prop_comps[1].to_int()
+ prop_color.b8 = prop_comps[2].to_int()
+ prop_color.a = 1.0
+ else:
+ push_error("Invalid Color format for \'" + property + "\' in entity \'" + def.classname + "\': " + prop_string)
+ properties[property] = prop_color
+ TYPE_DICTIONARY:
+ var prop_desc = def.class_property_descriptions[property]
+ if prop_desc is Array and prop_desc.size() > 1 and prop_desc[1] is int:
+ properties[property] = prop_string.to_int()
+ TYPE_ARRAY:
+ properties[property] = prop_string.to_int()
+ TYPE_VECTOR2:
+ var prop_comps: PackedFloat64Array = prop_string.split_floats(" ")
+ if prop_comps.size() > 1:
+ properties[property] = Vector2(prop_comps[0], prop_comps[1])
+ else:
+ push_error("Invalid Vector2 format for \'" + property + "\' in entity \'" + def.classname + "\': " + prop_string)
+ properties[property] = prop_default
+ TYPE_VECTOR2I:
+ var prop_vec: Vector2i = prop_default
+ var prop_comps: PackedStringArray = prop_string.split(" ")
+ if prop_comps.size() > 1:
+ for v in 2:
+ prop_vec[v] = prop_comps[v].to_int()
+ else:
+ push_error("Invalid Vector2i format for \'" + property + "\' in entity \'" + def.classname + "\': " + prop_string)
+ properties[property] = prop_vec
+ TYPE_VECTOR4:
+ var prop_comps: PackedFloat64Array = prop_string.split_floats(" ")
+ if prop_comps.size() > 3:
+ properties[property] = Vector4(prop_comps[0], prop_comps[1], prop_comps[2], prop_comps[3])
+ else:
+ push_error("Invalid Vector4 format for \'" + property + "\' in entity \'" + def.classname + "\': " + prop_string)
+ properties[property] = prop_default
+ TYPE_VECTOR4I:
+ var prop_vec: Vector4i = prop_default
+ var prop_comps: PackedStringArray = prop_string.split(" ")
+ if prop_comps.size() > 3:
+ for v in 4:
+ prop_vec[v] = prop_comps[v].to_int()
+ else:
+ push_error("Invalid Vector4i format for \'" + property + "\' in entity \'" + def.classname + "\': " + prop_string)
+ properties[property] = prop_vec
+ TYPE_STRING_NAME:
+ properties[property] = StringName(prop_string)
+ TYPE_NODE_PATH:
+ properties[property] = prop_string
+ TYPE_OBJECT:
+ properties[property] = prop_string
+
+ # Retrieve default properties.
+ var def_properties: Dictionary[String, Variant] = prop_defaults_cache.get(def.classname, def.retrieve_all_class_properties())
+ var def_descriptions: Dictionary[String, Variant] = prop_descriptions_cache.get(def.classname, def.retrieve_all_class_property_descriptions())
+
+ # Assign properties not defined with defaults from the entity definition
+ for property in def_properties:
+ if not property in properties:
+ var prop_default: Variant = def_properties[property]
+ # Flags
+ if prop_default is Array:
+ var prop_flags_sum := 0
+ for prop_flag in prop_default:
+ if prop_flag is Array and prop_flag.size() > 2:
+ if prop_flag[2] and prop_flag[1] is int:
+ prop_flags_sum += prop_flag[1]
+ properties[property] = prop_flags_sum
+ # Choices
+ elif prop_default is Dictionary:
+ var prop_desc = def_descriptions.get(property, "")
+ if prop_desc is Array and prop_desc.size() > 1 and (prop_desc[1] is int or prop_desc[1] is String):
+ properties[property] = prop_desc[1]
+ elif prop_default.size():
+ properties[property] = prop_default[prop_default.keys().front()]
+ else:
+ properties[property] = 0
+ # Materials, Shaders, and Sounds
+ elif prop_default is Resource:
+ properties[property] = prop_default.resource_path
+ # Target Destination and Target Source
+ elif prop_default is NodePath or prop_default is Object or prop_default == null:
+ properties[property] = ""
+ # Everything else
+ else:
+ properties[property] = prop_default
+
+ # Delete omitted groups
+ declare_step.emit("Removing omitted layers and groups")
+ for i in range(groups_data.size() - 1, -1, -1):
+ if groups_data[i].omit == true:
+ groups_data.remove_at(i)
+
+ declare_step.emit("Map parsing complete")
+ return parse_data
+
+## Parser subroutine called by [method parse_map_data], specializing in the Quake MAP format.
+func _parse_quake_map(map_data: PackedStringArray, map_settings: FuncGodotMapSettings, parse_data: _ParseData) -> _ParseData:
+ var entities_data: Array[_EntityData] = parse_data.entities
+ var groups_data: Array[_GroupData] = parse_data.groups
+ var ent: _EntityData = null
+ var brush: _BrushData = null
+ var patch: _PatchData = null
+ var scope: int = 0 # Scope level, to keep track of where we are in PatchDef parsing
+
+ for line in map_data:
+ line = line.replace("\t", "")
+
+ #region START DATA
+ # Start entity, brush, or patchdef
+ if line.begins_with("{"):
+ if not ent:
+ ent = _EntityData.new()
+ else:
+ if not patch:
+ brush = _BrushData.new()
+ else:
+ scope += 1
+ continue
+ #endregion
+
+ #region COMMIT DATA
+ # Commit entity or brush
+ if line.begins_with("}"):
+ if brush:
+ ent.brushes.append(brush)
+ brush = null
+ elif patch:
+ if scope:
+ scope -= 1
+ else:
+ ent.patches.append(patch)
+ patch = null
+ else:
+ # TrenchBroom layers and groups
+ if ent.properties["classname"] == "func_group" and ent.properties.has("_tb_type"):
+ # Merge TB Group / Layer structural brushes with worldspawn
+ if entities_data.size():
+ entities_data[0].brushes.append_array(ent.brushes)
+
+ # Create group data
+ var group: _GroupData = _GroupData.new()
+ var props: Dictionary = ent.properties
+ group.id = props["_tb_id"] as int
+ if props["_tb_type"] == "_tb_layer":
+ group.type = _GroupData.GroupType.GROUP
+ group.name = "layer_"
+ else:
+ group.name = "group_"
+ group.name = group.name + str(group.id)
+ if props["_tb_name"] != "Unnamed":
+ group.name = group.name + "_" + (props["_tb_name"] as String).replace(" ", "_")
+ if props.has("_tb_layer"):
+ group.parent_id = props["_tb_layer"] as int
+ if props.has("_tb_group"):
+ group.parent_id = props["_tb_group"] as int
+ if props.has("_tb_layer_omit_from_export"):
+ group.omit = true
+
+ # Commit group
+ groups_data.append(group)
+
+ # Commit entity
+ else:
+ entities_data.append(ent)
+ ent = null
+ continue
+ #endregion
+
+ #region PROPERTY DATA
+ # Retrieve key value pairs
+ if line.begins_with("\""):
+ var tokens: PackedStringArray = line.split("\" \"")
+ if tokens.size() < 2:
+ tokens = line.split("\"\"")
+ var key: String = tokens[0].trim_prefix("\"")
+ var value: String = tokens[1].trim_suffix("\"")
+ ent.properties[key] = value
+ #endregion
+
+ #region BRUSH DATA
+ if brush and line.begins_with("("):
+ line = line.replace("(","")
+ var tokens: PackedStringArray = line.split(" ) ")
+
+ # Retrieve plane data
+ var points: PackedVector3Array
+ points.resize(3)
+ for i in 3:
+ tokens[i] = tokens[i].trim_prefix("(")
+ var pts: PackedFloat64Array = tokens[i].split_floats(" ", false)
+ var point := Vector3(pts[0], pts[1], pts[2]) * map_settings.scale_factor
+ points[i] = point
+
+ var plane := Plane(points[0], points[1], points[2])
+ brush.planes.append(plane)
+
+ var face: _FaceData = _FaceData.new()
+ face.plane = plane
+
+ # Retrieve texture data
+ var tex: String = String()
+ if tokens[3].begins_with("\""): # textures with spaces get surrounded by double quotes
+ var last_quote := tokens[3].rfind("\"")
+ tex = tokens[3].substr(1, last_quote - 1)
+ tokens = tokens[3].substr(last_quote + 2).split(" ] ")
+ else:
+ tex = tokens[3].get_slice(" ", 0)
+ tokens = tokens[3].trim_prefix(tex + " ").split(" ] ")
+ face.texture = tex
+
+ # Check for origin brushes. Brushes must be completely textured with origin to be valid.
+ if brush.faces.is_empty():
+ if tex == map_settings.origin_texture:
+ brush.origin = true
+ elif brush.origin == true:
+ if tex != map_settings.origin_texture:
+ brush.origin = false
+
+ # Retrieve UV data
+ var uv: Transform2D = Transform2D.IDENTITY
+
+ # Valve 220: texname [ ux uy ux offsetX ] [vx vy vz offsetY] rotation scaleX scaleY
+ if tokens.size() > 1:
+ var coords: PackedFloat64Array
+ for i in 2:
+ coords = tokens[i].trim_prefix("[ ").split_floats(" ", false)
+ face.uv_axes.append(Vector3(coords[0], coords[1], coords[2])) # Save axis vectors separately
+ face.uv.origin[i] = coords[3] # UV offset stored as transform origin
+
+ coords = tokens[2].split_floats(" ", false)
+ # UV scale factor stored in basis
+ face.uv.x = Vector2(coords[1], 0.0) * map_settings.scale_factor
+ face.uv.y = Vector2(0.0, coords[2]) * map_settings.scale_factor
+
+ # Quake Standard: texname offsetX offsetY rotation scaleX scaleY
+ else:
+ var coords: PackedFloat64Array = tokens[0].split_floats(" ", false)
+ face.uv.origin = Vector2(coords[0], coords[1])
+
+ var r: float = deg_to_rad(coords[2])
+ face.uv.x = Vector2(cos(r), -sin(r)) * coords[3] * map_settings.scale_factor
+ face.uv.y = Vector2(sin(r), cos(r)) * coords[4] * map_settings.scale_factor
+
+ brush.faces.append(face)
+ continue
+ #endregion
+
+ #region PATCH DATA
+ if patch:
+ if line.begins_with("("):
+ line = line.replace("( ","")
+ # Retrieve patch control points
+ if patch.size:
+ var tokens: PackedStringArray = line.replace("(", "").split(" )", false)
+ for i in tokens.size():
+ var subtokens: PackedFloat64Array = tokens[i].split_floats(" ", false)
+ patch.points.append(Vector3(subtokens[0], subtokens[1], subtokens[2]))
+ patch.uvs.append(Vector2(subtokens[3], subtokens[4]))
+ # Retrieve patch size
+ else:
+ var tokens: PackedStringArray = line.replace(")","").split(" ", false)
+ patch.size.resize(tokens.size())
+ for i in tokens.size():
+ patch.size[i] = tokens[i].to_int()
+ # Retrieve patch texture
+ elif not line.begins_with(")"):
+ patch.texture = line.replace("\"","")
+
+ if line.begins_with("patchDef"):
+ brush = null
+ patch = _PatchData.new()
+ continue
+ #endregion
+
+ #region ASSIGN GROUPS
+ for e in entities_data:
+ var group_id: int = -1
+ if e.properties.has("_tb_layer"):
+ group_id = e.properties["_tb_layer"] as int
+ elif e.properties.has("_tb_group"):
+ group_id = e.properties["_tb_group"] as int
+ if group_id != -1:
+ for g in groups_data:
+ if g.id == group_id:
+ e.group = g
+ break
+ #endregion
+
+ return parse_data
+
+## Parser subroutine called by [method parse_map_data], specializing in the Valve Map Format used by Hammer based editors.
+func _parse_vmf(map_data: PackedStringArray, map_settings: FuncGodotMapSettings, parse_data: _ParseData) -> _ParseData:
+ var entities_data: Array[_EntityData] = parse_data.entities
+ var groups_data: Array[_GroupData] = parse_data.groups
+ var ent: _EntityData = null
+ var brush: _BrushData = null
+ var group: _GroupData = null
+ var group_parent_hierarchy: Array[_GroupData] = []
+ var scope: int = 0
+
+ for line in map_data:
+ line = line.replace("\t", "")
+
+ #region START DATA
+ if line.begins_with("entity") or line.begins_with("world"):
+ ent = _EntityData.new()
+ continue
+ if line.begins_with("solid"):
+ brush = _BrushData.new()
+ continue
+ if brush and line.begins_with("{"):
+ scope += 1
+ continue
+ if line == "visgroup":
+ if group != null:
+ groups_data.append(group)
+ group_parent_hierarchy.append(group)
+ group = _GroupData.new()
+ if group_parent_hierarchy.size():
+ group.parent = group_parent_hierarchy.back()
+ group.parent_id = group.parent.id
+ continue
+ #endregion
+
+ #region COMMIT DATA
+ if line.begins_with("}"):
+ if scope > 0:
+ scope -= 1
+ if not scope:
+ if brush:
+ if brush.faces.size():
+ ent.brushes.append(brush)
+ brush = null
+ elif ent:
+ entities_data.append(ent)
+ ent = null
+ elif group:
+ groups_data.append(group)
+ group = null
+ elif group_parent_hierarchy.size():
+ group_parent_hierarchy.pop_back()
+ continue
+ #endregion
+
+ # Retrieve key value pairs
+ if (ent or group) and line.begins_with("\""):
+ var tokens: PackedStringArray = line.split("\" \"")
+ var key: String = tokens[0].trim_prefix("\"")
+ var value: String = tokens[1].trim_suffix("\"")
+
+ #region BRUSH DATA
+ if brush:
+ if scope > 1:
+ match key:
+ "plane":
+ tokens = value.replace("(", "").split(")", false)
+ var points: PackedVector3Array
+ points.resize(3)
+ for i in 3:
+ tokens[i] = tokens[i].trim_prefix("(")
+ var pts: PackedFloat64Array = tokens[i].split_floats(" ", false)
+ var point: Vector3 = Vector3(pts[0], pts[1], pts[2]) * map_settings.scale_factor
+ points[i] = point
+ brush.planes.append(Plane(points[0], points[1], points[2]))
+ brush.faces.append(_FaceData.new())
+ brush.faces[-1].plane = brush.planes[-1]
+ continue
+ "material":
+ if brush.faces.size():
+ brush.faces[-1].texture = value
+ # Origin brush needs to be completely set to origin, otherwise it's invalid
+ if brush.faces.size() < 2:
+ if value == map_settings.origin_texture:
+ brush.origin = true
+ elif brush.origin == true:
+ if value != map_settings.origin_texture:
+ brush.origin = false
+ continue
+ "uaxis", "vaxis":
+ if brush.faces.size():
+ value = value.replace("[", "")
+ var vals: PackedFloat64Array = value.replace("]", "").split_floats(" ", false)
+ var face: _FaceData = brush.faces[-1]
+ face.uv_axes.append(Vector3(vals[0], vals[1], vals[2]))
+ if key.begins_with("u"):
+ face.uv.origin.x = vals[3] # Offset
+ face.uv.x *= vals[4] * map_settings.scale_factor # Scale
+ else:
+ face.uv.origin.y = vals[3] # Offset
+ face.uv.y *= vals[4] * map_settings.scale_factor # Scale
+ continue
+ "rotation":
+ # Rotation isn't used in Valve 220 mapping and VMFs are 220 exclusive
+ continue
+ "visgroupid":
+ # Don't put worldspawn into a group
+ if entities_data.size():
+ # Only nodes can be organized into groups in the SceneTree, so only use the first brush's group
+ if not ent.properties.has(key):
+ ent.properties[key] = value
+ #endregion
+ elif ent:
+ ent.properties[key] = value
+ continue
+ elif group:
+ if key == "name":
+ group.name = "group_%s_" + value
+ elif key == "visgroupid":
+ group.id = value.to_int()
+ group.name = group.name % value
+ group.name = group.name.replace(" ", "_")
+ continue
+
+ #region ASSIGN GROUPS
+ for e in entities_data:
+ if e.properties.has("visgroupid"):
+ var group_id: int = e.properties["visgroupid"] as int
+ for g in groups_data:
+ if g.id == group_id:
+ e.group = g
+ break
+ #endregion
+
+ return parse_data
diff --git a/demo/addons/func_godot/src/core/parser.gd.uid b/demo/addons/func_godot/src/core/parser.gd.uid
new file mode 100644
index 0000000..d13e6d7
--- /dev/null
+++ b/demo/addons/func_godot/src/core/parser.gd.uid
@@ -0,0 +1 @@
+uid://dflet6p5hbqts
diff --git a/demo/addons/func_godot/src/fgd/func_godot_fgd_base_class.gd b/demo/addons/func_godot/src/fgd/func_godot_fgd_base_class.gd
new file mode 100644
index 0000000..e64111a
--- /dev/null
+++ b/demo/addons/func_godot/src/fgd/func_godot_fgd_base_class.gd
@@ -0,0 +1,14 @@
+@tool
+@icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
+class_name FuncGodotFGDBaseClass extends FuncGodotFGDEntityClass
+## Special inheritance class for [FuncGodotFGDSolidClass] and [FuncGodotFGDPointClass] entity definitions.
+##
+## Inheritance class for [FuncGodotFGDSolidClass] and [FuncGodotFGDPointClass] entities,
+## used to shared or common properties and descriptions across different definitions.
+##
+## @tutorial(Quake Wiki Entity Article): https://quakewiki.org/wiki/Entity
+## @tutorial(Level Design Book: Entity Types and Settings): https://book.leveldesignbook.com/appendix/resources/formats/fgd#entity-types-and-settings-basic
+## @tutorial(Valve Developer Wiki FGD Article): https://developer.valvesoftware.com/wiki/FGD#Class_Types_and_Properties
+
+func _init() -> void:
+ prefix = "@BaseClass"
diff --git a/demo/addons/func_godot/src/fgd/func_godot_fgd_base_class.gd.uid b/demo/addons/func_godot/src/fgd/func_godot_fgd_base_class.gd.uid
new file mode 100644
index 0000000..7010a94
--- /dev/null
+++ b/demo/addons/func_godot/src/fgd/func_godot_fgd_base_class.gd.uid
@@ -0,0 +1 @@
+uid://ck575aqs1sbrb
diff --git a/demo/addons/func_godot/src/fgd/func_godot_fgd_entity_class.gd b/demo/addons/func_godot/src/fgd/func_godot_fgd_entity_class.gd
new file mode 100644
index 0000000..7fdd0e9
--- /dev/null
+++ b/demo/addons/func_godot/src/fgd/func_godot_fgd_entity_class.gd
@@ -0,0 +1,250 @@
+@icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
+@abstract class_name FuncGodotFGDEntityClass extends Resource
+## Entity definition template. WARNING! Not to be used directly! Use [FuncGodotFGDBaseClass], [FuncGodotFGDSolidClass], or [FuncGodotFGDPointClass] instead.
+##
+## Entity definition template. It holds all of the common entity class properties shared between [FuncGodotFGDBaseClass], [FuncGodotFGDSolidClass], or [FuncGodotFGDPointClass].
+## Not to be used directly, use one of the aforementioned FGD class types instead.
+##
+## @tutorial(Quake Wiki Entity Article): https://quakewiki.org/wiki/Entity
+## @tutorial(Level Design Book: Entity Types and Settings): https://book.leveldesignbook.com/appendix/resources/formats/fgd#entity-types-and-settings-basic
+## @tutorial(Valve Developer Wiki FGD Article): https://developer.valvesoftware.com/wiki/FGD#Class_Types_and_Properties
+## @tutorial(Valve Developer Wiki Entity Descriptions): https://developer.valvesoftware.com/wiki/FGD#Entity_Description
+
+var prefix: String = ""
+
+@export_group("Entity Definition")
+
+## Entity classname. [b][i]This is a required field in all entity types[/i][/b] as it is parsed by both the map editor and by FuncGodot on map build.
+@export var classname : String = ""
+
+## Entity description that appears in the map editor. Not required.
+@export_multiline var description : String = ""
+
+## Entity does not get written to the exported FGD. Entity is only used for [FuncGodotMap] build process.
+@export var func_godot_internal : bool = false
+
+## [FuncGodotFGDBaseClass] resources to inherit [member class_properties] and [member class_descriptions] from.
+@export var base_classes: Array[Resource] = []
+
+## Key value pair properties that will appear in the map editor. After building the [FuncGodotMap] in Godot, these properties will be added to a [Dictionary]
+## that gets applied to the generated node, as long as that node is a tool script with an exported `func_godot_properties` Dictionary.
+@export var class_properties : Dictionary[String, Variant] = {}
+
+## Map editor descriptions for previously defined key value pair properties. Optional but recommended.
+@export var class_property_descriptions : Dictionary[String, Variant] = {}
+
+## Automatically applies entity class properties to matching properties in the generated node.
+## When using this feature, class properties need to be the correct type or you may run into errors on map build.
+@export var auto_apply_to_matching_node_properties : bool = false
+
+## Appearance properties for the map editor. See the Valve Developer Wiki and TrenchBroom documentation for more information.
+@export var meta_properties : Dictionary[String, Variant] = {
+ "size": AABB(Vector3(-8, -8, -8), Vector3(8, 8, 8)),
+ "color": Color(0.8, 0.8, 0.8)
+}
+
+@export_group("Node Generation")
+
+## Node to generate on map build. This can be a built-in Godot class, a Script class, or a GDExtension class.
+## For Point Class entities that use Scene File instantiation leave this blank.
+@export var node_class := ""
+
+## Optional class property to use in naming the generated node. Overrides [member FuncGodotMapSettings.name_property].
+## Naming occurs before adding to the [SceneTree] and applying properties.
+## Nodes will be named `"entity_" + name_property`. An entity's name should be unique, otherwise you may run into unexpected behavior.
+@export var name_property := ""
+
+## Optional array of node groups to add the generated node to.
+@export var node_groups : Array[String] = []
+
+## Parses the definition and outputs it into the FGD format.
+func build_def_text(target_editor: FuncGodotFGDFile.FuncGodotTargetMapEditors = FuncGodotFGDFile.FuncGodotTargetMapEditors.TRENCHBROOM) -> String:
+ # Class prefix
+ var res : String = prefix
+
+ # Meta properties
+ var base_str = ""
+ var meta_props = meta_properties.duplicate()
+
+ for base_class in base_classes:
+ if not 'classname' in base_class:
+ continue
+
+ base_str += base_class.classname
+
+ if base_class != base_classes.back():
+ base_str += ", "
+
+ if base_str != "":
+ meta_props['base'] = base_str
+
+ for prop in meta_props:
+ if prefix == '@SolidClass':
+ if prop == "size" or prop == "model":
+ continue
+
+ if prop == 'model' and target_editor != FuncGodotFGDFile.FuncGodotTargetMapEditors.TRENCHBROOM:
+ continue
+
+ var value = meta_props[prop]
+ res += " " + prop + "("
+
+ if value is AABB:
+ res += "%s %s %s, %s %s %s" % [
+ value.position.x,
+ value.position.y,
+ value.position.z,
+ value.size.x,
+ value.size.y,
+ value.size.z
+ ]
+ elif value is Color:
+ res += "%s %s %s" % [
+ value.r8,
+ value.g8,
+ value.b8
+ ]
+ elif value is String:
+ res += value
+ elif value is Dictionary and target_editor == FuncGodotFGDFile.FuncGodotTargetMapEditors.TRENCHBROOM:
+ res += JSON.stringify(value)
+
+ res += ")"
+
+ res += " = " + classname
+
+ if prefix != "@BaseClass": # having a description in BaseClasses crashes some editors
+ var normalized_description = description.replace("\"", "\'")
+ if normalized_description != "":
+ res += " : \"%s\" " % [normalized_description]
+ else: # Having no description crashes some editors
+ res += " : \"" + classname + "\" "
+
+ if class_properties.size() > 0:
+ res += FuncGodotUtil.newline() + "[" + FuncGodotUtil.newline()
+ else:
+ res += "["
+
+ # Class properties
+ for prop in class_properties:
+ var value = class_properties[prop]
+ var prop_val = null
+ var prop_type := ""
+ var prop_description: String
+ if prop in class_property_descriptions:
+ # Optional default value for Choices can be set up as [String, int]
+ if value is Dictionary and class_property_descriptions[prop] is Array:
+ var prop_arr: Array = class_property_descriptions[prop]
+ if prop_arr.size() > 1 and (prop_arr[1] is int or prop_arr[1] is String):
+ var value_str : String = str(prop_arr[1]) if prop_arr[1] is int else "\"" + prop_arr[1] + "\""
+ prop_description = "\"" + prop_arr[0] + "\" : " + value_str
+ else:
+ prop_description = "\"\" : 0"
+ printerr(str(prop) + " has incorrect description format. Should be [String description, int / String default value].")
+ else:
+ prop_description = "\"" + class_property_descriptions[prop] + "\""
+ else:
+ prop_description = "\"\""
+
+ match typeof(value):
+ TYPE_INT:
+ prop_type = "integer"
+ prop_val = str(value)
+ TYPE_FLOAT:
+ prop_type = "float"
+ prop_val = "\"" + str(value) + "\""
+ TYPE_STRING:
+ prop_type = "string"
+ prop_val = "\"" + value + "\""
+ TYPE_BOOL:
+ prop_type = "choices"
+ prop_val = FuncGodotUtil.newline() + "\t[" + FuncGodotUtil.newline()
+ prop_val += "\t\t" + str(0) + " : \"No\"" + FuncGodotUtil.newline()
+ prop_val += "\t\t" + str(1) + " : \"Yes\"" + FuncGodotUtil.newline()
+ prop_val += "\t]"
+ TYPE_VECTOR2, TYPE_VECTOR2I:
+ prop_type = "string"
+ prop_val = "\"%s %s\"" % [value.x, value.y]
+ TYPE_VECTOR3, TYPE_VECTOR3I:
+ prop_type = "string"
+ prop_val = "\"%s %s %s\"" % [value.x, value.y, value.z]
+ TYPE_VECTOR4, TYPE_VECTOR4I:
+ prop_type = "string"
+ prop_val = "\"%s %s %s %s\"" % [value[0], value[1], value[2], value[3]]
+ TYPE_COLOR:
+ prop_type = "color255"
+ prop_val = "\"%s %s %s\"" % [value.r8, value.g8, value.b8]
+ TYPE_DICTIONARY:
+ prop_type = "choices"
+ prop_val = FuncGodotUtil.newline() + "\t[" + FuncGodotUtil.newline()
+ for choice in value:
+ var choice_val = value[choice]
+ if typeof(choice_val) == TYPE_STRING:
+ if not (choice_val as String).begins_with("\""):
+ choice_val = "\"" + choice_val + "\""
+ prop_val += "\t\t" + str(choice_val) + " : \"" + choice + "\"" + FuncGodotUtil.newline()
+ prop_val += "\t]"
+ TYPE_ARRAY:
+ prop_type = "flags"
+ prop_val = FuncGodotUtil.newline() + "\t[" + FuncGodotUtil.newline()
+ for arr_val in value:
+ prop_val += "\t\t" + str(arr_val[1]) + " : \"" + str(arr_val[0]) + "\" : " + ("1" if arr_val[2] else "0") + FuncGodotUtil.newline()
+ prop_val += "\t]"
+ TYPE_NODE_PATH:
+ prop_type = "target_destination"
+ prop_val = "\"\""
+ TYPE_OBJECT:
+ if value is Resource:
+ prop_val = "\"" + value.resource_path + "\""
+ if value is Material:
+ if target_editor != FuncGodotFGDFile.FuncGodotTargetMapEditors.JACK:
+ prop_type = "material"
+ else:
+ prop_type = "shader"
+ elif value is Texture2D:
+ prop_type = "decal"
+ elif value is AudioStream:
+ prop_type = "sound"
+ else:
+ prop_type = "target_source"
+ prop_val = "\"\""
+
+ if prop_val:
+ res += "\t"
+ res += prop
+ res += "("
+ res += prop_type
+ res += ")"
+
+ if not value is Array:
+ if not value is Dictionary or prop_description != "":
+ res += " : "
+ res += prop_description
+
+ if value is bool:
+ res += " : 1 = " if value else " : 0 = "
+ elif value is Dictionary or value is Array:
+ res += " = "
+ else:
+ res += " : "
+
+ res += prop_val
+ res += FuncGodotUtil.newline()
+
+ res += "]" + FuncGodotUtil.newline()
+
+ return res
+
+func retrieve_all_class_properties(properties: Dictionary[String, Variant] = {}) -> Dictionary[String, Variant]:
+ for key in class_properties.keys():
+ properties[key] = class_properties[key]
+ for b in base_classes:
+ properties = b.retrieve_all_class_properties(properties)
+ return properties
+
+func retrieve_all_class_property_descriptions(descriptions: Dictionary[String, Variant] = {}) -> Dictionary[String, Variant]:
+ for key in class_property_descriptions.keys():
+ descriptions[key] = class_property_descriptions[key]
+ for b in base_classes:
+ descriptions = b.retrieve_all_class_property_descriptions(descriptions)
+ return descriptions
diff --git a/demo/addons/func_godot/src/fgd/func_godot_fgd_entity_class.gd.uid b/demo/addons/func_godot/src/fgd/func_godot_fgd_entity_class.gd.uid
new file mode 100644
index 0000000..52edf46
--- /dev/null
+++ b/demo/addons/func_godot/src/fgd/func_godot_fgd_entity_class.gd.uid
@@ -0,0 +1 @@
+uid://cgkrrgcimlr8y
diff --git a/demo/addons/func_godot/src/fgd/func_godot_fgd_file.gd b/demo/addons/func_godot/src/fgd/func_godot_fgd_file.gd
new file mode 100644
index 0000000..e106aa8
--- /dev/null
+++ b/demo/addons/func_godot/src/fgd/func_godot_fgd_file.gd
@@ -0,0 +1,179 @@
+@tool
+@icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
+class_name FuncGodotFGDFile extends Resource
+## [Resource] file used to express a set of [FuncGodotFGDEntity] definitions.
+##
+## Can be exported as an FGD file for use with a Quake or Hammer-based map editor. Used in conjunction with [FuncGodotMapSetting] to generate nodes in a [FuncGodotMap] node.
+##
+## @tutorial(Level Design Book FGD Chapter): https://book.leveldesignbook.com/appendix/resources/formats/fgd
+## @tutorial(Valve Developer Wiki FGD Article): https://developer.valvesoftware.com/wiki/FGD
+
+## Supported map editors enum, used in conjunction with [member target_map_editor].
+enum FuncGodotTargetMapEditors {
+ OTHER,
+ TRENCHBROOM,
+ JACK,
+ NET_RADIANT_CUSTOM,
+}
+
+## Builds and exports the FGD file.
+@export_tool_button("Export FGD") var export_file := export_button
+
+func export_button() -> void:
+ do_export_file(target_map_editor)
+
+func do_export_file(target_editor: FuncGodotTargetMapEditors = FuncGodotTargetMapEditors.TRENCHBROOM, fgd_output_folder: String = "") -> void:
+ if fgd_output_folder.is_empty():
+ fgd_output_folder = FuncGodotLocalConfig.get_setting(FuncGodotLocalConfig.PROPERTY.FGD_OUTPUT_FOLDER) as String
+ if fgd_output_folder.is_empty():
+ printerr("Skipping export: No game config folder")
+ return
+
+ if fgd_name == "":
+ printerr("Skipping export: Empty FGD name")
+
+ if not DirAccess.dir_exists_absolute(fgd_output_folder):
+ if DirAccess.make_dir_recursive_absolute(fgd_output_folder) != OK:
+ printerr("Skipping export: Failed to create directory")
+ return
+
+ var fgd_file = fgd_output_folder.path_join(fgd_name + ".fgd")
+
+ var file_obj := FileAccess.open(fgd_file, FileAccess.WRITE)
+ if not file_obj:
+ printerr("Failed to open file for writing: ", fgd_file)
+ return
+
+ print("Exporting FGD to ", fgd_file)
+ file_obj.store_string(build_class_text(target_editor))
+ file_obj.close()
+
+@export_group("Map Editor")
+
+## Some map editors do not support the features found in others
+## (ex: TrenchBroom supports the "model" key word while others require "studio",
+## J.A.C.K. uses the "shader" key word while others use "material", etc...).
+## If you get errors in your map editor, try changing this setting and re-exporting.
+## This setting is overridden when the FGD is built via the Game Config resource.
+@export var target_map_editor: FuncGodotTargetMapEditors = FuncGodotTargetMapEditors.TRENCHBROOM
+
+# Some map editors do not support the "model" key word and require the "studio" key word instead.
+# If you get errors in your map editor, try changing this setting.
+# This setting is overridden when the FGD is built via the Game Config resource.
+#@export var model_key_word_supported: bool = true
+
+@export_group("FGD")
+
+## FGD output filename without the extension.
+@export var fgd_name: String = "FuncGodot"
+
+## Array of [FuncGodotFGDFile] resources to include in FGD file output. All of the entities included with these FuncGodotFGDFile resources will be prepended to the outputted FGD file.
+@export var base_fgd_files: Array[Resource] = []
+
+## Array of resources that inherit from [FuncGodotFGDEntityClass]. This array defines the entities that will be added to the exported FGD file and the nodes that will be generated in a [FuncGodotMap].
+@export var entity_definitions: Array[Resource] = []
+
+## Toggles whether [FuncGodotFGDModelPointClass] resources will generate models from their [PackedScene] files.
+@export var generate_model_point_class_models: bool = true
+
+func build_class_text(target_editor: FuncGodotTargetMapEditors = FuncGodotTargetMapEditors.TRENCHBROOM) -> String:
+ var res : String = ""
+
+ for base_fgd in base_fgd_files:
+ if base_fgd is FuncGodotFGDFile:
+ res += base_fgd.build_class_text(target_editor)
+ else:
+ printerr("Base Fgd Files contains incorrect resource type! Should only be type FuncGodotFGDFile.")
+
+ var entities = get_fgd_classes()
+ for ent in entities:
+ if not ent is FuncGodotFGDEntityClass:
+ continue
+ if ent.func_godot_internal:
+ continue
+ if ent is FuncGodotFGDModelPointClass:
+ ent._model_generation_enabled = generate_model_point_class_models
+
+ var ent_text = ent.build_def_text(target_editor)
+ res += ent_text
+ if ent != entities[-1]:
+ res += "\n"
+ return res
+
+## This getter does a little bit of validation. Providing only an array of non-null uniquely-named entity definitions
+func get_fgd_classes() -> Array:
+ var res : Array = []
+ for cur_ent_def_ind in range(entity_definitions.size()):
+ var cur_ent_def = entity_definitions[cur_ent_def_ind]
+ if cur_ent_def == null:
+ continue
+ elif not (cur_ent_def is FuncGodotFGDEntityClass):
+ printerr("Bad value in entity definition set at position %s! Not an entity defintion." % cur_ent_def_ind)
+ continue
+ res.append(cur_ent_def)
+ return res
+
+func get_entity_definitions() -> Dictionary[String, FuncGodotFGDEntityClass]:
+ var res: Dictionary[String, FuncGodotFGDEntityClass] = {}
+
+ for base_fgd in base_fgd_files:
+ var fgd_res = base_fgd.get_entity_definitions()
+ for key in fgd_res:
+ res[key] = fgd_res[key]
+
+ for ent in get_fgd_classes():
+ # Skip entities without classnames
+ if ent.classname.replace(" ","") == "":
+ printerr("Skipping " + ent.get_path() + ": Empty classname")
+ continue
+
+ if ent is FuncGodotFGDPointClass or ent is FuncGodotFGDSolidClass:
+ var entity_def = ent.duplicate()
+ var meta_properties: Dictionary[String, Variant] = {}
+ var class_properties: Dictionary[String, Variant] = {}
+ var class_property_descriptions: Dictionary[String, Variant] = {}
+
+ for base_class in _generate_base_class_list(entity_def):
+ for meta_property in base_class.meta_properties:
+ meta_properties[meta_property] = base_class.meta_properties[meta_property]
+
+ for class_property in base_class.class_properties:
+ class_properties[class_property] = base_class.class_properties[class_property]
+
+ for class_property_desc in base_class.class_property_descriptions:
+ class_property_descriptions[class_property_desc] = base_class.class_property_descriptions[class_property_desc]
+
+ for meta_property in entity_def.meta_properties:
+ meta_properties[meta_property] = entity_def.meta_properties[meta_property]
+
+ for class_property in entity_def.class_properties:
+ class_properties[class_property] = entity_def.class_properties[class_property]
+
+ for class_property_desc in entity_def.class_property_descriptions:
+ class_property_descriptions[class_property_desc] = entity_def.class_property_descriptions[class_property_desc]
+
+ entity_def.meta_properties = meta_properties
+ entity_def.class_properties = class_properties
+ entity_def.class_property_descriptions = class_property_descriptions
+
+ res[ent.classname] = entity_def
+ return res
+
+func _generate_base_class_list(entity_def : Resource, visited_base_classes = []) -> Array:
+ var base_classes : Array = []
+
+ visited_base_classes.append(entity_def.classname)
+
+ # End recursive search if no more base_classes
+ if len(entity_def.base_classes) == 0:
+ return base_classes
+
+ # Traverse up to the next level of hierarchy, if not already visited
+ for base_class in entity_def.base_classes:
+ if not base_class.classname in visited_base_classes:
+ base_classes.append(base_class)
+ base_classes += _generate_base_class_list(base_class, visited_base_classes)
+ else:
+ printerr(str("Entity '", entity_def.classname,"' contains cycle/duplicate to Entity '", base_class.classname, "'"))
+
+ return base_classes
diff --git a/demo/addons/func_godot/src/fgd/func_godot_fgd_file.gd.uid b/demo/addons/func_godot/src/fgd/func_godot_fgd_file.gd.uid
new file mode 100644
index 0000000..c963488
--- /dev/null
+++ b/demo/addons/func_godot/src/fgd/func_godot_fgd_file.gd.uid
@@ -0,0 +1 @@
+uid://drlmgulwbjwqu
diff --git a/demo/addons/func_godot/src/fgd/func_godot_fgd_model_point_class.gd b/demo/addons/func_godot/src/fgd/func_godot_fgd_model_point_class.gd
new file mode 100644
index 0000000..997ded1
--- /dev/null
+++ b/demo/addons/func_godot/src/fgd/func_godot_fgd_model_point_class.gd
@@ -0,0 +1,197 @@
+@tool
+@icon("res://addons/func_godot/icons/icon_godambler3d.svg")
+class_name FuncGodotFGDModelPointClass extends FuncGodotFGDPointClass
+## A special type of [FuncGodotFGDPointClass] entity that automatically generates a special simplified GLB model file for the map editor display.
+## Only supported in map editors that support GLTF or GLB.
+##
+## @tutorial(Quake Wiki Entity Article): https://quakewiki.org/wiki/Entity
+## @tutorial(Level Design Book: Entity Types and Settings): https://book.leveldesignbook.com/appendix/resources/formats/fgd#entity-types-and-settings-basic
+## @tutorial(Valve Developer Wiki FGD Article): https://developer.valvesoftware.com/wiki/FGD#Class_Types_and_Properties
+## @tutorial(dumptruck_ds' Quake Mapping Entities Tutorial): https://www.youtube.com/watch?v=gtL9f6_N2WM
+## @tutorial(Level Design Book: Display Models for Entities): https://book.leveldesignbook.com/appendix/resources/formats/fgd#display-models-for-entities
+## @tutorial(Valve Developer Wiki FGD Article: Entity Description Section): https://developer.valvesoftware.com/wiki/FGD#Entity_Description
+## @tutorial(TrenchBroom Manual: Display Models for Entities): https://trenchbroom.github.io/manual/latest/#display-models-for-entities
+
+enum TargetMapEditor {
+ GENERIC, ## Entity definition uses the [b]@studio[/b] key word. [member scale_expression] is ignored. Supported by all map editors.
+ TRENCHBROOM ## Entity definition uses the [b]@model[/b] key word. [member scale_expression] is applied if set.
+}
+
+@export var target_map_editor: TargetMapEditor = TargetMapEditor.GENERIC
+## Display model export folder relative to [member ProjectSettings.func_godot/model_point_class_save_path].
+@export var models_sub_folder : String = ""
+## Scale expression applied to model. Only used by TrenchBroom. If left empty, uses [member ProjectSettings.func_godot/default_inverse_scale_factor]. [br][br]Read the TrenchBroom Manual for more information on the "scale expression" feature.
+@export var scale_expression : String = ""
+## Model Point Class can override the 'size' meta property by auto-generating a value from the meshes' [AABB]. Proper generation requires [member scale_expression] set to a float or vector. [br][br][color=orange]WARNING:[/color] Generated size property unlikely to align cleanly to grid!
+@export var generate_size_property : bool = false
+## Degrees to rotate model prior to export. Different editors may handle GLTF transformations differently. If your model isn't oriented correctly, try modifying this property.
+@export var rotation_offset: Vector3 = Vector3(0.0, 0.0, 0.0)
+## Creates a .gdignore file in the model export folder to prevent Godot importing the display models. Only needs to be generated once.
+@export_tool_button("Generate GD Ignore File", "FileAccess") var generate_gd_ignore_file : Callable = _generate_gd_ignore_file
+
+var _model_generation_enabled: bool = false
+
+func _generate_gd_ignore_file() -> void:
+ if Engine.is_editor_hint():
+ var path: String = _get_game_path().path_join(_get_model_folder())
+ var error: Error = DirAccess.make_dir_recursive_absolute(path)
+ if error != Error.OK:
+ printerr("Failed creating dir for GDIgnore file", error)
+ return
+ path = path.path_join('.gdignore')
+ if FileAccess.file_exists(path):
+ return
+ var file: FileAccess = FileAccess.open(path, FileAccess.WRITE)
+ file.store_string('')
+ file.close()
+
+## Builds and saves the display model into the specified destination, then parses the definition and outputs it into the FGD format.
+func build_def_text(target_editor: FuncGodotFGDFile.FuncGodotTargetMapEditors = FuncGodotFGDFile.FuncGodotTargetMapEditors.TRENCHBROOM) -> String:
+ if _model_generation_enabled:
+ _generate_model()
+ _model_generation_enabled = false
+ return super()
+
+func _generate_model() -> void:
+ if not scene_file:
+ return
+
+ var gltf_state := GLTFState.new()
+ var path: String = _get_export_dir()
+ var node: Node3D = _get_node()
+ if not node:
+ return
+ if not _create_gltf_file(gltf_state, path, node):
+ printerr("could not create gltf file")
+ return
+ node.queue_free()
+
+ if target_map_editor == TargetMapEditor.TRENCHBROOM:
+ const model_key: String = "model"
+ if scale_expression.is_empty():
+ meta_properties[model_key] = '{"path": "%s", "scale": %s }' % [
+ _get_local_path(),
+ ProjectSettings.get_setting("func_godot/default_inverse_scale_factor", 32.0) as float
+ ]
+ else:
+ meta_properties[model_key] = '{"path": "%s", "scale": %s }' % [
+ _get_local_path(),
+ scale_expression
+ ]
+ else:
+ meta_properties["studio"] = '"%s"' % _get_local_path()
+
+ if generate_size_property:
+ meta_properties["size"] = _generate_size_from_aabb(gltf_state.meshes, gltf_state.get_nodes())
+
+func _get_node() -> Node3D:
+ var node := scene_file.instantiate()
+ if node is Node3D:
+ return node as Node3D
+ node.queue_free()
+ printerr("Scene is not of type 'Node3D'")
+ return null
+
+func _get_export_dir() -> String:
+ var work_dir: String = _get_game_path()
+ var model_dir: String = _get_model_folder()
+ return work_dir.path_join(model_dir).path_join('%s.glb' % classname)
+
+func _get_local_path() -> String:
+ return _get_model_folder().path_join('%s.glb' % classname)
+
+func _get_model_folder() -> String:
+ var model_dir: String = ProjectSettings.get_setting("func_godot/model_point_class_save_path", "") as String
+ if not models_sub_folder.is_empty():
+ model_dir = model_dir.path_join(models_sub_folder)
+ return model_dir
+
+func _get_game_path() -> String:
+ return FuncGodotLocalConfig.get_setting(FuncGodotLocalConfig.PROPERTY.MAP_EDITOR_GAME_PATH) as String
+
+func _create_gltf_file(gltf_state: GLTFState, path: String, node: Node3D) -> bool:
+ var global_export_path = path
+ var gltf_document := GLTFDocument.new()
+ gltf_state.create_animations = false
+
+ node.rotate_x(deg_to_rad(rotation_offset.x))
+ node.rotate_y(deg_to_rad(rotation_offset.y))
+ node.rotate_z(deg_to_rad(rotation_offset.z))
+
+ # With TrenchBroom we can specify a scale expression, but for other editors we need to scale our models manually.
+ if target_map_editor != TargetMapEditor.TRENCHBROOM:
+ var scale_factor: Vector3 = Vector3.ONE
+ if scale_expression.is_empty():
+ scale_factor *= ProjectSettings.get_setting("func_godot/default_inverse_scale_factor", 32.0) as float
+ else:
+ if scale_expression.begins_with('\''):
+ var scale_arr := scale_expression.split_floats(' ', false)
+ if scale_arr.size() == 3:
+ scale_factor *= Vector3(scale_arr[0], scale_arr[1], scale_arr[2])
+ elif scale_expression.to_float() > 0:
+ scale_factor *= scale_expression.to_float()
+ if scale_factor.length() == 0:
+ scale_factor = Vector3.ONE # Don't let the node scale into oblivion!
+ node.scale *= scale_factor
+
+ var error: Error = gltf_document.append_from_scene(node, gltf_state)
+ if error != Error.OK:
+ printerr("Failed appending to gltf document", error)
+ return false
+
+ call_deferred("_save_to_file_system", gltf_document, gltf_state, global_export_path)
+ return true
+
+func _save_to_file_system(gltf_document: GLTFDocument, gltf_state: GLTFState, path: String) -> void:
+ var error: Error = DirAccess.make_dir_recursive_absolute(path.get_base_dir())
+ if error != Error.OK:
+ printerr("Failed creating dir", error)
+ return
+
+ error = gltf_document.write_to_filesystem(gltf_state, path)
+ if error != Error.OK:
+ printerr("Failed writing to file system", error)
+ return
+ print('Exported model to ', path)
+
+func _generate_size_from_aabb(meshes: Array[GLTFMesh], nodes: Array[GLTFNode]) -> AABB:
+ var aabb := AABB()
+ for mesh in meshes:
+ aabb = aabb.merge(mesh.mesh.get_mesh().get_aabb())
+ var pos_ofs := Vector3.ZERO
+ if not nodes.is_empty():
+ var ct: int = 0
+ for node in nodes:
+ if node.parent == 0:
+ pos_ofs += node.position
+ ct += 1
+ pos_ofs /= maxi(ct, 1)
+ aabb.position += pos_ofs
+
+ # Reorient the AABB so it matches TrenchBroom's coordinate system
+ var size_prop := AABB()
+ size_prop.position = Vector3(aabb.position.z, aabb.position.x, aabb.position.y)
+ size_prop.size = Vector3(aabb.size.z, aabb.size.x, aabb.size.y)
+
+ # Scale the size bounds to our scale factor
+ # Scale factor will need to be set if we decide to auto-generate our bounds
+ var scale_factor: Vector3 = Vector3.ONE
+ if target_map_editor == TargetMapEditor.TRENCHBROOM:
+ if scale_expression.is_empty():
+ scale_factor *= ProjectSettings.get_setting("func_godot/default_inverse_scale_factor", 32.0) as float
+ else:
+ if scale_expression.begins_with('\''):
+ var scale_arr := scale_expression.split_floats(' ', false)
+ if scale_arr.size() == 3:
+ scale_factor *= Vector3(scale_arr[0], scale_arr[1], scale_arr[2])
+ elif scale_expression.to_float() > 0:
+ scale_factor *= scale_expression.to_float()
+
+ size_prop.position *= scale_factor
+ size_prop.size *= scale_factor
+ size_prop.size += size_prop.position
+ # Round the size so it can stay on grid level 1 at least
+ for i in 3:
+ size_prop.position[i] = round(size_prop.position[i])
+ size_prop.size[i] = round(size_prop.size[i])
+ return size_prop
diff --git a/demo/addons/func_godot/src/fgd/func_godot_fgd_model_point_class.gd.uid b/demo/addons/func_godot/src/fgd/func_godot_fgd_model_point_class.gd.uid
new file mode 100644
index 0000000..fc85996
--- /dev/null
+++ b/demo/addons/func_godot/src/fgd/func_godot_fgd_model_point_class.gd.uid
@@ -0,0 +1 @@
+uid://ldfqjtq0br35
diff --git a/demo/addons/func_godot/src/fgd/func_godot_fgd_point_class.gd b/demo/addons/func_godot/src/fgd/func_godot_fgd_point_class.gd
new file mode 100644
index 0000000..adbc35a
--- /dev/null
+++ b/demo/addons/func_godot/src/fgd/func_godot_fgd_point_class.gd
@@ -0,0 +1,120 @@
+@tool
+@icon("res://addons/func_godot/icons/icon_godambler3d.svg")
+class_name FuncGodotFGDPointClass extends FuncGodotFGDEntityClass
+## FGD PointClass entity definition.
+##
+## A resource used to define an FGD Point Class entity. PointClass entities can use either the [member FuncGodotFGDEntityClass.node_class]
+## or the [member scene_file] property to tell [FuncGodotMap] what to generate on map build.
+##
+## @tutorial(Quake Wiki Entity Article): https://quakewiki.org/wiki/Entity
+## @tutorial(Level Design Book: Entity Types and Settings): https://book.leveldesignbook.com/appendix/resources/formats/fgd#entity-types-and-settings-basic
+## @tutorial(Valve Developer Wiki FGD Article): https://developer.valvesoftware.com/wiki/FGD#Class_Types_and_Properties
+## @tutorial(dumptruck_ds' Quake Mapping Entities Tutorial): https://www.youtube.com/watch?v=gtL9f6_N2WM
+## @tutorial(Level Design Book: Display Models for Entities): https://book.leveldesignbook.com/appendix/resources/formats/fgd#display-models-for-entities
+## @tutorial(Valve Developer Wiki FGD Article: Entity Description Section): https://developer.valvesoftware.com/wiki/FGD#Entity_Description
+## @tutorial(TrenchBroom Manual: Display Models for Entities): https://trenchbroom.github.io/manual/latest/#display-models-for-entities
+
+func _init() -> void:
+ prefix = "@PointClass"
+
+## An optional [PackedScene] file to instantiate on map build. Overrides [member FuncGodotFGDEntityClass.node_class] and [member script_class].
+@export var scene_file: PackedScene
+
+## An optional [Script] resource to attach to the node generated on map build. Ignored if [member scene_file] is specified.
+@export var script_class: Script
+
+## Toggles whether entity will use `angles`, `mangle`, or `angle` to determine rotations on [FuncGodotMap] build, prioritizing the key value pairs in that order.
+## Set to [code]false[/code] if you would like to define how the generated node is rotated yourself.
+@export var apply_rotation_on_map_build : bool = true
+
+## Toggles whether entity will use `scale` to determine the generated node or scene's scale. This is performed on the top level node.
+## The property can be a [float], [Vector3], or [Vector2]. Set to [code]false[/code] if you would like to define how the generated node is scaled yourself.
+@export var apply_scale_on_map_build: bool = true
+
+## An optional [Array] of [FuncGodotFGDPointClassDisplayDescriptor] that describes how this Point Entity should appear in the map editor.
+## When using multiple display descriptors, only the first element found without [member FuncGodotFGDPointClassDisplayDescriptor.conditional]
+## will be used as the default display asset. If no descriptor is found without a condition, the last descriptor will become the default.[br][br]
+## Conditional display descriptors will be written to the FGD in the order set in the array.[br][br]
+## [color=orange]WARNING:[/color] Multiple descriptors are only supported by TrenchBroom! They will be omitted on export when
+## [member FuncGodotFGDFile.target_map_editor] is not set to [enum FuncGodotFGDFile.FuncGodotTargetMapEditors.TRENCHBROOM].
+@export var display_descriptors: Array[FuncGodotFGDPointClassDisplayDescriptor] = []
+
+func _build_model_branch_text(descriptor: FuncGodotFGDPointClassDisplayDescriptor) -> String:
+ if not descriptor:
+ return ''
+
+ var model_string: String = ''
+ var uses_options: bool = false
+
+ if not descriptor.scale.is_empty() or not descriptor.skin.is_empty() or not descriptor.frame.is_empty():
+ uses_options = true
+
+ if not uses_options:
+ return descriptor.display_asset_path
+
+ model_string = '{ \"path\": %s' % descriptor.display_asset_path
+
+ if not descriptor.skin.is_empty():
+ model_string += ', \"skin\": %s' % descriptor.skin
+ if not descriptor.frame.is_empty():
+ model_string += ', \"frame\": %s' % descriptor.frame
+ if not descriptor.scale.is_empty():
+ model_string += ', \"scale\": %s' % descriptor.scale
+
+ model_string += " }"
+
+ return model_string
+
+func _build_model_text() -> String:
+ var model_string: String = ''
+
+ if display_descriptors.is_empty():
+ return model_string
+
+ if display_descriptors.size() == 1:
+ return _build_model_branch_text(display_descriptors[0])
+
+ model_string = '{{'
+ var default_display: FuncGodotFGDPointClassDisplayDescriptor
+ for i in display_descriptors.size():
+ var d: FuncGodotFGDPointClassDisplayDescriptor = display_descriptors[i]
+
+ # Only set the first discovered descriptor without a condition to the default, which must be the last option in a list.
+ # If a conditional is not set, skip it.
+ if d.conditional.is_empty():
+ if not default_display:
+ default_display = d
+ else:
+ printerr(classname + " has a Point Class Display Descriptor without required conditionals set. Must have only 1 conditionless Display Descriptor!")
+ continue
+
+ model_string += '%s -> %s, ' % [d.conditional, _build_model_branch_text(d)]
+
+ if default_display:
+ model_string += '%s }}' % _build_model_branch_text(default_display)
+ else:
+ model_string = model_string.trim_suffix(', ')
+ model_string += ' }}'
+
+ return model_string
+
+func _build_studio_text() -> String:
+ var display_string = ""
+ for d in display_descriptors:
+ if d.display_asset_path.find('\"') != -1:
+ display_string = d.display_asset_path
+ else:
+ printerr(classname + " attempting to set an invalid value to @studio format during FGD export. Only relative file paths encapsulated by quotations are valid.")
+ return display_string
+
+func build_def_text(target_editor: FuncGodotFGDFile.FuncGodotTargetMapEditors = FuncGodotFGDFile.FuncGodotTargetMapEditors.TRENCHBROOM) -> String:
+ if not display_descriptors.is_empty():
+ if target_editor == FuncGodotFGDFile.FuncGodotTargetMapEditors.TRENCHBROOM:
+ var display_string: String = _build_model_text()
+ if not display_string.is_empty():
+ meta_properties["model"] = display_string
+ else:
+ var display_string: String = _build_studio_text()
+ if not display_string.is_empty():
+ meta_properties["studio"] = display_string
+ return super(target_editor)
diff --git a/demo/addons/func_godot/src/fgd/func_godot_fgd_point_class.gd.uid b/demo/addons/func_godot/src/fgd/func_godot_fgd_point_class.gd.uid
new file mode 100644
index 0000000..e0e62c2
--- /dev/null
+++ b/demo/addons/func_godot/src/fgd/func_godot_fgd_point_class.gd.uid
@@ -0,0 +1 @@
+uid://cxsqwtsqd8w33
diff --git a/demo/addons/func_godot/src/fgd/func_godot_fgd_point_class_display_descriptor.gd b/demo/addons/func_godot/src/fgd/func_godot_fgd_point_class_display_descriptor.gd
new file mode 100644
index 0000000..c8355f8
--- /dev/null
+++ b/demo/addons/func_godot/src/fgd/func_godot_fgd_point_class_display_descriptor.gd
@@ -0,0 +1,48 @@
+@tool
+@icon("res://addons/func_godot/icons/icon_godambler3d.svg")
+class_name FuncGodotFGDPointClassDisplayDescriptor extends Resource
+## Resource that describes how to display an FGD Point Class entity.
+##
+## A resource for [FuncGodotFGDPointClass] that describes how to display a point entity in a map editor.
+## Values entered into the different options are taken literally: paths should be enclosed within quotation marks,
+## while class property keys and integer values should omit them.[br][br]
+##
+## Most editors only support the [member display_asset] option. Exporting an FGD compatible with these editors will
+## automatically omit the unsupported options introduced by TrenchBroom when exporting from their respective game configuration resources
+## or setting [member FuncGodotFGDFile.target_map_editor] away from [enum FuncGodotFGDFile.FuncGodotTargetMapEditors.TRENCHBROOM].
+##
+## The extra options are considered advanced features and are unable to be evaluated by FuncGodot to ensure they were input correctly.
+## Exercise caution, care, and patience when attempting to use these, especially the [member conditional] option.
+##
+## @tutorial(Level Design Book: Display Models for Entities): https://book.leveldesignbook.com/appendix/resources/formats/fgd#display-models-for-entities
+## @tutorial(Valve Developer Wiki FGD Article: Entity Description Section): https://developer.valvesoftware.com/wiki/FGD#Entity_Description
+## @tutorial(TrenchBroom Manual: Display Models for Entities): https://trenchbroom.github.io/manual/latest/#display-models-for-entities
+## @tutorial(TrenchBroom Manual: Expression Language): https://trenchbroom.github.io/manual/latest/#expression_language
+
+## Either a file path to the asset that will be displayed for this point entity, relative to the map editor's game path,
+## or a class property key that can contain the path.[br][br]
+## For paths, you must surround the path with quotes, e.g: [code]"models/marsfrog.glb"[/code].
+## For properties, you must omit the quotes, e.g: [code]display_model_path[/code].[br][br]
+## Different editors support different file types: common ones include MDL, GLB, SPR, and PNG.
+@export var display_asset_path: String = ""
+
+@export_group("TrenchBroom Options")
+## Optional string that determines the scale of the display asset. This can be a number, a class property key, or
+## a scale expression in accordance with TrenchBroom's Expression Language. Leave blank to use the game configuration's default scale expression.[br][br]
+## [color=orange]WARNING:[/color] Only utilized by TrenchBroom!
+@export var scale: String = ""
+
+## Optional string that determines which skin the display asset should use. This can be either a number or a class property key.[br][br]
+## [color=orange]WARNING:[/color] Only utilized by TrenchBroom!
+@export var skin: String = ""
+
+## Optional string that determines the appearance of a display asset based on its file type. This can be either a number or a class property key.[br][br]
+## Traditional Quake MDL files will set the display to that frame of its animations (all animations in a Quake MDL are compiled into a single animation).
+## GLBs meanwhile seem to set themselves to the animation assigned to an index that matches the [code]frame[/code] value.[br][br]
+## [color=orange]WARNING:[/color] Only utilized by TrenchBroom!
+@export var frame: String = ""
+
+## Optional evaluation string that, when true, will force the Point Class to display the asset defined by [member display_asset_path].
+## Format should be [code]property == value[/code] or some other valid expression in accordance with TrenchBroom's Expression Language.[br][br]
+## [color=orange]WARNING:[/color] Only utilized by TrenchBroom!
+@export var conditional: String = ""
diff --git a/demo/addons/func_godot/src/fgd/func_godot_fgd_point_class_display_descriptor.gd.uid b/demo/addons/func_godot/src/fgd/func_godot_fgd_point_class_display_descriptor.gd.uid
new file mode 100644
index 0000000..efdfad4
--- /dev/null
+++ b/demo/addons/func_godot/src/fgd/func_godot_fgd_point_class_display_descriptor.gd.uid
@@ -0,0 +1 @@
+uid://d1nwwgcrner8b
diff --git a/demo/addons/func_godot/src/fgd/func_godot_fgd_solid_class.gd b/demo/addons/func_godot/src/fgd/func_godot_fgd_solid_class.gd
new file mode 100644
index 0000000..00f2665
--- /dev/null
+++ b/demo/addons/func_godot/src/fgd/func_godot_fgd_solid_class.gd
@@ -0,0 +1,106 @@
+@tool
+@icon("res://addons/func_godot/icons/icon_slipgate3d.svg")
+class_name FuncGodotFGDSolidClass extends FuncGodotFGDEntityClass
+## FGD SolidClass entity definition that generates a mesh from [FuncGodotData.BrushData].
+##
+## A [MeshInstance3D] will be generated by [FuncGodotMap] according to this definition's Visual Build settings.
+## If [member FuncGodotFGDEntityClass.node_class] inherits [CollisionObject3D]
+## then one or more [CollisionShape3D] nodes will be generated according to Collision Build settings.
+##
+## @tutorial(Quake Wiki Entity Article): https://quakewiki.org/wiki/Entity
+## @tutorial(Level Design Book: Entity Types and Settings): https://book.leveldesignbook.com/appendix/resources/formats/fgd#entity-types-and-settings-basic
+## @tutorial(Valve Developer Wiki FGD Article): https://developer.valvesoftware.com/wiki/FGD#Class_Types_and_Properties
+## @tutorial(dumptruck_ds' Quake Mapping Entities Tutorial): https://www.youtube.com/watch?v=gtL9f6_N2WM
+
+enum SpawnType {
+ WORLDSPAWN = 0, ## Builds the geometry of this entity relative to the FuncGodotMap position.
+ MERGE_WORLDSPAWN = 1, ## This entity's geometry is merged with the [b]worldspawn[/b] entity and this entity is removed. Behavior mimics [b]func_group[/b] in modern Quake compilers.
+ ENTITY = 2, ## This entity is built as its own object. It finds the origin of the entity based on [member origin_type].
+}
+
+enum OriginType {
+ AVERAGED = 0, ## Use averaged brush vertices for center position. This is the old Qodot behavior.
+ ABSOLUTE = 1, ## Use [code]origin[/code] class property in global coordinates as the center position.
+ RELATIVE = 2, ## Calculate center position using [code]origin[/code] class property as an offset to the entity's bounding box center.
+ BRUSH = 3, ## Calculate center position based on the bounding box center of all brushes using the 'origin' texture specified in the [FuncGodotMapSettings]. If no Origin Brush is found, fall back to BOUNDS_CENTER. This is the default option and recommended for most entities.
+ BOUNDS_CENTER = 4, ## Use the center of the entity's bounding box for center position.
+ BOUNDS_MINS = 5, ## Use the lowest bounding box coordinates for center position. This is standard Quake and Half-Life brush entity behavior.
+ BOUNDS_MAXS = 6, ## Use the highest bounding box coordinates for center position.
+}
+
+enum CollisionShapeType {
+ NONE, ## No collision shape is built. Useful for decorative geometry like vines, hanging wires, grass, etc...
+ CONVEX, ## Will build a Convex CollisionShape3D for each brush used to make this Solid Class. Required for non-[StaticBody3D] nodes like [Area3D].
+ CONCAVE ## Should have a concave collision shape
+}
+
+## Controls whether this Solid Class is the worldspawn, is combined with the worldspawn, or is spawned as its own free-standing entity.
+@export var spawn_type: SpawnType = SpawnType.ENTITY
+## Controls how this Solid Class determines its center position. Only valid if [member spawn_type] is set to ENTITY.
+@export var origin_type: OriginType = OriginType.BRUSH
+
+@export_group("Visual Build")
+## Controls whether a [MeshInstance3D] is built for this Solid Class.
+@export var build_visuals : bool = true
+## Global illumination mode for the generated [MeshInstance3D]. Setting to [b]GI_MODE_STATIC[/b] will unwrap the mesh's UV2 during build.
+@export var global_illumination_mode : GeometryInstance3D.GIMode = GeometryInstance3D.GI_MODE_STATIC
+## @deprecated: Use [member global_illumination_mode] instead. [br]Sets generated [MeshInstance3D] to be available for UV2 unwrapping after [FuncGodotMap] build. Utilized in baked lightmapping.
+@export var use_in_baked_light : bool = true
+## Shadow casting setting allows for further lightmapping customization.
+@export var shadow_casting_setting : GeometryInstance3D.ShadowCastingSetting = GeometryInstance3D.SHADOW_CASTING_SETTING_ON
+## Automatically build [OccluderInstance3D] for this entity.
+@export var build_occlusion : bool = false
+## This Solid Class' [MeshInstance3D] will only be visible for [Camera3D]s whose cull mask includes any of these render layers.
+@export_flags_3d_render var render_layers: int = 1
+
+@export_group("Collision Build")
+## Controls how collisions are built for this Solid Class.
+@export var collision_shape_type: CollisionShapeType = CollisionShapeType.CONVEX
+## The physics layers this Solid Class can be detected in.
+@export_flags_3d_physics var collision_layer: int = 1
+## The physics layers this Solid Class scans.
+@export_flags_3d_physics var collision_mask: int = 1
+## The priority used to solve colliding when penetration occurs. The higher the priority is, the lower the penetration into the Solid Class will be. This can for example be used to prevent the player from breaking through the boundaries of a level.
+@export var collision_priority: float = 1.0
+## The collision margin for the Solid Class' collision shapes. Not used in Godot Physics. See [Shape3D] for details.
+@export var collision_shape_margin: float = 0.04
+
+## The following properties tell FuncGodot to add a [i]"func_godot_mesh_data"[/i] Dictionary to the metadata of the generated node upon build.
+## This data is parallelized, so that each element of the array is ordered to reference the same face in the mesh.
+@export_group("Mesh Metadata")
+## Add a texture lookup table to the generated node's metadata on build.[br][br]
+## The data is split between an [Array] of [StringName] called [i]"texture_names"[/i] containing all currently used texture materials
+## and a [PackedInt32Array] called [i]"textures"[/i] where each element is an index corresponding to the [i]"texture_names"[/i] entries.
+@export var add_textures_metadata: bool = false
+## Add a [PackedVector3Array] called [i]"vertices"[/i] to the generated node's metadata on build.[br][br]
+## This is a list of every vertex in the generated node's [MeshInstance3D]. Every 3 vertices represent a single face.
+@export var add_vertex_metadata: bool = false
+## Add a [PackedVector3Array] called [i]"positions"[/i] to the generated node's metadata on build.[br][br]
+## This is a list of positions for each face, local to the generated node, calculated by averaging the vertices to find the face's center.
+@export var add_face_position_metadata: bool = false
+## Add a [PackedVector3Array] called [i]"normals"[/i] to the generated node's metadata on build.[br][br]
+## Contains a list of each face's normal.
+@export var add_face_normal_metadata: bool = false
+## Add a [Dictionary] called [i]"collision_shape_to_face_indices_map"[/i] in the generated node's metadata on build.[br][br]
+## Contains keys of strings, which are the names of child [CollisionShape3D] nodes, and values of
+## [PackedInt32Array], containing indices of that child's faces.[br][br]
+## For example, an element of [br][br][code]{ "entity_1_brush_0_collision_shape" : [0, 1, 3] }[/code][br][br]
+## shows that this solid class has been generated with one child collision shape named
+## [i]entity_1_brush_0_collision_shape[/i] which handles 3 faces of the mesh with collision, at indices 0, 1, and 3.
+@export var add_collision_shape_to_face_indices_metadata : bool = false
+## [s]Add a [Dictionary] called [i]"collision_shape_to_face_range_map"[/i] in the generated node's metadata on build.[br][br]
+## Contains keys of strings, which are the names of child [CollisionShape3D] nodes, and values of
+## [Vector2i], where [i]X[/i] represents the starting index of that child's faces and [i]Y[/i] represents the
+## ending index.[br][br]
+## For example, an element of [br][br][code]{ "entity_1_brush_0_collision_shape" : Vector2i(0, 15) }[/code][br][br]
+## shows that this solid class has been generated with one child collision shape named
+## [i]entity_1_brush_0_collision_shape[/i] which handles the first 15 faces of the parts of the mesh with collision.[/s]
+## @deprecated: No longer supported or planned as of 2025.7, but retained in case a contributor provides an appropriate solution in the future.
+@export var add_collision_shape_face_range_metadata: bool = false
+
+@export_group("Scripting")
+## An optional [Script] file to attach to the node generated on map build.
+@export var script_class: Script
+
+func _init():
+ prefix = "@SolidClass"
diff --git a/demo/addons/func_godot/src/fgd/func_godot_fgd_solid_class.gd.uid b/demo/addons/func_godot/src/fgd/func_godot_fgd_solid_class.gd.uid
new file mode 100644
index 0000000..508ffeb
--- /dev/null
+++ b/demo/addons/func_godot/src/fgd/func_godot_fgd_solid_class.gd.uid
@@ -0,0 +1 @@
+uid://5cow84q03m6a
diff --git a/demo/addons/func_godot/src/func_godot_plugin.gd b/demo/addons/func_godot/src/func_godot_plugin.gd
new file mode 100644
index 0000000..11c18b7
--- /dev/null
+++ b/demo/addons/func_godot/src/func_godot_plugin.gd
@@ -0,0 +1,131 @@
+@tool
+@icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
+class_name FuncGodotPlugin extends EditorPlugin
+
+var map_import_plugin : QuakeMapImportPlugin = null
+var palette_import_plugin : QuakePaletteImportPlugin = null
+var wad_import_plugin: QuakeWadImportPlugin = null
+
+#var func_godot_map_progress_bar: Control = null
+var edited_object_ref: WeakRef = weakref(null)
+
+func _get_plugin_name() -> String:
+ return "FuncGodot"
+
+func _handles(object: Object) -> bool:
+ return object is FuncGodotMap
+
+func _edit(object: Object) -> void:
+ edited_object_ref = weakref(object)
+
+#func _make_visible(visible: bool) -> void:
+ #if func_godot_map_progress_bar:
+ #func_godot_map_progress_bar.set_visible(visible)
+
+func _enter_tree() -> void:
+ # Import plugins
+ map_import_plugin = QuakeMapImportPlugin.new()
+ palette_import_plugin = QuakePaletteImportPlugin.new()
+ wad_import_plugin = QuakeWadImportPlugin.new()
+
+ add_import_plugin(map_import_plugin)
+ add_import_plugin(palette_import_plugin)
+ add_import_plugin(wad_import_plugin)
+
+ #func_godot_map_progress_bar = create_func_godot_map_progress_bar()
+ #func_godot_map_progress_bar.set_visible(false)
+ #add_control_to_container(EditorPlugin.CONTAINER_INSPECTOR_BOTTOM, func_godot_map_progress_bar)
+
+ add_custom_type("FuncGodotMap", "Node3D", preload("res://addons/func_godot/src/map/func_godot_map.gd"), null)
+
+ # Default Map Settings
+ if not ProjectSettings.has_setting("func_godot/default_map_settings"):
+ ProjectSettings.set_setting("func_godot/default_map_settings", "res://addons/func_godot/func_godot_default_map_settings.tres")
+ var property_info = {
+ "name": "func_godot/default_map_settings",
+ "type": TYPE_STRING,
+ "hint": PROPERTY_HINT_FILE,
+ "hint_string": "*.tres"
+ }
+ ProjectSettings.add_property_info(property_info)
+ ProjectSettings.set_as_basic("func_godot/default_map_settings", true)
+ ProjectSettings.set_initial_value("func_godot/default_map_settings", "res://addons/func_godot/func_godot_default_map_settings.tres")
+
+ # Default Inverse Scale Factor
+ if not ProjectSettings.has_setting("func_godot/default_inverse_scale_factor"):
+ ProjectSettings.set_setting("func_godot/default_inverse_scale_factor", 32.0)
+ var property_info = {
+ "name": "func_godot/default_inverse_scale_factor",
+ "type": TYPE_FLOAT
+ }
+ ProjectSettings.add_property_info(property_info)
+ ProjectSettings.set_as_basic("func_godot/default_inverse_scale_factor", true)
+ ProjectSettings.set_initial_value("func_godot/default_inverse_scale_factor", 32.0)
+
+ # Model Point Class Default Path
+ if not ProjectSettings.has_setting("func_godot/model_point_class_save_path"):
+ ProjectSettings.set_setting("func_godot/model_point_class_save_path", "")
+ var property_info = {
+ "name": "func_godot/model_point_class_save_path",
+ "type": TYPE_STRING
+ }
+ ProjectSettings.add_property_info(property_info)
+ ProjectSettings.set_as_basic("func_godot/model_point_class_save_path", true)
+ ProjectSettings.set_initial_value("func_godot/model_point_class_save_path", "")
+
+func _exit_tree() -> void:
+ remove_custom_type("FuncGodotMap")
+ remove_import_plugin(map_import_plugin)
+ remove_import_plugin(palette_import_plugin)
+ if wad_import_plugin:
+ remove_import_plugin(wad_import_plugin)
+
+ map_import_plugin = null
+ palette_import_plugin = null
+ wad_import_plugin = null
+
+ #if func_godot_map_progress_bar:
+ #remove_control_from_container(EditorPlugin.CONTAINER_INSPECTOR_BOTTOM, func_godot_map_progress_bar)
+ #func_godot_map_progress_bar.queue_free()
+ #func_godot_map_progress_bar = null
+
+# Create a progress bar for building a [FuncGodotMap]
+#func create_func_godot_map_progress_bar() -> Control:
+ #var progress_label = Label.new()
+ #progress_label.name = "ProgressLabel"
+ #progress_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
+ #progress_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
+ #
+ #var progress_bar := ProgressBar.new()
+ #progress_bar.name = "ProgressBar"
+ #progress_bar.show_percentage = false
+ #progress_bar.min_value = 0.0
+ #progress_bar.max_value = 1.0
+ #progress_bar.custom_minimum_size.y = 30
+ #progress_bar.set_anchors_and_offsets_preset(Control.PRESET_LEFT_WIDE)
+ #progress_bar.add_child(progress_label)
+ #progress_label.set_anchors_and_offsets_preset(Control.PRESET_LEFT_WIDE)
+ #progress_label.offset_top = -9
+ #progress_label.offset_left = 3
+ #
+ #return progress_bar
+
+# Update the build progress bar (see: [method create_func_godot_map_progress_bar]) to display the current step and progress (0-1)
+#func func_godot_map_build_progress(step: String, progress: float) -> void:
+ #var progress_label = func_godot_map_progress_bar.get_node("ProgressLabel")
+ #func_godot_map_progress_bar.value = progress
+ #progress_label.text = step.capitalize()
+
+## Callback for when the build process for a [FuncGodotMap] is finished.
+func func_godot_map_build_complete(func_godot_map: FuncGodotMap) -> void:
+ #var progress_label = func_godot_map_progress_bar.get_node("ProgressLabel")
+ #progress_label.text = "Build Complete"
+
+ #if func_godot_map.is_connected("build_progress",Callable(self,"func_godot_map_build_progress")):
+ #func_godot_map.disconnect("build_progress",Callable(self,"func_godot_map_build_progress"))
+
+ if func_godot_map.is_connected("build_complete",Callable(self,"func_godot_map_build_complete")):
+ func_godot_map.disconnect("build_complete",Callable(self,"func_godot_map_build_complete"))
+
+ if func_godot_map.is_connected("build_failed",Callable(self,"func_godot_map_build_complete")):
+ func_godot_map.disconnect("build_failed",Callable(self,"func_godot_map_build_complete"))
diff --git a/demo/addons/func_godot/src/func_godot_plugin.gd.uid b/demo/addons/func_godot/src/func_godot_plugin.gd.uid
new file mode 100644
index 0000000..c77b5be
--- /dev/null
+++ b/demo/addons/func_godot/src/func_godot_plugin.gd.uid
@@ -0,0 +1 @@
+uid://bqy3tr83l7di
diff --git a/demo/addons/func_godot/src/import/quake_map_file.gd b/demo/addons/func_godot/src/import/quake_map_file.gd
new file mode 100644
index 0000000..e1f7d1a
--- /dev/null
+++ b/demo/addons/func_godot/src/import/quake_map_file.gd
@@ -0,0 +1,14 @@
+@icon("res://addons/func_godot/icons/icon_quake_file.svg")
+class_name QuakeMapFile extends Resource
+## Map file that can be built by [FuncGodotMap].
+##
+## Map file that can be built by a [FuncGodotMap]. Supports the Quake and Valve map formats.
+##
+## @tutorial(Quake Wiki Map Format Article): https://quakewiki.org/wiki/Quake_Map_Format
+## @tutorial(Valve Developer Wiki VMF Article): https://developer.valvesoftware.com/wiki/VMF_(Valve_Map_Format)
+
+## Number of times this map file has been imported.
+@export var revision: int = 0
+
+## Raw map data.
+@export_multiline var map_data: String = ""
diff --git a/demo/addons/func_godot/src/import/quake_map_file.gd.uid b/demo/addons/func_godot/src/import/quake_map_file.gd.uid
new file mode 100644
index 0000000..9403fd7
--- /dev/null
+++ b/demo/addons/func_godot/src/import/quake_map_file.gd.uid
@@ -0,0 +1 @@
+uid://cxvwf50mehesf
diff --git a/demo/addons/func_godot/src/import/quake_map_import_plugin.gd b/demo/addons/func_godot/src/import/quake_map_import_plugin.gd
new file mode 100644
index 0000000..9470f3f
--- /dev/null
+++ b/demo/addons/func_godot/src/import/quake_map_import_plugin.gd
@@ -0,0 +1,43 @@
+@tool
+class_name QuakeMapImportPlugin extends EditorImportPlugin
+
+func _get_importer_name() -> String:
+ return 'func_godot.map'
+
+func _get_visible_name() -> String:
+ return 'Quake Map'
+
+func _get_resource_type() -> String:
+ return 'Resource'
+
+func _get_recognized_extensions() -> PackedStringArray:
+ return PackedStringArray(['map','vmf'])
+
+func _get_priority():
+ return 1.0
+
+func _get_save_extension() -> String:
+ return 'tres'
+
+func _get_import_options(path, preset):
+ return []
+
+func _get_preset_count() -> int:
+ return 0
+
+func _get_import_order():
+ return 0
+
+func _import(source_file, save_path, options, r_platform_variants, r_gen_files) -> Error:
+ var save_path_str = '%s.%s' % [save_path, _get_save_extension()]
+
+ var map_resource : QuakeMapFile = null
+
+ if ResourceLoader.exists(save_path_str):
+ map_resource = load(save_path_str) as QuakeMapFile
+ map_resource.revision += 1
+ else:
+ map_resource = QuakeMapFile.new()
+ map_resource.map_data = FileAccess.open(source_file, FileAccess.READ).get_as_text()
+
+ return ResourceSaver.save(map_resource, save_path_str)
diff --git a/demo/addons/func_godot/src/import/quake_map_import_plugin.gd.uid b/demo/addons/func_godot/src/import/quake_map_import_plugin.gd.uid
new file mode 100644
index 0000000..ab9eb4a
--- /dev/null
+++ b/demo/addons/func_godot/src/import/quake_map_import_plugin.gd.uid
@@ -0,0 +1 @@
+uid://dnsj08ot32vpc
diff --git a/demo/addons/func_godot/src/import/quake_palette_file.gd b/demo/addons/func_godot/src/import/quake_palette_file.gd
new file mode 100644
index 0000000..3cbb8a2
--- /dev/null
+++ b/demo/addons/func_godot/src/import/quake_palette_file.gd
@@ -0,0 +1,14 @@
+@icon("res://addons/func_godot/icons/icon_quake_file.svg")
+class_name QuakePaletteFile extends Resource
+## Quake LMP palette format file used with [QuakeWadFile].
+##
+## Quake LMP palette format file used in conjunction with a Quake WAD2 format [QuakeWadFile].
+## Not required for the Valve WAD3 format.
+##
+## @tutorial(Quake Wiki Palette Article): https://quakewiki.org/wiki/Quake_palette#palette.lmp
+
+## Collection of [Color]s retrieved from the LMP palette file.
+@export var colors: PackedColorArray
+
+func _init(colors):
+ self.colors = colors
diff --git a/demo/addons/func_godot/src/import/quake_palette_file.gd.uid b/demo/addons/func_godot/src/import/quake_palette_file.gd.uid
new file mode 100644
index 0000000..85ab1ac
--- /dev/null
+++ b/demo/addons/func_godot/src/import/quake_palette_file.gd.uid
@@ -0,0 +1 @@
+uid://dqhjx7jjbif5d
diff --git a/demo/addons/func_godot/src/import/quake_palette_import_plugin.gd b/demo/addons/func_godot/src/import/quake_palette_import_plugin.gd
new file mode 100644
index 0000000..54ce63d
--- /dev/null
+++ b/demo/addons/func_godot/src/import/quake_palette_import_plugin.gd
@@ -0,0 +1,58 @@
+@tool
+class_name QuakePaletteImportPlugin extends EditorImportPlugin
+
+func _get_importer_name() -> String:
+ return 'func_godot.palette'
+
+func _get_visible_name() -> String:
+ return 'Quake Palette'
+
+func _get_resource_type() -> String:
+ return 'Resource'
+
+func _get_recognized_extensions() -> PackedStringArray:
+ return PackedStringArray(['lmp'])
+
+func _get_save_extension() -> String:
+ return 'tres'
+
+func _get_import_options(path, preset):
+ return []
+
+func _get_preset_count() -> int:
+ return 0
+
+func _get_priority():
+ return 1.0
+
+func _get_import_order():
+ return 0
+
+func _import(source_file, save_path, options, r_platform_variants, r_gen_files) -> Error:
+ var save_path_str : String = '%s.%s' % [save_path, _get_save_extension()]
+
+ var file = FileAccess.open(source_file, FileAccess.READ)
+ if file == null:
+ var err = FileAccess.get_open_error()
+ printerr(['Error opening super.lmp file: ', err])
+ return err
+
+ var colors := PackedColorArray()
+
+ while true:
+ var red : int = file.get_8()
+ var green : int = file.get_8()
+ var blue : int = file.get_8()
+ var color := Color(red / 255.0, green / 255.0, blue / 255.0)
+
+ colors.append(color)
+
+ if file.eof_reached():
+ break
+
+ if colors.size() == 256:
+ break
+
+ var palette_resource := QuakePaletteFile.new(colors)
+
+ return ResourceSaver.save(palette_resource, save_path_str)
diff --git a/demo/addons/func_godot/src/import/quake_palette_import_plugin.gd.uid b/demo/addons/func_godot/src/import/quake_palette_import_plugin.gd.uid
new file mode 100644
index 0000000..f1d0bb2
--- /dev/null
+++ b/demo/addons/func_godot/src/import/quake_palette_import_plugin.gd.uid
@@ -0,0 +1 @@
+uid://c6k7hftart3u3
diff --git a/demo/addons/func_godot/src/import/quake_wad_file.gd b/demo/addons/func_godot/src/import/quake_wad_file.gd
new file mode 100644
index 0000000..e97113d
--- /dev/null
+++ b/demo/addons/func_godot/src/import/quake_wad_file.gd
@@ -0,0 +1,14 @@
+@icon("res://addons/func_godot/icons/icon_quake_file.svg")
+class_name QuakeWadFile extends Resource
+## Texture container in the WAD2 or WAD3 format.
+##
+## Texture container in the Quake WAD2 or Valve WAD3 format.
+##
+## @tutorial(Quake Wiki WAD Article): https://quakewiki.org/wiki/Texture_Wad
+## @tutorial(Valve Developer Wiki WAD3 Article): https://developer.valvesoftware.com/wiki/WAD
+
+## Collection of [ImageTexture] imported from the WAD file.
+@export var textures: Dictionary[String, ImageTexture]
+
+func _init(textures: Dictionary[String, ImageTexture] = {}):
+ self.textures = textures
diff --git a/demo/addons/func_godot/src/import/quake_wad_file.gd.uid b/demo/addons/func_godot/src/import/quake_wad_file.gd.uid
new file mode 100644
index 0000000..1d58e09
--- /dev/null
+++ b/demo/addons/func_godot/src/import/quake_wad_file.gd.uid
@@ -0,0 +1 @@
+uid://cij36hpqc46c
diff --git a/demo/addons/func_godot/src/import/quake_wad_import_plugin.gd b/demo/addons/func_godot/src/import/quake_wad_import_plugin.gd
new file mode 100644
index 0000000..fcc9df6
--- /dev/null
+++ b/demo/addons/func_godot/src/import/quake_wad_import_plugin.gd
@@ -0,0 +1,209 @@
+@tool
+class_name QuakeWadImportPlugin extends EditorImportPlugin
+
+enum WadFormat {
+ Quake,
+ HalfLife
+}
+
+enum QuakeWadEntryType {
+ Palette = 0x40,
+ SBarPic = 0x42,
+ MipsTexture = 0x44,
+ ConsolePic = 0x45
+}
+
+enum HalfLifeWadEntryType {
+ QPic = 0x42,
+ MipsTexture = 0x43,
+ FixedFont = 0x45
+}
+
+const TEXTURE_NAME_LENGTH := 16
+const MAX_MIP_LEVELS := 4
+
+func _get_importer_name() -> String:
+ return 'func_godot.wad'
+
+func _get_visible_name() -> String:
+ return 'Quake WAD'
+
+func _get_resource_type() -> String:
+ return 'Resource'
+
+func _get_recognized_extensions() -> PackedStringArray:
+ return PackedStringArray(['wad'])
+
+func _get_save_extension() -> String:
+ return 'res'
+
+func _get_option_visibility(path: String, option_name: StringName, options: Dictionary) -> bool:
+ return true
+
+func _get_import_options(path, preset) -> Array[Dictionary]:
+ return [
+ {
+ 'name': 'palette_file',
+ 'default_value': 'res://addons/func_godot/palette.lmp',
+ 'property_hint': PROPERTY_HINT_FILE,
+ 'hint_string': '*.lmp'
+ },
+ {
+ 'name': 'generate_mipmaps',
+ 'default_value': true,
+ 'property_hint': PROPERTY_HINT_NONE
+ }
+ ]
+
+func _get_preset_count() -> int:
+ return 0
+
+func _get_import_order() -> int:
+ return 0
+
+func _get_priority() -> float:
+ return 1.0
+
+func _import(source_file, save_path, options, r_platform_variants, r_gen_files) -> Error:
+ var save_path_str : String = '%s.%s' % [save_path, _get_save_extension()]
+
+ var file = FileAccess.open(source_file, FileAccess.READ)
+ if file == null:
+ var err = FileAccess.get_open_error()
+ printerr(['Error opening super.wad file: ', err])
+ return err
+
+ # Read WAD header
+ var magic : PackedByteArray = file.get_buffer(4)
+ var magic_string : String = magic.get_string_from_ascii()
+ var wad_format: int = WadFormat.Quake
+
+ if magic_string == 'WAD3':
+ wad_format = WadFormat.HalfLife
+ elif magic_string != 'WAD2':
+ printerr('Error: Invalid WAD magic')
+ return ERR_INVALID_DATA
+
+ var palette_path : String = options['palette_file']
+ var palette_file : QuakePaletteFile = load(palette_path) as QuakePaletteFile
+ if wad_format == WadFormat.Quake and not palette_file:
+ printerr('Error: Invalid Quake palette file')
+ file.close()
+ return ERR_CANT_ACQUIRE_RESOURCE
+
+ var num_entries : int = file.get_32()
+ var dir_offset : int = file.get_32()
+
+ # Read entry list
+ file.seek(0)
+ file.seek(dir_offset)
+
+ var entries : Array = []
+
+ for entry_idx in range(0, num_entries):
+ var offset : int = file.get_32()
+ var in_wad_size : int = file.get_32()
+ var size : int = file.get_32()
+ var type : int = file.get_8()
+ var compression : int = file.get_8()
+ var unknown : int = file.get_16()
+ var name : PackedByteArray = file.get_buffer(TEXTURE_NAME_LENGTH)
+ var name_string : String = name.get_string_from_ascii()
+
+ if (wad_format == WadFormat.Quake and type == int(QuakeWadEntryType.MipsTexture)) or (
+ wad_format == WadFormat.HalfLife and type == int(HalfLifeWadEntryType.MipsTexture)):
+ entries.append([
+ offset,
+ in_wad_size,
+ size,
+ type,
+ compression,
+ name_string
+ ])
+
+ # Read mip textures
+ var texture_data_array: Array = []
+ for entry in entries:
+ var offset : int = entry[0]
+ file.seek(offset)
+
+ var name : PackedByteArray = file.get_buffer(TEXTURE_NAME_LENGTH)
+ var name_string : String = name.get_string_from_ascii()
+
+ var width : int = file.get_32()
+ var height : int = file.get_32()
+
+ var mip_offsets : Array = []
+ for idx in range(0, MAX_MIP_LEVELS):
+ mip_offsets.append(file.get_32())
+
+ var num_pixels : int = width * height
+ var pixels : PackedByteArray = file.get_buffer(num_pixels)
+
+ if wad_format == WadFormat.Quake:
+ texture_data_array.append([name_string, width, height, pixels])
+ continue
+ # Half-Life WADs have a 256 color palette embedded in each texture
+ elif wad_format == WadFormat.HalfLife:
+ # Find the end of the mipmap data
+ file.seek(offset + mip_offsets[-1] + (width / 8) * (height / 8))
+ file.get_16()
+
+ var palette_colors := PackedColorArray()
+ for idx in 256:
+ var red : int = file.get_8()
+ var green : int = file.get_8()
+ var blue : int = file.get_8()
+ var color := Color(red / 255.0, green / 255.0, blue / 255.0)
+ palette_colors.append(color)
+
+ texture_data_array.append([name_string, width, height, pixels, palette_colors])
+
+ # Create texture resources
+ var textures : Dictionary[String, ImageTexture] = {}
+
+ for texture_data in texture_data_array:
+ var name : String = texture_data[0]
+ var width : int = texture_data[1]
+ var height : int = texture_data[2]
+ var pixels : PackedByteArray = texture_data[3]
+
+ var texture_image : Image
+ var pixels_rgb := PackedByteArray()
+
+ if wad_format == WadFormat.HalfLife:
+ var colors : PackedColorArray = texture_data[4]
+ for palette_color in pixels:
+ var rgb_color : Color = colors[palette_color]
+ pixels_rgb.append(rgb_color.r8)
+ pixels_rgb.append(rgb_color.g8)
+ pixels_rgb.append(rgb_color.b8)
+ # Color(0, 0, 255) is used for transparency in Half-Life
+ if rgb_color.b == 1 and rgb_color.r == 0 and rgb_color.b == 0:
+ pixels_rgb.append(0)
+ else:
+ pixels_rgb.append(255)
+ texture_image = Image.create_from_data(width, height, false, Image.FORMAT_RGBA8, pixels_rgb)
+
+ else: # WadFormat.Quake
+ for palette_color in pixels:
+ var rgb_color : Color = palette_file.colors[palette_color]
+ pixels_rgb.append(rgb_color.r8)
+ pixels_rgb.append(rgb_color.g8)
+ pixels_rgb.append(rgb_color.b8)
+ # Palette index 255 is used for transparency
+ if palette_color != 255:
+ pixels_rgb.append(255)
+ else:
+ pixels_rgb.append(0)
+ texture_image = Image.create_from_data(width, height, false, Image.FORMAT_RGBA8, pixels_rgb)
+
+ if options["generate_mipmaps"] == true:
+ texture_image.generate_mipmaps()
+
+ var texture := ImageTexture.create_from_image(texture_image) #,Texture2D.FLAG_MIPMAPS | Texture2D.FLAG_REPEAT | Texture2D.FLAG_ANISOTROPIC_FILTER
+ textures[name.to_lower()] = texture
+
+ # Save WAD resource
+ var wad_resource := QuakeWadFile.new(textures)
+ return ResourceSaver.save(wad_resource, save_path_str)
diff --git a/demo/addons/func_godot/src/import/quake_wad_import_plugin.gd.uid b/demo/addons/func_godot/src/import/quake_wad_import_plugin.gd.uid
new file mode 100644
index 0000000..fe3dc5e
--- /dev/null
+++ b/demo/addons/func_godot/src/import/quake_wad_import_plugin.gd.uid
@@ -0,0 +1 @@
+uid://ridgf32rxg6s
diff --git a/demo/addons/func_godot/src/map/func_godot_map.gd b/demo/addons/func_godot/src/map/func_godot_map.gd
new file mode 100644
index 0000000..b514b21
--- /dev/null
+++ b/demo/addons/func_godot/src/map/func_godot_map.gd
@@ -0,0 +1,152 @@
+@tool
+@icon("res://addons/func_godot/icons/icon_slipgate3d.svg")
+class_name FuncGodotMap extends Node3D
+## Scene generator node that parses a [QuakeMapFile] according to its [FuncGodotMapSettings].
+##
+## A scene generator node that parses a [QuakeMapFile]. It uses a [FuncGodotMapSettings]
+## and the [FuncGodotFGDFile] contained within in order to determine what is built and how it is built.[br][br]
+## If your map is not building correctly, double check your [member map_settings] to make sure you're using
+## the correct [FuncGodotMapSettings].
+
+const _SIGNATURE: String = "[MAP]"
+
+## Bitflag settings that control various aspects of the build process.
+enum BuildFlags {
+ UNWRAP_UV2 = 1 << 0, ## Unwrap UV2s during geometry generation for lightmap baking.
+ SHOW_PROFILE_INFO = 1 << 1, ## Print build step information during build process.
+ DISABLE_SMOOTHING = 1 << 2 ## Force disable processing of vertex normal smooth shading.
+}
+
+## Emitted when the build process fails.
+signal build_failed
+
+## Emitted when the build process succesfully completes.
+signal build_complete
+
+@export_tool_button("Build Map","CollisionShape3D") var _build_func: Callable = build
+@export_tool_button("Clear Map","Skeleton3D") var _clear_func: Callable = clear_children
+
+@export_category("Map")
+## Local path to MAP or VMF file to build a scene from.
+@export_file("*.map","*.vmf") var local_map_file: String = ""
+
+## Global path to MAP or VMF file to build a scene from. Overrides [member FuncGodotMap.local_map_file].
+@export_global_file("*.map","*.vmf") var global_map_file: String = ""
+
+# Map path used by code. Do it this way to support both global and local paths.
+var _map_file_internal: String = ""
+
+## Map settings resource that defines map build scale, textures location, entity definitions, and more.
+@export var map_settings: FuncGodotMapSettings = load(ProjectSettings.get_setting("func_godot/default_map_settings", "res://addons/func_godot/func_godot_default_map_settings.tres"))
+
+@export_category("Build")
+## [enum BuildFlags] that can affect certain aspects of the build process.
+@export_flags("Unwrap UV2:1", "Show Profiling Info:2", "Disable Smooth Shading:4") var build_flags: int = 0
+
+## The hyperplane is an initial plane that all geometry faces are cut from, like a large sheet of marble before a sculptor begins chiseling.
+## The hyperplane size would need to be able to cover your map's potential total area.
+## Smaller values can minimize floating point errors, reducing the effect of gaps between polygon seams.
+## Measured in Godot units, not Quake units.
+@export_range(256.0, 2048.0, 128.0) var hyperplane_size: float = 512.0
+
+## Map build failure handler. Displays error message and emits [signal build_failed] signal.
+func fail_build(reason: String, notify: bool = false) -> void:
+ push_error(_SIGNATURE, " ", reason)
+ if notify:
+ build_failed.emit()
+
+## Frees all children of the map node.[br]
+## [b][color=yellow]Warning:[/color][/b] This does not distinguish between nodes generated in the FuncGodot build process and other user created nodes.
+func clear_children() -> void:
+ for child in get_children():
+ remove_child(child)
+ child.queue_free()
+
+## Checks if a [QuakeMapFile] for the build process is provided and can be found.
+func verify() -> Error:
+ # Prioritize global map file path for building at runtime
+ _map_file_internal = global_map_file if global_map_file != "" else local_map_file
+
+ if _map_file_internal.is_empty():
+ fail_build("Cannot build empty map file.")
+ return ERR_INVALID_PARAMETER
+
+ # Retrieve real path if needed
+ if _map_file_internal.begins_with("uid://"):
+ var uid := ResourceUID.text_to_id(_map_file_internal)
+ if not ResourceUID.has_id(uid):
+ fail_build("Error: failed to retrieve path for UID (%s)" % _map_file_internal)
+ return ERR_DOES_NOT_EXIST
+ _map_file_internal = ResourceUID.get_id_path(uid)
+
+ if not FileAccess.file_exists(_map_file_internal):
+ if not FileAccess.file_exists(_map_file_internal + ".import"):
+ fail_build("Map file %s does not exist." % _map_file_internal)
+ return ERR_DOES_NOT_EXIST
+
+ return OK
+
+## Builds the [member global_map_file]. If not set, builds the [member local_map_file].
+## First cleans the map node of any children, then creates a [FuncGodotParser], [FuncGodotGeometryGenerator]
+## and [FuncGodotEntityAssembler] to parse and generate the map.
+func build() -> void:
+ var time_elapsed: float = Time.get_ticks_msec()
+
+ if build_flags & BuildFlags.SHOW_PROFILE_INFO:
+ FuncGodotUtil.print_profile_info("Building...", _SIGNATURE)
+
+ clear_children()
+
+ var verify_err: Error = verify()
+ if verify_err != OK:
+ fail_build("Verification failed: %s. Aborting map build" % error_string(verify_err), true)
+ return
+
+ if not map_settings:
+ push_warning("Map assembler does not have a map settings provided and will use default map settings.")
+ load(ProjectSettings.get_setting("func_godot/default_map_settings", "res://addons/func_godot/func_godot_default_map_settings.tres"))
+
+ # Parse and collect map data
+ var parser := FuncGodotParser.new()
+ if build_flags & BuildFlags.SHOW_PROFILE_INFO:
+ print("\nPARSER")
+ parser.declare_step.connect(FuncGodotUtil.print_profile_info.bind(parser._SIGNATURE))
+ var parse_data: FuncGodotData.ParseData = parser.parse_map_data(_map_file_internal, map_settings)
+
+ if parse_data.entities.is_empty():
+ return # Already printed failure message in parser, just return here
+
+ var entities: Array[FuncGodotData.EntityData] = parse_data.entities
+ var groups: Array[FuncGodotData.GroupData] = parse_data.groups
+
+ # Free up some memory now that we have the data
+ parser = null
+
+ # Retrieve geometry
+ var generator := FuncGodotGeometryGenerator.new(map_settings, hyperplane_size)
+ if build_flags & BuildFlags.SHOW_PROFILE_INFO:
+ print("\nGEOMETRY GENERATOR")
+ generator.declare_step.connect(FuncGodotUtil.print_profile_info.bind(generator._SIGNATURE))
+
+ # Generate surface and shape data
+ var generate_error := generator.build(build_flags, entities)
+ if generate_error != OK:
+ fail_build("Geometry generation failed: %s" % error_string(generate_error))
+ return
+
+ # Assemble entities and groups
+ var assembler := FuncGodotEntityAssembler.new(map_settings)
+ if build_flags & BuildFlags.SHOW_PROFILE_INFO:
+ print("\nENTITY ASSEMBLER")
+ assembler.declare_step.connect(FuncGodotUtil.print_profile_info.bind(assembler._SIGNATURE))
+ assembler.build(self, entities, groups)
+
+ time_elapsed = Time.get_ticks_msec() - time_elapsed
+
+ if build_flags & BuildFlags.SHOW_PROFILE_INFO:
+ print("\nCompleted in %s seconds" % (time_elapsed / 1000.0))
+
+ if build_flags & BuildFlags.SHOW_PROFILE_INFO:
+ print("")
+ FuncGodotUtil.print_profile_info("Build complete", _SIGNATURE)
+ build_complete.emit()
diff --git a/demo/addons/func_godot/src/map/func_godot_map.gd.uid b/demo/addons/func_godot/src/map/func_godot_map.gd.uid
new file mode 100644
index 0000000..4d03c85
--- /dev/null
+++ b/demo/addons/func_godot/src/map/func_godot_map.gd.uid
@@ -0,0 +1 @@
+uid://cwu5cf7a0awcd
diff --git a/demo/addons/func_godot/src/map/func_godot_map_settings.gd b/demo/addons/func_godot/src/map/func_godot_map_settings.gd
new file mode 100644
index 0000000..6085437
--- /dev/null
+++ b/demo/addons/func_godot/src/map/func_godot_map_settings.gd
@@ -0,0 +1,149 @@
+@tool
+@icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
+class_name FuncGodotMapSettings extends Resource
+## Reusable map settings configuration for [FuncGodotMap] nodes.
+
+#region BUILD
+@export_group("Build Settings")
+
+## Set automatically when [member inverse_scale_factor] is changed. Used primarily during the build process.
+var scale_factor: float = 0.03125
+
+## Ratio between map editor units and Godot units. FuncGodot will divide brush coordinates by this number and save the results to [member scale_factor].
+## This does not affect entity properties unless scripted to do so.
+@export var inverse_scale_factor: float = 32.0 :
+ set(value):
+ if value == 0.0:
+ printerr("Error: Cannot set Inverse Scale Factor to Zero")
+ return
+ inverse_scale_factor = value
+ scale_factor = 1.0 / value
+
+## [FuncGodotFGDFile] that translates map file classnames into Godot nodes and packed scenes.
+@export var entity_fgd: FuncGodotFGDFile = preload("res://addons/func_godot/fgd/func_godot_fgd.tres")
+
+## If true, will organize [SceneTree] using TrenchBroom Layers and Groups or Hammer Visgroups. Groups will be generated as [Node3D] nodes.
+## All non-entity structural brushes will be moved out of their groups and merged into the `Worldspawn` entity.
+## Any Layers toggled to be omitted from export in TrenchBroom and their child entities and groups will not be built.
+@export var use_groups_hierarchy: bool = false
+
+## Texel size for UV2 unwrapping.
+## Actual texel size is uv_unwrap_texel_size / [member inverse_scale_factor]. A ratio of 1/16 is usually a good place to start with
+## (if inverse_scale_factor is 32, start with a uv_unwrap_texel_size of 2).
+## Larger values will produce less detailed lightmaps. To conserve memory and filesize, use the largest value that still looks good.
+@export var uv_unwrap_texel_size: float = 2.0
+
+#endregion
+
+#region ENTITY
+@export_group("Entity Settings")
+
+## Optional array of node groups to add all generated nodes to.
+@export var entity_node_groups: Array[String] = []
+
+@export_subgroup("Entity Property Names")
+## Default class property to use in naming generated nodes. This setting is overridden by [member FuncGodotFGDEntityClass.name_property].
+## Naming occurs before adding to the [SceneTree] and applying properties.
+## Nodes will be named `"entity_" + name_property`. An entity's name should be unique, otherwise you may run into unexpected behavior.
+@export var entity_name_property: String = ""
+
+## Entity class property that determines whether the [FuncGodotFGDSolidClass] entity performs mesh smoothing operations.
+@export var entity_smoothing_property: String = "_phong"
+
+## Entity class property that contains the angular threshold that determines when a [FuncGodotFGDSolidClass] entity's mesh vertices are smoothed.
+@export var entity_smoothing_angle_property: String = "_phong_angle"
+
+## Entity class property that contains the snapping epsilon for generated vertices of [FuncGodotFGDSolidClass] entities.
+## Utilizing this property can help reduce instances of seams between polygons.
+@export var vertex_merge_distance_property: String = "_vertex_merge_distance"
+
+## Entity class property that tells whether interior faces should be culled for that brush entity.
+## Interior faces are faces with matching vertices or are flush within a larger face.
+## Note that this has a performance impact that scales with how many brushes are in the entity.
+@export var cull_interior_faces_property: String = "_cull_interior_faces"
+
+@export_subgroup("")
+#endregion
+
+#region TEXTURES
+@export_group("Textures")
+
+## Base directory for textures. When building materials, FuncGodot will search this directory for texture files with matching names to the textures assigned to map brush faces.
+@export_dir var base_texture_dir: String = "res://textures"
+
+## File extensions to search for texture data.
+@export var texture_file_extensions: Array[String] = ["png", "jpg", "jpeg", "bmp", "tga", "webp"]
+
+@export_subgroup("Hint Textures")
+## Optional path for the clip texture, relative to [member base_texture_dir].
+## Brush faces textured with the clip texture will have those faces removed from the generated [Mesh] but not the generated [Shape3D].
+@export var clip_texture: String = "clip":
+ set(tex):
+ clip_texture = tex.to_lower()
+
+## Optional path for the skip texture, relative to [member base_texture_dir].
+## Brush faces textured with the skip texture will have those faces removed from the generated [Mesh].
+## If [member FuncGodotFGDSolidClass.collision_shape_type] is set to concave then it will also remove collision from those faces in the generated [Shape3D].
+@export var skip_texture: String = "skip":
+ set(tex):
+ skip_texture = tex.to_lower()
+
+## Optional path for the origin texture, relative to [member base_texture_dir].
+## Brush faces textured with the origin texture will have those faces removed from the generated [Mesh] and [Shape3D].
+## The bounds of these faces will be used to calculate the origin point of the entity.
+@export var origin_texture: String = "origin":
+ set(tex):
+ origin_texture = tex.to_lower()
+@export_subgroup("")
+
+## Optional [QuakeWadFile] resources to apply textures from. See the [Quake Wiki](https://quakewiki.org/wiki/Texture_Wad) for more information on Quake Texture WADs.
+@export var texture_wads: Array[QuakeWadFile] = []
+
+#endregion
+
+#region MATERIALS
+@export_group("Materials")
+
+## Base directory for loading and saving materials. When building materials, FuncGodot will search this directory for material resources
+## with matching names to the textures assigned to map brush faces. If not found, will fall back to [member base_texture_dir].
+@export_dir var base_material_dir: String = ""
+
+## File extension to search for [Material] definitions
+@export var material_file_extension: String = "tres"
+
+## [Material] used as template when generating missing materials.
+@export var default_material: Material = preload("res://addons/func_godot/textures/default_material.tres")
+
+## Sampler2D uniform that supplies the Albedo in a custom shader when [member default_material] is a [ShaderMaterial].
+@export var default_material_albedo_uniform: String = ""
+
+## Automatic [ShaderMaterial] generation mapping patterns. Only used when [member default_material] is a ShaderMaterial.
+## Keys should be the names of the shader uniforms while the values should be the suffixes for the texture maps.
+## Patterns only use one replacement String: the texture name, ex: [code]"%s_normal"[/code].
+@export var shader_material_uniform_map_patterns: Dictionary[String, String] = {}
+
+@export_subgroup("BaseMaterial3D Map Patterns")
+## Automatic PBR material generation albedo map pattern.
+@export var albedo_map_pattern: String = "%s_albedo"
+## Automatic PBR material generation normal map pattern.
+@export var normal_map_pattern: String = "%s_normal"
+## Automatic PBR material generation metallic map pattern
+@export var metallic_map_pattern: String = "%s_metallic"
+## Automatic PBR material generation roughness map pattern
+@export var roughness_map_pattern: String = "%s_roughness"
+## Automatic PBR material generation emission map pattern
+@export var emission_map_pattern: String = "%s_emission"
+## Automatic PBR material generation ambient occlusion map pattern
+@export var ao_map_pattern: String = "%s_ao"
+## Automatic PBR material generation height map pattern
+@export var height_map_pattern: String = "%s_height"
+## Automatic PBR material generation ORM map pattern
+@export var orm_map_pattern: String = "%s_orm"
+@export_subgroup("")
+
+## Save automatically generated materials to disk, allowing reuse across [FuncGodotMap] nodes.
+## [i]NOTE: Materials do not use the [member default_material] settings after saving.[/i]
+@export var save_generated_materials: bool = true
+@export_group("")
+
+#endregion
diff --git a/demo/addons/func_godot/src/map/func_godot_map_settings.gd.uid b/demo/addons/func_godot/src/map/func_godot_map_settings.gd.uid
new file mode 100644
index 0000000..fccc85d
--- /dev/null
+++ b/demo/addons/func_godot/src/map/func_godot_map_settings.gd.uid
@@ -0,0 +1 @@
+uid://38q6k0ctahjn
diff --git a/demo/addons/func_godot/src/netradiant_custom/netradiant_custom_gamepack_config.gd b/demo/addons/func_godot/src/netradiant_custom/netradiant_custom_gamepack_config.gd
new file mode 100644
index 0000000..e6377d2
--- /dev/null
+++ b/demo/addons/func_godot/src/netradiant_custom/netradiant_custom_gamepack_config.gd
@@ -0,0 +1,322 @@
+@tool
+@icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
+class_name NetRadiantCustomGamePackConfig extends Resource
+## Builds a gamepack for NetRadiant Custom.
+##
+## Resource that builds a gamepack configuration for NetRadiant Custom.
+
+enum NetRadiantCustomMapType {
+ QUAKE_1, ## Removes PatchDef entries from the map file.
+ QUAKE_3 ## Allows the saving of PatchDef entries in the map file.
+}
+
+@export_tool_button("Export Gamepack") var _export_file: Callable = export_file
+
+## Gamepack folder and file name. Must be lower case and must not contain special characters.
+@export var gamepack_name : String = "func_godot":
+ set(new_name):
+ gamepack_name = new_name.to_lower()
+
+## Name of the game in NetRadiant Custom's gamepack list.
+@export var game_name : String = "FuncGodot"
+
+## Directory path containing your maps, textures, shaders, etc... relative to your project directory.
+@export var base_game_path : String = ""
+
+## [FuncGodotFGDFile] to include with this gamepack. If using multiple FGD file resources,
+## this should be the master FGD that contains them in [member FuncGodotFGDFile.base_fgd_files].
+@export var fgd_file : FuncGodotFGDFile = preload("res://addons/func_godot/fgd/func_godot_fgd.tres")
+
+## Toggles whether [FuncGodotFGDModelPointClass] resources will generate models from their [PackedScene] files.
+@export var generate_model_point_class_models: bool = true
+
+## Collection of [NetRadiantCustomShader] resources for shader file generation.
+@export var netradiant_custom_shaders : Array[Resource] = [
+ preload("res://addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_clip.tres"),
+ preload("res://addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_skip.tres"),
+ preload("res://addons/func_godot/game_config/netradiant_custom/netradiant_custom_shader_origin.tres")
+]
+
+## Supported model file types.
+@export var model_types : PackedStringArray = ["glb", "gltf", "obj"]
+
+## Supported audio file types.
+@export var sound_types : PackedStringArray = ["wav", "ogg"]
+
+## Quake map type NetRadiant will filter the map for, determining whether PatchDef entries are saved.
+## [color=red][b]WARNING![/b][/color] Toggling this option may be destructive!
+@export var map_type: NetRadiantCustomMapType = NetRadiantCustomMapType.QUAKE_3
+
+@export_group("Textures")
+## Supported texture file types.
+@export var texture_types : PackedStringArray = ["png", "jpg", "jpeg", "bmp", "tga"]
+
+## Default scale of textures in NetRadiant Custom.
+@export var default_scale : String = "1.0"
+
+## Clip texture path that gets applied to [i]weapclip[/i] and [i]nodraw[/i] shaders.
+@export var clip_texture: String = "textures/clip"
+
+## Skip texture path that gets applied to [i]caulk[/i] and [i]nodrawnonsolid[/i] shaders.
+@export var skip_texture: String = "textures/skip"
+
+@export_group("Build Menu")
+## Variables to include in the exported gamepack's [code]default_build_menu.xml[/code].[br][br]
+## Each [String] key defines a variable name, and its corresponding [String] value as the literal command-line string
+## to execute in place of this variable identifier[br][br]
+## Entries may be referred to by key in [member default_build_menu_commands] values.
+@export var default_build_menu_variables: Dictionary
+
+## Commands to include in the exported gamepack's [code]default_build_menu.xml[/code].[br][br]
+## Keys, specified as a [String], define the build option name as you want it to appear in NetRadiant Custom.[br][br]
+## Values represent commands taken within each option.[br][br]They may be either a [String] or an [Array] of [String] elements
+## that will be used as the full command-line text issued by each command [i]within[/i] its associated build option key.[br][br]
+## They may reference entries in [member default_build_menu_variables] by using brackets: [code][variable key name][/code]
+@export var default_build_menu_commands: Dictionary
+
+# Generates completed text for a .shader file.
+func _build_shader_text() -> String:
+ var shader_text: String = ""
+ for shader_res in netradiant_custom_shaders:
+ shader_text += (shader_res as NetRadiantCustomShader).texture_path + "\n{\n"
+ for shader_attrib in (shader_res as NetRadiantCustomShader).shader_attributes:
+ shader_text += "\t" + shader_attrib + "\n"
+ shader_text += "}\n"
+ return shader_text
+
+# Generates completed text for a .gamepack file.
+func _build_gamepack_text() -> String:
+ var texturetypes_str: String = ""
+ for texture_type in texture_types:
+ texturetypes_str += texture_type
+ if texture_type != texture_types[-1]:
+ texturetypes_str += " "
+
+ var modeltypes_str: String = ""
+ for model_type in model_types:
+ modeltypes_str += model_type
+ if model_type != model_types[-1]:
+ modeltypes_str += " "
+
+ var soundtypes_str: String = ""
+ for sound_type in sound_types:
+ soundtypes_str += sound_type
+ if sound_type != sound_types[-1]:
+ soundtypes_str += " "
+
+ var maptype_str: String
+
+ if map_type == NetRadiantCustomMapType.QUAKE_3:
+ maptype_str = "mapq3"
+ else:
+ maptype_str = "mapq1"
+
+ var gamepack_text: String = """
+
+"""
+
+ return gamepack_text % [
+ game_name,
+ game_name,
+ gamepack_name,
+ game_name,
+ gamepack_name,
+ base_game_path,
+ game_name,
+ game_name,
+ texturetypes_str,
+ modeltypes_str,
+ soundtypes_str,
+ maptype_str,
+ default_scale,
+ clip_texture,
+ skip_texture,
+ clip_texture,
+ skip_texture
+ ]
+
+## Exports this game's configuration with an icon, .cfg, and all accompanying FGD files in the [FuncGodotLocalConfig] [b]NetRadiant Custom Gamepacks Folder[/b].
+func export_file() -> void:
+ var game_path: String = FuncGodotLocalConfig.get_setting(FuncGodotLocalConfig.PROPERTY.MAP_EDITOR_GAME_PATH) as String
+ if game_path.is_empty():
+ printerr("Skipping export: Map Editor Game Path not set in Project Configuration")
+ return
+
+ var gamepacks_folder: String = FuncGodotLocalConfig.get_setting(FuncGodotLocalConfig.PROPERTY.NETRADIANT_CUSTOM_GAMEPACKS_FOLDER) as String
+ if gamepacks_folder.is_empty():
+ printerr("Skipping export: No NetRadiant Custom gamepacks folder")
+ return
+
+ # Make sure FGD file is set
+ if !fgd_file:
+ printerr("Skipping export: No FGD file")
+ return
+
+ # Make sure we're actually in the NetRadiant Custom gamepacks folder
+ if DirAccess.open(gamepacks_folder + "/games") == null:
+ printerr("Skipping export: No \'games\' folder. Is this the NetRadiant Custom gamepacks folder?")
+ return
+
+ # Create gamepack folders in case they do not exist
+ var gamepack_dir_paths: Array = [
+ gamepacks_folder + "/" + gamepack_name + ".game",
+ gamepacks_folder + "/" + gamepack_name + ".game/" + base_game_path,
+ gamepacks_folder + "/" + gamepack_name + ".game/scripts",
+ game_path + "/scripts"
+ ]
+ var err: Error
+
+ for path in gamepack_dir_paths:
+ if DirAccess.open(path) == null:
+ print("Couldn't open " + path + ", creating...")
+ err = DirAccess.make_dir_recursive_absolute(path)
+ if err != OK:
+ printerr("Skipping export: Failed to create directory")
+ return
+
+ var target_file_path: String
+ var file: FileAccess
+
+ # .gamepack
+ target_file_path = gamepacks_folder + "/games/" + gamepack_name + ".game"
+ print("Exporting NetRadiant Custom Gamepack to ", target_file_path)
+ file = FileAccess.open(target_file_path, FileAccess.WRITE)
+ if file != null:
+ file.store_string(_build_gamepack_text())
+ file.close()
+ else:
+ printerr("Error: Could not modify " + target_file_path)
+
+ # .shader
+ # NOTE: To work properly, this should go in the game path. For now, I'm leaving the export to NRC as well, so it can easily
+ # be repackaged for distribution. However, I believe in the end, it shouldn't exist there.
+ # We'll need to make a decision for this. - Vera
+ var shader_text: String = _build_shader_text()
+
+ # build to /scripts/
+ target_file_path = gamepacks_folder + "/" + gamepack_name + ".game/scripts/" + gamepack_name + ".shader"
+ print("Exporting NetRadiant Custom shader definitions to ", target_file_path)
+ file = FileAccess.open(target_file_path, FileAccess.WRITE)
+ if file != null:
+ file.store_string(shader_text)
+ file.close()
+ else:
+ printerr("Error: Could not modify " + target_file_path)
+
+ # build to /scripts/
+ target_file_path = game_path.path_join("scripts/%s.shader" % gamepack_name)
+ print("Exporting NetRadiant Custom shader definitions to ", target_file_path)
+ file = FileAccess.open(target_file_path, FileAccess.WRITE)
+ if file != null:
+ file.store_string(shader_text)
+ file.close()
+ else:
+ printerr("Error: could not modify " + target_file_path)
+
+ # shaderlist.txt - see above NOTE regarding duplication
+ target_file_path = gamepacks_folder + "/" + gamepack_name + ".game/scripts/shaderlist.txt"
+ print("Exporting NetRadiant Custom shader list to ", target_file_path)
+ file = FileAccess.open(target_file_path, FileAccess.WRITE)
+ if file != null:
+ file.store_string(gamepack_name)
+ file.close()
+ else:
+ printerr("Error: Could not modify " + target_file_path)
+
+ # game path/scripts/shaderlist.txt
+ target_file_path = game_path.path_join("scripts/shaderlist.txt")
+ print("Exporting NetRadiant Custom shader list to ", target_file_path)
+ file = FileAccess.open(target_file_path, FileAccess.WRITE)
+ if file != null:
+ file.store_string(gamepack_name)
+ file.close()
+ else:
+ printerr("Error: Could not modify " + target_file_path)
+
+ # default_build_menu.xml
+ target_file_path = gamepacks_folder + "/" + gamepack_name + ".game/default_build_menu.xml"
+ print("Exporting NetRadiant Custom default build menu to ", target_file_path)
+ file = FileAccess.open(target_file_path, FileAccess.WRITE)
+
+ if file != null:
+ file.store_string("\n\n")
+
+ for key in default_build_menu_variables.keys():
+ if key is String:
+ if default_build_menu_variables[key] is String:
+ file.store_string('\t%s\n' % [key, default_build_menu_variables[key]])
+
+ else:
+ push_error(
+ "Variable key '%s' value '%s' is invalid type: %s; should be: String" % [
+ key, default_build_menu_variables[key],
+ type_string(typeof(default_build_menu_variables[key]))
+ ])
+ else:
+ push_error(
+ "Variable '%s' is an invalid key type: %s; should be: String" % [
+ key, type_string(typeof(key))
+ ])
+
+
+ for key in default_build_menu_commands.keys():
+ if key is String:
+ file.store_string('\t\n' % key)
+
+ if default_build_menu_commands[key] is String:
+ file.store_string('\t\t%s\n\t\n' % default_build_menu_commands[key])
+
+ elif default_build_menu_commands[key] is Array:
+ for command in default_build_menu_commands[key]:
+ if command is String:
+ file.store_string('\t\t%s\n' % command)
+ else:
+ push_error("Build option '%s' has invalid command: %s with type: %s; should be: String" % [
+ key, command, type_string(typeof(command))
+ ])
+
+ file.store_string('\t\n')
+
+ else:
+ push_error("Build option '%s' is an invalid type: %s; should be: String" % [
+ key, type_string(typeof(key))
+ ])
+
+ file.store_string("")
+
+ # FGD
+ var export_fgd : FuncGodotFGDFile = fgd_file.duplicate()
+ export_fgd.generate_model_point_class_models = generate_model_point_class_models
+ export_fgd.do_export_file(FuncGodotFGDFile.FuncGodotTargetMapEditors.NET_RADIANT_CUSTOM, gamepacks_folder + "/" + gamepack_name + ".game/" + base_game_path)
+ print("NetRadiant Custom Gamepack export complete\n")
diff --git a/demo/addons/func_godot/src/netradiant_custom/netradiant_custom_gamepack_config.gd.uid b/demo/addons/func_godot/src/netradiant_custom/netradiant_custom_gamepack_config.gd.uid
new file mode 100644
index 0000000..03a566f
--- /dev/null
+++ b/demo/addons/func_godot/src/netradiant_custom/netradiant_custom_gamepack_config.gd.uid
@@ -0,0 +1 @@
+uid://dfhj3me2g5j0l
diff --git a/demo/addons/func_godot/src/netradiant_custom/netradiant_custom_shader.gd b/demo/addons/func_godot/src/netradiant_custom/netradiant_custom_shader.gd
new file mode 100644
index 0000000..20524da
--- /dev/null
+++ b/demo/addons/func_godot/src/netradiant_custom/netradiant_custom_shader.gd
@@ -0,0 +1,12 @@
+@icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
+class_name NetRadiantCustomShader
+extends Resource
+## Shader resource for NetRadiant Custom configurations.
+##
+## Resource that gets built into a shader file that applies a special effect to a specified texture in NetRadiant Custom.
+
+## Path to texture without extension, eg: [i]"textures/special/clip"[/i].
+@export var texture_path: String
+
+## Array of shader properties to apply to faces using [member texture_path].
+@export var shader_attributes : Array[String] = ["qer_trans 0.4"]
diff --git a/demo/addons/func_godot/src/netradiant_custom/netradiant_custom_shader.gd.uid b/demo/addons/func_godot/src/netradiant_custom/netradiant_custom_shader.gd.uid
new file mode 100644
index 0000000..eab3bf1
--- /dev/null
+++ b/demo/addons/func_godot/src/netradiant_custom/netradiant_custom_shader.gd.uid
@@ -0,0 +1 @@
+uid://dn86acprv4e86
diff --git a/demo/addons/func_godot/src/trenchbroom/trenchbroom_game_config.gd b/demo/addons/func_godot/src/trenchbroom/trenchbroom_game_config.gd
new file mode 100644
index 0000000..23fc1e6
--- /dev/null
+++ b/demo/addons/func_godot/src/trenchbroom/trenchbroom_game_config.gd
@@ -0,0 +1,333 @@
+@tool
+@icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
+class_name TrenchBroomGameConfig extends Resource
+## Game configuration definition for TrenchBroom.
+##
+## Defines a game for TrenchBroom to express a set of entity definitions and editor behaviors.
+##
+## @tutorial(TrenchBroom Manual Game Configuration Information): https://trenchbroom.github.io/manual/latest/#game_configuration
+
+enum GameConfigVersion {
+ Latest,
+ Version4,
+ Version8,
+ Version9
+}
+
+@export_tool_button("Export GameConfig") var _export_file: Callable = export_file
+
+## Name of the game in TrenchBroom's game list.
+@export var game_name : String = "FuncGodot"
+
+## Icon for TrenchBroom's game list.
+@export var icon : Texture2D = preload("res://addons/func_godot/icon32.png")
+
+## Available map formats when creating a new map in TrenchBroom. The order of elements in the array is the order TrenchBroom will list the available formats.
+## The [i]"initialmap"[/i] key value is optional.
+@export var map_formats: Array[Dictionary] = [
+ { "format": "Valve", "initialmap": "initial_valve.map" },
+ { "format": "Standard", "initialmap": "initial_standard.map" },
+ { "format": "Quake2", "initialmap": "initial_quake2.map" },
+ { "format": "Quake3" }
+]
+
+@export_group("Textures")
+
+## Path to top level textures folder relative to the game path. Also referred to as materials in the latest versions of TrenchBroom.
+@export var textures_root_folder: String = "textures"
+
+## Textures matching these patterns will be hidden from TrenchBroom.
+@export var texture_exclusion_patterns: Array[String] = ["*_albedo", "*_ao", "*_emission", "*_height", "*_metallic", "*_normal", "*_orm", "*_roughness", "*_sss"]
+
+## Palette path relative to your Game Path. Only needed for Quake WAD2 files. Half-Life WAD3 files contain the palettes within the texture information.
+@export var palette_path: String = "textures/palette.lmp"
+
+@export_group("Entities")
+
+## [FuncGodotFGDFile] resource to include with this game. If using multiple FGD File resources,
+## this should be the master FGD File that contains them in [member FuncGodotFGDFile.base_fgd_files].
+@export var fgd_file : FuncGodotFGDFile = preload("res://addons/func_godot/fgd/func_godot_fgd.tres")
+
+## Scale expression that modifies the default display scale of entities in TrenchBroom.
+## See [url="https://trenchbroom.github.io/manual/latest/#game_configuration_files_entities"]TrenchBroom Manual Entity Configuration Information[/url] for more information.
+@export var entity_scale: String = "32"
+
+## Toggles whether [FuncGodotFGDModelPointClass] resources will generate models from their [PackedScene] files.
+@export var generate_model_point_class_models: bool = true
+
+## Arrays containing the [TrenchbroomTag] resource type.
+@export_group("Tags")
+
+## [TrenchbroomTag] resources that apply to brush entities.
+@export var brush_tags : Array[Resource] = []
+
+## [TrenchbroomTag] resources that apply to brush faces.
+@export var brushface_tags : Array[Resource] = [
+ preload("res://addons/func_godot/game_config/trenchbroom/tb_face_tag_clip.tres"),
+ preload("res://addons/func_godot/game_config/trenchbroom/tb_face_tag_skip.tres"),
+ preload("res://addons/func_godot/game_config/trenchbroom/tb_face_tag_origin.tres")
+]
+
+@export_group("Face Attributes")
+
+## Default scale of textures on new brushes and when UV scale is reset.
+@export var default_uv_scale : Vector2 = Vector2(1, 1)
+
+@export_group("Compatibility")
+
+## Game configuration format compatible with the version of TrenchBroom being used.
+@export var game_config_version: GameConfigVersion = GameConfigVersion.Latest
+
+# Matches tag key enum to the [String] name used in .cfg
+static func _get_match_key(tag_match_type: int) -> String:
+ match tag_match_type:
+ TrenchBroomTag.TagMatchType.TEXTURE:
+ return "material"
+ TrenchBroomTag.TagMatchType.CLASSNAME:
+ return "classname"
+ _:
+ push_error("Tag match type %s is not valid" % [tag_match_type])
+ return "ERROR"
+
+# Generates completed text for a .cfg file.
+func _build_class_text() -> String:
+ var map_formats_str : String = ""
+ for map_format in map_formats:
+ map_formats_str += "{ \"format\": \"" + map_format.format + "\""
+ if map_format.has("initialmap"):
+ map_formats_str += ", \"initialmap\": \"" + map_format.initialmap + "\""
+ if map_format != map_formats[-1]:
+ map_formats_str += " },\n\t\t"
+ else:
+ map_formats_str += " }"
+
+ var texture_exclusion_patterns_str := ""
+ for tex_pattern in texture_exclusion_patterns:
+ texture_exclusion_patterns_str += "\"" + tex_pattern + "\""
+ if tex_pattern != texture_exclusion_patterns[-1]:
+ texture_exclusion_patterns_str += ", "
+
+ var fgd_filename_str : String = "\"" + fgd_file.fgd_name + ".fgd\""
+
+ var brush_tags_str = _parse_tags(brush_tags)
+ var brushface_tags_str = _parse_tags(brushface_tags)
+ var uv_scale_str = _parse_default_uv_scale(default_uv_scale)
+
+ var config_text : String = ""
+ match game_config_version:
+ GameConfigVersion.Latest, GameConfigVersion.Version8, GameConfigVersion.Version9:
+ config_text = _get_game_config_v9v8_text() % [
+ game_name,
+ map_formats_str,
+ textures_root_folder,
+ texture_exclusion_patterns_str,
+ palette_path,
+ fgd_filename_str,
+ entity_scale,
+ brush_tags_str,
+ brushface_tags_str,
+ uv_scale_str
+ ]
+
+ GameConfigVersion.Version4:
+ config_text = _get_game_config_v4_text() % [
+ game_name,
+ map_formats_str,
+ textures_root_folder,
+ texture_exclusion_patterns_str,
+ palette_path,
+ fgd_filename_str,
+ entity_scale,
+ brush_tags_str,
+ brushface_tags_str,
+ uv_scale_str
+ ]
+
+ _:
+ push_error("Unsupported Game Config Version!")
+
+ return config_text
+
+# Converts brush, face, and attribute tags into a .cfg-usable String.
+func _parse_tags(tags: Array) -> String:
+ var tags_str := ""
+ for brush_tag in tags:
+ if brush_tag.tag_match_type >= TrenchBroomTag.TagMatchType.size():
+ continue
+ tags_str += "{\n"
+ tags_str += "\t\t\t\t\"name\": \"%s\",\n" % brush_tag.tag_name
+ var attribs_str := ""
+ for brush_tag_attrib in brush_tag.tag_attributes:
+ attribs_str += "\"%s\"" % brush_tag_attrib
+ if brush_tag_attrib != brush_tag.tag_attributes[-1]:
+ attribs_str += ", "
+ tags_str += "\t\t\t\t\"attribs\": [ %s ],\n" % attribs_str
+ tags_str += "\t\t\t\t\"match\": \"%s\",\n" % _get_match_key(brush_tag.tag_match_type)
+ tags_str += "\t\t\t\t\"pattern\": \"%s\"" % brush_tag.tag_pattern
+ if brush_tag.texture_name != "":
+ tags_str += ",\n"
+ tags_str += "\t\t\t\t\"material\": \"%s\"" % brush_tag.texture_name
+ tags_str += "\n"
+ tags_str += "\t\t\t}"
+ if brush_tag != tags[-1]:
+ tags_str += ","
+ if game_config_version > GameConfigVersion.Latest and game_config_version < GameConfigVersion.Version9:
+ tags_str = tags_str.replace("material", "texture")
+ return tags_str
+
+# Converts array of flags to .cfg String.
+func _parse_flags(flags: Array) -> String:
+ var flags_str := ""
+ for attrib_flag in flags:
+ flags_str += "{\n"
+ flags_str += "\t\t\t\t\"name\": \"%s\",\n" % attrib_flag.attrib_name
+ flags_str += "\t\t\t\t\"description\": \"%s\"\n" % attrib_flag.attrib_description
+ flags_str += "\t\t\t}"
+ if attrib_flag != flags[-1]:
+ flags_str += ","
+ return flags_str
+
+# Converts default uv scale vector to .cfg String.
+func _parse_default_uv_scale(texture_scale : Vector2) -> String:
+ var entry_str = "\"scale\": [{x}, {y}]"
+ return entry_str.format({
+ "x": texture_scale.x,
+ "y": texture_scale.y
+ })
+
+## Exports this game's configuration with an icon, .cfg, and all accompanying FGD files in the [FuncGodotLocalConfig] [b]Trenchbroom Game Config Folder[/b].
+func export_file() -> void:
+ var config_folder: String = FuncGodotLocalConfig.get_setting(FuncGodotLocalConfig.PROPERTY.TRENCHBROOM_GAME_CONFIG_FOLDER) as String
+ if config_folder.is_empty():
+ printerr("Skipping export: No TrenchBroom Game folder")
+ return
+
+ # Make sure FGD file is set
+ if not fgd_file:
+ printerr("Skipping export: No FGD file")
+ return
+
+ var config_dir := DirAccess.open(config_folder)
+ # Create config folder in case it does not exist
+ if config_dir == null:
+ print("Couldn't open directory, creating...")
+ var err := DirAccess.make_dir_recursive_absolute(config_folder)
+ if err != OK:
+ printerr("Skipping export: Failed to create directory")
+ return
+
+ # Icon
+ var icon_path : String = config_folder + "/icon.png"
+ print("Exporting icon to ", icon_path)
+ var export_icon : Image = icon.get_image()
+ export_icon.resize(32, 32, Image.INTERPOLATE_LANCZOS)
+ export_icon.save_png(icon_path)
+
+ # .cfg
+ var target_file_path: String = config_folder + "/GameConfig.cfg"
+ print("Exporting TrenchBroom Game Config to ", target_file_path)
+ var file = FileAccess.open(target_file_path, FileAccess.WRITE)
+ file.store_string(_build_class_text())
+ file.close()
+
+ # FGD
+ var export_fgd : FuncGodotFGDFile = fgd_file.duplicate()
+ export_fgd.generate_model_point_class_models = generate_model_point_class_models
+ export_fgd.do_export_file(FuncGodotFGDFile.FuncGodotTargetMapEditors.TRENCHBROOM, config_folder)
+ print("TrenchBroom Game Config export complete\n")
+
+#region GameConfigDeclarations
+func _get_game_config_v4_text() -> String:
+ return """\
+{
+ "version": 4,
+ "name": "%s",
+ "icon": "icon.png",
+ "fileformats": [
+ %s
+ ],
+ "filesystem": {
+ "searchpath": ".",
+ "packageformat": { "extension": ".zip", "format": "zip" }
+ },
+ "textures": {
+ "package": { "type": "directory", "root": "%s" },
+ "format": { "extensions": ["jpg", "jpeg", "tga", "png", "D", "C"], "format": "image" },
+ "excludes": [ %s ],
+ "palette": "%s",
+ "attribute": ["_tb_textures", "wad"]
+ },
+ "entities": {
+ "definitions": [ %s ],
+ "defaultcolor": "0.6 0.6 0.6 1.0",
+ "modelformats": [ "bsp, mdl, md2" ],
+ "scale": %s
+ },
+ "tags": {
+ "brush": [
+ %s
+ ],
+ "brushface": [
+ %s
+ ]
+ },
+ "faceattribs": {
+ "defaults": {
+ %s
+ },
+ "contentflags": [],
+ "surfaceflags": []
+ }
+}
+ """
+
+func _get_game_config_v9v8_text() -> String:
+ var config_text: String = """\
+{
+ "version": 9,
+ "name": "%s",
+ "icon": "icon.png",
+ "fileformats": [
+ %s
+ ],
+ "filesystem": {
+ "searchpath": ".",
+ "packageformat": { "extension": ".zip", "format": "zip" }
+ },
+ "materials": {
+ "root": "%s",
+ "extensions": [".bmp", ".exr", ".hdr", ".jpeg", ".jpg", ".png", ".tga", ".webp", ".D", ".C"],
+ "excludes": [ %s ],
+ "palette": "%s",
+ "attribute": "wad"
+ },
+ "entities": {
+ "definitions": [ %s ],
+ "defaultcolor": "0.6 0.6 0.6 1.0",
+ "scale": %s
+ },
+ "tags": {
+ "brush": [
+ %s
+ ],
+ "brushface": [
+ %s
+ ]
+ },
+ "faceattribs": {
+ "defaults": {
+ %s
+ },
+ "contentflags": [],
+ "surfaceflags": []
+ }
+}
+ """
+
+ if game_config_version == GameConfigVersion.Version8:
+ config_text = config_text.replace(": 9,", ": 8,")
+ config_text = config_text.replace("material", "texture")
+
+ return config_text
+
+#endregion
diff --git a/demo/addons/func_godot/src/trenchbroom/trenchbroom_game_config.gd.uid b/demo/addons/func_godot/src/trenchbroom/trenchbroom_game_config.gd.uid
new file mode 100644
index 0000000..7ee2754
--- /dev/null
+++ b/demo/addons/func_godot/src/trenchbroom/trenchbroom_game_config.gd.uid
@@ -0,0 +1 @@
+uid://cx44c4vnq8bt5
diff --git a/demo/addons/func_godot/src/trenchbroom/trenchbroom_tag.gd b/demo/addons/func_godot/src/trenchbroom/trenchbroom_tag.gd
new file mode 100644
index 0000000..9588251
--- /dev/null
+++ b/demo/addons/func_godot/src/trenchbroom/trenchbroom_tag.gd
@@ -0,0 +1,30 @@
+@icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
+class_name TrenchBroomTag extends Resource
+## Pattern matching tag added to [TrenchbroomGameConfig] for appearance and menu filtering purposes.
+##
+## Pattern matching tags to enable a number of features in TrenchBroom, including display appearance and menu filtering options.
+## This resource gets added to the [TrenchBroomGameConfig] resource. Does not affect appearance or functionality in Godot.
+##
+## @tutorial(TrenchBroom Manual Game Configuration): https://trenchbroom.github.io/manual/latest/#game_configuration_files
+## @tutorial(TrenchBroom Manual Special Brush Face Types): https://trenchbroom.github.io/manual/latest/#special_brush_face_types
+
+enum TagMatchType {
+ TEXTURE, ## Tag applies to any brush face with a texture matching the texture name.
+ CLASSNAME ## Tag applies to any brush entity with a class name matching the tag pattern.
+}
+
+## Name to define this tag. Not used as the matching pattern.
+@export var tag_name: String
+
+## The attributes applied to matching faces or brush entities. Only "_transparent" is supported in TrenchBroom, which makes matching faces or brush entities transparent.
+@export var tag_attributes : Array[String] = ["transparent"]
+
+## Determines how the tag is matched. See [constant TagMatchType].
+@export var tag_match_type: TagMatchType
+
+## A string that filters which flag, param, or classname to use. [code]*[/code] can be used as a wildcard to include multiple options.
+## [b]Example:[/b] [code]trigger*[/code] with [constant TagMatchType] [i]Classname[/i] will apply this tag to all brush entities with the [code]trigger[/code] prefix.
+@export var tag_pattern: String
+
+## A string that filters which textures recieve these attributes. Only used with a [constant TagMatchType] of [i]Texture[/i].
+@export var texture_name: String
diff --git a/demo/addons/func_godot/src/trenchbroom/trenchbroom_tag.gd.uid b/demo/addons/func_godot/src/trenchbroom/trenchbroom_tag.gd.uid
new file mode 100644
index 0000000..9971557
--- /dev/null
+++ b/demo/addons/func_godot/src/trenchbroom/trenchbroom_tag.gd.uid
@@ -0,0 +1 @@
+uid://b66qdknwqpfup
diff --git a/demo/addons/func_godot/src/util/func_godot_local_config.gd b/demo/addons/func_godot/src/util/func_godot_local_config.gd
new file mode 100644
index 0000000..8faef14
--- /dev/null
+++ b/demo/addons/func_godot/src/util/func_godot_local_config.gd
@@ -0,0 +1,137 @@
+@tool
+@icon("res://addons/func_godot/icons/icon_godot_ranger.svg")
+class_name FuncGodotLocalConfig extends Resource
+## Local machine project wide settings. [color=red]WARNING![/color] Do not create your own! Use the resource in [i]addons/func_godot[/i].
+##
+## Local machine project wide settings. Can define global defaults for some FuncGodot properties.
+## [color=red][b]DO NOT CREATE A NEW RESOURCE![/b][/color] This resource works by saving a configuration file to your game's [b][i]user://[/i][/b] folder
+## and pulling the properties from that config file rather than this resource. Use the premade [b][i]addons/func_godot/func_godot_local_config.tres[/i][/b] instead.
+## [br][br]
+## [b]Fgd Output Folder :[/b] Global directory path that [FuncGodotFGDFile] saves to when exported. Overridden when exported from a game configuration resource like [TrenchBroomGameConfig].[br][br]
+## [b]Trenchbroom Game Config Folder :[/b] Global directory path where your TrenchBroom game configuration should be saved to. Consult the [url="https://trenchbroom.github.io/manual/latest/#game_configuration_files"]TrenchBroom Manual's Game Configuration documentation[/url] for more information.[br][br]
+## [b]Netradiant Custom Gamepacks Folder :[/b] Global directory path where your NetRadiant Custom gamepacks are saved. On Windows this is the [i]gamepacks[/i] folder in your NetRadiant Custom installation.[br][br]
+## [b]Map Editor Game Path :[/b] Global directory path to your mapping folder where all of your mapping assets exist. This is usually either your project folder or a subfolder within it.[br][br]
+## [b]Game Path Models Folder :[/b] Relative directory path from your Map Editor Game Path to a subfolder containing any display models you might use for your map editor. Currently only used by [FuncGodotFGDModelPointClass].[br][br]
+## [b]Default Inverse Scale Factor :[/b] Scale factor that affects how [FuncGodotFGDModelPointClass] entities scale their map editor display models. Not used with TrenchBroom, use [member TrenchBroomGameConfig.entity_scale] expression instead.[br][br]
+
+enum PROPERTY {
+ FGD_OUTPUT_FOLDER,
+ TRENCHBROOM_GAME_CONFIG_FOLDER,
+ NETRADIANT_CUSTOM_GAMEPACKS_FOLDER,
+ MAP_EDITOR_GAME_PATH,
+ #GAME_PATH_MODELS_FOLDER,
+ #DEFAULT_INVERSE_SCALE
+}
+
+@export_tool_button("Export func_godot settings", "Save") var _save_settings = export_func_godot_settings
+@export_tool_button("Reload func_godot settings", "Reload") var _load_settings = reload_func_godot_settings
+
+const _CONFIG_PROPERTIES: Array[Dictionary] = [
+ {
+ "name": "fgd_output_folder",
+ "usage": PROPERTY_USAGE_EDITOR,
+ "type": TYPE_STRING,
+ "hint": PROPERTY_HINT_GLOBAL_DIR,
+ "func_godot_type": PROPERTY.FGD_OUTPUT_FOLDER
+ },
+ {
+ "name": "trenchbroom_game_config_folder",
+ "usage": PROPERTY_USAGE_EDITOR,
+ "type": TYPE_STRING,
+ "hint": PROPERTY_HINT_GLOBAL_DIR,
+ "func_godot_type": PROPERTY.TRENCHBROOM_GAME_CONFIG_FOLDER
+ },
+ {
+ "name": "netradiant_custom_gamepacks_folder",
+ "usage": PROPERTY_USAGE_EDITOR,
+ "type": TYPE_STRING,
+ "hint": PROPERTY_HINT_GLOBAL_DIR,
+ "func_godot_type": PROPERTY.NETRADIANT_CUSTOM_GAMEPACKS_FOLDER
+ },
+ {
+ "name": "map_editor_game_path",
+ "usage": PROPERTY_USAGE_EDITOR,
+ "type": TYPE_STRING,
+ "hint": PROPERTY_HINT_GLOBAL_DIR,
+ "func_godot_type": PROPERTY.MAP_EDITOR_GAME_PATH
+ },
+]
+
+var _settings_dict: Dictionary
+var _loaded := false
+
+## Retrieve a setting from the local configuration.
+static func get_setting(name: PROPERTY) -> Variant:
+ var settings: FuncGodotLocalConfig = load("res://addons/func_godot/func_godot_local_config.tres")
+ settings.reload_func_godot_settings()
+ return settings._settings_dict.get(PROPERTY.keys()[name], '') as Variant
+
+func _get_property_list() -> Array:
+ return _CONFIG_PROPERTIES.duplicate()
+
+func _get(property: StringName) -> Variant:
+ var config = _get_config_property(property)
+ if config == null and not config is Dictionary:
+ return null
+ _try_loading()
+ return _settings_dict.get(PROPERTY.keys()[config['func_godot_type']], _get_default_value(config['type']))
+
+func _set(property: StringName, value: Variant) -> bool:
+ var config = _get_config_property(property)
+ if config == null and not config is Dictionary:
+ return false
+ _settings_dict[PROPERTY.keys()[config['func_godot_type']]] = value
+ return true
+
+func _get_default_value(type) -> Variant:
+ match type:
+ TYPE_STRING: return ''
+ TYPE_INT: return 0
+ TYPE_FLOAT: return 0.0
+ TYPE_BOOL: return false
+ TYPE_VECTOR2: return Vector2.ZERO
+ TYPE_VECTOR3: return Vector3.ZERO
+ TYPE_ARRAY: return []
+ TYPE_DICTIONARY: return {}
+ push_error("Invalid setting type. Returning null")
+ return null
+
+func _get_config_property(name: StringName) -> Variant:
+ for config in _CONFIG_PROPERTIES:
+ if config['name'] == name:
+ return config
+ return null
+
+## Reload this system's configuration settings into the Local Config resource.
+func reload_func_godot_settings() -> void:
+ _loaded = true
+ var path = "user://func_godot_config.json"
+ if not FileAccess.file_exists(path):
+ var application_name: String = ProjectSettings.get('application/config/name')
+ application_name = application_name.replace(" ", "_")
+ path = "user://" + application_name + "_FuncGodotConfig.json"
+ if not FileAccess.file_exists(path):
+ return
+ var settings = FileAccess.get_file_as_string(path)
+ _settings_dict = {}
+ if not settings or settings.is_empty():
+ return
+ settings = JSON.parse_string(settings)
+ for key in settings.keys():
+ _settings_dict[key] = settings[key]
+ notify_property_list_changed()
+
+func _try_loading() -> void:
+ if not _loaded:
+ reload_func_godot_settings()
+
+## Export the current resource settings to a configuration file in this game's [i]user://[/i] folder.
+func export_func_godot_settings() -> void:
+ if _settings_dict.size() == 0:
+ return
+ var path = "user://func_godot_config.json"
+ var file = FileAccess.open(path, FileAccess.WRITE)
+ var json = JSON.stringify(_settings_dict)
+ file.store_line(json)
+ _loaded = false
+ print("Saved settings to ", file.get_path_absolute())
diff --git a/demo/addons/func_godot/src/util/func_godot_local_config.gd.uid b/demo/addons/func_godot/src/util/func_godot_local_config.gd.uid
new file mode 100644
index 0000000..d8b979c
--- /dev/null
+++ b/demo/addons/func_godot/src/util/func_godot_local_config.gd.uid
@@ -0,0 +1 @@
+uid://xsjnhahhyein
diff --git a/demo/addons/func_godot/src/util/func_godot_util.gd b/demo/addons/func_godot/src/util/func_godot_util.gd
new file mode 100644
index 0000000..f1a006b
--- /dev/null
+++ b/demo/addons/func_godot/src/util/func_godot_util.gd
@@ -0,0 +1,463 @@
+class_name FuncGodotUtil
+## Static class with a number of reuseable utility methods that can be called at Editor or Run Time.
+
+const _VERTEX_EPSILON: float = 0.008
+
+const _VEC3_UP_ID := Vector3(0.0, 0.0, 1.0)
+const _VEC3_RIGHT_ID := Vector3(0.0, 1.0, 0.0)
+const _VEC3_FORWARD_ID := Vector3(1.0, 0.0, 0.0)
+
+## Connected by the [FuncGodotMap] node to the build process' sub-components if the
+## [member FuncGodotMap.build_flags]'s SHOW_PROFILE_INFO flag is set.
+static func print_profile_info(message: String, signature: String) -> void:
+ prints(signature, message)
+
+## Return a [String] that corresponds to the current [OS]'s newline control characters.
+static func newline() -> String:
+ if OS.get_name() == "Windows":
+ return "\r\n"
+ else:
+ return "\n"
+
+#region MATH
+
+static func op_vec3_sum(lhs: Vector3, rhs: Vector3) -> Vector3:
+ return lhs + rhs
+
+static func op_vec3_avg(array: Array[Vector3]) -> Vector3:
+ if array.is_empty():
+ push_error("Cannot average empty Vector3 array!")
+ return Vector3()
+ return array.reduce(op_vec3_sum, Vector3()) / array.size()
+
+## Conversion from id tech coordinate system to Godot, from a top-down perspective.
+static func id_to_opengl(vec: Vector3) -> Vector3:
+ return Vector3(vec.y, vec.z, vec.x)
+
+## Check if a point is inside a convex hull defined by a series of planes by an epsilon constant.
+static func is_point_in_convex_hull(planes: Array[Plane], vertex: Vector3) -> bool:
+ for plane in planes:
+ var distance: float = plane.normal.dot(vertex) - plane.d
+ if distance > _VERTEX_EPSILON:
+ return false
+ return true
+
+#endregion
+
+#region PATCH DEF
+
+## Returns the control points that defines a cubic curve for a equivalent input quadratic curve.
+static func elevate_quadratic(p0: Vector3, p1: Vector3, p2: Vector3) -> Array[Vector3]:
+ return [p0, p0 + (2.0/3.0) * (p1 - p0), p2 + (2.0/3.0) * (p1 - p2), p2 ]
+
+## Create a Curve3D and bake points.
+static func create_curve(start: Vector3, control: Vector3, end: Vector3, bake_interval: float = 0.05) -> Curve3D:
+ var ret := Curve3D.new()
+ ret.bake_interval = bake_interval
+ update_ref_curve(ret, start, control, end, bake_interval)
+ return ret
+
+## Update a Curve3D given quadratic inputs.
+static func update_ref_curve(curve: Curve3D, p0: Vector3, p1: Vector3, p2: Vector3, bake_interval: float = 0.05) -> void:
+ curve.clear_points()
+ curve.bake_interval = bake_interval
+ curve.add_point(p0, (p1 - p0) * (2.0 / 3.0))
+ curve.add_point(p1, (p1 - p0) * (1.0 / 3.0), (p2 - p1) * (1.0 / 3.0))
+ curve.add_point(p2, (p2 - p1 * (2.0 / 3.0)))
+
+#endregion
+
+#region TEXTURES
+
+## Fallback texture if the one defined in the [QuakeMapFile] cannot be found.
+const default_texture_path: String = "res://addons/func_godot/textures/default_texture.png"
+
+const _pbr_textures: PackedInt32Array = [
+ StandardMaterial3D.TEXTURE_ALBEDO,
+ StandardMaterial3D.TEXTURE_NORMAL,
+ StandardMaterial3D.TEXTURE_METALLIC,
+ StandardMaterial3D.TEXTURE_ROUGHNESS,
+ StandardMaterial3D.TEXTURE_EMISSION,
+ StandardMaterial3D.TEXTURE_AMBIENT_OCCLUSION,
+ StandardMaterial3D.TEXTURE_HEIGHTMAP,
+ ORMMaterial3D.TEXTURE_ORM,
+ ]
+
+# Used during auto-PBR processing. Must match the _pbr_textures order.
+# -1 means the feature is permanantly enabled.
+const _pbr_features: PackedInt32Array = [
+ -1,
+ BaseMaterial3D.FEATURE_NORMAL_MAPPING,
+ -1,
+ -1,
+ BaseMaterial3D.FEATURE_EMISSION,
+ BaseMaterial3D.FEATURE_AMBIENT_OCCLUSION,
+ BaseMaterial3D.FEATURE_HEIGHT_MAPPING,
+ -1,
+]
+
+## Searches for a Texture2D within the base texture directory or the WAD files added to map settings.
+## If not found, a default texture is returned.
+static func load_texture(texture_name: String, wad_resources: Array[QuakeWadFile], map_settings: FuncGodotMapSettings) -> Texture2D:
+ for texture_file_extension in map_settings.texture_file_extensions:
+ var texture_path: String = map_settings.base_texture_dir.path_join(texture_name + "." + texture_file_extension)
+ if ResourceLoader.exists(texture_path):
+ var texture_file = load(texture_path)
+ if texture_file is Texture2D:
+ return texture_file
+ else:
+ printerr("Error: Texture load failed! (%s) not a valid Texture2D resource", texture_path)
+
+ var texture_name_lower: String = texture_name.to_lower()
+ for wad in wad_resources:
+ if texture_name_lower in wad.textures:
+ return wad.textures[texture_name_lower]
+
+ return load(default_texture_path)
+
+## Filters faces textured with Skip during the geometry generation step of the build process.
+static func is_skip(texture: String, map_settings: FuncGodotMapSettings) -> bool:
+ if map_settings:
+ return texture.to_lower() == map_settings.skip_texture
+ return false
+
+## Filters faces textured with Clip during the geometry generation step of the build process.
+static func is_clip(texture: String, map_settings: FuncGodotMapSettings) -> bool:
+ if map_settings:
+ return texture.to_lower() == map_settings.clip_texture
+ return false
+
+## Filters faces textured with Origin during the parsing and geometry generation steps of the build process.
+static func is_origin(texture: String, map_settings: FuncGodotMapSettings) -> bool:
+ if map_settings:
+ return texture.to_lower() == map_settings.origin_texture
+ return false
+
+## Filters faces textured with any of the tool textures during the geometry generation step of the build process.
+static func filter_face(texture: String, map_settings: FuncGodotMapSettings) -> bool:
+ if map_settings:
+ texture = texture.to_lower()
+ if (texture == map_settings.skip_texture
+ or texture == map_settings.clip_texture
+ or texture == map_settings.origin_texture
+ ):
+ return true
+ return false
+
+## Adds PBR textures to an existing [BaseMaterial3D].
+static func build_base_material(map_settings: FuncGodotMapSettings, material: BaseMaterial3D, texture: String) -> void:
+ var path: String = map_settings.base_texture_dir.path_join(texture)
+ # Check if there is a subfolder with our PBR textures
+ if DirAccess.open(path):
+ path = path.path_join(texture)
+
+ var pbr_suffixes: PackedStringArray = [
+ map_settings.albedo_map_pattern,
+ map_settings.normal_map_pattern,
+ map_settings.metallic_map_pattern,
+ map_settings.roughness_map_pattern,
+ map_settings.emission_map_pattern,
+ map_settings.ao_map_pattern,
+ map_settings.height_map_pattern,
+ map_settings.orm_map_pattern,
+ ]
+
+ for i in pbr_suffixes.size():
+ if not pbr_suffixes[i].is_empty():
+ var pbr: String = pbr_suffixes[i]
+ var token: int = pbr.find("%s", 0)
+ if token != -1:
+ if pbr.find("%s", token + 1) != -1:
+ token = 2
+ else:
+ token = 1
+
+ if token < 1:
+ printerr("No string replacement tokens found in auto-PBR pattern \'" + pbr + "\'! Must have at least one instance of \'%s\' per pattern.")
+ continue
+
+ if token > 0:
+ for texture_file_extension in map_settings.texture_file_extensions:
+ if token > 1:
+ pbr = pbr_suffixes[i] % [path, texture_file_extension]
+ else:
+ pbr = pbr_suffixes[i] % [path]
+ pbr += "." + texture_file_extension
+ if ResourceLoader.exists(pbr):
+ print(pbr)
+ if _pbr_features[i] > -1:
+ material.set_feature(_pbr_features[i], true)
+ material.set_texture(_pbr_textures[i], load(pbr))
+ break
+
+## Builds both materials and sizes dictionaries for use in the geometry generation step of the build process.
+## Both dictionaries use texture names as keys. The materials dictionary uses [Material] as values,
+## while the sizes dictionary saves the albedo texture sizes to aid in UV mapping.
+static func build_texture_map(entity_data: Array[FuncGodotData.EntityData], map_settings: FuncGodotMapSettings) -> Array[Dictionary]:
+ var texture_materials: Dictionary[String, Material] = {}
+ var texture_sizes: Dictionary[String, Vector2] = {}
+
+ # Prepare WAD files
+ var wad_resources: Array[QuakeWadFile] = []
+ for wad in map_settings.texture_wads:
+ if wad and not wad in wad_resources:
+ wad_resources.append(wad)
+
+ for entity in entity_data:
+ if not entity.is_visual():
+ continue
+
+ for brush in entity.brushes:
+ for face in brush.faces:
+ var texture_name: String = face.texture
+
+ if filter_face(texture_name, map_settings):
+ continue
+ if texture_materials.has(texture_name):
+ continue
+
+ var material_path: String = map_settings.base_material_dir if not map_settings.base_material_dir.is_empty() else map_settings.base_texture_dir
+ material_path = material_path.path_join(texture_name) + "." + map_settings.material_file_extension
+ material_path = material_path.replace("*", "")
+
+ if ResourceLoader.exists(material_path):
+ var material: Material = load(material_path)
+ texture_materials[texture_name] = material
+ if material is BaseMaterial3D:
+ var albedo = material.albedo_texture
+ if albedo is Texture2D:
+ texture_sizes[texture_name] = material.albedo_texture.get_size()
+ elif material is ShaderMaterial:
+ var albedo = material.get_shader_parameter(map_settings.default_material_albedo_uniform)
+ if albedo is Texture2D:
+ texture_sizes[texture_name] = albedo.get_size()
+ if not texture_sizes.has(texture_name):
+ var texture: Texture2D = load_texture(texture_name, wad_resources, map_settings)
+ if texture:
+ texture_sizes[texture_name] = texture.get_size()
+ if not texture_sizes.has(texture_name):
+ texture_sizes[texture_name] = Vector2.ONE * map_settings.inverse_scale_factor
+
+ # Material generation
+ elif map_settings.default_material:
+ var material = map_settings.default_material.duplicate(false)
+ var texture: Texture2D = load_texture(texture_name, wad_resources, map_settings)
+ texture_sizes[texture_name] = texture.get_size()
+
+ if material is BaseMaterial3D:
+ material.albedo_texture = texture
+ build_base_material(map_settings, material, texture_name)
+ elif material is ShaderMaterial:
+ material.set_shader_parameter(map_settings.default_material_albedo_uniform, texture)
+ var path: String = map_settings.base_texture_dir
+ for uniform in map_settings.shader_material_uniform_map_patterns.keys():
+ if map_settings.shader_material_uniform_map_patterns[uniform].find("%s") < 0:
+ printerr("No string replacement tokens fuond in ShaderMaterial uniform map pattern \'" + map_settings.shader_material_uniform_map_patterns[uniform] + "\'! Must have one instance of \'%s\' per pattern.")
+ continue
+ for texture_file_extension in map_settings.texture_file_extensions:
+ var uniform_texture_path: String = map_settings.shader_material_uniform_map_patterns[uniform] % [texture_name] + "." + texture_file_extension
+ uniform_texture_path = path.path_join(uniform_texture_path)
+ if ResourceLoader.exists(uniform_texture_path):
+ material.set_shader_parameter(uniform, load(uniform_texture_path))
+ break
+
+ if (map_settings.save_generated_materials and material
+ and texture_name != map_settings.clip_texture
+ and texture_name != map_settings.skip_texture
+ and texture_name != map_settings.origin_texture
+ and texture.resource_path != default_texture_path):
+ # Make sure our material directory exists
+ var dir := DirAccess.open(material_path.get_base_dir())
+ if not dir:
+ dir = DirAccess.open("res://")
+ dir.make_dir_recursive(material_path.get_base_dir().trim_prefix("res://"))
+ # Save the new material
+ ResourceSaver.save(material, material_path)
+
+ texture_materials[texture_name] = material
+ else: # No default material exists
+ printerr("Error: No default material found in map settings")
+
+ return [texture_materials, texture_sizes]
+
+#endregion
+
+#region UV MAPPING
+
+## Returns UV coordinate calculated from the Valve 220 UV format.
+static func get_valve_uv(vertex: Vector3, u_axis: Vector3, v_axis: Vector3, uv_basis := Transform2D.IDENTITY, texture_size := Vector2.ONE) -> Vector2:
+ var uv := Vector2(u_axis.dot(vertex), v_axis.dot(vertex))
+ var scale := Vector2(uv_basis.x.x, uv_basis.y.y)
+ uv += (uv_basis.origin * scale)
+ uv /= scale;
+ uv.x /= texture_size.x
+ uv.y /= texture_size.y
+ return uv
+
+## Returns UV coordinate calculated from the original id Standard UV format.
+static func get_quake_uv(vertex: Vector3, normal: Vector3, uv_in := Transform2D.IDENTITY, texture_size := Vector2.ONE) -> Vector2:
+ var uv_out: Vector2
+ var nx := absf(normal.dot(Vector3.RIGHT))
+ var ny := absf(normal.dot(Vector3.UP))
+ var nz := absf(normal.dot(Vector3.FORWARD))
+
+ if ny >= nx and ny >= nz:
+ uv_out = Vector2(vertex.x, -vertex.z)
+ elif nx >= ny and nx >= nz:
+ uv_out = Vector2(vertex.y, -vertex.z)
+ else:
+ uv_out = Vector2(vertex.x, vertex.y)
+
+ uv_out = uv_out.rotated(uv_in.get_rotation())
+ uv_out /= uv_in.get_scale()
+ uv_out += uv_in.origin
+ uv_out /= texture_size
+ return uv_out
+
+## Determines which UV format is being used and returns the UV coordinate.
+static func get_face_vertex_uv(vertex: Vector3, face: FuncGodotData.FaceData, texture_size: Vector2) -> Vector2:
+ if face.uv_axes.size() >= 2:
+ return get_valve_uv(vertex, face.uv_axes[0], face.uv_axes[1], face.uv, texture_size)
+ else:
+ return get_quake_uv(vertex, face.plane.normal, face.uv, texture_size)
+
+## Returns the tangent calculated from the Valve 220 UV format.
+static func get_valve_tangent(u: Vector3, v: Vector3, normal: Vector3) -> PackedFloat32Array:
+ var u_axis: Vector3 = u.normalized()
+ var v_axis: Vector3 = v.normalized()
+ var v_sign: float = -signf(normal.cross(u_axis).dot(v_axis))
+ return [u_axis.x, u_axis.y, u_axis.z, v_sign]
+
+ # NOTE: we may still need to orthonormalize tangents. Just in case, here's a rough outline.
+ #var tangent: Vector3 = u.normalized()
+ #tangent = (tangent - normal * normal.dot(tangent)).normalized()
+ #
+ ## in the case of parallel U or V axes to planar normal, reconstruct the tangent
+ #if tangent.length_squared() < 0.01:
+ # if absf(normal.y) < 0.9:
+ # tangent = Vector3.UP.cross(normal)
+ # else:
+ # tangent = Vector3.RIGHT.cross(normal)
+ #
+ #tangent = tangent.normalized()
+ #return [tangent.x, tangent.y, tangent.z, -signf(normal.cross(tangent).dot(v.normalized))]
+
+## Returns the tangent calculated from the original id Standard UV format.
+static func get_quake_tangent(normal: Vector3, uv_y_scale: float, uv_rotation: float) -> PackedFloat32Array:
+ var dx := normal.dot(_VEC3_RIGHT_ID)
+ var dy := normal.dot(_VEC3_UP_ID)
+ var dz := normal.dot(_VEC3_FORWARD_ID)
+ var dxa := absf(dx)
+ var dya := absf(dy)
+ var dza := absf(dz)
+ var u_axis: Vector3
+ var v_sign: float = 0.0
+
+ if dya >= dxa and dya >= dza:
+ u_axis = _VEC3_FORWARD_ID
+ v_sign = signf(dy)
+ elif dxa >= dya and dxa >= dza:
+ u_axis = _VEC3_FORWARD_ID
+ v_sign = -signf(dx)
+ elif dza >= dya and dza >= dxa:
+ u_axis = _VEC3_RIGHT_ID
+ v_sign = signf(dz)
+
+ v_sign *= signf(uv_y_scale)
+ u_axis = u_axis.rotated(normal, deg_to_rad(-uv_rotation) * v_sign)
+ return [u_axis.x, u_axis.y, u_axis.z, v_sign]
+
+static func get_face_tangent(face: FuncGodotData.FaceData) -> PackedFloat32Array:
+ if face.uv_axes.size() >= 2:
+ return get_valve_tangent(face.uv_axes[0], face.uv_axes[1], face.plane.normal)
+ else:
+ return get_quake_tangent(face.plane.normal, face.uv.get_scale().y, face.uv.get_rotation())
+
+#endregion
+
+#region MESH
+
+static func smooth_mesh_by_angle(mesh: ArrayMesh, angle_deg: float = 89.0) -> ArrayMesh:
+ if not mesh:
+ push_error("Need a source mesh to smooth")
+ return null
+
+ var angle: float = deg_to_rad(clampf(angle_deg, 0.0, 360.0))
+
+ var mesh_vertices: Array[Vector3] = []
+ var mesh_normals: Array[Vector3] = []
+ var surface_data: Array[Dictionary] = []
+ var mdt: MeshDataTool
+ var st := SurfaceTool.new()
+
+ # Collect surface information
+ for surface_index in mesh.get_surface_count():
+ mdt = MeshDataTool.new()
+
+ if mdt.create_from_surface(mesh, surface_index) != OK:
+ continue
+
+ var info: Dictionary = {
+ "mdt": mdt,
+ "ofs": mesh_vertices.size(),
+ "mat": mesh.surface_get_material(surface_index)
+ }
+
+ surface_data.append(info)
+
+ for i in mdt.get_vertex_count():
+ mesh_vertices.append(mdt.get_vertex(i))
+ mesh_normals.append(mdt.get_vertex_normal(i))
+
+ var groups: Dictionary = {}
+
+ # Group vertices by position
+ for i in mesh_vertices.size():
+ var pos := mesh_vertices[i]
+
+ # this is likely already snapped from the map building process
+ var key := pos.snappedf(_VERTEX_EPSILON)
+
+ if not groups.has(key):
+ groups[key] = [i]
+ else:
+ groups[key].append(i)
+
+ # Collect normals. Likely optimizable.
+ for group in groups.values():
+ for i in group:
+ var this := mesh_normals[i]
+ var normal_out := Vector3()
+ for j in group:
+ var other := mesh_normals[j]
+ if this.angle_to(other) <= angle:
+ normal_out += other
+
+ mesh_normals[i] = normal_out.normalized()
+
+ var smoothed_mesh := ArrayMesh.new()
+
+ # Construct smoothed output mesh
+ for dict in surface_data:
+ mdt = dict["mdt"]
+ var offset: int = dict["ofs"]
+ for i in mdt.get_vertex_count():
+ mdt.set_vertex_normal(i, mesh_normals[offset + i])
+
+ st = SurfaceTool.new()
+ st.begin(Mesh.PRIMITIVE_TRIANGLES)
+ st.set_material(dict["mat"])
+
+ for i in mdt.get_face_count():
+ for j in 3:
+ var index := mdt.get_face_vertex(i, j)
+ st.set_normal(mdt.get_vertex_normal(index))
+ st.set_uv(mdt.get_vertex_uv(index))
+ st.set_tangent(mdt.get_vertex_tangent(index))
+ st.add_vertex(mdt.get_vertex(index))
+
+ smoothed_mesh = st.commit(smoothed_mesh)
+
+ return smoothed_mesh
+
+#endregion
diff --git a/demo/addons/func_godot/src/util/func_godot_util.gd.uid b/demo/addons/func_godot/src/util/func_godot_util.gd.uid
new file mode 100644
index 0000000..21f14f2
--- /dev/null
+++ b/demo/addons/func_godot/src/util/func_godot_util.gd.uid
@@ -0,0 +1 @@
+uid://bursmx2g1betd
diff --git a/demo/addons/func_godot/textures/clip.png b/demo/addons/func_godot/textures/clip.png
new file mode 100644
index 0000000..50f8011
Binary files /dev/null and b/demo/addons/func_godot/textures/clip.png differ
diff --git a/demo/addons/func_godot/textures/default_material.tres b/demo/addons/func_godot/textures/default_material.tres
new file mode 100644
index 0000000..30c0e59
--- /dev/null
+++ b/demo/addons/func_godot/textures/default_material.tres
@@ -0,0 +1,8 @@
+[gd_resource type="StandardMaterial3D" load_steps=2 format=3 uid="uid://cvex6toty8yn7"]
+
+[ext_resource type="Texture2D" uid="uid://cyg2snr1w5xw5" path="res://addons/func_godot/textures/default_texture.png" id="1_ncj77"]
+
+[resource]
+albedo_texture = ExtResource("1_ncj77")
+metallic_specular = 0.0
+texture_filter = 2
diff --git a/demo/addons/func_godot/textures/default_texture.png b/demo/addons/func_godot/textures/default_texture.png
new file mode 100644
index 0000000..359168e
Binary files /dev/null and b/demo/addons/func_godot/textures/default_texture.png differ
diff --git a/demo/addons/func_godot/textures/origin.png b/demo/addons/func_godot/textures/origin.png
new file mode 100644
index 0000000..4d1b155
Binary files /dev/null and b/demo/addons/func_godot/textures/origin.png differ
diff --git a/demo/addons/func_godot/textures/skip.png b/demo/addons/func_godot/textures/skip.png
new file mode 100644
index 0000000..65a58a5
Binary files /dev/null and b/demo/addons/func_godot/textures/skip.png differ
diff --git a/demo/data/config/brogue_map_settings.tres b/demo/data/config/brogue_map_settings.tres
new file mode 100644
index 0000000..bf1fdc5
--- /dev/null
+++ b/demo/data/config/brogue_map_settings.tres
@@ -0,0 +1,11 @@
+[gd_resource type="Resource" script_class="FuncGodotMapSettings" format=3 uid="uid://by3eva1ib4gyl"]
+
+[ext_resource type="Script" uid="uid://38q6k0ctahjn" path="res://addons/func_godot/src/map/func_godot_map_settings.gd" id="1_script"]
+[ext_resource type="Material" uid="uid://cvex6toty8yn7" path="res://addons/func_godot/textures/default_material.tres" id="2_material"]
+[ext_resource type="Resource" path="res://data/fgd/brogue_fgd.tres" id="3_fgd"]
+[ext_resource type="Script" uid="uid://cij36hpqc46c" path="res://addons/func_godot/src/import/quake_wad_file.gd" id="4_ajbga"]
+
+[resource]
+script = ExtResource("1_script")
+entity_fgd = ExtResource("3_fgd")
+base_texture_dir = "res://textures/"
diff --git a/demo/data/entities/func_lava/func_lava.tres b/demo/data/entities/func_lava/func_lava.tres
new file mode 100644
index 0000000..dae4e65
--- /dev/null
+++ b/demo/data/entities/func_lava/func_lava.tres
@@ -0,0 +1,14 @@
+[gd_resource type="Resource" script_class="FuncGodotFGDSolidClass" load_steps=2 format=3]
+
+[ext_resource type="Script" uid="uid://5cow84q03m6a" path="res://addons/func_godot/src/fgd/func_godot_fgd_solid_class.gd" id="1_script"]
+
+[resource]
+script = ExtResource("1_script")
+collision_shape_type = 2
+collision_mask = 0
+classname = "func_lava"
+description = "Lava volume. Party takes damage on entry and lasting burn debuff. Swap for a custom Area3D script to drive behavior."
+meta_properties = Dictionary[String, Variant]({
+"color": Color(0.95, 0.4, 0.1, 1)
+})
+node_class = "Area3D"
diff --git a/demo/data/entities/func_lava/func_lava.tscn b/demo/data/entities/func_lava/func_lava.tscn
new file mode 100644
index 0000000..b3d6790
--- /dev/null
+++ b/demo/data/entities/func_lava/func_lava.tscn
@@ -0,0 +1,3 @@
+[gd_scene format=3]
+
+[node name="func_lava" type="Area3D"]
diff --git a/demo/data/entities/func_water/func_water.tres b/demo/data/entities/func_water/func_water.tres
new file mode 100644
index 0000000..bf9f234
--- /dev/null
+++ b/demo/data/entities/func_water/func_water.tres
@@ -0,0 +1,14 @@
+[gd_resource type="Resource" script_class="FuncGodotFGDSolidClass" load_steps=2 format=3]
+
+[ext_resource type="Script" uid="uid://5cow84q03m6a" path="res://addons/func_godot/src/fgd/func_godot_fgd_solid_class.gd" id="1_script"]
+
+[resource]
+script = ExtResource("1_script")
+collision_shape_type = 2
+collision_mask = 0
+classname = "func_water"
+description = "Water volume. Party takes no damage but movement is slowed and fire is extinguished. Swap for a custom Area3D script to drive behavior."
+meta_properties = Dictionary[String, Variant]({
+"color": Color(0.15, 0.4, 0.75, 1)
+})
+node_class = "Area3D"
diff --git a/demo/data/entities/func_water/func_water.tscn b/demo/data/entities/func_water/func_water.tscn
new file mode 100644
index 0000000..98c6962
--- /dev/null
+++ b/demo/data/entities/func_water/func_water.tscn
@@ -0,0 +1,3 @@
+[gd_scene format=3]
+
+[node name="func_water" type="Area3D"]
diff --git a/demo/data/entities/point_door/point_door.tres b/demo/data/entities/point_door/point_door.tres
new file mode 100644
index 0000000..2ec035b
--- /dev/null
+++ b/demo/data/entities/point_door/point_door.tres
@@ -0,0 +1,15 @@
+[gd_resource type="Resource" script_class="FuncGodotFGDPointClass" load_steps=3 format=3]
+
+[ext_resource type="Script" uid="uid://cxsqwtsqd8w33" path="res://addons/func_godot/src/fgd/func_godot_fgd_point_class.gd" id="1_script"]
+[ext_resource type="PackedScene" path="res://data/entities/point_door/point_door.tscn" id="2_scene"]
+
+[resource]
+script = ExtResource("1_script")
+scene_file = ExtResource("2_scene")
+apply_scale_on_map_build = false
+classname = "point_door"
+description = "Door marker placed on a door cell. 'angle' controls orientation. Behavior (open/close, locked) stubbed."
+meta_properties = Dictionary[String, Variant]({
+"color": Color(0.62, 0.38, 0.18, 1),
+"size": AABB(-8, -16, -8, 16, 48, 16)
+})
diff --git a/demo/data/entities/point_door/point_door.tscn b/demo/data/entities/point_door/point_door.tscn
new file mode 100644
index 0000000..218fc5a
--- /dev/null
+++ b/demo/data/entities/point_door/point_door.tscn
@@ -0,0 +1,3 @@
+[gd_scene format=3]
+
+[node name="point_door" type="Marker3D"]
diff --git a/demo/data/entities/point_player_start/point_player_start.tres b/demo/data/entities/point_player_start/point_player_start.tres
new file mode 100644
index 0000000..42d6214
--- /dev/null
+++ b/demo/data/entities/point_player_start/point_player_start.tres
@@ -0,0 +1,15 @@
+[gd_resource type="Resource" script_class="FuncGodotFGDPointClass" load_steps=3 format=3]
+
+[ext_resource type="Script" uid="uid://cxsqwtsqd8w33" path="res://addons/func_godot/src/fgd/func_godot_fgd_point_class.gd" id="1_script"]
+[ext_resource type="PackedScene" path="res://data/entities/point_player_start/point_player_start.tscn" id="2_scene"]
+
+[resource]
+script = ExtResource("1_script")
+scene_file = ExtResource("2_scene")
+apply_scale_on_map_build = false
+classname = "point_player_start"
+description = "Dungeon party spawn at game start. Emitted once per exported .map at level 0's first stair-up (or first walkable cell if none)."
+meta_properties = Dictionary[String, Variant]({
+"color": Color(0.2, 0.6, 1, 1),
+"size": AABB(-16, -16, -16, 32, 32, 32)
+})
diff --git a/demo/data/entities/point_player_start/point_player_start.tscn b/demo/data/entities/point_player_start/point_player_start.tscn
new file mode 100644
index 0000000..558895f
--- /dev/null
+++ b/demo/data/entities/point_player_start/point_player_start.tscn
@@ -0,0 +1,3 @@
+[gd_scene format=3]
+
+[node name="point_player_start" type="Marker3D"]
diff --git a/demo/data/entities/point_stair_down/point_stair_down.tres b/demo/data/entities/point_stair_down/point_stair_down.tres
new file mode 100644
index 0000000..bc99ca9
--- /dev/null
+++ b/demo/data/entities/point_stair_down/point_stair_down.tres
@@ -0,0 +1,15 @@
+[gd_resource type="Resource" script_class="FuncGodotFGDPointClass" load_steps=3 format=3]
+
+[ext_resource type="Script" uid="uid://cxsqwtsqd8w33" path="res://addons/func_godot/src/fgd/func_godot_fgd_point_class.gd" id="1_script"]
+[ext_resource type="PackedScene" path="res://data/entities/point_stair_down/point_stair_down.tscn" id="2_scene"]
+
+[resource]
+script = ExtResource("1_script")
+scene_file = ExtResource("2_scene")
+apply_scale_on_map_build = false
+classname = "point_stair_down"
+description = "Interact marker placed on a cell that should descend to the next level. Behavior (pickup, transition) stubbed."
+meta_properties = Dictionary[String, Variant]({
+"color": Color(0.85, 0.25, 0.2, 1),
+"size": AABB(-16, -16, -16, 32, 32, 32)
+})
diff --git a/demo/data/entities/point_stair_down/point_stair_down.tscn b/demo/data/entities/point_stair_down/point_stair_down.tscn
new file mode 100644
index 0000000..39fa70d
--- /dev/null
+++ b/demo/data/entities/point_stair_down/point_stair_down.tscn
@@ -0,0 +1,3 @@
+[gd_scene format=3]
+
+[node name="point_stair_down" type="Marker3D"]
diff --git a/demo/data/entities/point_stair_up/point_stair_up.tres b/demo/data/entities/point_stair_up/point_stair_up.tres
new file mode 100644
index 0000000..97c939d
--- /dev/null
+++ b/demo/data/entities/point_stair_up/point_stair_up.tres
@@ -0,0 +1,15 @@
+[gd_resource type="Resource" script_class="FuncGodotFGDPointClass" load_steps=3 format=3]
+
+[ext_resource type="Script" uid="uid://cxsqwtsqd8w33" path="res://addons/func_godot/src/fgd/func_godot_fgd_point_class.gd" id="1_script"]
+[ext_resource type="PackedScene" path="res://data/entities/point_stair_up/point_stair_up.tscn" id="2_scene"]
+
+[resource]
+script = ExtResource("1_script")
+scene_file = ExtResource("2_scene")
+apply_scale_on_map_build = false
+classname = "point_stair_up"
+description = "Interact marker placed on a cell that should climb to the previous level. Behavior (pickup, transition) stubbed."
+meta_properties = Dictionary[String, Variant]({
+"color": Color(0.2, 0.8, 0.3, 1),
+"size": AABB(-16, -16, -16, 32, 32, 32)
+})
diff --git a/demo/data/entities/point_stair_up/point_stair_up.tscn b/demo/data/entities/point_stair_up/point_stair_up.tscn
new file mode 100644
index 0000000..a291d5e
--- /dev/null
+++ b/demo/data/entities/point_stair_up/point_stair_up.tscn
@@ -0,0 +1,3 @@
+[gd_scene format=3]
+
+[node name="point_stair_up" type="Marker3D"]
diff --git a/demo/data/fgd/brogue_fgd.tres b/demo/data/fgd/brogue_fgd.tres
new file mode 100644
index 0000000..5baf7fc
--- /dev/null
+++ b/demo/data/fgd/brogue_fgd.tres
@@ -0,0 +1,15 @@
+[gd_resource type="Resource" script_class="FuncGodotFGDFile" load_steps=9 format=3]
+
+[ext_resource type="Script" uid="uid://drlmgulwbjwqu" path="res://addons/func_godot/src/fgd/func_godot_fgd_file.gd" id="1_script"]
+[ext_resource type="Resource" uid="uid://crgpdahjaj" path="res://addons/func_godot/fgd/func_godot_fgd.tres" id="2_base"]
+[ext_resource type="Resource" path="res://data/entities/point_player_start/point_player_start.tres" id="3_player_start"]
+[ext_resource type="Resource" path="res://data/entities/point_stair_up/point_stair_up.tres" id="4_stair_up"]
+[ext_resource type="Resource" path="res://data/entities/point_stair_down/point_stair_down.tres" id="5_stair_down"]
+[ext_resource type="Resource" path="res://data/entities/point_door/point_door.tres" id="6_door"]
+[ext_resource type="Resource" path="res://data/entities/func_water/func_water.tres" id="7_water"]
+[ext_resource type="Resource" path="res://data/entities/func_lava/func_lava.tres" id="8_lava"]
+
+[resource]
+script = ExtResource("1_script")
+base_fgd_files = Array[Resource]([ExtResource("2_base")])
+entity_definitions = Array[Resource]([ExtResource("3_player_start"), ExtResource("4_stair_up"), ExtResource("5_stair_down"), ExtResource("6_door"), ExtResource("7_water"), ExtResource("8_lava")])
diff --git a/demo/docs/trenchbroom.md b/demo/docs/trenchbroom.md
new file mode 100644
index 0000000..bd6b14f
--- /dev/null
+++ b/demo/docs/trenchbroom.md
@@ -0,0 +1,117 @@
+# TrenchBroom round-trip
+
+Generate a dungeon procedurally → edit it by hand in TrenchBroom → load the
+edited map back into Godot with typed entities wired up via FuncGodot.
+
+```
+ BrogueGen / Blobber
+ │
+ ▼
+ export_map.gd ──► demo/maps/generated.map ◄──► TrenchBroom
+ │
+ ▼
+ demo/scenes/dungeon_from_map.tscn
+ │
+ ▼
+ FuncGodotMap
+ (instantiates entity scenes)
+```
+
+## Prerequisites
+
+- **Godot 4.6+** on PATH as `godot`.
+- **FuncGodot addon** installed under [demo/addons/func_godot/](../addons/func_godot/) and enabled in Project Settings → Plugins (already wired up in [demo/project.godot](../project.godot)).
+- **Brogue-Genesis GDExtension** built: `make godot` from the repo root. This produces [demo/addons/brogue_gen/bin/libbrogue_gen.linux.template_debug.x86_64.so](../addons/brogue_gen/bin/) and exposes the `BrogueGen` class to GDScript.
+- **TrenchBroom** installed (`flatpak install com.kristianduske.TrenchBroom` or distro package).
+
+## One-time setup
+
+Install the TrenchBroom game config + FGD:
+
+```
+make tb-sync
+```
+
+This writes `GameConfig.cfg` and `FuncGodot.fgd` into `~/.TrenchBroom/games/brogue-genesis/`. In TrenchBroom, open Preferences → Games → `brogue-genesis` → set the "Game Path" to this repo's `demo/` directory (anywhere works, but `demo/` lets TB find `maps/` relative paths).
+
+Re-run `make tb-sync` any time you:
+
+- Add or remove an entity under [data/entities/](../data/entities/).
+- Edit [data/fgd/brogue_fgd.tres](../data/fgd/brogue_fgd.tres).
+- Change the GameConfig template in [scripts/export_tb_config.gd](../scripts/export_tb_config.gd).
+
+## Day-to-day workflow
+
+### 1. Generate a map
+
+```
+make map # defaults: brogue, seed=42, depth=20, 1 level
+make map SEED=77 DEPTH=30 LEVELS=4 # override
+make map GENERATOR=blobber LEVELS=3 # use the 3D blobber path
+```
+
+Output lands at `demo/maps/generated.map` (`MAP_OUT=` overrides the path).
+
+### 2. Edit in TrenchBroom
+
+Launch TrenchBroom → New Map → pick the `brogue-genesis` game → File → Open `demo/maps/generated.map`. Move brushes, place entities, save back to the same path.
+
+Entities visible in TB's entity browser:
+
+| Classname | Type | Purpose |
+|----------------------|----------|------------------------------------------------------------|
+| `point_player_start` | point | Party spawn. Emitted once per map at level-0 stairs-up. |
+| `point_stair_up` | point | Interact marker to climb a level. |
+| `point_stair_down` | point | Interact marker to descend a level. |
+| `point_door` | point | Door marker (angle property controls orientation). |
+| `func_water` | solid | Water volume — slow movement, extinguish fire. |
+| `func_lava` | solid | Lava volume — damage on entry + burn debuff. |
+| `worldspawn` | solid | Default static geometry (floors, walls, ceilings). |
+
+### 3. Rebuild in Godot
+
+Open [demo/scenes/dungeon_from_map.tscn](../scenes/dungeon_from_map.tscn) → select the `FuncGodotMap` node → click **Build Map** in the Inspector. Brushes become `MeshInstance3D`s, entity brushes become `Area3D`s, point entities instantiate their scene files from `data/entities/{name}/`.
+
+Save the scene to persist the built nodes.
+
+## Makefile targets
+
+| Target | What it does | Variables |
+|------------|--------------------------------------------------------------------|--------------------------------------------|
+| `make map` | Run `export_map.gd` → writes `.map` file | `GENERATOR, SEED, DEPTH, LEVELS, MAP_OUT` |
+| `make tb-sync` | Run `export_tb_config.gd` → writes GameConfig.cfg + FuncGodot.fgd | `TB_GAME_DIR` (default: `~/.TrenchBroom/games/brogue-genesis`) |
+| `make godot` | Build the GDExtension | — |
+
+## Raw CLI (if Make isn't available)
+
+```
+# Generate a .map
+godot --headless --path demo --script scripts/export_map.gd -- \
+ --generator brogue --seed 42 --depth 20 --levels 2 \
+ --out /abs/path/demo/maps/generated.map
+
+# Sync TrenchBroom config
+godot --headless --path demo --script scripts/export_tb_config.gd -- \
+ /home/$USER/.TrenchBroom/games/brogue-genesis
+```
+
+Legacy positional form for `export_map.gd` (still supported, brogue only): `SEED DEPTH [LEVELS] OUT.map`.
+
+## Adding a new entity type
+
+1. Create `demo/data/entities//.tres` (a `FuncGodotFGDPointClass` or `FuncGodotFGDSolidClass`) and a `.tscn` scene for it to instantiate.
+2. Add the `.tres` to the `entity_definitions` array in [data/fgd/brogue_fgd.tres](../data/fgd/brogue_fgd.tres).
+3. If the exporter needs to emit it, teach [scripts/export_map.gd](../scripts/export_map.gd) to recognize whatever terrain/liquid tag drives it — see the `_emit_point_entities` / `_emit_liquid_entities` helpers.
+4. `make tb-sync` so TrenchBroom picks up the new class.
+
+## Troubleshooting
+
+**TrenchBroom says "Unknown game: brogue-genesis"** — Preferences → Games → make sure `brogue-genesis` appears. If not, `~/.TrenchBroom/games/brogue-genesis/GameConfig.cfg` is missing. Run `make tb-sync`.
+
+**FuncGodot "Build Map" does nothing / errors about missing class** — `brogue_fgd.tres` references a `.tres` that doesn't exist, or the addon isn't enabled. Check Project Settings → Plugins.
+
+**`BrogueGen.new()` crashes with "class not found" in headless** — The GDExtension wasn't built, or only the `template_debug` variant is present and Godot is launched against `template_release`. Run `make godot`. For editor-time tool scripts, the extension needs a `linux.editor.x86_64` entry in [addons/brogue_gen/brogue_gen.gdextension](../addons/brogue_gen/brogue_gen.gdextension) (already wired).
+
+**Player spawn ends up inside a wall** — Exporter picks level-0's first `T_STAIRS_UP` cell, else first walkable. If neither yields a good spawn, move the `point_player_start` entity by hand in TrenchBroom; next rebuild in Godot preserves it.
+
+**Lots of `point_door` entities from the blobber generator** — Blobber treats every `FT_DOOR_SILL` cell as a door, so multi-cell doorways emit multiple entities. A future pass can merge adjacent door cells into one entity with an `extent` property.
diff --git a/demo/project.godot b/demo/project.godot
index 27cc2a3..d49d055 100644
--- a/demo/project.godot
+++ b/demo/project.godot
@@ -15,8 +15,13 @@ compatibility/default_parent_skeleton_in_mesh_instance_3d=true
[application]
config/name="Brogue Genesis Demo"
+run/main_scene="uid://c8blbrbrbr001"
config/features=PackedStringArray("4.6", "GL Compatibility")
+[editor_plugins]
+
+enabled=PackedStringArray("res://addons/func_godot/plugin.cfg")
+
[rendering]
renderer/rendering_method="gl_compatibility"
diff --git a/demo/scenes/arcade.tscn b/demo/scenes/arcade.tscn
deleted file mode 100644
index 0f93a0d..0000000
--- a/demo/scenes/arcade.tscn
+++ /dev/null
@@ -1,127 +0,0 @@
-[gd_scene format=3 uid="uid://jysbib77g851"]
-
-[ext_resource type="Script" path="res://scripts/arcade/arcade_scene.gd" id="1_scene"]
-[ext_resource type="Script" path="res://scripts/arcade/player_arcade.gd" id="2_player"]
-[ext_resource type="Script" path="res://scripts/arcade/hud.gd" id="3_hud"]
-
-[sub_resource type="Environment" id="Env1"]
-background_mode = 1
-background_color = Color(0.03, 0.03, 0.05, 1)
-ambient_light_source = 2
-ambient_light_color = Color(0.55, 0.55, 0.65, 1)
-ambient_light_energy = 0.45
-fog_enabled = false
-
-[sub_resource type="CapsuleShape3D" id="PlayerCapsule"]
-radius = 0.3
-height = 1.7
-
-[node name="Arcade" type="Node3D"]
-script = ExtResource("1_scene")
-base_seed = 25
-start_depth = 1
-total_depths = 10
-
-[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
-environment = SubResource("Env1")
-
-[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
-transform = Transform3D(0.707107, -0.5, 0.5, 0, 0.707107, 0.707107, -0.707107, -0.5, 0.5, 0, 10, 0)
-light_color = Color(1, 0.95, 0.85, 1)
-light_energy = 0.9
-shadow_enabled = true
-
-[node name="World" type="Node3D" parent="."]
-
-[node name="Player" type="CharacterBody3D" parent="."]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 39, 1.1, 14)
-script = ExtResource("2_player")
-
-[node name="CollisionShape3D" type="CollisionShape3D" parent="Player"]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.85, 0)
-shape = SubResource("PlayerCapsule")
-
-[node name="Camera3D" type="Camera3D" parent="Player"]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0)
-current = true
-near = 0.05
-far = 500.0
-
-[node name="HUD" type="CanvasLayer" parent="."]
-script = ExtResource("3_hud")
-
-[node name="HPBar" type="Control" parent="HUD"]
-offset_left = 16.0
-offset_top = 16.0
-offset_right = 240.0
-offset_bottom = 56.0
-
-[node name="BG" type="ColorRect" parent="HUD/HPBar"]
-offset_right = 200.0
-offset_bottom = 20.0
-color = Color(0.1, 0.1, 0.12, 0.75)
-
-[node name="Fill" type="ColorRect" parent="HUD/HPBar"]
-offset_right = 200.0
-offset_bottom = 20.0
-color = Color(0.32, 0.82, 0.36, 1)
-
-[node name="Label" type="Label" parent="HUD/HPBar"]
-offset_top = 22.0
-offset_right = 240.0
-offset_bottom = 40.0
-theme_override_colors/font_color = Color(0.95, 0.95, 0.95, 1)
-theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
-theme_override_constants/outline_size = 3
-text = "HP 20 / 20"
-
-[node name="InfoLabel" type="Label" parent="HUD"]
-offset_left = 16.0
-offset_top = 64.0
-offset_right = 500.0
-offset_bottom = 88.0
-theme_override_colors/font_color = Color(0.95, 0.95, 0.95, 1)
-theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
-theme_override_constants/outline_size = 3
-text = "Depth 1 / 10"
-
-[node name="EndOverlay" type="Control" parent="HUD"]
-anchor_right = 1.0
-anchor_bottom = 1.0
-mouse_filter = 2
-
-[node name="Dim" type="ColorRect" parent="HUD/EndOverlay"]
-anchor_right = 1.0
-anchor_bottom = 1.0
-color = Color(0, 0, 0, 0.55)
-
-[node name="Title" type="Label" parent="HUD/EndOverlay"]
-anchor_left = 0.5
-anchor_top = 0.4
-anchor_right = 0.5
-anchor_bottom = 0.4
-offset_left = -200.0
-offset_top = -40.0
-offset_right = 200.0
-offset_bottom = 20.0
-theme_override_colors/font_color = Color(0.92, 0.28, 0.28, 1)
-theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
-theme_override_constants/outline_size = 4
-theme_override_font_sizes/font_size = 56
-text = "YOU DIED"
-horizontal_alignment = 1
-
-[node name="Hint" type="Label" parent="HUD/EndOverlay"]
-anchor_left = 0.5
-anchor_top = 0.55
-anchor_right = 0.5
-anchor_bottom = 0.55
-offset_left = -160.0
-offset_top = 0.0
-offset_right = 160.0
-offset_bottom = 30.0
-theme_override_colors/font_color = Color(0.9, 0.9, 0.9, 1)
-theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
-theme_override_constants/outline_size = 3
-text = "Press R to restart"
-horizontal_alignment = 1
diff --git a/demo/scenes/demo_3d.tscn b/demo/scenes/demo_3d.tscn
deleted file mode 100644
index 48a0905..0000000
--- a/demo/scenes/demo_3d.tscn
+++ /dev/null
@@ -1,32 +0,0 @@
-[gd_scene format=3 uid="uid://jysbic1s50to"]
-
-[ext_resource type="Script" uid="uid://brsi02a7ei24j" path="res://scripts/demo_3d.gd" id="1_demo3d"]
-[ext_resource type="Script" uid="uid://d0srrm35g1m0t" path="res://scripts/fly_camera.gd" id="2_flycam"]
-
-[sub_resource type="Environment" id="Env1"]
-background_mode = 1
-background_color = Color(0.04, 0.04, 0.05, 1)
-ambient_light_source = 2
-ambient_light_color = Color(0.6, 0.6, 0.7, 1)
-ambient_light_energy = 0.4
-
-[node name="Demo3D" type="Node3D" unique_id=36819859]
-script = ExtResource("1_demo3d")
-
-[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=1412341693]
-environment = SubResource("Env1")
-
-[node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=236128294]
-transform = Transform3D(0.707107, -0.5, 0.5, 0, 0.707107, 0.707107, -0.707107, -0.5, 0.5, 0, 10, 0)
-light_color = Color(1, 0.95, 0.85, 1)
-light_energy = 0.9
-shadow_enabled = true
-
-[node name="FlyCamera" type="Camera3D" parent="." unique_id=658486835]
-transform = Transform3D(1, 0, 0, 0, 0.819152, 0.573576, 0, -0.573576, 0.819152, 39, 14, 32)
-current = true
-near = 0.1
-far = 500.0
-script = ExtResource("2_flycam")
-
-[node name="Levels" type="Node3D" parent="." unique_id=1476525758]
diff --git a/demo/scenes/demo_blobber.tscn b/demo/scenes/demo_blobber.tscn
new file mode 100644
index 0000000..4394820
--- /dev/null
+++ b/demo/scenes/demo_blobber.tscn
@@ -0,0 +1,35 @@
+[gd_scene format=3 uid="uid://c8blbrbrbr001"]
+
+[ext_resource type="Script" uid="uid://b2r6hnyvt7ef7" path="res://scripts/dungeon_builder.gd" id="1_builder"]
+[ext_resource type="Script" path="res://scripts/blobber_party.gd" id="2_party"]
+
+[sub_resource type="Environment" id="Environment_1"]
+background_mode = 1
+background_color = Color(0.08, 0.08, 0.1, 1)
+ambient_light_source = 2
+ambient_light_color = Color(0.55, 0.55, 0.6, 1)
+ambient_light_energy = 0.5
+
+[node name="DemoBlobber" type="Node3D" unique_id=201275322]
+
+[node name="Dungeon" type="Node3D" parent="." unique_id=2089249369]
+script = ExtResource("1_builder")
+seed_value = 43
+num_levels = 3
+depth = 20
+party_path = NodePath("../Party")
+
+[node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=1913401313]
+transform = Transform3D(0.86602527, -0.35310844, 0.35399818, 0, 0.70799595, 0.70621645, -0.5000003, -0.6116013, 0.6131424, 80, 40, 80)
+shadow_enabled = true
+
+[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=1337946831]
+environment = SubResource("Environment_1")
+
+[node name="Party" type="Node3D" parent="." unique_id=987300286]
+script = ExtResource("2_party")
+
+[node name="Camera3D" type="Camera3D" parent="Party"]
+fov = 75.0
+near = 0.1
+far = 500.0
diff --git a/demo/scenes/demo_fps.tscn b/demo/scenes/demo_fps.tscn
deleted file mode 100644
index ae58985..0000000
--- a/demo/scenes/demo_fps.tscn
+++ /dev/null
@@ -1,58 +0,0 @@
-[gd_scene format=3 uid="uid://c0brogue3dfps"]
-
-[ext_resource type="Script" path="res://scripts/demo_fps.gd" id="1_demofps"]
-[ext_resource type="Script" path="res://scripts/player.gd" id="2_player"]
-[ext_resource type="Script" path="res://scripts/fps_overlay.gd" id="3_fpsov"]
-
-[sub_resource type="Environment" id="Env1"]
-background_mode = 1
-background_color = Color(0.04, 0.04, 0.05, 1)
-ambient_light_source = 2
-ambient_light_color = Color(0.6, 0.6, 0.7, 1)
-ambient_light_energy = 0.4
-fog_enabled = false
-
-[sub_resource type="CapsuleShape3D" id="Capsule1"]
-radius = 0.3
-height = 1.7
-
-[node name="DemoFPS" type="Node3D"]
-script = ExtResource("1_demofps")
-
-[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
-environment = SubResource("Env1")
-
-[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
-transform = Transform3D(0.707107, -0.5, 0.5, 0, 0.707107, 0.707107, -0.707107, -0.5, 0.5, 0, 10, 0)
-light_color = Color(1, 0.95, 0.85, 1)
-light_energy = 0.9
-shadow_enabled = true
-
-[node name="Levels" type="Node3D" parent="."]
-
-[node name="Player" type="CharacterBody3D" parent="."]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 39, 1.1, 14)
-script = ExtResource("2_player")
-
-[node name="CollisionShape3D" type="CollisionShape3D" parent="Player"]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.85, 0)
-shape = SubResource("Capsule1")
-
-[node name="Camera3D" type="Camera3D" parent="Player"]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0)
-current = true
-near = 0.05
-far = 500.0
-
-[node name="FPSOverlay" type="CanvasLayer" parent="."]
-script = ExtResource("3_fpsov")
-
-[node name="Label" type="Label" parent="FPSOverlay"]
-offset_left = 12.0
-offset_top = 12.0
-offset_right = 800.0
-offset_bottom = 80.0
-theme_override_colors/font_color = Color(0.95, 0.95, 0.95, 1)
-theme_override_colors/font_outline_color = Color(0, 0, 0, 1)
-theme_override_constants/outline_size = 3
-text = "FPS: --"
diff --git a/demo/scenes/demo_large.tscn b/demo/scenes/demo_large.tscn
index 5c1276d..66153b5 100644
--- a/demo/scenes/demo_large.tscn
+++ b/demo/scenes/demo_large.tscn
@@ -1,8 +1,152 @@
-[gd_scene format=3 uid="uid://c0broguelarge"]
+[gd_scene format=3 uid="uid://c2gdxshrhuv3t"]
-[ext_resource type="Script" path="res://scripts/demo_fps.gd" id="1_demofps"]
-[ext_resource type="Script" path="res://scripts/player.gd" id="2_player"]
-[ext_resource type="Script" path="res://scripts/fps_overlay.gd" id="3_fpsov"]
+[ext_resource type="Script" uid="uid://bj1fb7syiqys7" path="res://scripts/player.gd" id="2_player"]
+
+[sub_resource type="GDScript" id="GDScript_aw1u5"]
+script/source = "extends Node3D
+
+# First-person demo. Generates a grid of chunks (each a 79×29 dungeon) times
+# level_count stacked layers. Default is 1×1 chunks × 3 levels. Bump
+# chunks_x / chunks_y to stress-test the renderer with larger worlds.
+
+const T_NOTHING := 0
+const T_FLOOR := 1
+const T_WALL := 2
+const T_DOOR := 3
+const T_CORRIDOR := 4
+const T_LIQUID := 5
+const T_BRIDGE := 6
+const T_STAIRS_UP := 7
+const T_STAIRS_DOWN := 8
+
+const L_WATER := 1
+const L_LAVA := 2
+const L_CHASM := 3
+const L_BRIMSTONE := 4
+
+const TILE_FLOOR := 0
+const TILE_WALL := 1
+const TILE_DOOR := 2
+const TILE_CORRIDOR := 3
+const TILE_WATER := 4
+const TILE_LAVA := 5
+const TILE_BRIMSTONE := 6
+const TILE_BRIDGE := 7
+const TILE_STAIRS_UP := 8
+const TILE_STAIRS_DOWN := 9
+
+const CHUNK_W := 79
+const CHUNK_H := 29
+
+@export var base_seed: int = 2028
+@export var depth_start: int = 20
+@export var level_count: int = 3
+@export var level_spacing: float = 6.0
+@export var chunks_x: int = 1
+@export var chunks_y: int = 1
+@export var player_spawn_height: float = 1.1
+
+@onready var levels_root: Node3D = $Levels
+@onready var player: CharacterBody3D = $Player
+@onready var fps_overlay: Node = $FPSOverlay if has_node(\"FPSOverlay\") else null
+
+var _mesh_library: MeshLibrary
+
+func _ready() -> void:
+ var t0 := Time.get_ticks_msec()
+ _mesh_library = MeshLibraryBuilder.build(true)
+
+ var total_cells := 0
+ var total_chunks := 0
+ var spawn_world := Vector3.ZERO
+ var spawn_found := false
+
+ for level_index in range(level_count):
+ for cy in range(chunks_y):
+ for cx in range(chunks_x):
+ var seed := base_seed \\
+ + level_index * 1000 \\
+ + cy * chunks_x + cx
+ var depth := depth_start + level_index
+ var gen := BrogueGen.new()
+ var grid: Dictionary = gen.generate(seed, depth)
+ gen.free()
+
+ var grid_map := GridMap.new()
+ grid_map.name = \"L%d_C%d_%d\" % [level_index, cx, cy]
+ grid_map.mesh_library = _mesh_library
+ grid_map.cell_size = Vector3(1, 1, 1)
+ grid_map.position = Vector3(
+ cx * CHUNK_W,
+ -level_index * level_spacing,
+ cy * CHUNK_H
+ )
+ levels_root.add_child(grid_map)
+
+ var cells := _populate_level(grid_map, grid)
+ total_cells += cells
+ total_chunks += 1
+
+ # First up-stair on level 0, chunk (0,0) is the spawn point.
+ if not spawn_found and level_index == 0 and cx == 0 and cy == 0:
+ var up: Vector2i = grid[\"stairs_up\"] as Vector2i
+ if up.x >= 0:
+ spawn_world = Vector3(up.x, player_spawn_height, up.y)
+ spawn_found = true
+
+ if not spawn_found:
+ spawn_world = Vector3(CHUNK_W * 0.5, player_spawn_height, CHUNK_H * 0.5)
+ player.position = spawn_world
+
+ var build_ms := Time.get_ticks_msec() - t0
+ var info := \"%d chunks (%dx%d x %d lvls) %d placed cells build=%d ms\" % [
+ total_chunks, chunks_x, chunks_y, level_count, total_cells, build_ms,
+ ]
+ print(info)
+ print(\"Player spawned at %s\" % spawn_world)
+ if fps_overlay and fps_overlay.has_method(\"set_subtitle\"):
+ fps_overlay.set_subtitle(info)
+
+# Place every non-chasm cell into the GridMap. Returns the number of cells
+# actually placed (excludes T_NOTHING and chasms).
+func _populate_level(grid_map: GridMap, grid: Dictionary) -> int:
+ var w: int = grid[\"width\"]
+ var h: int = grid[\"height\"]
+ var terrain: PackedByteArray = grid[\"terrain\"]
+ var liquid: PackedByteArray = grid[\"liquid\"]
+
+ var placed := 0
+ for y in range(h):
+ for x in range(w):
+ var idx := y * w + x
+ var t: int = terrain[idx]
+ var liq: int = liquid[idx]
+ if t == T_LIQUID and liq == L_CHASM:
+ continue
+ var tile_id := _tile_for(t, liq)
+ if tile_id == -1:
+ continue
+ grid_map.set_cell_item(Vector3i(x, 0, y), tile_id)
+ placed += 1
+ return placed
+
+func _tile_for(terrain: int, liquid: int) -> int:
+ match terrain:
+ T_FLOOR: return TILE_FLOOR
+ T_CORRIDOR: return TILE_CORRIDOR
+ T_DOOR: return TILE_DOOR
+ T_WALL: return TILE_WALL
+ T_BRIDGE: return TILE_BRIDGE
+ T_STAIRS_UP: return TILE_STAIRS_UP
+ T_STAIRS_DOWN: return TILE_STAIRS_DOWN
+ T_LIQUID:
+ match liquid:
+ L_WATER: return TILE_WATER
+ L_LAVA: return TILE_LAVA
+ L_BRIMSTONE: return TILE_BRIMSTONE
+ _: return TILE_WATER
+ _: return -1
+"
[sub_resource type="Environment" id="Env1"]
background_mode = 1
@@ -10,51 +154,65 @@ background_color = Color(0.02, 0.02, 0.03, 1)
ambient_light_source = 2
ambient_light_color = Color(0.6, 0.6, 0.7, 1)
ambient_light_energy = 0.35
-fog_enabled = false
[sub_resource type="CapsuleShape3D" id="Capsule1"]
radius = 0.3
height = 1.7
-[node name="DemoLarge" type="Node3D"]
-script = ExtResource("1_demofps")
-base_seed = 2028
-depth_start = 20
-level_count = 3
-level_spacing = 6.0
+[sub_resource type="GDScript" id="GDScript_5guhk"]
+script/source = "extends CanvasLayer
+
+# Cheap FPS + stats overlay. Attach as a child of any scene.
+# The parent scene can call set_subtitle(text) to add a line below the FPS.
+
+@onready var label: Label = $Label
+var _subtitle := \"\"
+
+func set_subtitle(text: String) -> void:
+ _subtitle = text
+
+func _process(_delta: float) -> void:
+ var fps := Engine.get_frames_per_second()
+ var frame_ms := 0.0 if fps <= 0 else 1000.0 / fps
+ var txt := \"FPS: %d %.2f ms\" % [fps, frame_ms]
+ if _subtitle != \"\":
+ txt += \"\\n\" + _subtitle
+ label.text = txt
+"
+
+[node name="DemoLarge" type="Node3D" unique_id=1477096723]
+script = SubResource("GDScript_aw1u5")
chunks_x = 5
chunks_y = 5
-player_spawn_height = 1.1
-[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
+[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=1826007557]
environment = SubResource("Env1")
-[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
+[node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=332057448]
transform = Transform3D(0.707107, -0.5, 0.5, 0, 0.707107, 0.707107, -0.707107, -0.5, 0.5, 0, 10, 0)
light_color = Color(1, 0.95, 0.85, 1)
light_energy = 0.9
shadow_enabled = true
-[node name="Levels" type="Node3D" parent="."]
+[node name="Levels" type="Node3D" parent="." unique_id=933491194]
-[node name="Player" type="CharacterBody3D" parent="."]
+[node name="Player" type="CharacterBody3D" parent="." unique_id=1414808402]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 39, 1.1, 14)
script = ExtResource("2_player")
-[node name="CollisionShape3D" type="CollisionShape3D" parent="Player"]
+[node name="CollisionShape3D" type="CollisionShape3D" parent="Player" unique_id=587919818]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.85, 0)
shape = SubResource("Capsule1")
-[node name="Camera3D" type="Camera3D" parent="Player"]
+[node name="Camera3D" type="Camera3D" parent="Player" unique_id=680522156]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0)
current = true
-near = 0.05
far = 2000.0
-[node name="FPSOverlay" type="CanvasLayer" parent="."]
-script = ExtResource("3_fpsov")
+[node name="FPSOverlay" type="CanvasLayer" parent="." unique_id=315989227]
+script = SubResource("GDScript_5guhk")
-[node name="Label" type="Label" parent="FPSOverlay"]
+[node name="Label" type="Label" parent="FPSOverlay" unique_id=1568814951]
offset_left = 12.0
offset_top = 12.0
offset_right = 1000.0
diff --git a/demo/scenes/dungeon_from_map.tscn b/demo/scenes/dungeon_from_map.tscn
new file mode 100644
index 0000000..c476429
--- /dev/null
+++ b/demo/scenes/dungeon_from_map.tscn
@@ -0,0 +1,17 @@
+[gd_scene format=3 uid="uid://bq41mttbygl2a"]
+
+[ext_resource type="Script" uid="uid://cwu5cf7a0awcd" path="res://addons/func_godot/src/map/func_godot_map.gd" id="1_map_script"]
+[ext_resource type="Resource" path="res://data/config/brogue_map_settings.tres" id="2_map_settings"]
+
+[node name="DungeonFromMap" type="Node3D" unique_id=693515113]
+
+[node name="FuncGodotMap" type="Node3D" parent="." unique_id=858233376]
+script = ExtResource("1_map_script")
+local_map_file = "res://maps/generated.map"
+map_settings = ExtResource("2_map_settings")
+
+[node name="Camera3D" type="Camera3D" parent="." unique_id=125559999]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 50, 100)
+
+[node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=391021715]
+transform = Transform3D(1, 0, 0, 0, 0.707, 0.707, 0, -0.707, 0.707, 0, 10, 0)
diff --git a/demo/scenes/game_root.tscn b/demo/scenes/game_root.tscn
deleted file mode 100644
index 67ee524..0000000
--- a/demo/scenes/game_root.tscn
+++ /dev/null
@@ -1,281 +0,0 @@
-[gd_scene format=4 uid="uid://b20ymr64w7s60"]
-
-[ext_resource type="Script" uid="uid://ox0s7xjdj3lw" path="res://scenes/generator.gd" id="1_78s5d"]
-
-[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_78s5d"]
-albedo_color = Color(0.64, 0.54, 0.42, 1)
-roughness = 0.8
-
-[sub_resource type="BoxMesh" id="BoxMesh_ni5ms"]
-material = SubResource("StandardMaterial3D_78s5d")
-size = Vector3(1, 0.05, 1)
-
-[sub_resource type="BoxShape3D" id="BoxShape3D_uebra"]
-size = Vector3(1, 0.05, 1)
-
-[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_jb3nc"]
-albedo_color = Color(0.28, 0.28, 0.32, 1)
-roughness = 0.8
-
-[sub_resource type="BoxMesh" id="BoxMesh_iusj4"]
-material = SubResource("StandardMaterial3D_jb3nc")
-size = Vector3(1, 3, 1)
-
-[sub_resource type="BoxShape3D" id="BoxShape3D_a1rh6"]
-size = Vector3(1, 3, 1)
-
-[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_j7aru"]
-albedo_color = Color(0.72, 0.36, 0.2, 1)
-roughness = 0.8
-
-[sub_resource type="BoxMesh" id="BoxMesh_ylec3"]
-material = SubResource("StandardMaterial3D_j7aru")
-size = Vector3(1, 0.8, 1)
-
-[sub_resource type="BoxShape3D" id="BoxShape3D_6v30a"]
-size = Vector3(1, 0.8, 1)
-
-[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_1brn0"]
-albedo_color = Color(0.44, 0.37, 0.29, 1)
-roughness = 0.8
-
-[sub_resource type="BoxMesh" id="BoxMesh_melsq"]
-material = SubResource("StandardMaterial3D_1brn0")
-size = Vector3(1, 0.05, 1)
-
-[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_5dyip"]
-transparency = 1
-albedo_color = Color(0.18, 0.42, 0.78, 0.75)
-roughness = 0.8
-
-[sub_resource type="BoxMesh" id="BoxMesh_ywsqi"]
-material = SubResource("StandardMaterial3D_5dyip")
-size = Vector3(1, 0.05, 1)
-
-[sub_resource type="BoxShape3D" id="BoxShape3D_i7pw8"]
-size = Vector3(1, 0.05, 1)
-
-[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_x5456"]
-albedo_color = Color(0.9, 0.3, 0.12, 1)
-roughness = 0.8
-emission_enabled = true
-emission = Color(1, 0.45, 0.15, 1)
-emission_energy_multiplier = 1.5
-
-[sub_resource type="BoxMesh" id="BoxMesh_6puu6"]
-material = SubResource("StandardMaterial3D_x5456")
-size = Vector3(1, 0.05, 1)
-
-[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_k6861"]
-albedo_color = Color(0.72, 0.5, 0.22, 1)
-roughness = 0.8
-emission_enabled = true
-emission = Color(0.95, 0.65, 0.25, 1)
-emission_energy_multiplier = 0.8
-
-[sub_resource type="BoxMesh" id="BoxMesh_x42w8"]
-material = SubResource("StandardMaterial3D_k6861")
-size = Vector3(1, 0.05, 1)
-
-[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_1vvmy"]
-albedo_color = Color(0.55, 0.38, 0.22, 1)
-roughness = 0.8
-
-[sub_resource type="BoxMesh" id="BoxMesh_0aloa"]
-material = SubResource("StandardMaterial3D_1vvmy")
-size = Vector3(1, 0.1, 1)
-
-[sub_resource type="BoxShape3D" id="BoxShape3D_35vvr"]
-size = Vector3(1, 0.1, 1)
-
-[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_so1mv"]
-albedo_color = Color(0.74, 0.88, 0.94, 1)
-roughness = 0.8
-
-[sub_resource type="ArrayMesh" id="ArrayMesh_6y7e7"]
-_surfaces = [{
-"aabb": AABB(-0.5, 0, -0.5, 1, 0.7, 1),
-"format": 34359738375,
-"material": SubResource("StandardMaterial3D_so1mv"),
-"primitive": 3,
-"uv_scale": Vector4(0, 0, 0, 0),
-"vertex_count": 72,
-"vertex_data": PackedByteArray("AAAAvwAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAPzMzMz8AAAAAAAAAvwAAAAAAAAAAAAAAPzMzMz8AAAAAAAAAvzMzMz8AAAAAAAAAPwAAAAAAAAA/AAAAvwAAAAAAAAA/AAAAvzMzMz8AAAA/AAAAPwAAAAAAAAA/AAAAvzMzMz8AAAA/AAAAPzMzMz8AAAA/AAAAPwAAAAAAAAAAAAAAPwAAAAAAAAA/AAAAPzMzMz8AAAA/AAAAPwAAAAAAAAAAAAAAPzMzMz8AAAA/AAAAPzMzMz8AAAAAAAAAvwAAAAAAAAA/AAAAvwAAAAAAAAAAAAAAvzMzMz8AAAAAAAAAvwAAAAAAAAA/AAAAvzMzMz8AAAAAAAAAvzMzMz8AAAA/AAAAvzMzMz8AAAAAAAAAPzMzMz8AAAAAAAAAPzMzMz8AAAA/AAAAvzMzMz8AAAAAAAAAPzMzMz8AAAA/AAAAvzMzMz8AAAA/AAAAvwAAAAAAAAA/AAAAPwAAAAAAAAA/AAAAPwAAAAAAAAAAAAAAvwAAAAAAAAA/AAAAPwAAAAAAAAAAAAAAvwAAAAAAAAAAAAAAvwAAAAAAAAC/AAAAPwAAAAAAAAC/AAAAP0jhej4AAAC/AAAAvwAAAAAAAAC/AAAAP0jhej4AAAC/AAAAv0jhej4AAAC/AAAAPwAAAAAAAAAAAAAAvwAAAAAAAAAAAAAAv0jhej4AAAAAAAAAPwAAAAAAAAAAAAAAv0jhej4AAAAAAAAAP0jhej4AAAAAAAAAPwAAAAAAAAC/AAAAPwAAAAAAAAAAAAAAP0jhej4AAAAAAAAAPwAAAAAAAAC/AAAAP0jhej4AAAAAAAAAP0jhej4AAAC/AAAAvwAAAAAAAAAAAAAAvwAAAAAAAAC/AAAAv0jhej4AAAC/AAAAvwAAAAAAAAAAAAAAv0jhej4AAAC/AAAAv0jhej4AAAAAAAAAv0jhej4AAAC/AAAAP0jhej4AAAC/AAAAP0jhej4AAAAAAAAAv0jhej4AAAC/AAAAP0jhej4AAAAAAAAAv0jhej4AAAAAAAAAvwAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAPwAAAAAAAAC/AAAAvwAAAAAAAAAAAAAAPwAAAAAAAAC/AAAAvwAAAAAAAAC///////9/AID//////38AgP//////fwCA//////9/AID//////38AgP//////fwCA/3//f/9/AID/f/9//38AgP9//3//fwCA/3//f/9/AID/f/9//38AgP9//3//fwCA////f/9//7////9//3//v////3//f/+/////f/9//7////9//3//v////3//f/+/AAD/f/9//78AAP9//3//vwAA/3//f/+/AAD/f/9//78AAP9//3//vwAA/3//f/+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//38AAAAA/7//fwAAAAD/v/9/AAAAAP+//38AAAAA/7//fwAAAAD/v/9/AAAAAP+///////9/AID//////38AgP//////fwCA//////9/AID//////38AgP//////fwCA/3//f/9/AID/f/9//38AgP9//3//fwCA/3//f/9/AID/f/9//38AgP9//3//fwCA////f/9//7////9//3//v////3//f/+/////f/9//7////9//3//v////3//f/+/AAD/f/9//78AAP9//3//vwAA/3//f/+/AAD/f/9//78AAP9//3//vwAA/3//f/+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//38AAAAA/7//fwAAAAD/v/9/AAAAAP+//38AAAAA/7//fwAAAAD/v/9/AAAAAP+/")
-}]
-
-[sub_resource type="BoxShape3D" id="BoxShape3D_0ns5a"]
-size = Vector3(1, 0.7, 1)
-
-[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_lfyj4"]
-albedo_color = Color(0.2, 0.44, 0.8, 1)
-roughness = 0.8
-
-[sub_resource type="ArrayMesh" id="ArrayMesh_3isrv"]
-_surfaces = [{
-"aabb": AABB(-0.5, 0, -0.5, 1, 0.7, 1),
-"format": 34359738375,
-"material": SubResource("StandardMaterial3D_lfyj4"),
-"primitive": 3,
-"uv_scale": Vector4(0, 0, 0, 0),
-"vertex_count": 72,
-"vertex_data": PackedByteArray("AAAAvwAAAAAAAAC/AAAAPwAAAAAAAAC/AAAAP0jhej4AAAC/AAAAvwAAAAAAAAC/AAAAP0jhej4AAAC/AAAAv0jhej4AAAC/AAAAPwAAAAAAAAAAAAAAvwAAAAAAAAAAAAAAv0jhej4AAAAAAAAAPwAAAAAAAAAAAAAAv0jhej4AAAAAAAAAP0jhej4AAAAAAAAAPwAAAAAAAAC/AAAAPwAAAAAAAAAAAAAAP0jhej4AAAAAAAAAPwAAAAAAAAC/AAAAP0jhej4AAAAAAAAAP0jhej4AAAC/AAAAvwAAAAAAAAAAAAAAvwAAAAAAAAC/AAAAv0jhej4AAAC/AAAAvwAAAAAAAAAAAAAAv0jhej4AAAC/AAAAv0jhej4AAAAAAAAAv0jhej4AAAC/AAAAP0jhej4AAAC/AAAAP0jhej4AAAAAAAAAv0jhej4AAAC/AAAAP0jhej4AAAAAAAAAv0jhej4AAAAAAAAAvwAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAPwAAAAAAAAC/AAAAvwAAAAAAAAAAAAAAPwAAAAAAAAC/AAAAvwAAAAAAAAC/AAAAvwAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAPzMzMz8AAAAAAAAAvwAAAAAAAAAAAAAAPzMzMz8AAAAAAAAAvzMzMz8AAAAAAAAAPwAAAAAAAAA/AAAAvwAAAAAAAAA/AAAAvzMzMz8AAAA/AAAAPwAAAAAAAAA/AAAAvzMzMz8AAAA/AAAAPzMzMz8AAAA/AAAAPwAAAAAAAAAAAAAAPwAAAAAAAAA/AAAAPzMzMz8AAAA/AAAAPwAAAAAAAAAAAAAAPzMzMz8AAAA/AAAAPzMzMz8AAAAAAAAAvwAAAAAAAAA/AAAAvwAAAAAAAAAAAAAAvzMzMz8AAAAAAAAAvwAAAAAAAAA/AAAAvzMzMz8AAAAAAAAAvzMzMz8AAAA/AAAAvzMzMz8AAAAAAAAAPzMzMz8AAAAAAAAAPzMzMz8AAAA/AAAAvzMzMz8AAAAAAAAAPzMzMz8AAAA/AAAAvzMzMz8AAAA/AAAAvwAAAAAAAAA/AAAAPwAAAAAAAAA/AAAAPwAAAAAAAAAAAAAAvwAAAAAAAAA/AAAAPwAAAAAAAAAAAAAAvwAAAAAAAAAA//////9/AID//////38AgP//////fwCA//////9/AID//////38AgP//////fwCA/3//f/9/AID/f/9//38AgP9//3//fwCA/3//f/9/AID/f/9//38AgP9//3//fwCA////f/9//7////9//3//v////3//f/+/////f/9//7////9//3//v////3//f/+/AAD/f/9//78AAP9//3//vwAA/3//f/+/AAD/f/9//78AAP9//3//vwAA/3//f/+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//38AAAAA/7//fwAAAAD/v/9/AAAAAP+//38AAAAA/7//fwAAAAD/v/9/AAAAAP+///////9/AID//////38AgP//////fwCA//////9/AID//////38AgP//////fwCA/3//f/9/AID/f/9//38AgP9//3//fwCA/3//f/9/AID/f/9//38AgP9//3//fwCA////f/9//7////9//3//v////3//f/+/////f/9//7////9//3//v////3//f/+/AAD/f/9//78AAP9//3//vwAA/3//f/+/AAD/f/9//78AAP9//3//vwAA/3//f/+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//38AAAAA/7//fwAAAAD/v/9/AAAAAP+//38AAAAA/7//fwAAAAD/v/9/AAAAAP+/")
-}]
-
-[sub_resource type="MeshLibrary" id="MeshLibrary_r4kn7"]
-item/0/name = "Floor"
-item/0/mesh = SubResource("BoxMesh_ni5ms")
-item/0/mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.025, 0)
-item/0/mesh_cast_shadow = 1
-item/0/shapes = [SubResource("BoxShape3D_uebra"), Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.025, 0)]
-item/0/navigation_mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
-item/0/navigation_layers = 1
-item/1/name = "Wall"
-item/1/mesh = SubResource("BoxMesh_iusj4")
-item/1/mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0)
-item/1/mesh_cast_shadow = 1
-item/1/shapes = [SubResource("BoxShape3D_a1rh6"), Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0)]
-item/1/navigation_mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
-item/1/navigation_layers = 1
-item/2/name = "Door"
-item/2/mesh = SubResource("BoxMesh_ylec3")
-item/2/mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.4, 0)
-item/2/mesh_cast_shadow = 1
-item/2/shapes = [SubResource("BoxShape3D_6v30a"), Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.4, 0)]
-item/2/navigation_mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
-item/2/navigation_layers = 1
-item/3/name = "Corridor"
-item/3/mesh = SubResource("BoxMesh_melsq")
-item/3/mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.025, 0)
-item/3/mesh_cast_shadow = 1
-item/3/shapes = [SubResource("BoxShape3D_uebra"), Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.025, 0)]
-item/3/navigation_mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
-item/3/navigation_layers = 1
-item/4/name = "Water"
-item/4/mesh = SubResource("BoxMesh_ywsqi")
-item/4/mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.025, 0)
-item/4/mesh_cast_shadow = 1
-item/4/shapes = [SubResource("BoxShape3D_i7pw8"), Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.025, 0)]
-item/4/navigation_mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
-item/4/navigation_layers = 1
-item/5/name = "Lava"
-item/5/mesh = SubResource("BoxMesh_6puu6")
-item/5/mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.025, 0)
-item/5/mesh_cast_shadow = 1
-item/5/shapes = [SubResource("BoxShape3D_i7pw8"), Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.025, 0)]
-item/5/navigation_mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
-item/5/navigation_layers = 1
-item/6/name = "Brimstone"
-item/6/mesh = SubResource("BoxMesh_x42w8")
-item/6/mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.025, 0)
-item/6/mesh_cast_shadow = 1
-item/6/shapes = [SubResource("BoxShape3D_i7pw8"), Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.025, 0)]
-item/6/navigation_mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
-item/6/navigation_layers = 1
-item/7/name = "Bridge"
-item/7/mesh = SubResource("BoxMesh_0aloa")
-item/7/mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.05, 0)
-item/7/mesh_cast_shadow = 1
-item/7/shapes = [SubResource("BoxShape3D_35vvr"), Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.05, 0)]
-item/7/navigation_mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
-item/7/navigation_layers = 1
-item/8/name = "StairsUp"
-item/8/mesh = SubResource("ArrayMesh_6y7e7")
-item/8/mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
-item/8/mesh_cast_shadow = 1
-item/8/shapes = [SubResource("BoxShape3D_0ns5a"), Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.35, 0)]
-item/8/navigation_mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
-item/8/navigation_layers = 1
-item/9/name = "StairsDown"
-item/9/mesh = SubResource("ArrayMesh_3isrv")
-item/9/mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
-item/9/mesh_cast_shadow = 1
-item/9/shapes = [SubResource("BoxShape3D_0ns5a"), Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.35, 0)]
-item/9/navigation_mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
-item/9/navigation_layers = 1
-
-[node name="GameRoot" type="Node3D" unique_id=1455452502]
-
-[node name="Generator" type="Node3D" parent="." unique_id=754140715]
-script = ExtResource("1_78s5d")
-
-[node name="Levels" type="Node3D" parent="." unique_id=164573799]
-unique_name_in_owner = true
-
-[node name="Level0" type="GridMap" parent="Levels" unique_id=1090482578]
-mesh_library = SubResource("MeshLibrary_r4kn7")
-cell_size = Vector3(1, 1, 1)
-data = {
-"cells": PackedInt32Array(9, 1, 1, 10, 1, 1, 11, 1, 1, 75, 1, 1, 76, 1, 1, 77, 1, 1, 8, 2, 1, 9, 2, 1, 10, 2, 0, 11, 2, 1, 12, 2, 1, 63, 2, 1, 64, 2, 1, 65, 2, 1, 66, 2, 1, 67, 2, 1, 68, 2, 1, 69, 2, 1, 70, 2, 1, 71, 2, 1, 72, 2, 1, 73, 2, 1, 74, 2, 1, 75, 2, 1, 76, 2, 0, 77, 2, 1, 78, 2, 1, 7, 3, 1, 8, 3, 1, 9, 3, 0, 10, 3, 0, 11, 3, 0, 12, 3, 1, 13, 3, 1, 31, 3, 1, 32, 3, 1, 33, 3, 1, 34, 3, 1, 35, 3, 1, 36, 3, 1, 37, 3, 1, 40, 3, 1, 41, 3, 1, 42, 3, 1, 45, 3, 1, 46, 3, 1, 47, 3, 1, 48, 3, 1, 49, 3, 1, 50, 3, 1, 51, 3, 1, 52, 3, 1, 53, 3, 1, 54, 3, 1, 63, 3, 1, 64, 3, 0, 65, 3, 0, 66, 3, 0, 67, 3, 0, 68, 3, 0, 69, 3, 0, 70, 3, 0, 71, 3, 0, 72, 3, 2, 73, 3, 3, 74, 3, 3, 75, 3, 0, 76, 3, 0, 77, 3, 0, 78, 3, 1, 7, 4, 1, 8, 4, 0, 9, 4, 0, 10, 4, 0, 11, 4, 0, 12, 4, 0, 13, 4, 1, 19, 4, 1, 20, 4, 1, 21, 4, 1, 22, 4, 1, 23, 4, 1, 24, 4, 1, 25, 4, 1, 31, 4, 1, 32, 4, 0, 33, 4, 0, 34, 4, 0, 35, 4, 0, 36, 4, 0, 37, 4, 1, 38, 4, 1, 39, 4, 1, 40, 4, 1, 41, 4, 0, 42, 4, 1, 43, 4, 1, 45, 4, 1, 46, 4, 0, 47, 4, 0, 48, 4, 0, 49, 4, 0, 50, 4, 0, 51, 4, 0, 52, 4, 0, 53, 4, 0, 54, 4, 1, 63, 4, 1, 64, 4, 0, 65, 4, 0, 66, 4, 0, 67, 4, 0, 68, 4, 0, 69, 4, 0, 70, 4, 0, 71, 4, 0, 72, 4, 1, 73, 4, 1, 74, 4, 1, 75, 4, 1, 76, 4, 0, 77, 4, 1, 78, 4, 1, 7, 5, 1, 8, 5, 1, 9, 5, 0, 10, 5, 0, 11, 5, 0, 12, 5, 1, 13, 5, 1, 14, 5, 1, 15, 5, 1, 16, 5, 1, 17, 5, 1, 18, 5, 1, 19, 5, 1, 20, 5, 0, 21, 5, 0, 22, 5, 0, 23, 5, 0, 24, 5, 0, 25, 5, 1, 26, 5, 1, 27, 5, 1, 28, 5, 1, 29, 5, 1, 30, 5, 1, 31, 5, 1, 32, 5, 0, 33, 5, 0, 34, 5, 0, 35, 5, 0, 36, 5, 0, 37, 5, 1, 38, 5, 1, 39, 5, 0, 40, 5, 0, 41, 5, 0, 42, 5, 0, 43, 5, 1, 45, 5, 1, 46, 5, 0, 47, 5, 0, 48, 5, 0, 49, 5, 0, 50, 5, 0, 51, 5, 0, 52, 5, 0, 53, 5, 0, 54, 5, 1, 58, 5, 1, 59, 5, 1, 60, 5, 1, 63, 5, 1, 64, 5, 0, 65, 5, 0, 66, 5, 0, 67, 5, 0, 68, 5, 0, 69, 5, 0, 70, 5, 0, 71, 5, 0, 72, 5, 1, 74, 5, 1, 75, 5, 9, 76, 5, 0, 77, 5, 0, 78, 5, 1, 8, 6, 1, 9, 6, 1, 10, 6, 0, 11, 6, 3, 12, 6, 3, 13, 6, 3, 14, 6, 3, 15, 6, 3, 16, 6, 3, 17, 6, 3, 18, 6, 3, 19, 6, 2, 20, 6, 0, 21, 6, 0, 22, 6, 0, 23, 6, 0, 24, 6, 0, 25, 6, 3, 26, 6, 3, 27, 6, 3, 28, 6, 3, 29, 6, 3, 30, 6, 3, 31, 6, 2, 32, 6, 0, 33, 6, 0, 34, 6, 0, 35, 6, 0, 36, 6, 0, 37, 6, 2, 38, 6, 0, 39, 6, 0, 40, 6, 0, 41, 6, 0, 42, 6, 1, 43, 6, 1, 45, 6, 1, 46, 6, 0, 47, 6, 0, 48, 6, 0, 49, 6, 0, 50, 6, 0, 51, 6, 0, 52, 6, 0, 53, 6, 0, 54, 6, 1, 57, 6, 1, 58, 6, 1, 59, 6, 0, 60, 6, 1, 61, 6, 1, 63, 6, 1, 64, 6, 1, 65, 6, 1, 66, 6, 1, 67, 6, 1, 68, 6, 1, 69, 6, 1, 70, 6, 1, 71, 6, 3, 72, 6, 1, 74, 6, 1, 75, 6, 1, 76, 6, 0, 77, 6, 1, 78, 6, 1, 8, 7, 1, 9, 7, 1, 10, 7, 2, 11, 7, 1, 12, 7, 1, 13, 7, 1, 14, 7, 1, 15, 7, 1, 16, 7, 1, 17, 7, 1, 18, 7, 1, 19, 7, 1, 20, 7, 0, 21, 7, 0, 22, 7, 0, 23, 7, 0, 24, 7, 0, 25, 7, 1, 26, 7, 1, 27, 7, 1, 28, 7, 1, 29, 7, 1, 30, 7, 1, 31, 7, 1, 32, 7, 3, 33, 7, 1, 34, 7, 1, 35, 7, 1, 36, 7, 1, 37, 7, 1, 38, 7, 1, 39, 7, 0, 40, 7, 1, 41, 7, 3, 42, 7, 1, 45, 7, 1, 46, 7, 1, 47, 7, 1, 48, 7, 1, 49, 7, 1, 50, 7, 1, 51, 7, 3, 52, 7, 1, 53, 7, 1, 54, 7, 1, 56, 7, 1, 57, 7, 1, 58, 7, 0, 59, 7, 0, 60, 7, 0, 61, 7, 1, 62, 7, 1, 67, 7, 1, 68, 7, 1, 69, 7, 1, 70, 7, 1, 71, 7, 2, 72, 7, 1, 73, 7, 1, 74, 7, 1, 75, 7, 1, 76, 7, 1, 77, 7, 1, 7, 8, 1, 8, 8, 1, 9, 8, 0, 10, 8, 0, 11, 8, 3, 12, 8, 3, 13, 8, 3, 14, 8, 3, 15, 8, 3, 16, 8, 3, 17, 8, 3, 18, 8, 3, 19, 8, 2, 20, 8, 0, 21, 8, 0, 22, 8, 0, 23, 8, 0, 24, 8, 0, 25, 8, 1, 31, 8, 1, 32, 8, 3, 33, 8, 1, 38, 8, 1, 39, 8, 1, 40, 8, 1, 41, 8, 2, 42, 8, 1, 50, 8, 1, 51, 8, 3, 52, 8, 1, 56, 8, 1, 57, 8, 0, 58, 8, 0, 59, 8, 0, 60, 8, 0, 61, 8, 0, 62, 8, 1, 67, 8, 1, 68, 8, 0, 69, 8, 0, 70, 8, 0, 71, 8, 0, 72, 8, 0, 73, 8, 0, 74, 8, 1, 7, 9, 1, 8, 9, 0, 9, 9, 0, 10, 9, 0, 11, 9, 0, 12, 9, 1, 13, 9, 1, 14, 9, 1, 15, 9, 1, 16, 9, 1, 17, 9, 1, 18, 9, 1, 19, 9, 1, 20, 9, 1, 21, 9, 1, 22, 9, 1, 23, 9, 1, 24, 9, 1, 25, 9, 1, 31, 9, 1, 32, 9, 3, 33, 9, 1, 34, 9, 1, 38, 9, 1, 39, 9, 0, 40, 9, 0, 41, 9, 0, 42, 9, 1, 50, 9, 1, 51, 9, 3, 52, 9, 1, 56, 9, 1, 57, 9, 1, 58, 9, 0, 59, 9, 0, 60, 9, 0, 61, 9, 1, 62, 9, 1, 67, 9, 1, 68, 9, 0, 69, 9, 0, 70, 9, 0, 71, 9, 0, 72, 9, 0, 73, 9, 0, 74, 9, 1, 7, 10, 1, 8, 10, 0, 9, 10, 0, 10, 10, 0, 11, 10, 0, 12, 10, 1, 29, 10, 1, 30, 10, 1, 31, 10, 1, 32, 10, 2, 33, 10, 0, 34, 10, 1, 35, 10, 1, 38, 10, 1, 39, 10, 0, 40, 10, 0, 41, 10, 0, 42, 10, 1, 50, 10, 1, 51, 10, 3, 52, 10, 1, 57, 10, 1, 58, 10, 1, 59, 10, 0, 60, 10, 1, 61, 10, 1, 67, 10, 1, 68, 10, 1, 69, 10, 3, 70, 10, 1, 71, 10, 1, 72, 10, 1, 73, 10, 1, 74, 10, 1, 7, 11, 1, 8, 11, 1, 9, 11, 0, 10, 11, 0, 11, 11, 1, 12, 11, 1, 28, 11, 1, 29, 11, 1, 30, 11, 0, 31, 11, 1, 32, 11, 0, 33, 11, 0, 34, 11, 0, 35, 11, 1, 36, 11, 1, 38, 11, 1, 39, 11, 3, 40, 11, 1, 41, 11, 1, 42, 11, 1, 50, 11, 1, 51, 11, 2, 52, 11, 1, 53, 11, 1, 54, 11, 1, 58, 11, 1, 59, 11, 3, 60, 11, 1, 68, 11, 1, 69, 11, 3, 70, 11, 1, 8, 12, 1, 9, 12, 2, 10, 12, 1, 11, 12, 1, 16, 12, 1, 17, 12, 1, 18, 12, 1, 19, 12, 1, 20, 12, 1, 21, 12, 1, 22, 12, 1, 23, 12, 1, 24, 12, 1, 25, 12, 1, 26, 12, 1, 27, 12, 1, 28, 12, 1, 29, 12, 0, 30, 12, 0, 31, 12, 0, 32, 12, 0, 33, 12, 0, 34, 12, 0, 35, 12, 0, 36, 12, 1, 37, 12, 1, 38, 12, 1, 39, 12, 2, 40, 12, 1, 41, 12, 1, 42, 12, 1, 43, 12, 1, 44, 12, 1, 45, 12, 1, 46, 12, 1, 47, 12, 1, 48, 12, 1, 49, 12, 1, 50, 12, 1, 51, 12, 0, 52, 12, 0, 53, 12, 0, 54, 12, 1, 58, 12, 1, 59, 12, 2, 60, 12, 1, 61, 12, 1, 62, 12, 1, 67, 12, 1, 68, 12, 1, 69, 12, 2, 70, 12, 1, 6, 13, 1, 7, 13, 1, 8, 13, 1, 9, 13, 3, 10, 13, 1, 16, 13, 1, 17, 13, 0, 18, 13, 0, 19, 13, 0, 20, 13, 3, 21, 13, 3, 22, 13, 3, 23, 13, 3, 24, 13, 3, 25, 13, 3, 26, 13, 3, 27, 13, 3, 28, 13, 3, 29, 13, 2, 30, 13, 0, 31, 13, 0, 32, 13, 0, 33, 13, 0, 34, 13, 0, 35, 13, 1, 36, 13, 1, 37, 13, 0, 38, 13, 0, 39, 13, 0, 40, 13, 0, 41, 13, 0, 42, 13, 2, 43, 13, 3, 44, 13, 3, 45, 13, 3, 46, 13, 3, 47, 13, 3, 48, 13, 3, 49, 13, 3, 50, 13, 3, 51, 13, 0, 52, 13, 0, 53, 13, 0, 54, 13, 1, 58, 13, 1, 59, 13, 0, 60, 13, 0, 61, 13, 0, 62, 13, 1, 63, 13, 1, 64, 13, 1, 65, 13, 1, 66, 13, 1, 67, 13, 1, 68, 13, 0, 69, 13, 0, 70, 13, 1, 6, 14, 1, 7, 14, 0, 8, 14, 0, 9, 14, 0, 10, 14, 1, 11, 14, 1, 12, 14, 1, 13, 14, 1, 14, 14, 1, 15, 14, 1, 16, 14, 1, 17, 14, 0, 18, 14, 0, 19, 14, 0, 20, 14, 1, 21, 14, 1, 22, 14, 1, 23, 14, 1, 24, 14, 1, 25, 14, 1, 26, 14, 1, 27, 14, 1, 28, 14, 1, 29, 14, 1, 30, 14, 0, 31, 14, 0, 32, 14, 0, 33, 14, 0, 34, 14, 0, 35, 14, 3, 36, 14, 2, 37, 14, 0, 38, 14, 0, 39, 14, 8, 40, 14, 0, 41, 14, 0, 42, 14, 1, 43, 14, 1, 44, 14, 1, 45, 14, 1, 46, 14, 1, 47, 14, 1, 48, 14, 1, 49, 14, 1, 50, 14, 1, 51, 14, 1, 52, 14, 2, 53, 14, 1, 54, 14, 1, 58, 14, 1, 59, 14, 0, 60, 14, 0, 61, 14, 0, 62, 14, 2, 63, 14, 3, 64, 14, 3, 65, 14, 3, 66, 14, 3, 67, 14, 3, 68, 14, 0, 69, 14, 0, 70, 14, 1, 6, 15, 1, 7, 15, 0, 8, 15, 0, 9, 15, 0, 10, 15, 2, 11, 15, 0, 12, 15, 0, 13, 15, 0, 14, 15, 0, 15, 15, 1, 16, 15, 1, 17, 15, 0, 18, 15, 0, 19, 15, 0, 20, 15, 1, 21, 15, 1, 22, 15, 1, 23, 15, 1, 24, 15, 1, 25, 15, 1, 29, 15, 1, 30, 15, 1, 31, 15, 0, 32, 15, 0, 33, 15, 0, 34, 15, 0, 35, 15, 0, 36, 15, 1, 37, 15, 1, 38, 15, 1, 39, 15, 1, 40, 15, 2, 41, 15, 1, 42, 15, 1, 51, 15, 1, 52, 15, 3, 53, 15, 1, 58, 15, 1, 59, 15, 0, 60, 15, 0, 61, 15, 0, 62, 15, 3, 63, 15, 3, 64, 15, 2, 65, 15, 0, 66, 15, 1, 67, 15, 1, 68, 15, 0, 69, 15, 0, 70, 15, 1, 6, 16, 1, 7, 16, 0, 8, 16, 0, 9, 16, 0, 10, 16, 1, 11, 16, 0, 12, 16, 0, 13, 16, 0, 14, 16, 0, 15, 16, 3, 16, 16, 2, 17, 16, 0, 18, 16, 0, 19, 16, 0, 20, 16, 2, 21, 16, 3, 22, 16, 3, 23, 16, 3, 24, 16, 0, 25, 16, 1, 26, 16, 1, 27, 16, 1, 30, 16, 1, 31, 16, 1, 32, 16, 0, 33, 16, 0, 34, 16, 0, 35, 16, 1, 36, 16, 1, 39, 16, 1, 40, 16, 3, 41, 16, 1, 51, 16, 1, 52, 16, 3, 53, 16, 1, 58, 16, 1, 59, 16, 1, 60, 16, 1, 61, 16, 1, 62, 16, 1, 63, 16, 1, 64, 16, 0, 65, 16, 0, 66, 16, 0, 67, 16, 1, 68, 16, 1, 69, 16, 1, 70, 16, 1, 73, 16, 1, 74, 16, 1, 75, 16, 1, 76, 16, 1, 77, 16, 1, 6, 17, 1, 7, 17, 0, 8, 17, 0, 9, 17, 0, 10, 17, 1, 11, 17, 1, 12, 17, 1, 13, 17, 1, 14, 17, 1, 15, 17, 1, 16, 17, 1, 17, 17, 1, 18, 17, 2, 19, 17, 1, 20, 17, 1, 21, 17, 1, 22, 17, 0, 23, 17, 0, 24, 17, 0, 25, 17, 0, 26, 17, 0, 27, 17, 1, 31, 17, 1, 32, 17, 1, 33, 17, 0, 34, 17, 1, 35, 17, 1, 39, 17, 1, 40, 17, 3, 41, 17, 1, 51, 17, 1, 52, 17, 3, 53, 17, 1, 63, 17, 1, 64, 17, 1, 65, 17, 0, 66, 17, 1, 67, 17, 1, 72, 17, 1, 73, 17, 1, 74, 17, 0, 75, 17, 0, 76, 17, 0, 77, 17, 1, 78, 17, 1, 6, 18, 1, 7, 18, 1, 8, 18, 2, 9, 18, 1, 10, 18, 1, 11, 18, 1, 17, 18, 1, 18, 18, 3, 19, 18, 1, 20, 18, 1, 21, 18, 1, 22, 18, 0, 23, 18, 0, 24, 18, 0, 25, 18, 0, 26, 18, 0, 27, 18, 1, 28, 18, 1, 32, 18, 1, 33, 18, 1, 34, 18, 1, 39, 18, 1, 40, 18, 3, 41, 18, 1, 49, 18, 1, 50, 18, 1, 51, 18, 1, 52, 18, 3, 53, 18, 1, 54, 18, 1, 55, 18, 1, 56, 18, 1, 57, 18, 1, 63, 18, 1, 64, 18, 0, 65, 18, 0, 66, 18, 0, 67, 18, 1, 72, 18, 1, 73, 18, 0, 74, 18, 0, 75, 18, 0, 76, 18, 0, 77, 18, 0, 78, 18, 1, 7, 19, 1, 8, 19, 3, 9, 19, 1, 10, 19, 0, 11, 19, 1, 12, 19, 1, 13, 19, 1, 17, 19, 1, 18, 19, 3, 19, 19, 1, 20, 19, 1, 21, 19, 0, 22, 19, 0, 23, 19, 0, 24, 19, 0, 25, 19, 0, 26, 19, 0, 27, 19, 0, 28, 19, 1, 32, 19, 1, 33, 19, 1, 34, 19, 1, 39, 19, 1, 40, 19, 3, 41, 19, 1, 42, 19, 1, 43, 19, 1, 44, 19, 1, 49, 19, 1, 50, 19, 0, 51, 19, 0, 52, 19, 0, 53, 19, 0, 54, 19, 0, 55, 19, 0, 56, 19, 0, 57, 19, 1, 63, 19, 1, 64, 19, 1, 65, 19, 0, 66, 19, 3, 67, 19, 1, 72, 19, 1, 73, 19, 0, 74, 19, 0, 75, 19, 0, 76, 19, 0, 77, 19, 0, 78, 19, 1, 7, 20, 1, 8, 20, 0, 9, 20, 0, 10, 20, 0, 11, 20, 0, 12, 20, 0, 13, 20, 1, 15, 20, 1, 16, 20, 1, 17, 20, 1, 18, 20, 0, 19, 20, 1, 20, 20, 1, 21, 20, 1, 22, 20, 0, 23, 20, 0, 24, 20, 0, 25, 20, 0, 26, 20, 0, 27, 20, 1, 28, 20, 1, 31, 20, 1, 32, 20, 1, 33, 20, 0, 34, 20, 1, 35, 20, 1, 36, 20, 1, 37, 20, 1, 38, 20, 1, 39, 20, 1, 40, 20, 0, 41, 20, 0, 42, 20, 0, 43, 20, 0, 44, 20, 1, 49, 20, 1, 50, 20, 0, 51, 20, 0, 52, 20, 0, 53, 20, 0, 54, 20, 0, 55, 20, 0, 56, 20, 0, 57, 20, 1, 64, 20, 1, 65, 20, 1, 66, 20, 3, 67, 20, 1, 72, 20, 1, 73, 20, 0, 74, 20, 0, 75, 20, 0, 76, 20, 0, 77, 20, 0, 78, 20, 1, 6, 21, 1, 7, 21, 1, 8, 21, 0, 9, 21, 0, 10, 21, 0, 11, 21, 0, 12, 21, 0, 13, 21, 1, 14, 21, 1, 15, 21, 1, 16, 21, 0, 17, 21, 0, 18, 21, 0, 19, 21, 0, 20, 21, 0, 21, 21, 1, 22, 21, 0, 23, 21, 0, 24, 21, 0, 25, 21, 0, 26, 21, 0, 27, 21, 1, 30, 21, 1, 31, 21, 1, 32, 21, 0, 33, 21, 0, 34, 21, 0, 35, 21, 3, 36, 21, 3, 37, 21, 3, 38, 21, 3, 39, 21, 2, 40, 21, 0, 41, 21, 0, 42, 21, 0, 43, 21, 0, 44, 21, 1, 49, 21, 1, 50, 21, 0, 51, 21, 0, 52, 21, 0, 53, 21, 0, 54, 21, 0, 55, 21, 0, 56, 21, 0, 57, 21, 1, 58, 21, 1, 59, 21, 1, 60, 21, 1, 61, 21, 1, 62, 21, 1, 63, 21, 1, 64, 21, 1, 65, 21, 1, 66, 21, 3, 67, 21, 1, 72, 21, 1, 73, 21, 1, 74, 21, 0, 75, 21, 0, 76, 21, 0, 77, 21, 1, 78, 21, 1, 6, 22, 1, 7, 22, 0, 8, 22, 0, 9, 22, 0, 10, 22, 0, 11, 22, 0, 12, 22, 0, 13, 22, 0, 14, 22, 1, 15, 22, 1, 16, 22, 0, 17, 22, 0, 18, 22, 0, 19, 22, 0, 20, 22, 0, 21, 22, 1, 22, 22, 1, 23, 22, 1, 24, 22, 0, 25, 22, 1, 26, 22, 1, 27, 22, 1, 30, 22, 1, 31, 22, 0, 32, 22, 0, 33, 22, 0, 34, 22, 0, 35, 22, 0, 36, 22, 1, 37, 22, 1, 38, 22, 1, 39, 22, 1, 40, 22, 0, 41, 22, 0, 42, 22, 0, 43, 22, 0, 44, 22, 1, 49, 22, 1, 50, 22, 0, 51, 22, 0, 52, 22, 0, 53, 22, 0, 54, 22, 0, 55, 22, 0, 56, 22, 0, 57, 22, 2, 58, 22, 3, 59, 22, 3, 60, 22, 3, 61, 22, 3, 62, 22, 3, 63, 22, 3, 64, 22, 0, 65, 22, 0, 66, 22, 2, 67, 22, 1, 73, 22, 1, 74, 22, 1, 75, 22, 1, 76, 22, 3, 77, 22, 1, 6, 23, 1, 7, 23, 1, 8, 23, 0, 9, 23, 0, 10, 23, 0, 11, 23, 0, 12, 23, 0, 13, 23, 1, 14, 23, 1, 15, 23, 0, 16, 23, 0, 17, 23, 0, 18, 23, 0, 19, 23, 0, 20, 23, 0, 21, 23, 0, 22, 23, 1, 23, 23, 1, 24, 23, 1, 25, 23, 1, 30, 23, 1, 31, 23, 1, 32, 23, 0, 33, 23, 0, 34, 23, 0, 35, 23, 1, 36, 23, 1, 39, 23, 1, 40, 23, 0, 41, 23, 0, 42, 23, 0, 43, 23, 0, 44, 23, 1, 49, 23, 1, 50, 23, 2, 51, 23, 1, 52, 23, 1, 53, 23, 1, 54, 23, 1, 55, 23, 1, 56, 23, 1, 57, 23, 1, 58, 23, 1, 59, 23, 1, 60, 23, 1, 61, 23, 1, 62, 23, 1, 63, 23, 0, 64, 23, 0, 65, 23, 0, 66, 23, 0, 67, 23, 1, 68, 23, 1, 69, 23, 1, 70, 23, 1, 71, 23, 1, 72, 23, 1, 73, 23, 1, 74, 23, 1, 75, 23, 1, 76, 23, 2, 77, 23, 1, 7, 24, 1, 8, 24, 0, 9, 24, 0, 10, 24, 0, 11, 24, 0, 12, 24, 0, 13, 24, 1, 14, 24, 1, 15, 24, 1, 16, 24, 0, 17, 24, 0, 18, 24, 0, 19, 24, 0, 20, 24, 0, 21, 24, 1, 22, 24, 1, 31, 24, 1, 32, 24, 2, 33, 24, 0, 34, 24, 1, 35, 24, 1, 39, 24, 1, 40, 24, 1, 41, 24, 1, 42, 24, 1, 43, 24, 1, 44, 24, 1, 49, 24, 1, 50, 24, 3, 51, 24, 1, 52, 24, 1, 53, 24, 1, 54, 24, 1, 55, 24, 1, 56, 24, 1, 61, 24, 1, 62, 24, 0, 63, 24, 0, 64, 24, 0, 65, 24, 0, 66, 24, 0, 67, 24, 0, 68, 24, 2, 69, 24, 3, 70, 24, 3, 71, 24, 3, 72, 24, 0, 73, 24, 0, 74, 24, 0, 75, 24, 0, 76, 24, 0, 77, 24, 1, 7, 25, 1, 8, 25, 1, 9, 25, 1, 10, 25, 0, 11, 25, 1, 12, 25, 1, 13, 25, 1, 15, 25, 1, 16, 25, 0, 17, 25, 0, 18, 25, 0, 19, 25, 0, 20, 25, 0, 21, 25, 1, 30, 25, 1, 31, 25, 1, 32, 25, 3, 33, 25, 1, 34, 25, 1, 35, 25, 1, 49, 25, 1, 50, 25, 0, 51, 25, 0, 52, 25, 0, 53, 25, 0, 54, 25, 0, 55, 25, 0, 56, 25, 1, 61, 25, 1, 62, 25, 1, 63, 25, 0, 64, 25, 0, 65, 25, 0, 66, 25, 0, 67, 25, 1, 68, 25, 1, 69, 25, 1, 70, 25, 1, 71, 25, 1, 72, 25, 0, 73, 25, 0, 74, 25, 0, 75, 25, 0, 76, 25, 0, 77, 25, 1, 9, 26, 1, 10, 26, 1, 11, 26, 1, 15, 26, 1, 16, 26, 1, 17, 26, 1, 18, 26, 0, 19, 26, 1, 20, 26, 1, 21, 26, 1, 30, 26, 1, 31, 26, 0, 32, 26, 0, 33, 26, 0, 34, 26, 0, 35, 26, 1, 49, 26, 1, 50, 26, 0, 51, 26, 0, 52, 26, 0, 53, 26, 0, 54, 26, 0, 55, 26, 0, 56, 26, 1, 62, 26, 1, 63, 26, 1, 64, 26, 0, 65, 26, 0, 66, 26, 1, 67, 26, 1, 71, 26, 1, 72, 26, 0, 73, 26, 0, 74, 26, 0, 75, 26, 0, 76, 26, 0, 77, 26, 1, 17, 27, 1, 18, 27, 1, 19, 27, 1, 30, 27, 1, 31, 27, 0, 32, 27, 0, 33, 27, 0, 34, 27, 0, 35, 27, 1, 49, 27, 1, 50, 27, 1, 51, 27, 1, 52, 27, 1, 53, 27, 1, 54, 27, 1, 55, 27, 1, 56, 27, 1, 63, 27, 1, 64, 27, 1, 65, 27, 1, 66, 27, 1, 71, 27, 1, 72, 27, 1, 73, 27, 1, 74, 27, 1, 75, 27, 1, 76, 27, 1, 77, 27, 1, 30, 28, 1, 31, 28, 1, 32, 28, 1, 33, 28, 1, 34, 28, 1, 35, 28, 1)
-}
-
-[node name="Level1" type="GridMap" parent="Levels" unique_id=1314841420]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -6, 0)
-mesh_library = SubResource("MeshLibrary_r4kn7")
-cell_size = Vector3(1, 1, 1)
-data = {
-"cells": PackedInt32Array(17, 0, 1, 18, 0, 1, 19, 0, 1, 29, 0, 1, 30, 0, 1, 31, 0, 1, 41, 0, 1, 42, 0, 1, 43, 0, 1, 44, 0, 1, 45, 0, 1, 46, 0, 1, 47, 0, 1, 16, 1, 1, 17, 1, 1, 18, 1, 0, 19, 1, 1, 20, 1, 1, 21, 1, 1, 22, 1, 1, 23, 1, 1, 24, 1, 1, 28, 1, 1, 29, 1, 1, 30, 1, 0, 31, 1, 1, 32, 1, 1, 41, 1, 1, 42, 1, 0, 43, 1, 0, 44, 1, 0, 45, 1, 0, 46, 1, 0, 47, 1, 1, 15, 2, 1, 16, 2, 1, 17, 2, 0, 18, 2, 0, 19, 2, 0, 20, 2, 1, 21, 2, 1, 22, 2, 0, 23, 2, 0, 24, 2, 1, 25, 2, 1, 26, 2, 1, 27, 2, 1, 28, 2, 1, 29, 2, 0, 30, 2, 0, 31, 2, 0, 32, 2, 1, 33, 2, 1, 41, 2, 1, 42, 2, 0, 43, 2, 0, 44, 2, 0, 45, 2, 0, 46, 2, 0, 47, 2, 1, 15, 3, 1, 16, 3, 0, 17, 3, 0, 18, 3, 0, 19, 3, 0, 20, 3, 0, 21, 3, 1, 22, 3, 0, 23, 3, 0, 24, 3, 2, 25, 3, 3, 26, 3, 3, 27, 3, 3, 28, 3, 0, 29, 3, 0, 30, 3, 0, 31, 3, 0, 32, 3, 0, 33, 3, 1, 41, 3, 1, 42, 3, 1, 43, 3, 1, 44, 3, 1, 45, 3, 3, 46, 3, 1, 47, 3, 1, 59, 3, 1, 60, 3, 1, 61, 3, 1, 1, 4, 1, 2, 4, 1, 3, 4, 1, 4, 4, 1, 5, 4, 1, 15, 4, 1, 16, 4, 1, 17, 4, 0, 18, 4, 0, 19, 4, 0, 20, 4, 3, 21, 4, 2, 22, 4, 0, 23, 4, 0, 24, 4, 1, 25, 4, 1, 26, 4, 1, 27, 4, 1, 28, 4, 1, 29, 4, 0, 30, 4, 0, 31, 4, 0, 32, 4, 1, 33, 4, 1, 44, 4, 1, 45, 4, 3, 46, 4, 1, 48, 4, 1, 49, 4, 1, 50, 4, 1, 51, 4, 1, 52, 4, 1, 53, 4, 1, 54, 4, 1, 57, 4, 1, 58, 4, 1, 59, 4, 1, 60, 4, 0, 61, 4, 1, 62, 4, 1, 63, 4, 1, 1, 5, 1, 2, 5, 0, 3, 5, 0, 4, 5, 0, 5, 5, 1, 6, 5, 1, 7, 5, 1, 8, 5, 1, 9, 5, 1, 10, 5, 1, 11, 5, 1, 12, 5, 1, 13, 5, 1, 14, 5, 1, 15, 5, 1, 16, 5, 1, 17, 5, 1, 18, 5, 0, 19, 5, 1, 20, 5, 1, 21, 5, 1, 22, 5, 3, 23, 5, 1, 24, 5, 1, 28, 5, 1, 29, 5, 1, 30, 5, 0, 31, 5, 1, 32, 5, 1, 44, 5, 1, 45, 5, 3, 46, 5, 1, 48, 5, 1, 49, 5, 0, 50, 5, 0, 51, 5, 0, 52, 5, 0, 53, 5, 0, 54, 5, 1, 57, 5, 1, 58, 5, 0, 59, 5, 0, 60, 5, 0, 61, 5, 0, 62, 5, 0, 63, 5, 1, 68, 5, 1, 69, 5, 1, 70, 5, 1, 71, 5, 1, 72, 5, 1, 1, 6, 1, 2, 6, 0, 3, 6, 0, 4, 6, 0, 5, 6, 3, 6, 6, 3, 7, 6, 3, 8, 6, 3, 9, 6, 3, 10, 6, 2, 11, 6, 0, 12, 6, 0, 13, 6, 0, 14, 6, 0, 15, 6, 0, 16, 6, 0, 17, 6, 1, 18, 6, 1, 19, 6, 1, 21, 6, 1, 22, 6, 3, 23, 6, 1, 29, 6, 1, 30, 6, 1, 31, 6, 1, 44, 6, 1, 45, 6, 3, 46, 6, 1, 48, 6, 1, 49, 6, 0, 50, 6, 0, 51, 6, 0, 52, 6, 0, 53, 6, 0, 54, 6, 1, 56, 6, 1, 57, 6, 1, 58, 6, 0, 59, 6, 0, 60, 6, 0, 61, 6, 0, 62, 6, 0, 63, 6, 1, 64, 6, 1, 67, 6, 1, 68, 6, 1, 69, 6, 0, 70, 6, 1, 71, 6, 0, 72, 6, 1, 73, 6, 1, 1, 7, 1, 2, 7, 0, 3, 7, 0, 4, 7, 0, 5, 7, 1, 6, 7, 1, 7, 7, 1, 8, 7, 1, 9, 7, 1, 10, 7, 1, 11, 7, 0, 12, 7, 0, 13, 7, 0, 14, 7, 0, 15, 7, 0, 16, 7, 0, 17, 7, 1, 21, 7, 1, 22, 7, 3, 23, 7, 1, 28, 7, 1, 29, 7, 1, 30, 7, 1, 31, 7, 1, 32, 7, 1, 33, 7, 1, 34, 7, 1, 35, 7, 1, 36, 7, 1, 44, 7, 1, 45, 7, 3, 46, 7, 1, 48, 7, 1, 49, 7, 3, 50, 7, 1, 51, 7, 1, 52, 7, 1, 53, 7, 1, 54, 7, 1, 56, 7, 1, 57, 7, 0, 58, 7, 0, 59, 7, 0, 60, 7, 0, 61, 7, 0, 62, 7, 0, 63, 7, 0, 64, 7, 1, 66, 7, 1, 67, 7, 1, 68, 7, 0, 69, 7, 0, 70, 7, 0, 71, 7, 0, 72, 7, 0, 73, 7, 1, 74, 7, 1, 1, 8, 1, 2, 8, 1, 3, 8, 2, 4, 8, 1, 5, 8, 1, 10, 8, 1, 11, 8, 0, 12, 8, 0, 13, 8, 0, 14, 8, 0, 15, 8, 0, 16, 8, 0, 17, 8, 1, 21, 8, 1, 22, 8, 3, 23, 8, 1, 28, 8, 1, 29, 8, 0, 30, 8, 0, 31, 8, 0, 32, 8, 0, 33, 8, 0, 34, 8, 0, 35, 8, 0, 36, 8, 2, 44, 8, 1, 45, 8, 2, 46, 8, 1, 47, 8, 1, 48, 8, 1, 49, 8, 2, 50, 8, 1, 51, 8, 1, 52, 8, 1, 53, 8, 1, 54, 8, 1, 55, 8, 1, 56, 8, 1, 57, 8, 1, 58, 8, 0, 59, 8, 0, 60, 8, 0, 61, 8, 0, 62, 8, 0, 63, 8, 1, 64, 8, 1, 66, 8, 1, 67, 8, 0, 68, 8, 0, 69, 8, 0, 70, 8, 0, 71, 8, 0, 72, 8, 0, 73, 8, 0, 74, 8, 1, 2, 9, 1, 3, 9, 3, 4, 9, 1, 5, 9, 1, 6, 9, 1, 7, 9, 1, 8, 9, 1, 10, 9, 1, 11, 9, 1, 12, 9, 2, 13, 9, 1, 14, 9, 1, 15, 9, 1, 16, 9, 3, 17, 9, 1, 18, 9, 1, 19, 9, 1, 20, 9, 1, 21, 9, 1, 22, 9, 2, 23, 9, 1, 24, 9, 1, 25, 9, 1, 26, 9, 1, 27, 9, 1, 28, 9, 1, 29, 9, 0, 30, 9, 0, 31, 9, 0, 32, 9, 0, 33, 9, 0, 34, 9, 0, 35, 9, 0, 36, 9, 1, 44, 9, 1, 45, 9, 0, 46, 9, 0, 47, 9, 0, 48, 9, 0, 49, 9, 0, 50, 9, 0, 51, 9, 0, 52, 9, 0, 53, 9, 3, 54, 9, 3, 55, 9, 3, 56, 9, 3, 57, 9, 2, 58, 9, 0, 59, 9, 0, 60, 9, 0, 61, 9, 0, 62, 9, 0, 63, 9, 1, 66, 9, 1, 67, 9, 1, 68, 9, 0, 69, 9, 0, 70, 9, 0, 71, 9, 0, 72, 9, 0, 73, 9, 1, 74, 9, 1, 2, 10, 1, 3, 10, 0, 4, 10, 0, 5, 10, 0, 6, 10, 0, 7, 10, 0, 8, 10, 1, 11, 10, 1, 12, 10, 3, 13, 10, 1, 15, 10, 1, 16, 10, 3, 17, 10, 1, 18, 10, 1, 19, 10, 0, 20, 10, 0, 21, 10, 0, 22, 10, 0, 23, 10, 0, 24, 10, 0, 25, 10, 0, 26, 10, 0, 27, 10, 2, 28, 10, 3, 29, 10, 0, 30, 10, 0, 31, 10, 0, 32, 10, 0, 33, 10, 0, 34, 10, 0, 35, 10, 0, 36, 10, 1, 37, 10, 2, 38, 10, 1, 39, 10, 0, 40, 10, 1, 41, 10, 1, 42, 10, 1, 44, 10, 1, 45, 10, 0, 46, 10, 0, 47, 10, 0, 48, 10, 0, 49, 10, 0, 50, 10, 0, 51, 10, 0, 52, 10, 0, 53, 10, 1, 54, 10, 1, 55, 10, 1, 56, 10, 1, 57, 10, 1, 58, 10, 1, 59, 10, 3, 60, 10, 0, 61, 10, 1, 62, 10, 1, 63, 10, 1, 67, 10, 1, 68, 10, 1, 69, 10, 0, 70, 10, 1, 71, 10, 0, 72, 10, 1, 73, 10, 1, 2, 11, 1, 3, 11, 0, 4, 11, 0, 5, 11, 0, 6, 11, 0, 7, 11, 9, 8, 11, 1, 9, 11, 1, 10, 11, 1, 11, 11, 1, 12, 11, 3, 13, 11, 1, 15, 11, 1, 16, 11, 3, 17, 11, 1, 18, 11, 1, 19, 11, 0, 20, 11, 0, 21, 11, 0, 22, 11, 0, 23, 11, 0, 24, 11, 0, 25, 11, 0, 26, 11, 0, 27, 11, 1, 28, 11, 1, 29, 11, 1, 30, 11, 1, 31, 11, 1, 32, 11, 1, 33, 11, 1, 34, 11, 1, 35, 11, 1, 36, 11, 1, 37, 11, 0, 38, 11, 0, 39, 11, 0, 40, 11, 0, 41, 11, 0, 42, 11, 1, 43, 11, 1, 44, 11, 1, 45, 11, 0, 46, 11, 0, 47, 11, 0, 48, 11, 0, 49, 11, 0, 50, 11, 0, 51, 11, 0, 52, 11, 0, 53, 11, 1, 58, 11, 1, 59, 11, 3, 60, 11, 1, 61, 11, 1, 68, 11, 1, 69, 11, 1, 70, 11, 1, 71, 11, 3, 72, 11, 1, 2, 12, 1, 3, 12, 0, 4, 12, 0, 5, 12, 0, 6, 12, 0, 7, 12, 1, 8, 12, 1, 9, 12, 1, 10, 12, 0, 11, 12, 1, 12, 12, 3, 13, 12, 1, 15, 12, 1, 16, 12, 3, 17, 12, 1, 18, 12, 1, 19, 12, 0, 20, 12, 0, 21, 12, 0, 22, 12, 0, 23, 12, 0, 24, 12, 0, 25, 12, 0, 26, 12, 0, 27, 12, 3, 28, 12, 3, 29, 12, 3, 30, 12, 3, 31, 12, 3, 32, 12, 3, 33, 12, 3, 34, 12, 3, 35, 12, 2, 36, 12, 0, 37, 12, 0, 38, 12, 0, 39, 12, 0, 40, 12, 0, 41, 12, 0, 42, 12, 0, 43, 12, 1, 44, 12, 1, 45, 12, 1, 46, 12, 1, 47, 12, 1, 48, 12, 1, 49, 12, 1, 50, 12, 1, 51, 12, 1, 52, 12, 1, 53, 12, 1, 58, 12, 1, 59, 12, 3, 60, 12, 1, 68, 12, 1, 69, 12, 1, 70, 12, 0, 71, 12, 2, 72, 12, 1, 2, 13, 1, 3, 13, 1, 4, 13, 1, 5, 13, 1, 6, 13, 1, 7, 13, 1, 8, 13, 0, 9, 13, 0, 10, 13, 0, 11, 13, 0, 12, 13, 3, 13, 13, 1, 15, 13, 1, 16, 13, 3, 17, 13, 1, 18, 13, 1, 19, 13, 1, 20, 13, 1, 21, 13, 1, 22, 13, 2, 23, 13, 1, 24, 13, 2, 25, 13, 1, 26, 13, 1, 27, 13, 1, 28, 13, 1, 29, 13, 1, 30, 13, 1, 31, 13, 1, 32, 13, 1, 33, 13, 1, 34, 13, 1, 35, 13, 1, 36, 13, 0, 37, 13, 0, 38, 13, 0, 39, 13, 0, 40, 13, 0, 41, 13, 0, 42, 13, 0, 43, 13, 1, 44, 13, 1, 58, 13, 1, 59, 13, 3, 60, 13, 1, 67, 13, 1, 68, 13, 1, 69, 13, 0, 70, 13, 0, 71, 13, 0, 72, 13, 1, 73, 13, 1, 6, 14, 1, 7, 14, 0, 8, 14, 0, 9, 14, 0, 10, 14, 0, 11, 14, 0, 12, 14, 0, 13, 14, 1, 15, 14, 1, 16, 14, 2, 17, 14, 1, 18, 14, 0, 19, 14, 0, 20, 14, 0, 21, 14, 1, 22, 14, 3, 23, 14, 1, 24, 14, 0, 25, 14, 0, 26, 14, 0, 27, 14, 0, 28, 14, 0, 29, 14, 1, 30, 14, 1, 31, 14, 1, 32, 14, 1, 33, 14, 1, 34, 14, 1, 35, 14, 0, 36, 14, 0, 37, 14, 0, 38, 14, 0, 39, 14, 8, 40, 14, 0, 41, 14, 0, 42, 14, 0, 43, 14, 0, 44, 14, 1, 49, 14, 1, 50, 14, 1, 51, 14, 1, 52, 14, 1, 53, 14, 1, 54, 14, 1, 55, 14, 1, 56, 14, 1, 57, 14, 1, 58, 14, 1, 59, 14, 2, 60, 14, 1, 61, 14, 1, 62, 14, 1, 63, 14, 1, 64, 14, 1, 65, 14, 1, 66, 14, 1, 67, 14, 1, 68, 14, 0, 69, 14, 0, 70, 14, 0, 71, 14, 0, 72, 14, 0, 73, 14, 1, 6, 15, 1, 7, 15, 1, 8, 15, 0, 9, 15, 0, 10, 15, 0, 11, 15, 0, 12, 15, 0, 13, 15, 1, 14, 15, 1, 15, 15, 1, 16, 15, 0, 17, 15, 0, 18, 15, 0, 19, 15, 0, 20, 15, 0, 21, 15, 0, 22, 15, 0, 23, 15, 1, 24, 15, 0, 25, 15, 0, 26, 15, 0, 27, 15, 0, 28, 15, 0, 29, 15, 3, 30, 15, 3, 31, 15, 3, 32, 15, 3, 33, 15, 3, 34, 15, 3, 35, 15, 2, 36, 15, 0, 37, 15, 0, 38, 15, 0, 39, 15, 0, 40, 15, 0, 41, 15, 0, 42, 15, 0, 43, 15, 1, 44, 15, 1, 49, 15, 1, 50, 15, 0, 51, 15, 0, 52, 15, 0, 53, 15, 0, 54, 15, 2, 55, 15, 3, 56, 15, 3, 57, 15, 3, 58, 15, 3, 59, 15, 0, 60, 15, 0, 61, 15, 0, 62, 15, 0, 63, 15, 0, 64, 15, 2, 65, 15, 3, 66, 15, 3, 67, 15, 0, 68, 15, 0, 69, 15, 0, 70, 15, 0, 71, 15, 0, 72, 15, 1, 73, 15, 1, 7, 16, 1, 8, 16, 1, 9, 16, 0, 10, 16, 0, 11, 16, 0, 12, 16, 0, 13, 16, 0, 14, 16, 1, 15, 16, 1, 16, 16, 0, 17, 16, 0, 18, 16, 0, 19, 16, 0, 20, 16, 0, 21, 16, 0, 22, 16, 0, 23, 16, 1, 24, 16, 0, 25, 16, 0, 26, 16, 0, 27, 16, 0, 28, 16, 0, 29, 16, 1, 30, 16, 1, 31, 16, 1, 32, 16, 1, 33, 16, 1, 34, 16, 1, 35, 16, 1, 36, 16, 0, 37, 16, 0, 38, 16, 0, 39, 16, 0, 40, 16, 0, 41, 16, 0, 42, 16, 0, 43, 16, 1, 44, 16, 1, 45, 16, 1, 46, 16, 1, 47, 16, 1, 48, 16, 1, 49, 16, 1, 50, 16, 0, 51, 16, 0, 52, 16, 0, 53, 16, 0, 54, 16, 1, 55, 16, 1, 56, 16, 1, 57, 16, 1, 58, 16, 1, 59, 16, 0, 60, 16, 0, 61, 16, 0, 62, 16, 0, 63, 16, 0, 64, 16, 1, 65, 16, 1, 66, 16, 1, 67, 16, 0, 68, 16, 0, 69, 16, 0, 70, 16, 0, 71, 16, 0, 72, 16, 1, 8, 17, 1, 9, 17, 1, 10, 17, 0, 11, 17, 0, 12, 17, 0, 13, 17, 1, 14, 17, 1, 15, 17, 1, 16, 17, 0, 17, 17, 0, 18, 17, 0, 19, 17, 0, 20, 17, 0, 21, 17, 0, 22, 17, 0, 23, 17, 1, 24, 17, 1, 25, 17, 1, 26, 17, 1, 27, 17, 1, 28, 17, 1, 29, 17, 1, 35, 17, 1, 36, 17, 1, 37, 17, 0, 38, 17, 0, 39, 17, 0, 40, 17, 0, 41, 17, 0, 42, 17, 2, 43, 17, 3, 44, 17, 3, 45, 17, 3, 46, 17, 3, 47, 17, 3, 48, 17, 3, 49, 17, 3, 50, 17, 0, 51, 17, 0, 52, 17, 0, 53, 17, 0, 54, 17, 1, 58, 17, 1, 59, 17, 0, 60, 17, 0, 61, 17, 0, 62, 17, 0, 63, 17, 0, 64, 17, 1, 66, 17, 1, 67, 17, 1, 68, 17, 0, 69, 17, 0, 70, 17, 0, 71, 17, 2, 72, 17, 1, 9, 18, 1, 10, 18, 1, 11, 18, 0, 12, 18, 1, 13, 18, 1, 15, 18, 1, 16, 18, 1, 17, 18, 1, 18, 18, 0, 19, 18, 0, 20, 18, 0, 21, 18, 1, 22, 18, 1, 23, 18, 1, 27, 18, 1, 28, 18, 1, 29, 18, 1, 30, 18, 1, 31, 18, 1, 36, 18, 1, 37, 18, 1, 38, 18, 1, 39, 18, 0, 40, 18, 1, 41, 18, 1, 42, 18, 1, 43, 18, 1, 44, 18, 1, 45, 18, 1, 46, 18, 1, 47, 18, 1, 48, 18, 1, 49, 18, 1, 50, 18, 0, 51, 18, 0, 52, 18, 0, 53, 18, 0, 54, 18, 1, 58, 18, 1, 59, 18, 1, 60, 18, 2, 61, 18, 1, 62, 18, 1, 63, 18, 1, 64, 18, 1, 67, 18, 1, 68, 18, 1, 69, 18, 0, 70, 18, 1, 71, 18, 3, 72, 18, 1, 2, 19, 1, 3, 19, 1, 4, 19, 1, 5, 19, 1, 6, 19, 1, 7, 19, 1, 10, 19, 1, 11, 19, 1, 12, 19, 1, 17, 19, 1, 18, 19, 1, 19, 19, 1, 20, 19, 1, 21, 19, 1, 25, 19, 1, 26, 19, 1, 27, 19, 1, 28, 19, 0, 29, 19, 0, 30, 19, 0, 31, 19, 1, 32, 19, 1, 33, 19, 1, 38, 19, 1, 39, 19, 2, 40, 19, 1, 49, 19, 1, 50, 19, 1, 51, 19, 2, 52, 19, 1, 53, 19, 1, 54, 19, 1, 57, 19, 1, 58, 19, 1, 59, 19, 1, 60, 19, 3, 61, 19, 1, 62, 19, 1, 68, 19, 1, 69, 19, 1, 70, 19, 1, 71, 19, 3, 72, 19, 1, 2, 20, 1, 3, 20, 0, 4, 20, 0, 5, 20, 0, 6, 20, 0, 7, 20, 1, 15, 20, 1, 16, 20, 1, 17, 20, 1, 25, 20, 1, 26, 20, 0, 27, 20, 0, 28, 20, 0, 29, 20, 0, 30, 20, 0, 31, 20, 0, 32, 20, 0, 33, 20, 1, 34, 20, 1, 35, 20, 1, 36, 20, 1, 37, 20, 1, 38, 20, 1, 39, 20, 3, 40, 20, 1, 41, 20, 1, 50, 20, 1, 51, 20, 3, 52, 20, 1, 57, 20, 1, 58, 20, 0, 59, 20, 0, 60, 20, 0, 61, 20, 0, 62, 20, 1, 70, 20, 1, 71, 20, 3, 72, 20, 1, 2, 21, 1, 3, 21, 0, 4, 21, 0, 5, 21, 0, 6, 21, 0, 7, 21, 1, 8, 21, 1, 9, 21, 1, 10, 21, 1, 11, 21, 1, 12, 21, 1, 13, 21, 1, 14, 21, 1, 15, 21, 1, 16, 21, 0, 17, 21, 1, 18, 21, 1, 19, 21, 1, 20, 21, 1, 21, 21, 1, 22, 21, 1, 23, 21, 1, 24, 21, 1, 25, 21, 1, 26, 21, 0, 27, 21, 0, 28, 21, 0, 29, 21, 0, 30, 21, 0, 31, 21, 0, 32, 21, 0, 33, 21, 3, 34, 21, 3, 35, 21, 3, 36, 21, 2, 37, 21, 0, 38, 21, 0, 39, 21, 0, 40, 21, 0, 41, 21, 1, 50, 21, 1, 51, 21, 3, 52, 21, 1, 53, 21, 1, 54, 21, 1, 57, 21, 1, 58, 21, 0, 59, 21, 0, 60, 21, 0, 61, 21, 0, 62, 21, 1, 67, 21, 1, 68, 21, 1, 69, 21, 1, 70, 21, 1, 71, 21, 3, 72, 21, 1, 2, 22, 1, 3, 22, 0, 4, 22, 0, 5, 22, 0, 6, 22, 0, 7, 22, 3, 8, 22, 3, 9, 22, 3, 10, 22, 3, 11, 22, 3, 12, 22, 3, 13, 22, 3, 14, 22, 2, 15, 22, 0, 16, 22, 0, 17, 22, 0, 18, 22, 3, 19, 22, 3, 20, 22, 3, 21, 22, 3, 22, 22, 3, 23, 22, 3, 24, 22, 3, 25, 22, 2, 26, 22, 0, 27, 22, 0, 28, 22, 0, 29, 22, 0, 30, 22, 0, 31, 22, 0, 32, 22, 0, 33, 22, 1, 34, 22, 1, 35, 22, 1, 36, 22, 1, 37, 22, 0, 38, 22, 0, 39, 22, 0, 40, 22, 0, 41, 22, 1, 50, 22, 1, 51, 22, 0, 52, 22, 0, 53, 22, 0, 54, 22, 1, 57, 22, 1, 58, 22, 0, 59, 22, 0, 60, 22, 0, 61, 22, 0, 62, 22, 1, 67, 22, 1, 68, 22, 0, 69, 22, 0, 70, 22, 0, 71, 22, 0, 72, 22, 1, 2, 23, 1, 3, 23, 1, 4, 23, 1, 5, 23, 1, 6, 23, 2, 7, 23, 1, 8, 23, 1, 9, 23, 1, 10, 23, 1, 11, 23, 1, 12, 23, 1, 13, 23, 1, 14, 23, 1, 15, 23, 1, 16, 23, 0, 17, 23, 2, 18, 23, 1, 19, 23, 1, 20, 23, 1, 21, 23, 1, 22, 23, 1, 23, 23, 1, 24, 23, 1, 25, 23, 1, 26, 23, 1, 27, 23, 1, 28, 23, 0, 29, 23, 0, 30, 23, 0, 31, 23, 1, 32, 23, 1, 33, 23, 1, 36, 23, 1, 37, 23, 0, 38, 23, 0, 39, 23, 0, 40, 23, 0, 41, 23, 1, 50, 23, 1, 51, 23, 0, 52, 23, 0, 53, 23, 0, 54, 23, 1, 57, 23, 1, 58, 23, 0, 59, 23, 0, 60, 23, 0, 61, 23, 0, 62, 23, 1, 67, 23, 1, 68, 23, 0, 69, 23, 0, 70, 23, 0, 71, 23, 0, 72, 23, 1, 5, 24, 1, 6, 24, 3, 7, 24, 1, 15, 24, 1, 16, 24, 1, 17, 24, 3, 18, 24, 1, 19, 24, 1, 20, 24, 1, 21, 24, 1, 22, 24, 1, 27, 24, 1, 28, 24, 1, 29, 24, 1, 30, 24, 1, 31, 24, 1, 36, 24, 1, 37, 24, 1, 38, 24, 1, 39, 24, 1, 40, 24, 1, 41, 24, 1, 50, 24, 1, 51, 24, 0, 52, 24, 0, 53, 24, 0, 54, 24, 1, 57, 24, 1, 58, 24, 1, 59, 24, 1, 60, 24, 1, 61, 24, 1, 62, 24, 1, 67, 24, 1, 68, 24, 1, 69, 24, 1, 70, 24, 1, 71, 24, 1, 72, 24, 1, 4, 25, 1, 5, 25, 1, 6, 25, 3, 7, 25, 1, 8, 25, 1, 9, 25, 1, 10, 25, 1, 11, 25, 1, 16, 25, 1, 17, 25, 0, 18, 25, 0, 19, 25, 0, 20, 25, 0, 21, 25, 0, 22, 25, 1, 50, 25, 1, 51, 25, 1, 52, 25, 1, 53, 25, 1, 54, 25, 1, 4, 26, 1, 5, 26, 0, 6, 26, 0, 7, 26, 0, 8, 26, 0, 9, 26, 0, 10, 26, 0, 11, 26, 1, 16, 26, 1, 17, 26, 0, 18, 26, 0, 19, 26, 0, 20, 26, 0, 21, 26, 0, 22, 26, 1, 4, 27, 1, 5, 27, 0, 6, 27, 0, 7, 27, 0, 8, 27, 0, 9, 27, 0, 10, 27, 0, 11, 27, 1, 16, 27, 1, 17, 27, 0, 18, 27, 0, 19, 27, 0, 20, 27, 0, 21, 27, 0, 22, 27, 1, 4, 28, 1, 5, 28, 1, 6, 28, 1, 7, 28, 1, 8, 28, 1, 9, 28, 1, 10, 28, 1, 11, 28, 1, 16, 28, 1, 17, 28, 1, 18, 28, 1, 19, 28, 1, 20, 28, 1, 21, 28, 1, 22, 28, 1)
-}
-
-[node name="Level2" type="GridMap" parent="Levels" unique_id=2059516963]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -12, 0)
-mesh_library = SubResource("MeshLibrary_r4kn7")
-cell_size = Vector3(1, 1, 1)
-data = {
-"cells": PackedInt32Array(5, 0, 1, 6, 0, 1, 7, 0, 1, 8, 0, 1, 9, 0, 1, 10, 0, 1, 11, 0, 1, 12, 0, 1, 16, 0, 1, 17, 0, 1, 18, 0, 1, 19, 0, 1, 20, 0, 1, 21, 0, 1, 22, 0, 1, 23, 0, 1, 24, 0, 1, 25, 0, 1, 5, 1, 1, 6, 1, 0, 7, 1, 0, 8, 1, 0, 9, 1, 0, 10, 1, 0, 11, 1, 0, 12, 1, 1, 13, 1, 1, 14, 1, 1, 15, 1, 1, 16, 1, 1, 17, 1, 0, 18, 1, 0, 19, 1, 0, 20, 1, 0, 21, 1, 0, 22, 1, 0, 23, 1, 0, 24, 1, 0, 25, 1, 1, 31, 1, 1, 32, 1, 1, 33, 1, 1, 5, 2, 1, 6, 2, 0, 7, 2, 0, 8, 2, 0, 9, 2, 0, 10, 2, 0, 11, 2, 0, 12, 2, 3, 13, 2, 3, 14, 2, 3, 15, 2, 3, 16, 2, 2, 17, 2, 0, 18, 2, 0, 19, 2, 0, 20, 2, 0, 21, 2, 0, 22, 2, 0, 23, 2, 0, 24, 2, 0, 25, 2, 1, 30, 2, 1, 31, 2, 1, 32, 2, 0, 33, 2, 1, 34, 2, 1, 35, 2, 1, 36, 2, 1, 59, 2, 1, 60, 2, 1, 61, 2, 1, 62, 2, 1, 63, 2, 1, 64, 2, 1, 65, 2, 1, 5, 3, 1, 6, 3, 1, 7, 3, 1, 8, 3, 1, 9, 3, 1, 10, 3, 1, 11, 3, 1, 12, 3, 1, 13, 3, 1, 14, 3, 1, 15, 3, 1, 16, 3, 1, 17, 3, 0, 18, 3, 0, 19, 3, 0, 20, 3, 0, 21, 3, 0, 22, 3, 0, 23, 3, 0, 24, 3, 0, 25, 3, 1, 30, 3, 1, 31, 3, 0, 32, 3, 0, 33, 3, 0, 34, 3, 0, 35, 3, 0, 36, 3, 1, 37, 3, 1, 38, 3, 1, 39, 3, 1, 40, 3, 1, 41, 3, 1, 42, 3, 1, 43, 3, 1, 44, 3, 1, 45, 3, 1, 46, 3, 1, 53, 3, 1, 54, 3, 1, 55, 3, 1, 56, 3, 1, 57, 3, 1, 58, 3, 1, 59, 3, 1, 60, 3, 0, 61, 3, 0, 62, 3, 0, 63, 3, 0, 64, 3, 0, 65, 3, 1, 16, 4, 1, 17, 4, 0, 18, 4, 0, 19, 4, 0, 20, 4, 0, 21, 4, 0, 22, 4, 0, 23, 4, 0, 24, 4, 0, 25, 4, 1, 30, 4, 1, 31, 4, 1, 32, 4, 0, 33, 4, 0, 34, 4, 0, 35, 4, 0, 36, 4, 0, 37, 4, 1, 38, 4, 1, 39, 4, 0, 40, 4, 0, 41, 4, 0, 42, 4, 0, 43, 4, 0, 44, 4, 0, 45, 4, 0, 46, 4, 1, 52, 4, 1, 53, 4, 1, 54, 4, 0, 55, 4, 1, 56, 4, 1, 57, 4, 0, 58, 4, 1, 59, 4, 1, 60, 4, 0, 61, 4, 0, 62, 4, 0, 63, 4, 0, 64, 4, 0, 65, 4, 1, 66, 4, 1, 67, 4, 1, 68, 4, 1, 69, 4, 1, 70, 4, 1, 71, 4, 1, 72, 4, 1, 73, 4, 1, 74, 4, 1, 16, 5, 1, 17, 5, 1, 18, 5, 1, 19, 5, 1, 20, 5, 1, 21, 5, 1, 22, 5, 3, 23, 5, 1, 24, 5, 1, 25, 5, 1, 31, 5, 1, 32, 5, 0, 33, 5, 0, 34, 5, 0, 35, 5, 0, 36, 5, 0, 37, 5, 1, 38, 5, 1, 39, 5, 0, 40, 5, 0, 41, 5, 0, 42, 5, 0, 43, 5, 0, 44, 5, 0, 45, 5, 0, 46, 5, 1, 47, 5, 1, 48, 5, 1, 49, 5, 1, 50, 5, 1, 51, 5, 1, 52, 5, 1, 53, 5, 0, 54, 5, 0, 55, 5, 0, 56, 5, 0, 57, 5, 0, 58, 5, 0, 59, 5, 2, 60, 5, 0, 61, 5, 0, 62, 5, 0, 63, 5, 0, 64, 5, 0, 65, 5, 2, 66, 5, 3, 67, 5, 3, 68, 5, 3, 69, 5, 3, 70, 5, 3, 71, 5, 3, 72, 5, 3, 73, 5, 0, 74, 5, 1, 75, 5, 1, 19, 6, 1, 20, 6, 1, 21, 6, 1, 22, 6, 2, 23, 6, 1, 24, 6, 1, 25, 6, 1, 26, 6, 1, 31, 6, 1, 32, 6, 1, 33, 6, 0, 34, 6, 0, 35, 6, 0, 36, 6, 1, 37, 6, 1, 38, 6, 1, 39, 6, 0, 40, 6, 0, 41, 6, 0, 42, 6, 0, 43, 6, 0, 44, 6, 0, 45, 6, 0, 46, 6, 2, 47, 6, 3, 48, 6, 3, 49, 6, 3, 50, 6, 3, 51, 6, 3, 52, 6, 0, 53, 6, 0, 54, 6, 0, 55, 6, 0, 56, 6, 0, 57, 6, 0, 58, 6, 1, 59, 6, 1, 60, 6, 0, 61, 6, 0, 62, 6, 0, 63, 6, 0, 64, 6, 0, 65, 6, 1, 66, 6, 1, 67, 6, 1, 68, 6, 1, 69, 6, 1, 70, 6, 1, 71, 6, 1, 72, 6, 0, 73, 6, 0, 74, 6, 0, 75, 6, 1, 76, 6, 1, 3, 7, 1, 4, 7, 1, 5, 7, 1, 6, 7, 1, 7, 7, 1, 8, 7, 1, 9, 7, 1, 10, 7, 1, 11, 7, 1, 12, 7, 1, 13, 7, 1, 14, 7, 1, 15, 7, 1, 16, 7, 1, 17, 7, 1, 18, 7, 1, 19, 7, 1, 20, 7, 0, 21, 7, 0, 22, 7, 0, 23, 7, 0, 24, 7, 0, 25, 7, 0, 26, 7, 1, 32, 7, 1, 33, 7, 3, 34, 7, 0, 35, 7, 1, 36, 7, 1, 38, 7, 1, 39, 7, 3, 40, 7, 1, 41, 7, 1, 42, 7, 1, 43, 7, 1, 44, 7, 1, 45, 7, 1, 46, 7, 1, 47, 7, 1, 48, 7, 1, 49, 7, 1, 50, 7, 1, 51, 7, 1, 52, 7, 1, 53, 7, 0, 54, 7, 0, 55, 7, 0, 56, 7, 1, 57, 7, 1, 58, 7, 1, 59, 7, 1, 60, 7, 1, 61, 7, 1, 62, 7, 1, 63, 7, 3, 64, 7, 1, 65, 7, 1, 66, 7, 0, 67, 7, 1, 68, 7, 1, 69, 7, 1, 70, 7, 1, 71, 7, 0, 72, 7, 0, 73, 7, 0, 74, 7, 0, 75, 7, 0, 76, 7, 1, 3, 8, 1, 4, 8, 0, 5, 8, 0, 6, 8, 0, 7, 8, 0, 8, 8, 0, 9, 8, 0, 10, 8, 3, 11, 8, 3, 12, 8, 3, 13, 8, 3, 14, 8, 3, 15, 8, 3, 16, 8, 3, 17, 8, 3, 18, 8, 3, 19, 8, 2, 20, 8, 0, 21, 8, 0, 22, 8, 0, 23, 8, 0, 24, 8, 0, 25, 8, 0, 26, 8, 1, 32, 8, 1, 33, 8, 3, 34, 8, 1, 35, 8, 1, 38, 8, 1, 39, 8, 3, 40, 8, 1, 42, 8, 1, 43, 8, 1, 44, 8, 1, 45, 8, 1, 51, 8, 1, 52, 8, 1, 53, 8, 0, 54, 8, 0, 55, 8, 0, 56, 8, 1, 57, 8, 1, 62, 8, 1, 63, 8, 3, 64, 8, 0, 65, 8, 0, 66, 8, 0, 67, 8, 0, 68, 8, 0, 69, 8, 1, 70, 8, 1, 71, 8, 1, 72, 8, 0, 73, 8, 0, 74, 8, 0, 75, 8, 1, 76, 8, 1, 3, 9, 1, 4, 9, 0, 5, 9, 0, 6, 9, 0, 7, 9, 0, 8, 9, 0, 9, 9, 0, 10, 9, 1, 11, 9, 1, 12, 9, 1, 13, 9, 1, 14, 9, 1, 15, 9, 1, 16, 9, 1, 17, 9, 1, 18, 9, 1, 19, 9, 1, 20, 9, 0, 21, 9, 0, 22, 9, 0, 23, 9, 0, 24, 9, 0, 25, 9, 0, 26, 9, 1, 32, 9, 1, 33, 9, 3, 34, 9, 1, 38, 9, 1, 39, 9, 3, 40, 9, 1, 41, 9, 1, 42, 9, 1, 43, 9, 0, 44, 9, 0, 45, 9, 1, 46, 9, 1, 47, 9, 1, 48, 9, 1, 49, 9, 1, 50, 9, 1, 51, 9, 1, 52, 9, 0, 53, 9, 0, 54, 9, 0, 55, 9, 0, 56, 9, 0, 57, 9, 1, 58, 9, 1, 59, 9, 1, 60, 9, 1, 61, 9, 1, 62, 9, 1, 63, 9, 2, 64, 9, 0, 65, 9, 0, 66, 9, 0, 67, 9, 0, 68, 9, 0, 69, 9, 1, 70, 9, 1, 71, 9, 1, 72, 9, 1, 73, 9, 0, 74, 9, 2, 75, 9, 1, 3, 10, 1, 4, 10, 0, 5, 10, 0, 6, 10, 0, 7, 10, 0, 8, 10, 0, 9, 10, 0, 10, 10, 1, 19, 10, 1, 20, 10, 1, 21, 10, 1, 22, 10, 1, 23, 10, 3, 24, 10, 1, 25, 10, 1, 26, 10, 1, 32, 10, 1, 33, 10, 3, 34, 10, 1, 38, 10, 1, 39, 10, 3, 40, 10, 1, 41, 10, 1, 42, 10, 0, 43, 10, 0, 44, 10, 0, 45, 10, 0, 46, 10, 3, 47, 10, 3, 48, 10, 3, 49, 10, 3, 50, 10, 3, 51, 10, 3, 52, 10, 2, 53, 10, 0, 54, 10, 0, 55, 10, 0, 56, 10, 2, 57, 10, 3, 58, 10, 3, 59, 10, 3, 60, 10, 3, 61, 10, 3, 62, 10, 3, 63, 10, 0, 64, 10, 0, 65, 10, 0, 66, 10, 0, 67, 10, 0, 68, 10, 0, 69, 10, 0, 70, 10, 1, 72, 10, 1, 73, 10, 1, 74, 10, 3, 75, 10, 1, 3, 11, 1, 4, 11, 1, 5, 11, 1, 6, 11, 1, 7, 11, 1, 8, 11, 1, 9, 11, 1, 10, 11, 1, 19, 11, 1, 20, 11, 1, 21, 11, 1, 22, 11, 1, 23, 11, 2, 24, 11, 1, 32, 11, 1, 33, 11, 3, 34, 11, 1, 38, 11, 1, 39, 11, 3, 40, 11, 1, 41, 11, 1, 42, 11, 0, 43, 11, 0, 44, 11, 0, 45, 11, 0, 46, 11, 1, 47, 11, 1, 48, 11, 1, 49, 11, 1, 50, 11, 1, 51, 11, 1, 52, 11, 1, 53, 11, 1, 54, 11, 0, 55, 11, 1, 56, 11, 1, 57, 11, 1, 58, 11, 1, 59, 11, 1, 60, 11, 1, 61, 11, 1, 62, 11, 1, 63, 11, 1, 64, 11, 0, 65, 11, 0, 66, 11, 0, 67, 11, 0, 68, 11, 0, 69, 11, 1, 70, 11, 1, 71, 11, 1, 72, 11, 1, 73, 11, 1, 74, 11, 3, 75, 11, 1, 76, 11, 1, 77, 11, 1, 78, 11, 1, 9, 12, 1, 10, 12, 1, 11, 12, 1, 12, 12, 1, 13, 12, 1, 14, 12, 1, 15, 12, 1, 16, 12, 1, 17, 12, 1, 18, 12, 1, 19, 12, 1, 20, 12, 0, 21, 12, 0, 22, 12, 0, 23, 12, 0, 24, 12, 1, 25, 12, 1, 31, 12, 1, 32, 12, 1, 33, 12, 2, 34, 12, 1, 35, 12, 1, 36, 12, 1, 37, 12, 1, 38, 12, 1, 39, 12, 2, 40, 12, 1, 41, 12, 1, 42, 12, 1, 43, 12, 0, 44, 12, 0, 45, 12, 1, 46, 12, 1, 47, 12, 1, 48, 12, 1, 49, 12, 1, 50, 12, 1, 51, 12, 1, 52, 12, 1, 53, 12, 1, 54, 12, 1, 55, 12, 1, 63, 12, 1, 64, 12, 0, 65, 12, 0, 66, 12, 0, 67, 12, 0, 68, 12, 0, 69, 12, 1, 71, 12, 1, 72, 12, 0, 73, 12, 0, 74, 12, 0, 75, 12, 0, 76, 12, 0, 77, 12, 0, 78, 12, 1, 3, 13, 1, 4, 13, 1, 5, 13, 1, 6, 13, 1, 7, 13, 1, 9, 13, 1, 10, 13, 0, 11, 13, 0, 12, 13, 0, 13, 13, 0, 14, 13, 0, 15, 13, 0, 16, 13, 0, 17, 13, 0, 18, 13, 1, 19, 13, 1, 20, 13, 0, 21, 13, 0, 22, 13, 0, 23, 13, 0, 24, 13, 0, 25, 13, 1, 26, 13, 1, 27, 13, 1, 28, 13, 1, 29, 13, 1, 30, 13, 1, 31, 13, 1, 32, 13, 0, 33, 13, 0, 34, 13, 0, 35, 13, 0, 36, 13, 1, 37, 13, 1, 38, 13, 0, 39, 13, 0, 40, 13, 0, 41, 13, 1, 42, 13, 1, 43, 13, 1, 44, 13, 1, 45, 13, 1, 47, 13, 1, 48, 13, 0, 49, 13, 0, 50, 13, 0, 51, 13, 0, 52, 13, 1, 63, 13, 1, 64, 13, 1, 65, 13, 2, 66, 13, 0, 67, 13, 1, 68, 13, 1, 69, 13, 1, 71, 13, 1, 72, 13, 0, 73, 13, 0, 74, 13, 0, 75, 13, 0, 76, 13, 0, 77, 13, 0, 78, 13, 1, 3, 14, 1, 4, 14, 0, 5, 14, 0, 6, 14, 0, 7, 14, 1, 8, 14, 1, 9, 14, 1, 10, 14, 0, 11, 14, 7, 12, 14, 7, 13, 14, 0, 14, 14, 0, 15, 14, 0, 16, 14, 0, 17, 14, 0, 18, 14, 3, 19, 14, 2, 20, 14, 0, 21, 14, 0, 22, 14, 0, 23, 14, 0, 24, 14, 0, 25, 14, 3, 26, 14, 3, 27, 14, 3, 28, 14, 3, 29, 14, 3, 30, 14, 3, 31, 14, 2, 32, 14, 0, 33, 14, 0, 34, 14, 0, 35, 14, 0, 36, 14, 3, 37, 14, 2, 38, 14, 0, 39, 14, 8, 40, 14, 0, 41, 14, 1, 42, 14, 1, 43, 14, 1, 44, 14, 1, 45, 14, 1, 46, 14, 1, 47, 14, 1, 48, 14, 0, 49, 14, 0, 50, 14, 0, 51, 14, 0, 52, 14, 1, 63, 14, 1, 64, 14, 1, 65, 14, 3, 66, 14, 1, 67, 14, 1, 71, 14, 1, 72, 14, 0, 73, 14, 0, 74, 14, 0, 75, 14, 0, 76, 14, 0, 77, 14, 0, 78, 14, 1, 3, 15, 1, 4, 15, 0, 5, 15, 0, 6, 15, 0, 7, 15, 3, 8, 15, 3, 9, 15, 2, 10, 15, 0, 11, 15, 7, 12, 15, 7, 13, 15, 0, 14, 15, 0, 15, 15, 0, 16, 15, 0, 17, 15, 0, 18, 15, 1, 19, 15, 1, 20, 15, 0, 21, 15, 0, 22, 15, 0, 23, 15, 0, 24, 15, 1, 25, 15, 1, 26, 15, 1, 27, 15, 1, 28, 15, 1, 29, 15, 1, 30, 15, 1, 31, 15, 1, 32, 15, 1, 33, 15, 1, 34, 15, 1, 35, 15, 2, 36, 15, 1, 37, 15, 1, 38, 15, 0, 39, 15, 0, 40, 15, 0, 41, 15, 2, 42, 15, 3, 43, 15, 3, 44, 15, 3, 45, 15, 3, 46, 15, 3, 47, 15, 3, 48, 15, 0, 49, 15, 0, 50, 15, 0, 51, 15, 0, 52, 15, 1, 62, 15, 1, 63, 15, 1, 64, 15, 0, 65, 15, 3, 66, 15, 1, 71, 15, 1, 72, 15, 0, 73, 15, 0, 74, 15, 0, 75, 15, 0, 76, 15, 0, 77, 15, 0, 78, 15, 1, 3, 16, 1, 4, 16, 0, 5, 16, 0, 6, 16, 0, 7, 16, 1, 8, 16, 1, 9, 16, 7, 12, 16, 1, 13, 16, 1, 14, 16, 1, 15, 16, 1, 16, 16, 2, 17, 16, 1, 18, 16, 1, 19, 16, 1, 20, 16, 1, 21, 16, 2, 22, 16, 1, 23, 16, 1, 24, 16, 1, 34, 16, 1, 35, 16, 3, 36, 16, 1, 37, 16, 1, 38, 16, 1, 39, 16, 1, 40, 16, 1, 41, 16, 1, 42, 16, 1, 43, 16, 1, 44, 16, 1, 45, 16, 1, 46, 16, 1, 47, 16, 1, 48, 16, 1, 49, 16, 2, 50, 16, 1, 51, 16, 1, 52, 16, 1, 53, 16, 1, 54, 16, 1, 55, 16, 1, 56, 16, 1, 57, 16, 1, 58, 16, 1, 59, 16, 1, 60, 16, 1, 61, 16, 1, 62, 16, 1, 63, 16, 0, 64, 16, 0, 65, 16, 0, 66, 16, 1, 67, 16, 1, 71, 16, 1, 72, 16, 1, 73, 16, 1, 74, 16, 1, 75, 16, 2, 76, 16, 1, 77, 16, 1, 78, 16, 1, 3, 17, 1, 4, 17, 1, 5, 17, 1, 6, 17, 1, 7, 17, 1, 9, 17, 7, 15, 17, 1, 16, 17, 0, 17, 17, 0, 18, 17, 0, 19, 17, 0, 20, 17, 1, 21, 17, 3, 22, 17, 1, 34, 17, 1, 35, 17, 3, 36, 17, 1, 46, 17, 1, 47, 17, 1, 48, 17, 1, 49, 17, 3, 50, 17, 1, 51, 17, 1, 52, 17, 0, 53, 17, 0, 54, 17, 3, 55, 17, 3, 56, 17, 3, 57, 17, 3, 58, 17, 3, 59, 17, 3, 60, 17, 3, 61, 17, 2, 62, 17, 0, 63, 17, 0, 64, 17, 0, 65, 17, 0, 66, 17, 0, 67, 17, 1, 72, 17, 1, 73, 17, 1, 74, 17, 1, 75, 17, 3, 76, 17, 1, 7, 18, 1, 8, 18, 1, 9, 18, 7, 10, 18, 1, 11, 18, 1, 15, 18, 1, 16, 18, 0, 17, 18, 0, 18, 18, 0, 19, 18, 0, 20, 18, 1, 21, 18, 3, 22, 18, 1, 24, 18, 1, 25, 18, 1, 26, 18, 1, 34, 18, 1, 35, 18, 3, 36, 18, 1, 37, 18, 1, 38, 18, 1, 45, 18, 1, 46, 18, 1, 47, 18, 0, 48, 18, 1, 49, 18, 3, 50, 18, 2, 51, 18, 0, 52, 18, 0, 53, 18, 0, 54, 18, 0, 55, 18, 1, 56, 18, 1, 57, 18, 1, 58, 18, 1, 59, 18, 1, 60, 18, 1, 61, 18, 1, 62, 18, 0, 63, 18, 0, 64, 18, 0, 65, 18, 0, 66, 18, 0, 67, 18, 1, 71, 18, 1, 72, 18, 1, 73, 18, 0, 74, 18, 1, 75, 18, 3, 76, 18, 1, 7, 19, 1, 8, 19, 0, 9, 19, 0, 10, 19, 0, 11, 19, 1, 15, 19, 1, 16, 19, 1, 17, 19, 1, 18, 19, 3, 19, 19, 1, 20, 19, 1, 21, 19, 3, 22, 19, 1, 23, 19, 1, 24, 19, 1, 25, 19, 0, 26, 19, 1, 27, 19, 1, 34, 19, 1, 35, 19, 3, 36, 19, 1, 37, 19, 0, 38, 19, 1, 39, 19, 1, 40, 19, 1, 44, 19, 1, 45, 19, 1, 46, 19, 0, 47, 19, 0, 48, 19, 0, 49, 19, 3, 50, 19, 1, 51, 19, 0, 52, 19, 0, 53, 19, 0, 54, 19, 0, 55, 19, 1, 59, 19, 1, 60, 19, 1, 61, 19, 0, 62, 19, 0, 63, 19, 0, 64, 19, 0, 65, 19, 0, 66, 19, 1, 67, 19, 1, 70, 19, 1, 71, 19, 1, 72, 19, 0, 73, 19, 0, 74, 19, 0, 75, 19, 3, 76, 19, 1, 7, 20, 1, 8, 20, 0, 9, 20, 0, 10, 20, 0, 11, 20, 1, 16, 20, 1, 17, 20, 1, 18, 20, 2, 19, 20, 1, 20, 20, 1, 21, 20, 3, 22, 20, 1, 23, 20, 1, 24, 20, 0, 25, 20, 0, 26, 20, 0, 27, 20, 1, 28, 20, 1, 34, 20, 1, 35, 20, 0, 36, 20, 0, 37, 20, 0, 38, 20, 0, 39, 20, 0, 40, 20, 1, 44, 20, 1, 45, 20, 0, 46, 20, 0, 47, 20, 0, 48, 20, 0, 49, 20, 0, 50, 20, 1, 51, 20, 1, 52, 20, 0, 53, 20, 0, 54, 20, 2, 55, 20, 1, 58, 20, 1, 59, 20, 1, 60, 20, 0, 61, 20, 0, 62, 20, 0, 63, 20, 1, 64, 20, 0, 65, 20, 1, 66, 20, 1, 70, 20, 1, 71, 20, 0, 72, 20, 0, 73, 20, 0, 74, 20, 0, 75, 20, 0, 76, 20, 1, 7, 21, 1, 8, 21, 0, 9, 21, 0, 10, 21, 0, 11, 21, 1, 12, 21, 1, 13, 21, 1, 14, 21, 1, 15, 21, 1, 16, 21, 1, 17, 21, 0, 18, 21, 0, 19, 21, 0, 20, 21, 0, 21, 21, 0, 22, 21, 1, 23, 21, 0, 24, 21, 0, 25, 21, 0, 26, 21, 0, 27, 21, 0, 28, 21, 1, 33, 21, 1, 34, 21, 1, 35, 21, 0, 36, 21, 0, 37, 21, 0, 38, 21, 0, 39, 21, 0, 40, 21, 1, 41, 21, 1, 43, 21, 1, 44, 21, 1, 45, 21, 0, 46, 21, 0, 47, 21, 0, 48, 21, 0, 49, 21, 1, 50, 21, 1, 51, 21, 1, 52, 21, 1, 53, 21, 1, 54, 21, 3, 55, 21, 1, 56, 21, 1, 58, 21, 1, 59, 21, 0, 60, 21, 0, 61, 21, 0, 62, 21, 0, 63, 21, 0, 64, 21, 1, 65, 21, 1, 70, 21, 1, 71, 21, 1, 72, 21, 0, 73, 21, 0, 74, 21, 0, 75, 21, 1, 76, 21, 1, 7, 22, 1, 8, 22, 0, 9, 22, 0, 10, 22, 0, 11, 22, 3, 12, 22, 3, 13, 22, 3, 14, 22, 3, 15, 22, 3, 16, 22, 2, 17, 22, 0, 18, 22, 0, 19, 22, 0, 20, 22, 0, 21, 22, 0, 22, 22, 1, 23, 22, 1, 24, 22, 0, 25, 22, 0, 26, 22, 0, 27, 22, 1, 28, 22, 1, 33, 22, 1, 34, 22, 0, 35, 22, 0, 36, 22, 0, 37, 22, 0, 38, 22, 0, 39, 22, 0, 40, 22, 0, 41, 22, 1, 43, 22, 1, 44, 22, 0, 45, 22, 0, 46, 22, 0, 47, 22, 0, 48, 22, 0, 49, 22, 1, 51, 22, 1, 52, 22, 1, 53, 22, 0, 54, 22, 0, 55, 22, 0, 56, 22, 1, 57, 22, 1, 58, 22, 1, 59, 22, 1, 60, 22, 0, 61, 22, 0, 62, 22, 0, 63, 22, 2, 64, 22, 1, 65, 22, 1, 71, 22, 1, 72, 22, 1, 73, 22, 0, 74, 22, 1, 75, 22, 1, 7, 23, 1, 8, 23, 1, 9, 23, 1, 10, 23, 1, 11, 23, 1, 12, 23, 1, 13, 23, 1, 14, 23, 1, 15, 23, 1, 16, 23, 1, 17, 23, 0, 18, 23, 0, 19, 23, 0, 20, 23, 0, 21, 23, 0, 22, 23, 2, 23, 23, 3, 24, 23, 3, 25, 23, 0, 26, 23, 1, 27, 23, 1, 33, 23, 1, 34, 23, 1, 35, 23, 0, 36, 23, 0, 37, 23, 0, 38, 23, 0, 39, 23, 0, 40, 23, 1, 41, 23, 1, 43, 23, 1, 44, 23, 1, 45, 23, 0, 46, 23, 0, 47, 23, 0, 48, 23, 1, 49, 23, 1, 51, 23, 1, 52, 23, 0, 53, 23, 0, 54, 23, 0, 55, 23, 0, 56, 23, 0, 57, 23, 1, 59, 23, 1, 60, 23, 1, 61, 23, 0, 62, 23, 1, 63, 23, 3, 64, 23, 0, 65, 23, 1, 66, 23, 1, 67, 23, 1, 72, 23, 1, 73, 23, 2, 74, 23, 1, 16, 24, 1, 17, 24, 0, 18, 24, 0, 19, 24, 0, 20, 24, 0, 21, 24, 0, 22, 24, 1, 23, 24, 1, 24, 24, 1, 25, 24, 1, 26, 24, 1, 34, 24, 1, 35, 24, 0, 36, 24, 0, 37, 24, 0, 38, 24, 0, 39, 24, 0, 40, 24, 1, 44, 24, 1, 45, 24, 1, 46, 24, 0, 47, 24, 1, 48, 24, 1, 51, 24, 1, 52, 24, 0, 53, 24, 0, 54, 24, 0, 55, 24, 0, 56, 24, 0, 57, 24, 1, 60, 24, 1, 61, 24, 1, 62, 24, 1, 63, 24, 0, 64, 24, 0, 65, 24, 0, 66, 24, 0, 67, 24, 1, 68, 24, 1, 69, 24, 1, 70, 24, 1, 71, 24, 1, 72, 24, 1, 73, 24, 3, 74, 24, 1, 16, 25, 1, 17, 25, 1, 18, 25, 1, 19, 25, 1, 20, 25, 1, 21, 25, 1, 22, 25, 1, 34, 25, 1, 35, 25, 1, 36, 25, 1, 37, 25, 0, 38, 25, 1, 39, 25, 1, 40, 25, 1, 45, 25, 1, 46, 25, 1, 47, 25, 1, 51, 25, 1, 52, 25, 0, 53, 25, 0, 54, 25, 0, 55, 25, 0, 56, 25, 0, 57, 25, 1, 62, 25, 1, 63, 25, 1, 64, 25, 0, 65, 25, 0, 66, 25, 0, 67, 25, 0, 68, 25, 1, 69, 25, 1, 70, 25, 0, 71, 25, 0, 72, 25, 0, 73, 25, 0, 74, 25, 1, 36, 26, 1, 37, 26, 1, 38, 26, 1, 51, 26, 1, 52, 26, 1, 53, 26, 0, 54, 26, 0, 55, 26, 0, 56, 26, 1, 57, 26, 1, 63, 26, 1, 64, 26, 1, 65, 26, 1, 66, 26, 0, 67, 26, 1, 68, 26, 1, 69, 26, 1, 70, 26, 9, 71, 26, 0, 72, 26, 0, 73, 26, 0, 74, 26, 1, 52, 27, 1, 53, 27, 1, 54, 27, 1, 55, 27, 1, 56, 27, 1, 65, 27, 1, 66, 27, 1, 67, 27, 1, 69, 27, 1, 70, 27, 1, 71, 27, 1, 72, 27, 1, 73, 27, 1, 74, 27, 1)
-}
-
-[node name="Level3" type="GridMap" parent="Levels" unique_id=875050561]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -18, 0)
-mesh_library = SubResource("MeshLibrary_r4kn7")
-cell_size = Vector3(1, 1, 1)
-data = {
-"cells": PackedInt32Array(4, 0, 1, 5, 0, 1, 6, 0, 1, 7, 0, 1, 8, 0, 1, 9, 0, 1, 4, 1, 1, 5, 1, 0, 6, 1, 0, 7, 1, 0, 8, 1, 0, 9, 1, 1, 4, 2, 1, 5, 2, 0, 6, 2, 0, 7, 2, 0, 8, 2, 0, 9, 2, 1, 55, 2, 1, 56, 2, 1, 57, 2, 1, 58, 2, 1, 59, 2, 1, 66, 2, 1, 67, 2, 1, 68, 2, 1, 4, 3, 1, 5, 3, 1, 6, 3, 1, 7, 3, 1, 8, 3, 3, 9, 3, 1, 21, 3, 1, 22, 3, 1, 23, 3, 1, 24, 3, 1, 25, 3, 1, 55, 3, 1, 56, 3, 0, 57, 3, 0, 58, 3, 0, 59, 3, 1, 60, 3, 1, 61, 3, 1, 62, 3, 1, 63, 3, 1, 64, 3, 1, 65, 3, 1, 66, 3, 1, 67, 3, 0, 68, 3, 1, 69, 3, 1, 5, 4, 1, 6, 4, 1, 7, 4, 1, 8, 4, 2, 9, 4, 1, 10, 4, 1, 11, 4, 1, 12, 4, 1, 18, 4, 1, 19, 4, 1, 20, 4, 1, 21, 4, 1, 22, 4, 0, 23, 4, 0, 24, 4, 0, 25, 4, 1, 26, 4, 1, 27, 4, 1, 28, 4, 1, 45, 4, 1, 46, 4, 1, 47, 4, 1, 48, 4, 1, 55, 4, 1, 56, 4, 0, 57, 4, 0, 58, 4, 0, 59, 4, 3, 60, 4, 3, 61, 4, 3, 62, 4, 2, 63, 4, 0, 64, 4, 1, 65, 4, 1, 66, 4, 0, 67, 4, 0, 68, 4, 0, 69, 4, 1, 5, 5, 1, 6, 5, 0, 7, 5, 0, 8, 5, 0, 9, 5, 0, 10, 5, 0, 11, 5, 0, 12, 5, 1, 18, 5, 1, 19, 5, 0, 20, 5, 0, 21, 5, 0, 22, 5, 0, 23, 5, 0, 24, 5, 0, 25, 5, 0, 26, 5, 0, 27, 5, 0, 28, 5, 1, 29, 5, 1, 30, 5, 1, 31, 5, 1, 32, 5, 1, 33, 5, 1, 34, 5, 1, 35, 5, 1, 36, 5, 1, 37, 5, 1, 38, 5, 1, 39, 5, 1, 45, 5, 0, 46, 5, 0, 47, 5, 0, 48, 5, 1, 55, 5, 1, 56, 5, 1, 57, 5, 1, 58, 5, 1, 59, 5, 1, 60, 5, 1, 61, 5, 1, 62, 5, 0, 63, 5, 0, 64, 5, 0, 65, 5, 1, 66, 5, 1, 67, 5, 0, 68, 5, 1, 69, 5, 1, 5, 6, 1, 6, 6, 0, 7, 6, 0, 8, 6, 0, 9, 6, 0, 10, 6, 0, 11, 6, 0, 12, 6, 1, 13, 6, 1, 14, 6, 1, 15, 6, 1, 16, 6, 1, 17, 6, 1, 18, 6, 1, 19, 6, 0, 20, 6, 0, 21, 6, 0, 22, 6, 0, 23, 6, 0, 24, 6, 0, 25, 6, 0, 26, 6, 0, 27, 6, 0, 28, 6, 3, 29, 6, 3, 30, 6, 3, 31, 6, 3, 32, 6, 3, 33, 6, 2, 34, 6, 0, 35, 6, 0, 36, 6, 0, 37, 6, 0, 38, 6, 0, 39, 6, 1, 45, 6, 0, 46, 6, 0, 47, 6, 0, 48, 6, 1, 52, 6, 1, 53, 6, 1, 54, 6, 1, 55, 6, 1, 56, 6, 1, 57, 6, 1, 58, 6, 1, 59, 6, 1, 60, 6, 1, 61, 6, 0, 62, 6, 0, 63, 6, 0, 64, 6, 0, 65, 6, 0, 66, 6, 0, 67, 6, 0, 68, 6, 0, 69, 6, 1, 70, 6, 1, 5, 7, 1, 6, 7, 0, 7, 7, 0, 8, 7, 0, 9, 7, 0, 10, 7, 0, 11, 7, 0, 12, 7, 3, 13, 7, 3, 14, 7, 3, 15, 7, 3, 16, 7, 3, 17, 7, 3, 18, 7, 2, 19, 7, 0, 20, 7, 0, 21, 7, 0, 22, 7, 0, 23, 7, 0, 24, 7, 0, 25, 7, 0, 26, 7, 0, 27, 7, 0, 28, 7, 1, 29, 7, 1, 30, 7, 1, 31, 7, 1, 32, 7, 1, 33, 7, 1, 34, 7, 0, 35, 7, 0, 36, 7, 0, 37, 7, 0, 38, 7, 0, 39, 7, 1, 44, 7, 1, 45, 7, 0, 46, 7, 0, 47, 7, 0, 48, 7, 1, 49, 7, 1, 50, 7, 1, 51, 7, 1, 52, 7, 1, 53, 7, 0, 54, 7, 0, 55, 7, 0, 56, 7, 2, 57, 7, 3, 58, 7, 3, 59, 7, 3, 60, 7, 3, 61, 7, 0, 62, 7, 0, 63, 7, 0, 64, 7, 0, 65, 7, 0, 66, 7, 0, 67, 7, 0, 68, 7, 0, 69, 7, 0, 70, 7, 1, 5, 8, 1, 6, 8, 0, 7, 8, 0, 8, 8, 0, 9, 8, 0, 10, 8, 0, 11, 8, 0, 12, 8, 1, 13, 8, 1, 14, 8, 1, 15, 8, 1, 16, 8, 1, 17, 8, 1, 18, 8, 1, 19, 8, 1, 20, 8, 1, 21, 8, 1, 22, 8, 0, 23, 8, 0, 24, 8, 0, 25, 8, 1, 26, 8, 1, 27, 8, 1, 28, 8, 1, 33, 8, 1, 34, 8, 0, 35, 8, 0, 36, 8, 0, 37, 8, 0, 38, 8, 0, 39, 8, 2, 40, 8, 7, 41, 8, 7, 42, 8, 7, 43, 8, 7, 44, 8, 3, 45, 8, 0, 46, 8, 0, 47, 8, 0, 48, 8, 2, 49, 8, 3, 50, 8, 3, 51, 8, 3, 52, 8, 3, 53, 8, 0, 54, 8, 0, 55, 8, 0, 56, 8, 1, 57, 8, 1, 58, 8, 1, 59, 8, 1, 60, 8, 1, 61, 8, 1, 62, 8, 0, 63, 8, 0, 64, 8, 0, 65, 8, 1, 66, 8, 0, 67, 8, 0, 68, 8, 0, 69, 8, 2, 70, 8, 1, 5, 9, 1, 6, 9, 1, 7, 9, 1, 8, 9, 1, 9, 9, 1, 10, 9, 1, 11, 9, 1, 12, 9, 1, 21, 9, 1, 22, 9, 1, 23, 9, 1, 24, 9, 1, 25, 9, 1, 33, 9, 1, 34, 9, 0, 35, 9, 0, 36, 9, 0, 37, 9, 0, 38, 9, 0, 39, 9, 1, 44, 9, 1, 45, 9, 1, 46, 9, 2, 47, 9, 1, 48, 9, 1, 49, 9, 1, 50, 9, 1, 51, 9, 1, 52, 9, 1, 53, 9, 0, 54, 9, 0, 55, 9, 0, 56, 9, 1, 61, 9, 1, 62, 9, 1, 63, 9, 0, 64, 9, 1, 65, 9, 1, 66, 9, 1, 67, 9, 0, 68, 9, 1, 69, 9, 3, 70, 9, 1, 3, 10, 1, 4, 10, 1, 5, 10, 1, 6, 10, 1, 7, 10, 1, 8, 10, 1, 9, 10, 1, 33, 10, 1, 34, 10, 1, 35, 10, 1, 36, 10, 1, 37, 10, 3, 38, 10, 1, 39, 10, 1, 43, 10, 1, 44, 10, 1, 45, 10, 1, 46, 10, 3, 47, 10, 1, 48, 10, 1, 49, 10, 1, 52, 10, 1, 53, 10, 1, 54, 10, 1, 55, 10, 1, 56, 10, 1, 62, 10, 1, 63, 10, 1, 64, 10, 1, 66, 10, 1, 67, 10, 1, 68, 10, 1, 69, 10, 3, 70, 10, 1, 71, 10, 1, 72, 10, 1, 73, 10, 1, 74, 10, 1, 75, 10, 1, 76, 10, 1, 77, 10, 1, 3, 11, 1, 4, 11, 0, 5, 11, 0, 6, 11, 0, 7, 11, 0, 8, 11, 0, 9, 11, 1, 12, 11, 1, 13, 11, 1, 14, 11, 1, 15, 11, 1, 16, 11, 1, 17, 11, 1, 18, 11, 1, 19, 11, 1, 26, 11, 1, 27, 11, 1, 28, 11, 1, 36, 11, 1, 37, 11, 2, 38, 11, 1, 39, 11, 1, 40, 11, 1, 41, 11, 1, 42, 11, 1, 43, 11, 0, 44, 11, 0, 45, 11, 0, 46, 11, 0, 47, 11, 0, 48, 11, 0, 49, 11, 1, 52, 11, 1, 53, 11, 1, 54, 11, 1, 55, 11, 1, 56, 11, 1, 68, 11, 1, 69, 11, 0, 70, 11, 0, 71, 11, 0, 72, 11, 0, 73, 11, 0, 74, 11, 0, 75, 11, 0, 76, 11, 0, 77, 11, 1, 3, 12, 1, 4, 12, 0, 5, 12, 0, 6, 12, 0, 7, 12, 0, 8, 12, 0, 9, 12, 1, 12, 12, 1, 13, 12, 0, 14, 12, 0, 15, 12, 0, 16, 12, 0, 17, 12, 0, 18, 12, 0, 19, 12, 1, 20, 12, 1, 21, 12, 1, 22, 12, 1, 23, 12, 1, 24, 12, 1, 25, 12, 1, 26, 12, 1, 27, 12, 0, 28, 12, 1, 29, 12, 1, 30, 12, 1, 31, 12, 1, 32, 12, 1, 33, 12, 1, 34, 12, 1, 35, 12, 1, 36, 12, 1, 37, 12, 0, 38, 12, 0, 39, 12, 0, 40, 12, 0, 41, 12, 0, 42, 12, 2, 43, 12, 0, 44, 12, 0, 45, 12, 0, 46, 12, 0, 47, 12, 0, 48, 12, 0, 49, 12, 1, 50, 12, 1, 51, 12, 1, 52, 12, 1, 53, 12, 0, 54, 12, 0, 55, 12, 0, 56, 12, 1, 57, 12, 1, 58, 12, 1, 61, 12, 1, 62, 12, 1, 63, 12, 1, 64, 12, 1, 65, 12, 1, 66, 12, 1, 67, 12, 1, 68, 12, 1, 69, 12, 0, 70, 12, 0, 71, 12, 0, 72, 12, 0, 73, 12, 0, 74, 12, 0, 75, 12, 0, 76, 12, 0, 77, 12, 1, 3, 13, 1, 4, 13, 0, 5, 13, 0, 6, 13, 0, 7, 13, 0, 8, 13, 0, 9, 13, 1, 12, 13, 1, 13, 13, 0, 14, 13, 0, 15, 13, 0, 16, 13, 0, 17, 13, 0, 18, 13, 0, 19, 13, 3, 20, 13, 3, 21, 13, 3, 22, 13, 3, 23, 13, 3, 24, 13, 3, 25, 13, 2, 26, 13, 0, 27, 13, 0, 28, 13, 0, 29, 13, 3, 30, 13, 3, 31, 13, 3, 32, 13, 3, 33, 13, 3, 34, 13, 3, 35, 13, 3, 36, 13, 2, 37, 13, 0, 38, 13, 0, 39, 13, 0, 40, 13, 0, 41, 13, 0, 42, 13, 1, 43, 13, 0, 44, 13, 0, 45, 13, 0, 46, 13, 0, 47, 13, 0, 48, 13, 0, 49, 13, 1, 50, 13, 1, 51, 13, 0, 52, 13, 0, 53, 13, 0, 54, 13, 0, 55, 13, 0, 56, 13, 0, 57, 13, 0, 58, 13, 1, 59, 13, 1, 60, 13, 1, 61, 13, 1, 62, 13, 0, 63, 13, 0, 64, 13, 0, 65, 13, 0, 66, 13, 0, 67, 13, 1, 68, 13, 1, 69, 13, 0, 70, 13, 0, 71, 13, 0, 72, 13, 0, 73, 13, 0, 74, 13, 0, 75, 13, 0, 76, 13, 0, 77, 13, 1, 3, 14, 1, 4, 14, 1, 5, 14, 1, 6, 14, 3, 7, 14, 1, 8, 14, 1, 9, 14, 1, 10, 14, 1, 11, 14, 1, 12, 14, 1, 13, 14, 0, 14, 14, 0, 15, 14, 0, 16, 14, 0, 17, 14, 0, 18, 14, 0, 19, 14, 1, 20, 14, 1, 21, 14, 1, 22, 14, 1, 23, 14, 1, 24, 14, 1, 25, 14, 0, 26, 14, 0, 27, 14, 0, 28, 14, 0, 29, 14, 0, 30, 14, 1, 31, 14, 1, 32, 14, 1, 33, 14, 1, 34, 14, 1, 35, 14, 1, 36, 14, 1, 37, 14, 0, 38, 14, 0, 39, 14, 8, 40, 14, 0, 41, 14, 0, 42, 14, 1, 43, 14, 1, 44, 14, 1, 45, 14, 1, 46, 14, 1, 47, 14, 1, 48, 14, 1, 49, 14, 1, 50, 14, 1, 51, 14, 0, 52, 14, 0, 53, 14, 0, 54, 14, 0, 55, 14, 0, 56, 14, 0, 57, 14, 0, 58, 14, 2, 59, 14, 3, 60, 14, 3, 61, 14, 3, 62, 14, 0, 63, 14, 0, 64, 14, 0, 65, 14, 0, 66, 14, 0, 67, 14, 1, 68, 14, 1, 69, 14, 0, 70, 14, 0, 71, 14, 0, 72, 14, 0, 73, 14, 0, 74, 14, 0, 75, 14, 0, 76, 14, 9, 77, 14, 1, 5, 15, 1, 6, 15, 3, 7, 15, 1, 8, 15, 1, 9, 15, 1, 10, 15, 0, 11, 15, 1, 12, 15, 1, 13, 15, 1, 14, 15, 1, 15, 15, 1, 16, 15, 1, 17, 15, 1, 18, 15, 1, 19, 15, 1, 23, 15, 1, 24, 15, 1, 25, 15, 0, 26, 15, 0, 27, 15, 0, 28, 15, 0, 29, 15, 1, 30, 15, 1, 36, 15, 1, 37, 15, 0, 38, 15, 0, 39, 15, 0, 40, 15, 0, 41, 15, 0, 42, 15, 2, 43, 15, 3, 44, 15, 3, 45, 15, 3, 46, 15, 3, 47, 15, 3, 48, 15, 3, 49, 15, 3, 50, 15, 3, 51, 15, 0, 52, 15, 0, 53, 15, 0, 54, 15, 0, 55, 15, 0, 56, 15, 0, 57, 15, 0, 58, 15, 1, 59, 15, 1, 60, 15, 1, 61, 15, 1, 62, 15, 0, 63, 15, 0, 64, 15, 0, 65, 15, 0, 66, 15, 0, 67, 15, 1, 68, 15, 1, 69, 15, 1, 70, 15, 1, 71, 15, 1, 72, 15, 1, 73, 15, 1, 74, 15, 1, 75, 15, 1, 76, 15, 1, 77, 15, 1, 5, 16, 1, 6, 16, 3, 7, 16, 1, 8, 16, 0, 9, 16, 0, 10, 16, 0, 11, 16, 0, 12, 16, 0, 13, 16, 1, 14, 16, 1, 22, 16, 1, 23, 16, 1, 24, 16, 0, 25, 16, 0, 26, 16, 0, 27, 16, 0, 28, 16, 1, 29, 16, 1, 36, 16, 1, 37, 16, 1, 38, 16, 1, 39, 16, 2, 40, 16, 1, 41, 16, 1, 42, 16, 1, 43, 16, 1, 44, 16, 1, 45, 16, 1, 46, 16, 1, 47, 16, 1, 48, 16, 1, 49, 16, 1, 50, 16, 1, 51, 16, 1, 52, 16, 1, 53, 16, 0, 54, 16, 0, 55, 16, 0, 56, 16, 2, 57, 16, 1, 58, 16, 1, 61, 16, 1, 62, 16, 1, 63, 16, 1, 64, 16, 1, 65, 16, 1, 66, 16, 1, 67, 16, 1, 68, 16, 1, 69, 16, 1, 70, 16, 1, 71, 16, 1, 72, 16, 1, 73, 16, 1, 74, 16, 1, 5, 17, 1, 6, 17, 3, 7, 17, 0, 8, 17, 0, 9, 17, 0, 10, 17, 0, 11, 17, 0, 12, 17, 0, 13, 17, 0, 14, 17, 1, 22, 17, 1, 23, 17, 0, 24, 17, 0, 25, 17, 0, 26, 17, 0, 27, 17, 0, 28, 17, 1, 38, 17, 1, 39, 17, 3, 40, 17, 1, 44, 17, 1, 45, 17, 1, 46, 17, 1, 47, 17, 1, 48, 17, 1, 49, 17, 1, 50, 17, 1, 51, 17, 1, 52, 17, 1, 53, 17, 1, 54, 17, 1, 55, 17, 1, 56, 17, 3, 57, 17, 1, 68, 17, 1, 69, 17, 0, 70, 17, 0, 71, 17, 0, 72, 17, 0, 73, 17, 0, 74, 17, 1, 5, 18, 1, 6, 18, 2, 7, 18, 0, 8, 18, 0, 9, 18, 0, 10, 18, 0, 11, 18, 0, 12, 18, 0, 13, 18, 0, 14, 18, 1, 15, 18, 1, 16, 18, 1, 17, 18, 1, 18, 18, 1, 19, 18, 1, 20, 18, 1, 21, 18, 1, 22, 18, 1, 23, 18, 1, 24, 18, 0, 25, 18, 0, 26, 18, 0, 27, 18, 1, 28, 18, 1, 38, 18, 1, 39, 18, 3, 40, 18, 1, 41, 18, 1, 42, 18, 1, 43, 18, 1, 44, 18, 1, 45, 18, 0, 46, 18, 0, 47, 18, 0, 48, 18, 0, 49, 18, 0, 50, 18, 0, 51, 18, 1, 55, 18, 1, 56, 18, 3, 57, 18, 1, 58, 18, 1, 59, 18, 1, 68, 18, 1, 69, 18, 0, 70, 18, 0, 71, 18, 0, 72, 18, 0, 73, 18, 0, 74, 18, 1, 5, 19, 1, 6, 19, 0, 7, 19, 0, 8, 19, 0, 9, 19, 0, 10, 19, 0, 11, 19, 0, 12, 19, 0, 13, 19, 0, 14, 19, 0, 15, 19, 3, 16, 19, 3, 17, 19, 3, 18, 19, 3, 19, 19, 3, 20, 19, 3, 21, 19, 3, 22, 19, 3, 23, 19, 3, 24, 19, 2, 25, 19, 0, 26, 19, 1, 27, 19, 1, 38, 19, 1, 39, 19, 3, 40, 19, 0, 41, 19, 0, 42, 19, 0, 43, 19, 2, 44, 19, 3, 45, 19, 0, 46, 19, 0, 47, 19, 0, 48, 19, 0, 49, 19, 0, 50, 19, 0, 51, 19, 1, 55, 19, 1, 56, 19, 3, 57, 19, 1, 58, 19, 0, 59, 19, 1, 60, 19, 1, 61, 19, 1, 68, 19, 1, 69, 19, 3, 70, 19, 1, 71, 19, 1, 72, 19, 1, 73, 19, 2, 74, 19, 1, 5, 20, 1, 6, 20, 1, 7, 20, 0, 8, 20, 0, 9, 20, 0, 10, 20, 0, 11, 20, 0, 12, 20, 0, 13, 20, 0, 14, 20, 1, 15, 20, 1, 16, 20, 1, 17, 20, 1, 18, 20, 1, 19, 20, 1, 20, 20, 1, 21, 20, 2, 22, 20, 1, 23, 20, 1, 24, 20, 1, 25, 20, 1, 26, 20, 1, 38, 20, 1, 39, 20, 0, 40, 20, 0, 41, 20, 0, 42, 20, 0, 43, 20, 0, 44, 20, 1, 45, 20, 0, 46, 20, 0, 47, 20, 0, 48, 20, 0, 49, 20, 0, 50, 20, 0, 51, 20, 1, 55, 20, 1, 56, 20, 0, 57, 20, 0, 58, 20, 0, 59, 20, 0, 60, 20, 0, 61, 20, 1, 68, 20, 1, 69, 20, 3, 70, 20, 1, 72, 20, 1, 73, 20, 3, 74, 20, 1, 6, 21, 1, 7, 21, 0, 8, 21, 0, 9, 21, 0, 10, 21, 0, 11, 21, 0, 12, 21, 0, 13, 21, 0, 14, 21, 1, 15, 21, 1, 16, 21, 1, 17, 21, 1, 18, 21, 1, 19, 21, 0, 20, 21, 0, 21, 21, 0, 22, 21, 0, 23, 21, 0, 24, 21, 1, 29, 21, 1, 30, 21, 1, 31, 21, 1, 32, 21, 1, 33, 21, 1, 38, 21, 1, 39, 21, 0, 40, 21, 0, 41, 21, 0, 42, 21, 0, 43, 21, 0, 44, 21, 1, 45, 21, 1, 46, 21, 1, 47, 21, 1, 48, 21, 1, 49, 21, 1, 50, 21, 1, 51, 21, 1, 54, 21, 1, 55, 21, 1, 56, 21, 0, 57, 21, 0, 58, 21, 0, 59, 21, 0, 60, 21, 0, 61, 21, 1, 62, 21, 1, 66, 21, 1, 67, 21, 1, 68, 21, 1, 69, 21, 2, 70, 21, 1, 71, 21, 1, 72, 21, 1, 73, 21, 3, 74, 21, 1, 6, 22, 1, 7, 22, 1, 8, 22, 0, 9, 22, 0, 10, 22, 0, 11, 22, 0, 12, 22, 0, 13, 22, 2, 14, 22, 3, 15, 22, 3, 16, 22, 3, 17, 22, 3, 18, 22, 3, 19, 22, 0, 20, 22, 0, 21, 22, 0, 22, 22, 0, 23, 22, 0, 24, 22, 1, 25, 22, 1, 26, 22, 1, 27, 22, 1, 28, 22, 1, 29, 22, 1, 30, 22, 0, 31, 22, 0, 32, 22, 0, 33, 22, 1, 34, 22, 1, 35, 22, 1, 36, 22, 1, 38, 22, 1, 39, 22, 0, 40, 22, 0, 41, 22, 0, 42, 22, 0, 43, 22, 0, 44, 22, 1, 46, 22, 1, 47, 22, 1, 48, 22, 1, 49, 22, 1, 50, 22, 1, 51, 22, 1, 52, 22, 1, 53, 22, 1, 54, 22, 1, 55, 22, 0, 56, 22, 0, 57, 22, 0, 58, 22, 0, 59, 22, 0, 60, 22, 0, 61, 22, 0, 62, 22, 1, 63, 22, 1, 64, 22, 1, 65, 22, 1, 66, 22, 1, 67, 22, 0, 68, 22, 0, 69, 22, 0, 70, 22, 0, 71, 22, 0, 72, 22, 0, 73, 22, 3, 74, 22, 1, 7, 23, 1, 8, 23, 1, 9, 23, 1, 10, 23, 0, 11, 23, 1, 12, 23, 1, 13, 23, 1, 14, 23, 1, 15, 23, 1, 16, 23, 1, 17, 23, 1, 18, 23, 1, 19, 23, 0, 20, 23, 0, 21, 23, 0, 22, 23, 0, 23, 23, 0, 24, 23, 2, 25, 23, 3, 26, 23, 3, 27, 23, 0, 28, 23, 0, 29, 23, 0, 30, 23, 0, 31, 23, 0, 32, 23, 0, 33, 23, 0, 34, 23, 0, 35, 23, 0, 36, 23, 1, 38, 23, 1, 39, 23, 1, 40, 23, 0, 41, 23, 0, 42, 23, 0, 43, 23, 1, 44, 23, 1, 46, 23, 1, 47, 23, 0, 48, 23, 0, 49, 23, 0, 50, 23, 0, 51, 23, 0, 52, 23, 3, 53, 23, 3, 54, 23, 3, 55, 23, 2, 56, 23, 0, 57, 23, 0, 58, 23, 0, 59, 23, 0, 60, 23, 0, 61, 23, 2, 62, 23, 3, 63, 23, 3, 64, 23, 3, 65, 23, 3, 66, 23, 3, 67, 23, 0, 68, 23, 0, 69, 23, 0, 70, 23, 0, 71, 23, 0, 72, 23, 0, 73, 23, 3, 74, 23, 1, 9, 24, 1, 10, 24, 1, 11, 24, 1, 18, 24, 1, 19, 24, 0, 20, 24, 0, 21, 24, 0, 22, 24, 0, 23, 24, 0, 24, 24, 1, 25, 24, 1, 26, 24, 1, 27, 24, 0, 28, 24, 0, 29, 24, 0, 30, 24, 0, 31, 24, 0, 32, 24, 0, 33, 24, 0, 34, 24, 0, 35, 24, 0, 36, 24, 1, 39, 24, 1, 40, 24, 1, 41, 24, 1, 42, 24, 1, 43, 24, 1, 46, 24, 1, 47, 24, 0, 48, 24, 0, 49, 24, 0, 50, 24, 0, 51, 24, 0, 52, 24, 1, 53, 24, 1, 54, 24, 1, 55, 24, 1, 56, 24, 0, 57, 24, 0, 58, 24, 0, 59, 24, 0, 60, 24, 0, 61, 24, 1, 62, 24, 1, 63, 24, 1, 64, 24, 1, 65, 24, 1, 66, 24, 1, 67, 24, 1, 68, 24, 1, 69, 24, 1, 70, 24, 1, 71, 24, 1, 72, 24, 1, 73, 24, 3, 74, 24, 1, 75, 24, 1, 76, 24, 1, 18, 25, 1, 19, 25, 1, 20, 25, 1, 21, 25, 1, 22, 25, 1, 23, 25, 1, 24, 25, 1, 26, 25, 1, 27, 25, 0, 28, 25, 0, 29, 25, 0, 30, 25, 0, 31, 25, 0, 32, 25, 0, 33, 25, 0, 34, 25, 0, 35, 25, 0, 36, 25, 1, 46, 25, 1, 47, 25, 1, 48, 25, 1, 49, 25, 1, 50, 25, 1, 51, 25, 1, 52, 25, 1, 55, 25, 1, 56, 25, 1, 57, 25, 1, 58, 25, 0, 59, 25, 1, 60, 25, 1, 61, 25, 1, 71, 25, 1, 72, 25, 1, 73, 25, 0, 74, 25, 1, 75, 25, 0, 76, 25, 1, 77, 25, 1, 26, 26, 1, 27, 26, 1, 28, 26, 1, 29, 26, 1, 30, 26, 0, 31, 26, 0, 32, 26, 0, 33, 26, 1, 34, 26, 1, 35, 26, 1, 36, 26, 1, 57, 26, 1, 58, 26, 1, 59, 26, 1, 71, 26, 1, 72, 26, 0, 73, 26, 0, 74, 26, 0, 75, 26, 0, 76, 26, 0, 77, 26, 1, 29, 27, 1, 30, 27, 1, 31, 27, 1, 32, 27, 1, 33, 27, 1, 71, 27, 1, 72, 27, 1, 73, 27, 0, 74, 27, 1, 75, 27, 0, 76, 27, 1, 77, 27, 1, 72, 28, 1, 73, 28, 1, 74, 28, 1, 75, 28, 1, 76, 28, 1)
-}
-
-[node name="Level4" type="GridMap" parent="Levels" unique_id=1589783909]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -24, 0)
-mesh_library = SubResource("MeshLibrary_r4kn7")
-cell_size = Vector3(1, 1, 1)
-data = {
-"cells": PackedInt32Array(12, 1, 1, 13, 1, 1, 14, 1, 1, 15, 1, 1, 16, 1, 1, 17, 1, 1, 18, 1, 1, 19, 1, 1, 20, 1, 1, 21, 1, 1, 12, 2, 1, 13, 2, 0, 14, 2, 0, 15, 2, 0, 16, 2, 0, 17, 2, 0, 18, 2, 0, 19, 2, 0, 20, 2, 0, 21, 2, 1, 54, 2, 1, 55, 2, 1, 56, 2, 1, 0, 3, 1, 1, 3, 1, 2, 3, 1, 3, 3, 1, 4, 3, 1, 5, 3, 1, 6, 3, 1, 7, 3, 1, 8, 3, 1, 9, 3, 1, 10, 3, 1, 11, 3, 1, 12, 3, 1, 13, 3, 0, 14, 3, 0, 15, 3, 0, 16, 3, 0, 17, 3, 0, 18, 3, 0, 19, 3, 0, 20, 3, 0, 21, 3, 1, 22, 3, 1, 23, 3, 1, 24, 3, 1, 25, 3, 1, 26, 3, 1, 27, 3, 1, 28, 3, 1, 29, 3, 1, 53, 3, 1, 54, 3, 1, 55, 3, 0, 56, 3, 1, 57, 3, 1, 0, 4, 1, 1, 4, 0, 2, 4, 0, 3, 4, 0, 4, 4, 0, 5, 4, 0, 6, 4, 3, 7, 4, 3, 8, 4, 3, 9, 4, 3, 10, 4, 3, 11, 4, 3, 12, 4, 2, 13, 4, 0, 14, 4, 0, 15, 4, 0, 16, 4, 0, 17, 4, 0, 18, 4, 0, 19, 4, 0, 20, 4, 0, 21, 4, 3, 22, 4, 3, 23, 4, 3, 24, 4, 3, 25, 4, 2, 26, 4, 0, 27, 4, 0, 28, 4, 0, 29, 4, 1, 30, 4, 1, 52, 4, 1, 53, 4, 1, 54, 4, 0, 55, 4, 0, 56, 4, 0, 57, 4, 1, 58, 4, 1, 0, 5, 1, 1, 5, 9, 2, 5, 0, 3, 5, 0, 4, 5, 0, 5, 5, 0, 6, 5, 1, 7, 5, 1, 8, 5, 1, 9, 5, 1, 10, 5, 1, 11, 5, 1, 12, 5, 1, 13, 5, 1, 14, 5, 2, 15, 5, 1, 16, 5, 1, 17, 5, 1, 18, 5, 1, 19, 5, 1, 20, 5, 1, 21, 5, 1, 22, 5, 1, 23, 5, 1, 24, 5, 1, 25, 5, 0, 26, 5, 0, 27, 5, 0, 28, 5, 0, 29, 5, 0, 30, 5, 1, 34, 5, 1, 35, 5, 1, 36, 5, 1, 37, 5, 1, 38, 5, 1, 40, 5, 1, 41, 5, 1, 42, 5, 1, 43, 5, 1, 44, 5, 1, 46, 5, 1, 47, 5, 1, 48, 5, 1, 49, 5, 1, 50, 5, 1, 51, 5, 1, 52, 5, 1, 53, 5, 0, 54, 5, 0, 55, 5, 0, 56, 5, 0, 57, 5, 0, 58, 5, 1, 0, 6, 1, 1, 6, 1, 2, 6, 1, 3, 6, 1, 4, 6, 1, 5, 6, 1, 6, 6, 1, 13, 6, 1, 14, 6, 3, 15, 6, 1, 16, 6, 1, 17, 6, 1, 18, 6, 1, 24, 6, 1, 25, 6, 0, 26, 6, 0, 27, 6, 0, 28, 6, 0, 29, 6, 0, 30, 6, 1, 31, 6, 1, 32, 6, 1, 33, 6, 1, 34, 6, 1, 35, 6, 0, 36, 6, 1, 37, 6, 0, 38, 6, 1, 39, 6, 1, 40, 6, 1, 41, 6, 0, 42, 6, 0, 43, 6, 0, 44, 6, 1, 46, 6, 1, 47, 6, 0, 48, 6, 0, 49, 6, 0, 50, 6, 0, 51, 6, 0, 52, 6, 1, 53, 6, 1, 54, 6, 0, 55, 6, 0, 56, 6, 0, 57, 6, 1, 58, 6, 1, 66, 6, 1, 67, 6, 1, 68, 6, 1, 13, 7, 1, 14, 7, 3, 15, 7, 1, 16, 7, 0, 17, 7, 0, 18, 7, 1, 19, 7, 1, 24, 7, 1, 25, 7, 0, 26, 7, 0, 27, 7, 0, 28, 7, 0, 29, 7, 0, 30, 7, 3, 31, 7, 3, 32, 7, 3, 33, 7, 2, 34, 7, 0, 35, 7, 0, 36, 7, 0, 37, 7, 0, 38, 7, 0, 39, 7, 3, 40, 7, 2, 41, 7, 0, 42, 7, 0, 43, 7, 0, 44, 7, 1, 46, 7, 1, 47, 7, 0, 48, 7, 0, 49, 7, 0, 50, 7, 0, 51, 7, 0, 52, 7, 1, 53, 7, 1, 54, 7, 3, 55, 7, 0, 56, 7, 1, 57, 7, 1, 65, 7, 1, 66, 7, 1, 67, 7, 0, 68, 7, 1, 69, 7, 1, 5, 8, 1, 6, 8, 1, 7, 8, 1, 8, 8, 1, 9, 8, 1, 10, 8, 1, 11, 8, 1, 12, 8, 1, 13, 8, 1, 14, 8, 3, 15, 8, 0, 16, 8, 0, 17, 8, 0, 18, 8, 0, 19, 8, 1, 24, 8, 1, 25, 8, 1, 26, 8, 0, 27, 8, 0, 28, 8, 0, 29, 8, 1, 30, 8, 1, 31, 8, 1, 32, 8, 1, 33, 8, 0, 34, 8, 0, 35, 8, 0, 36, 8, 0, 37, 8, 0, 38, 8, 0, 39, 8, 0, 40, 8, 1, 41, 8, 3, 42, 8, 1, 43, 8, 2, 44, 8, 1, 45, 8, 1, 46, 8, 1, 47, 8, 0, 48, 8, 0, 49, 8, 0, 50, 8, 0, 51, 8, 0, 52, 8, 1, 53, 8, 1, 54, 8, 3, 55, 8, 1, 56, 8, 1, 64, 8, 1, 65, 8, 1, 66, 8, 0, 67, 8, 0, 68, 8, 0, 69, 8, 1, 5, 9, 1, 6, 9, 0, 7, 9, 0, 8, 9, 0, 9, 9, 0, 10, 9, 3, 11, 9, 3, 12, 9, 2, 13, 9, 0, 14, 9, 0, 15, 9, 0, 16, 9, 0, 17, 9, 0, 18, 9, 1, 19, 9, 1, 25, 9, 1, 26, 9, 1, 27, 9, 1, 28, 9, 1, 29, 9, 1, 32, 9, 1, 33, 9, 1, 34, 9, 0, 35, 9, 0, 36, 9, 0, 37, 9, 0, 38, 9, 0, 39, 9, 1, 40, 9, 1, 41, 9, 3, 42, 9, 1, 43, 9, 3, 44, 9, 1, 45, 9, 0, 46, 9, 1, 47, 9, 3, 48, 9, 1, 49, 9, 1, 50, 9, 1, 51, 9, 1, 52, 9, 1, 53, 9, 1, 54, 9, 3, 55, 9, 1, 56, 9, 1, 64, 9, 1, 65, 9, 0, 66, 9, 0, 67, 9, 0, 68, 9, 1, 69, 9, 1, 70, 9, 1, 71, 9, 1, 72, 9, 1, 73, 9, 1, 74, 9, 1, 5, 10, 1, 6, 10, 0, 7, 10, 0, 8, 10, 0, 9, 10, 0, 10, 10, 1, 11, 10, 1, 12, 10, 0, 13, 10, 0, 14, 10, 0, 15, 10, 0, 16, 10, 0, 17, 10, 0, 18, 10, 1, 33, 10, 1, 34, 10, 1, 35, 10, 0, 36, 10, 0, 37, 10, 0, 38, 10, 0, 39, 10, 1, 40, 10, 1, 41, 10, 3, 42, 10, 1, 43, 10, 3, 44, 10, 0, 45, 10, 0, 46, 10, 0, 47, 10, 2, 48, 10, 1, 52, 10, 1, 53, 10, 1, 54, 10, 2, 55, 10, 0, 56, 10, 1, 57, 10, 1, 58, 10, 1, 64, 10, 1, 65, 10, 3, 66, 10, 0, 67, 10, 2, 68, 10, 3, 69, 10, 0, 70, 10, 0, 71, 10, 0, 72, 10, 0, 73, 10, 0, 74, 10, 1, 5, 11, 1, 6, 11, 2, 7, 11, 1, 8, 11, 1, 9, 11, 1, 10, 11, 1, 11, 11, 1, 12, 11, 1, 13, 11, 0, 14, 11, 0, 15, 11, 0, 16, 11, 0, 17, 11, 1, 18, 11, 1, 34, 11, 1, 35, 11, 1, 36, 11, 1, 37, 11, 0, 38, 11, 1, 39, 11, 1, 40, 11, 1, 41, 11, 3, 42, 11, 1, 43, 11, 0, 44, 11, 0, 45, 11, 0, 46, 11, 0, 47, 11, 0, 48, 11, 1, 51, 11, 1, 52, 11, 1, 53, 11, 0, 54, 11, 0, 55, 11, 0, 56, 11, 0, 57, 11, 0, 58, 11, 1, 59, 11, 1, 64, 11, 1, 65, 11, 3, 66, 11, 1, 67, 11, 1, 68, 11, 1, 69, 11, 0, 70, 11, 0, 71, 11, 0, 72, 11, 0, 73, 11, 0, 74, 11, 1, 5, 12, 1, 6, 12, 0, 7, 12, 0, 8, 12, 0, 9, 12, 0, 10, 12, 0, 11, 12, 0, 12, 12, 1, 13, 12, 1, 14, 12, 1, 15, 12, 0, 16, 12, 1, 17, 12, 1, 25, 12, 1, 26, 12, 1, 27, 12, 1, 28, 12, 1, 29, 12, 1, 35, 12, 1, 36, 12, 1, 37, 12, 1, 38, 12, 1, 39, 12, 1, 40, 12, 1, 41, 12, 2, 42, 12, 1, 43, 12, 1, 44, 12, 0, 45, 12, 0, 46, 12, 0, 47, 12, 1, 48, 12, 1, 51, 12, 1, 52, 12, 0, 53, 12, 0, 54, 12, 0, 55, 12, 0, 56, 12, 0, 57, 12, 0, 58, 12, 0, 59, 12, 1, 64, 12, 1, 65, 12, 3, 66, 12, 1, 68, 12, 1, 69, 12, 1, 70, 12, 1, 71, 12, 1, 72, 12, 1, 73, 12, 1, 74, 12, 1, 5, 13, 1, 6, 13, 0, 7, 13, 0, 8, 13, 0, 9, 13, 0, 10, 13, 0, 11, 13, 0, 12, 13, 1, 14, 13, 1, 15, 13, 1, 16, 13, 1, 24, 13, 1, 25, 13, 1, 26, 13, 0, 27, 13, 0, 28, 13, 0, 29, 13, 1, 30, 13, 1, 31, 13, 1, 32, 13, 1, 33, 13, 1, 34, 13, 1, 35, 13, 1, 36, 13, 0, 37, 13, 0, 38, 13, 0, 39, 13, 0, 40, 13, 0, 41, 13, 0, 42, 13, 1, 43, 13, 1, 44, 13, 1, 45, 13, 0, 46, 13, 1, 47, 13, 1, 50, 13, 1, 51, 13, 1, 52, 13, 0, 53, 13, 0, 54, 13, 0, 55, 13, 0, 56, 13, 0, 57, 13, 0, 58, 13, 0, 59, 13, 1, 60, 13, 1, 64, 13, 1, 65, 13, 3, 66, 13, 1, 5, 14, 1, 6, 14, 1, 7, 14, 1, 8, 14, 1, 9, 14, 1, 10, 14, 1, 11, 14, 3, 12, 14, 1, 13, 14, 1, 14, 14, 1, 15, 14, 1, 16, 14, 1, 17, 14, 1, 18, 14, 1, 24, 14, 1, 25, 14, 0, 26, 14, 0, 27, 14, 0, 28, 14, 0, 29, 14, 0, 30, 14, 3, 31, 14, 3, 32, 14, 3, 33, 14, 3, 34, 14, 3, 35, 14, 2, 36, 14, 0, 37, 14, 0, 38, 14, 0, 39, 14, 8, 40, 14, 0, 41, 14, 0, 42, 14, 1, 43, 14, 1, 44, 14, 1, 45, 14, 2, 46, 14, 1, 47, 14, 1, 48, 14, 1, 49, 14, 1, 50, 14, 1, 51, 14, 0, 52, 14, 0, 53, 14, 0, 54, 14, 0, 55, 14, 0, 56, 14, 0, 57, 14, 0, 58, 14, 0, 59, 14, 0, 60, 14, 1, 63, 14, 1, 64, 14, 1, 65, 14, 3, 66, 14, 1, 7, 15, 1, 8, 15, 1, 9, 15, 1, 10, 15, 1, 11, 15, 3, 12, 15, 1, 13, 15, 0, 14, 15, 0, 15, 15, 0, 16, 15, 0, 17, 15, 0, 18, 15, 1, 19, 15, 1, 20, 15, 1, 21, 15, 1, 22, 15, 1, 23, 15, 1, 24, 15, 1, 25, 15, 0, 26, 15, 0, 27, 15, 0, 28, 15, 0, 29, 15, 0, 30, 15, 1, 31, 15, 1, 32, 15, 1, 33, 15, 1, 34, 15, 1, 35, 15, 1, 36, 15, 0, 37, 15, 0, 38, 15, 0, 39, 15, 0, 40, 15, 0, 41, 15, 0, 42, 15, 2, 43, 15, 3, 44, 15, 3, 45, 15, 3, 46, 15, 3, 47, 15, 3, 48, 15, 3, 49, 15, 3, 50, 15, 3, 51, 15, 3, 52, 15, 0, 53, 15, 0, 54, 15, 0, 55, 15, 0, 56, 15, 0, 57, 15, 0, 58, 15, 0, 59, 15, 1, 60, 15, 1, 62, 15, 1, 63, 15, 1, 64, 15, 0, 65, 15, 2, 66, 15, 1, 7, 16, 1, 8, 16, 0, 9, 16, 0, 10, 16, 0, 11, 16, 3, 12, 16, 1, 13, 16, 0, 14, 16, 0, 15, 16, 0, 16, 16, 0, 17, 16, 0, 18, 16, 3, 19, 16, 3, 20, 16, 3, 21, 16, 3, 22, 16, 3, 23, 16, 3, 24, 16, 2, 25, 16, 0, 26, 16, 0, 27, 16, 0, 28, 16, 0, 29, 16, 0, 30, 16, 1, 32, 16, 1, 33, 16, 1, 34, 16, 1, 35, 16, 1, 36, 16, 1, 37, 16, 1, 38, 16, 1, 39, 16, 1, 40, 16, 1, 41, 16, 2, 42, 16, 1, 43, 16, 0, 44, 16, 0, 45, 16, 0, 46, 16, 1, 47, 16, 1, 48, 16, 1, 49, 16, 1, 50, 16, 1, 51, 16, 1, 52, 16, 0, 53, 16, 0, 54, 16, 0, 55, 16, 0, 56, 16, 0, 57, 16, 0, 58, 16, 0, 59, 16, 1, 61, 16, 1, 62, 16, 1, 63, 16, 0, 64, 16, 0, 65, 16, 0, 66, 16, 1, 67, 16, 1, 7, 17, 1, 8, 17, 0, 9, 17, 0, 10, 17, 0, 11, 17, 3, 12, 17, 1, 13, 17, 0, 14, 17, 0, 15, 17, 0, 16, 17, 0, 17, 17, 0, 18, 17, 1, 19, 17, 1, 20, 17, 1, 21, 17, 1, 22, 17, 1, 23, 17, 1, 24, 17, 1, 25, 17, 1, 26, 17, 0, 27, 17, 0, 28, 17, 0, 29, 17, 1, 30, 17, 1, 31, 17, 1, 32, 17, 1, 33, 17, 0, 34, 17, 0, 35, 17, 0, 36, 17, 1, 37, 17, 1, 40, 17, 1, 41, 17, 3, 42, 17, 1, 43, 17, 0, 44, 17, 0, 45, 17, 0, 46, 17, 1, 51, 17, 1, 52, 17, 1, 53, 17, 0, 54, 17, 0, 55, 17, 0, 56, 17, 0, 57, 17, 0, 58, 17, 1, 59, 17, 1, 60, 17, 1, 61, 17, 1, 62, 17, 0, 63, 17, 0, 64, 17, 0, 65, 17, 0, 66, 17, 0, 67, 17, 1, 7, 18, 1, 8, 18, 1, 9, 18, 3, 10, 18, 1, 11, 18, 3, 12, 18, 1, 13, 18, 0, 14, 18, 0, 15, 18, 0, 16, 18, 0, 17, 18, 0, 18, 18, 1, 25, 18, 1, 26, 18, 2, 27, 18, 1, 28, 18, 1, 29, 18, 1, 31, 18, 1, 32, 18, 0, 33, 18, 0, 34, 18, 0, 35, 18, 0, 36, 18, 0, 37, 18, 1, 40, 18, 1, 41, 18, 3, 42, 18, 1, 43, 18, 0, 44, 18, 0, 45, 18, 0, 46, 18, 1, 51, 18, 1, 52, 18, 1, 53, 18, 1, 54, 18, 1, 55, 18, 0, 56, 18, 2, 57, 18, 3, 58, 18, 3, 59, 18, 3, 60, 18, 3, 61, 18, 3, 62, 18, 0, 63, 18, 0, 64, 18, 0, 65, 18, 0, 66, 18, 1, 67, 18, 1, 8, 19, 1, 9, 19, 3, 10, 19, 1, 11, 19, 2, 12, 19, 1, 13, 19, 1, 14, 19, 1, 15, 19, 2, 16, 19, 1, 17, 19, 1, 18, 19, 1, 25, 19, 1, 26, 19, 3, 27, 19, 1, 31, 19, 1, 32, 19, 0, 33, 19, 0, 34, 19, 0, 35, 19, 0, 36, 19, 0, 37, 19, 1, 38, 19, 1, 39, 19, 1, 40, 19, 1, 41, 19, 3, 42, 19, 1, 43, 19, 1, 44, 19, 1, 45, 19, 3, 46, 19, 1, 50, 19, 1, 51, 19, 1, 52, 19, 0, 53, 19, 3, 54, 19, 3, 55, 19, 3, 56, 19, 3, 57, 19, 3, 58, 19, 3, 59, 19, 3, 60, 19, 3, 61, 19, 3, 62, 19, 2, 63, 19, 0, 64, 19, 0, 65, 19, 2, 66, 19, 1, 8, 20, 1, 9, 20, 2, 10, 20, 1, 11, 20, 0, 12, 20, 0, 13, 20, 0, 14, 20, 1, 15, 20, 3, 16, 20, 1, 25, 20, 1, 26, 20, 3, 27, 20, 1, 28, 20, 1, 29, 20, 1, 30, 20, 1, 31, 20, 1, 32, 20, 0, 33, 20, 0, 34, 20, 0, 35, 20, 0, 36, 20, 0, 37, 20, 1, 38, 20, 1, 39, 20, 0, 40, 20, 0, 41, 20, 0, 42, 20, 1, 43, 20, 1, 44, 20, 1, 45, 20, 3, 46, 20, 1, 50, 20, 1, 51, 20, 0, 52, 20, 0, 53, 20, 0, 54, 20, 1, 55, 20, 1, 56, 20, 1, 57, 20, 1, 58, 20, 1, 59, 20, 1, 60, 20, 1, 61, 20, 1, 62, 20, 1, 63, 20, 1, 64, 20, 1, 65, 20, 3, 66, 20, 1, 2, 21, 1, 3, 21, 1, 4, 21, 1, 5, 21, 1, 6, 21, 1, 7, 21, 1, 8, 21, 1, 9, 21, 0, 10, 21, 0, 11, 21, 0, 12, 21, 0, 13, 21, 0, 14, 21, 0, 15, 21, 0, 16, 21, 1, 25, 21, 1, 26, 21, 3, 27, 21, 0, 28, 21, 2, 29, 21, 3, 30, 21, 3, 31, 21, 3, 32, 21, 3, 33, 21, 0, 34, 21, 0, 35, 21, 0, 36, 21, 1, 37, 21, 1, 38, 21, 0, 39, 21, 0, 40, 21, 0, 41, 21, 0, 42, 21, 0, 43, 21, 1, 44, 21, 1, 45, 21, 3, 46, 21, 1, 50, 21, 1, 51, 21, 1, 52, 21, 0, 53, 21, 1, 54, 21, 0, 55, 21, 1, 56, 21, 1, 64, 21, 1, 65, 21, 3, 66, 21, 1, 2, 22, 1, 3, 22, 0, 4, 22, 0, 5, 22, 0, 6, 22, 0, 7, 22, 1, 8, 22, 1, 9, 22, 0, 10, 22, 0, 11, 22, 0, 12, 22, 0, 13, 22, 0, 14, 22, 0, 15, 22, 0, 16, 22, 1, 25, 22, 1, 26, 22, 0, 27, 22, 0, 28, 22, 0, 29, 22, 0, 30, 22, 1, 31, 22, 1, 32, 22, 1, 33, 22, 1, 34, 22, 1, 35, 22, 1, 36, 22, 1, 37, 22, 1, 38, 22, 0, 39, 22, 0, 40, 22, 0, 41, 22, 0, 42, 22, 0, 43, 22, 1, 44, 22, 1, 45, 22, 2, 46, 22, 1, 47, 22, 1, 48, 22, 1, 49, 22, 1, 50, 22, 1, 51, 22, 1, 52, 22, 0, 53, 22, 0, 54, 22, 0, 55, 22, 0, 56, 22, 1, 57, 22, 1, 58, 22, 1, 59, 22, 1, 60, 22, 1, 61, 22, 1, 62, 22, 1, 63, 22, 1, 64, 22, 1, 65, 22, 3, 66, 22, 1, 67, 22, 1, 2, 23, 1, 3, 23, 0, 4, 23, 0, 5, 23, 0, 6, 23, 0, 7, 23, 1, 8, 23, 1, 9, 23, 0, 10, 23, 0, 11, 23, 0, 12, 23, 0, 13, 23, 0, 14, 23, 0, 15, 23, 0, 16, 23, 1, 25, 23, 1, 26, 23, 1, 27, 23, 0, 28, 23, 0, 29, 23, 0, 30, 23, 0, 31, 23, 1, 32, 23, 1, 37, 23, 1, 38, 23, 0, 39, 23, 0, 40, 23, 0, 41, 23, 0, 42, 23, 0, 43, 23, 2, 44, 23, 3, 45, 23, 0, 46, 23, 0, 47, 23, 0, 48, 23, 0, 49, 23, 1, 50, 23, 1, 51, 23, 0, 52, 23, 0, 53, 23, 0, 54, 23, 0, 55, 23, 2, 56, 23, 3, 57, 23, 3, 58, 23, 3, 59, 23, 0, 60, 23, 0, 61, 23, 0, 62, 23, 2, 63, 23, 0, 64, 23, 0, 65, 23, 0, 66, 23, 0, 67, 23, 1, 2, 24, 1, 3, 24, 0, 4, 24, 0, 5, 24, 0, 6, 24, 0, 7, 24, 3, 8, 24, 3, 9, 24, 3, 10, 24, 2, 11, 24, 0, 12, 24, 0, 13, 24, 0, 14, 24, 1, 15, 24, 1, 16, 24, 1, 26, 24, 1, 27, 24, 0, 28, 24, 0, 29, 24, 0, 30, 24, 0, 31, 24, 0, 32, 24, 1, 37, 24, 1, 38, 24, 2, 39, 24, 0, 40, 24, 0, 41, 24, 0, 42, 24, 1, 43, 24, 1, 44, 24, 1, 45, 24, 0, 46, 24, 0, 47, 24, 0, 48, 24, 0, 49, 24, 2, 50, 24, 0, 51, 24, 0, 52, 24, 0, 53, 24, 0, 54, 24, 0, 55, 24, 1, 56, 24, 1, 57, 24, 1, 58, 24, 1, 59, 24, 0, 60, 24, 0, 61, 24, 0, 62, 24, 1, 63, 24, 0, 64, 24, 0, 65, 24, 0, 66, 24, 0, 67, 24, 1, 2, 25, 1, 3, 25, 0, 4, 25, 0, 5, 25, 0, 6, 25, 0, 7, 25, 1, 8, 25, 1, 9, 25, 1, 10, 25, 1, 11, 25, 1, 12, 25, 1, 13, 25, 1, 14, 25, 1, 26, 25, 1, 27, 25, 1, 28, 25, 0, 29, 25, 0, 30, 25, 0, 31, 25, 1, 32, 25, 1, 35, 25, 1, 36, 25, 1, 37, 25, 1, 38, 25, 3, 39, 25, 1, 40, 25, 1, 41, 25, 1, 42, 25, 1, 44, 25, 1, 45, 25, 0, 46, 25, 0, 47, 25, 0, 48, 25, 0, 49, 25, 1, 50, 25, 1, 51, 25, 0, 52, 25, 0, 53, 25, 0, 54, 25, 1, 55, 25, 1, 58, 25, 1, 59, 25, 0, 60, 25, 0, 61, 25, 0, 62, 25, 1, 63, 25, 1, 64, 25, 1, 65, 25, 1, 66, 25, 1, 67, 25, 1, 2, 26, 1, 3, 26, 1, 4, 26, 1, 5, 26, 1, 6, 26, 1, 7, 26, 1, 27, 26, 1, 28, 26, 1, 29, 26, 0, 30, 26, 1, 31, 26, 1, 35, 26, 1, 36, 26, 0, 37, 26, 0, 38, 26, 0, 39, 26, 0, 40, 26, 1, 44, 26, 1, 45, 26, 1, 46, 26, 1, 47, 26, 1, 48, 26, 1, 49, 26, 1, 50, 26, 1, 51, 26, 1, 52, 26, 0, 53, 26, 1, 54, 26, 1, 58, 26, 1, 59, 26, 0, 60, 26, 0, 61, 26, 0, 62, 26, 1, 28, 27, 1, 29, 27, 1, 30, 27, 1, 35, 27, 1, 36, 27, 0, 37, 27, 0, 38, 27, 0, 39, 27, 0, 40, 27, 1, 51, 27, 1, 52, 27, 1, 53, 27, 1, 58, 27, 1, 59, 27, 1, 60, 27, 1, 61, 27, 1, 62, 27, 1, 35, 28, 1, 36, 28, 1, 37, 28, 1, 38, 28, 1, 39, 28, 1, 40, 28, 1)
-}
-
-[node name="Level5" type="GridMap" parent="Levels" unique_id=1477503892]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -30, 0)
-mesh_library = SubResource("MeshLibrary_r4kn7")
-cell_size = Vector3(1, 1, 1)
-data = {
-"cells": PackedInt32Array(64, 0, 1, 65, 0, 1, 66, 0, 1, 67, 0, 1, 68, 0, 1, 15, 1, 1, 16, 1, 1, 17, 1, 1, 18, 1, 1, 19, 1, 1, 20, 1, 1, 21, 1, 1, 30, 1, 1, 31, 1, 1, 32, 1, 1, 33, 1, 1, 34, 1, 1, 35, 1, 1, 36, 1, 1, 45, 1, 1, 46, 1, 1, 47, 1, 1, 48, 1, 1, 49, 1, 1, 50, 1, 1, 51, 1, 1, 52, 1, 1, 53, 1, 1, 54, 1, 1, 55, 1, 1, 56, 1, 1, 57, 1, 1, 58, 1, 1, 59, 1, 1, 60, 1, 1, 61, 1, 1, 64, 1, 1, 65, 1, 0, 66, 1, 0, 67, 1, 0, 68, 1, 1, 15, 2, 1, 16, 2, 0, 17, 2, 0, 18, 2, 0, 19, 2, 0, 20, 2, 0, 21, 2, 1, 30, 2, 1, 31, 2, 0, 32, 2, 0, 33, 2, 0, 34, 2, 0, 35, 2, 0, 36, 2, 1, 37, 2, 1, 38, 2, 1, 39, 2, 1, 40, 2, 1, 41, 2, 1, 42, 2, 1, 43, 2, 1, 44, 2, 1, 45, 2, 1, 46, 2, 0, 47, 2, 0, 48, 2, 0, 49, 2, 0, 50, 2, 2, 51, 2, 3, 52, 2, 3, 53, 2, 3, 54, 2, 3, 55, 2, 3, 56, 2, 3, 57, 2, 0, 58, 2, 0, 59, 2, 0, 60, 2, 0, 61, 2, 1, 64, 2, 1, 65, 2, 0, 66, 2, 0, 67, 2, 0, 68, 2, 1, 69, 2, 1, 70, 2, 1, 71, 2, 1, 72, 2, 1, 73, 2, 1, 74, 2, 1, 15, 3, 1, 16, 3, 0, 17, 3, 0, 18, 3, 0, 19, 3, 0, 20, 3, 0, 21, 3, 1, 22, 3, 1, 23, 3, 1, 24, 3, 1, 25, 3, 1, 26, 3, 1, 27, 3, 1, 28, 3, 1, 29, 3, 1, 30, 3, 1, 31, 3, 0, 32, 3, 0, 33, 3, 0, 34, 3, 0, 35, 3, 0, 36, 3, 3, 37, 3, 3, 38, 3, 3, 39, 3, 3, 40, 3, 3, 41, 3, 3, 42, 3, 3, 43, 3, 3, 44, 3, 3, 45, 3, 2, 46, 3, 0, 47, 3, 0, 48, 3, 0, 49, 3, 0, 50, 3, 0, 51, 3, 1, 52, 3, 1, 53, 3, 1, 54, 3, 1, 55, 3, 1, 56, 3, 1, 57, 3, 0, 58, 3, 0, 59, 3, 0, 60, 3, 0, 61, 3, 1, 64, 3, 1, 65, 3, 0, 66, 3, 0, 67, 3, 0, 68, 3, 2, 69, 3, 0, 70, 3, 0, 71, 3, 0, 72, 3, 0, 73, 3, 0, 74, 3, 1, 15, 4, 1, 16, 4, 0, 17, 4, 0, 18, 4, 0, 19, 4, 0, 20, 4, 0, 21, 4, 3, 22, 4, 3, 23, 4, 3, 24, 4, 3, 25, 4, 3, 26, 4, 3, 27, 4, 3, 28, 4, 3, 29, 4, 3, 30, 4, 2, 31, 4, 0, 32, 4, 0, 33, 4, 0, 34, 4, 0, 35, 4, 0, 36, 4, 1, 37, 4, 0, 38, 4, 0, 39, 4, 0, 40, 4, 0, 41, 4, 1, 42, 4, 1, 43, 4, 1, 44, 4, 1, 45, 4, 1, 46, 4, 0, 47, 4, 0, 48, 4, 0, 49, 4, 0, 50, 4, 0, 51, 4, 1, 52, 4, 1, 53, 4, 1, 54, 4, 1, 55, 4, 1, 56, 4, 1, 57, 4, 0, 58, 4, 0, 59, 4, 0, 60, 4, 0, 61, 4, 1, 64, 4, 1, 65, 4, 1, 66, 4, 3, 67, 4, 1, 68, 4, 1, 69, 4, 0, 70, 4, 0, 71, 4, 0, 72, 4, 0, 73, 4, 0, 74, 4, 1, 15, 5, 1, 16, 5, 1, 17, 5, 1, 18, 5, 1, 19, 5, 1, 20, 5, 1, 21, 5, 1, 22, 5, 1, 23, 5, 1, 24, 5, 1, 25, 5, 1, 26, 5, 1, 27, 5, 0, 28, 5, 0, 29, 5, 0, 30, 5, 1, 31, 5, 1, 32, 5, 1, 33, 5, 1, 34, 5, 1, 35, 5, 1, 36, 5, 1, 37, 5, 0, 38, 5, 0, 39, 5, 0, 40, 5, 0, 41, 5, 2, 42, 5, 3, 43, 5, 3, 44, 5, 3, 45, 5, 3, 46, 5, 0, 47, 5, 0, 48, 5, 0, 49, 5, 0, 50, 5, 1, 51, 5, 1, 52, 5, 0, 53, 5, 0, 54, 5, 0, 55, 5, 1, 56, 5, 1, 57, 5, 0, 58, 5, 0, 59, 5, 0, 60, 5, 0, 61, 5, 1, 65, 5, 1, 66, 5, 3, 67, 5, 1, 68, 5, 1, 69, 5, 0, 70, 5, 0, 71, 5, 0, 72, 5, 0, 73, 5, 0, 74, 5, 1, 23, 6, 1, 24, 6, 0, 25, 6, 0, 26, 6, 0, 27, 6, 0, 28, 6, 0, 29, 6, 0, 30, 6, 0, 31, 6, 0, 32, 6, 0, 33, 6, 3, 34, 6, 3, 35, 6, 3, 36, 6, 2, 37, 6, 0, 38, 6, 0, 39, 6, 0, 40, 6, 0, 41, 6, 1, 42, 6, 1, 43, 6, 1, 44, 6, 1, 45, 6, 1, 46, 6, 1, 47, 6, 1, 48, 6, 2, 49, 6, 1, 50, 6, 1, 51, 6, 1, 52, 6, 0, 53, 6, 0, 54, 6, 0, 55, 6, 1, 56, 6, 1, 57, 6, 1, 58, 6, 1, 59, 6, 1, 60, 6, 2, 61, 6, 1, 65, 6, 1, 66, 6, 3, 67, 6, 1, 68, 6, 1, 69, 6, 0, 70, 6, 0, 71, 6, 0, 72, 6, 0, 73, 6, 0, 74, 6, 1, 23, 7, 1, 24, 7, 0, 25, 7, 0, 26, 7, 0, 27, 7, 0, 28, 7, 0, 29, 7, 0, 30, 7, 0, 31, 7, 0, 32, 7, 0, 33, 7, 1, 34, 7, 1, 35, 7, 1, 36, 7, 1, 37, 7, 0, 38, 7, 0, 39, 7, 0, 40, 7, 0, 41, 7, 1, 46, 7, 1, 47, 7, 1, 48, 7, 3, 49, 7, 1, 50, 7, 1, 51, 7, 1, 52, 7, 1, 53, 7, 1, 54, 7, 3, 55, 7, 1, 59, 7, 1, 60, 7, 3, 61, 7, 1, 62, 7, 1, 63, 7, 1, 64, 7, 1, 65, 7, 1, 66, 7, 2, 67, 7, 1, 68, 7, 1, 69, 7, 3, 70, 7, 1, 71, 7, 1, 72, 7, 1, 73, 7, 1, 74, 7, 1, 13, 8, 1, 14, 8, 1, 15, 8, 1, 16, 8, 1, 17, 8, 1, 18, 8, 1, 19, 8, 1, 20, 8, 1, 23, 8, 1, 24, 8, 0, 25, 8, 0, 26, 8, 0, 27, 8, 0, 28, 8, 0, 29, 8, 0, 30, 8, 0, 31, 8, 0, 32, 8, 0, 33, 8, 1, 36, 8, 1, 37, 8, 1, 38, 8, 1, 39, 8, 1, 40, 8, 3, 41, 8, 1, 46, 8, 1, 47, 8, 0, 48, 8, 0, 49, 8, 0, 50, 8, 1, 53, 8, 1, 54, 8, 3, 55, 8, 1, 56, 8, 1, 57, 8, 1, 58, 8, 1, 59, 8, 1, 60, 8, 3, 61, 8, 1, 62, 8, 1, 63, 8, 0, 64, 8, 0, 65, 8, 0, 66, 8, 0, 67, 8, 0, 68, 8, 1, 69, 8, 3, 70, 8, 1, 71, 8, 1, 72, 8, 1, 73, 8, 0, 74, 8, 1, 75, 8, 1, 13, 9, 1, 14, 9, 0, 15, 9, 0, 16, 9, 0, 17, 9, 0, 18, 9, 0, 19, 9, 0, 20, 9, 1, 23, 9, 1, 24, 9, 2, 25, 9, 1, 26, 9, 1, 27, 9, 0, 28, 9, 0, 29, 9, 0, 30, 9, 2, 31, 9, 1, 32, 9, 1, 33, 9, 1, 39, 9, 1, 40, 9, 3, 41, 9, 1, 46, 9, 1, 47, 9, 0, 48, 9, 0, 49, 9, 0, 50, 9, 1, 53, 9, 1, 54, 9, 3, 55, 9, 2, 56, 9, 0, 57, 9, 0, 58, 9, 0, 59, 9, 0, 60, 9, 0, 61, 9, 1, 62, 9, 1, 63, 9, 0, 64, 9, 0, 65, 9, 0, 66, 9, 0, 67, 9, 0, 68, 9, 1, 69, 9, 3, 70, 9, 1, 71, 9, 0, 72, 9, 0, 73, 9, 0, 74, 9, 0, 75, 9, 1, 13, 10, 1, 14, 10, 0, 15, 10, 0, 16, 10, 0, 17, 10, 0, 18, 10, 0, 19, 10, 0, 20, 10, 1, 22, 10, 1, 23, 10, 1, 24, 10, 3, 25, 10, 1, 26, 10, 1, 27, 10, 1, 28, 10, 1, 29, 10, 1, 30, 10, 3, 31, 10, 1, 39, 10, 1, 40, 10, 3, 41, 10, 1, 46, 10, 1, 47, 10, 0, 48, 10, 0, 49, 10, 0, 50, 10, 1, 51, 10, 1, 52, 10, 1, 53, 10, 1, 54, 10, 2, 55, 10, 1, 56, 10, 0, 57, 10, 0, 58, 10, 0, 59, 10, 0, 60, 10, 0, 61, 10, 2, 62, 10, 3, 63, 10, 0, 64, 10, 0, 65, 10, 0, 66, 10, 0, 67, 10, 0, 68, 10, 1, 69, 10, 3, 70, 10, 0, 71, 10, 0, 72, 10, 0, 73, 10, 0, 74, 10, 1, 75, 10, 1, 13, 11, 6, 14, 11, 6, 15, 11, 1, 16, 11, 1, 17, 11, 1, 18, 11, 1, 19, 11, 3, 20, 11, 1, 22, 11, 1, 23, 11, 0, 24, 11, 0, 25, 11, 0, 26, 11, 0, 27, 11, 1, 29, 11, 1, 30, 11, 3, 31, 11, 1, 32, 11, 1, 33, 11, 1, 36, 11, 1, 37, 11, 1, 38, 11, 1, 39, 11, 1, 40, 11, 3, 41, 11, 1, 46, 11, 1, 47, 11, 0, 48, 11, 0, 49, 11, 0, 50, 11, 2, 51, 11, 0, 52, 11, 0, 53, 11, 0, 54, 11, 0, 55, 11, 1, 56, 11, 0, 57, 11, 0, 58, 11, 0, 59, 11, 0, 60, 11, 0, 61, 11, 1, 62, 11, 1, 63, 11, 1, 64, 11, 1, 65, 11, 1, 66, 11, 1, 67, 11, 2, 68, 11, 1, 69, 11, 2, 70, 11, 1, 71, 11, 0, 72, 11, 3, 73, 11, 1, 74, 11, 1, 12, 12, 6, 13, 12, 6, 14, 12, 6, 15, 12, 6, 16, 12, 6, 18, 12, 1, 19, 12, 3, 20, 12, 1, 22, 12, 1, 23, 12, 0, 24, 12, 0, 25, 12, 0, 26, 12, 0, 27, 12, 1, 29, 12, 1, 30, 12, 0, 31, 12, 0, 32, 12, 0, 33, 12, 1, 35, 12, 1, 36, 12, 1, 37, 12, 0, 38, 12, 1, 39, 12, 0, 40, 12, 2, 41, 12, 1, 42, 12, 1, 43, 12, 1, 44, 12, 1, 45, 12, 1, 46, 12, 1, 47, 12, 1, 48, 12, 1, 49, 12, 1, 50, 12, 1, 51, 12, 0, 52, 12, 0, 53, 12, 0, 54, 12, 0, 55, 12, 1, 56, 12, 1, 57, 12, 1, 58, 12, 1, 59, 12, 1, 60, 12, 1, 61, 12, 1, 62, 12, 1, 63, 12, 1, 64, 12, 1, 65, 12, 1, 66, 12, 1, 67, 12, 0, 68, 12, 0, 69, 12, 0, 70, 12, 1, 71, 12, 1, 72, 12, 2, 73, 12, 1, 12, 13, 6, 13, 13, 6, 14, 13, 6, 15, 13, 6, 16, 13, 6, 17, 13, 6, 18, 13, 1, 19, 13, 3, 20, 13, 1, 22, 13, 1, 23, 13, 0, 24, 13, 0, 25, 13, 0, 26, 13, 0, 27, 13, 1, 29, 13, 1, 30, 13, 0, 31, 13, 0, 32, 13, 0, 33, 13, 1, 34, 13, 1, 35, 13, 1, 36, 13, 0, 37, 13, 0, 38, 13, 0, 39, 13, 0, 40, 13, 0, 41, 13, 2, 42, 13, 3, 43, 13, 3, 44, 13, 3, 45, 13, 3, 46, 13, 3, 47, 13, 3, 48, 13, 3, 49, 13, 3, 50, 13, 3, 51, 13, 0, 52, 13, 0, 53, 13, 0, 54, 13, 0, 55, 13, 2, 56, 13, 3, 57, 13, 3, 58, 13, 3, 59, 13, 3, 60, 13, 3, 61, 13, 3, 62, 13, 3, 63, 13, 3, 64, 13, 0, 65, 13, 0, 66, 13, 0, 67, 13, 0, 68, 13, 0, 69, 13, 0, 70, 13, 0, 71, 13, 0, 72, 13, 0, 73, 13, 1, 2, 14, 1, 3, 14, 1, 4, 14, 1, 11, 14, 1, 12, 14, 6, 13, 14, 6, 14, 14, 6, 15, 14, 6, 16, 14, 6, 18, 14, 1, 19, 14, 3, 20, 14, 1, 21, 14, 1, 22, 14, 1, 23, 14, 2, 24, 14, 1, 25, 14, 1, 26, 14, 1, 27, 14, 1, 29, 14, 1, 30, 14, 2, 31, 14, 1, 32, 14, 1, 33, 14, 1, 34, 14, 1, 35, 14, 0, 36, 14, 0, 37, 14, 0, 38, 14, 0, 39, 14, 8, 40, 14, 0, 41, 14, 0, 42, 14, 1, 43, 14, 1, 44, 14, 1, 45, 14, 1, 46, 14, 1, 47, 14, 1, 48, 14, 1, 49, 14, 1, 50, 14, 1, 51, 14, 0, 52, 14, 0, 53, 14, 0, 54, 14, 0, 55, 14, 1, 56, 14, 1, 57, 14, 1, 58, 14, 1, 59, 14, 1, 60, 14, 1, 61, 14, 1, 62, 14, 1, 63, 14, 1, 64, 14, 0, 65, 14, 0, 66, 14, 0, 67, 14, 0, 68, 14, 0, 69, 14, 0, 70, 14, 0, 71, 14, 0, 72, 14, 0, 73, 14, 1, 1, 15, 1, 2, 15, 1, 3, 15, 9, 4, 15, 1, 5, 15, 1, 6, 15, 1, 9, 15, 1, 10, 15, 1, 11, 15, 1, 12, 15, 0, 13, 15, 6, 14, 15, 6, 15, 15, 1, 18, 15, 1, 19, 15, 2, 20, 15, 0, 21, 15, 1, 22, 15, 1, 23, 15, 3, 24, 15, 1, 29, 15, 1, 30, 15, 3, 31, 15, 1, 34, 15, 1, 35, 15, 0, 36, 15, 0, 37, 15, 0, 38, 15, 0, 39, 15, 0, 40, 15, 0, 41, 15, 2, 42, 15, 1, 43, 15, 1, 44, 15, 1, 45, 15, 1, 50, 15, 1, 51, 15, 1, 52, 15, 1, 53, 15, 1, 54, 15, 1, 55, 15, 1, 63, 15, 1, 64, 15, 0, 65, 15, 0, 66, 15, 0, 67, 15, 0, 68, 15, 0, 69, 15, 0, 70, 15, 0, 71, 15, 0, 72, 15, 0, 73, 15, 1, 1, 16, 1, 2, 16, 0, 3, 16, 0, 4, 16, 0, 5, 16, 0, 6, 16, 1, 7, 16, 1, 8, 16, 1, 9, 16, 1, 10, 16, 0, 11, 16, 0, 12, 16, 0, 13, 16, 0, 14, 16, 0, 15, 16, 1, 17, 16, 1, 18, 16, 1, 19, 16, 0, 20, 16, 0, 21, 16, 0, 22, 16, 1, 23, 16, 3, 24, 16, 1, 29, 16, 1, 30, 16, 3, 31, 16, 1, 34, 16, 1, 35, 16, 1, 36, 16, 0, 37, 16, 0, 38, 16, 0, 39, 16, 0, 40, 16, 1, 41, 16, 3, 42, 16, 1, 43, 16, 1, 44, 16, 0, 45, 16, 1, 46, 16, 1, 47, 16, 1, 51, 16, 1, 52, 16, 1, 53, 16, 1, 54, 16, 1, 55, 16, 1, 56, 16, 1, 57, 16, 1, 58, 16, 6, 59, 16, 6, 60, 16, 6, 62, 16, 6, 63, 16, 6, 64, 16, 6, 65, 16, 1, 66, 16, 1, 67, 16, 0, 68, 16, 0, 69, 16, 0, 70, 16, 1, 71, 16, 1, 72, 16, 2, 73, 16, 1, 1, 17, 1, 2, 17, 1, 3, 17, 0, 4, 17, 0, 5, 17, 0, 6, 17, 0, 7, 17, 3, 8, 17, 3, 9, 17, 2, 10, 17, 0, 11, 17, 0, 12, 17, 0, 13, 17, 0, 14, 17, 0, 15, 17, 1, 16, 17, 1, 17, 17, 1, 18, 17, 0, 19, 17, 0, 20, 17, 0, 21, 17, 0, 22, 17, 0, 23, 17, 0, 24, 17, 1, 25, 17, 1, 27, 17, 1, 28, 17, 1, 29, 17, 1, 30, 17, 3, 31, 17, 1, 32, 17, 1, 33, 17, 1, 35, 17, 1, 36, 17, 1, 37, 17, 0, 38, 17, 1, 39, 17, 1, 40, 17, 1, 41, 17, 3, 42, 17, 0, 43, 17, 0, 44, 17, 0, 45, 17, 0, 46, 17, 0, 47, 17, 1, 48, 17, 1, 49, 17, 1, 50, 17, 1, 51, 17, 1, 52, 17, 0, 53, 17, 0, 54, 17, 0, 55, 17, 0, 56, 17, 0, 57, 17, 6, 58, 17, 6, 59, 17, 6, 60, 17, 6, 61, 17, 6, 62, 17, 6, 63, 17, 6, 64, 17, 6, 65, 17, 6, 66, 17, 1, 67, 17, 1, 68, 17, 1, 69, 17, 1, 70, 17, 1, 71, 17, 1, 72, 17, 3, 73, 17, 1, 2, 18, 1, 3, 18, 1, 4, 18, 1, 5, 18, 0, 6, 18, 1, 7, 18, 1, 8, 18, 1, 9, 18, 0, 10, 18, 0, 11, 18, 0, 12, 18, 0, 13, 18, 0, 14, 18, 0, 15, 18, 0, 16, 18, 3, 17, 18, 3, 18, 18, 2, 19, 18, 0, 20, 18, 0, 21, 18, 0, 22, 18, 0, 23, 18, 0, 24, 18, 0, 25, 18, 1, 26, 18, 1, 27, 18, 1, 28, 18, 0, 29, 18, 0, 30, 18, 0, 31, 18, 0, 32, 18, 0, 33, 18, 1, 36, 18, 1, 37, 18, 1, 38, 18, 1, 40, 18, 1, 41, 18, 3, 42, 18, 0, 43, 18, 0, 44, 18, 0, 45, 18, 0, 46, 18, 0, 47, 18, 2, 48, 18, 3, 49, 18, 3, 50, 18, 3, 51, 18, 3, 52, 18, 0, 53, 18, 0, 54, 18, 0, 55, 18, 0, 56, 18, 0, 57, 18, 6, 58, 18, 6, 59, 18, 6, 60, 18, 6, 61, 18, 6, 62, 18, 6, 63, 18, 6, 64, 18, 6, 65, 18, 6, 70, 18, 1, 71, 18, 1, 72, 18, 0, 73, 18, 1, 74, 18, 1, 4, 19, 1, 5, 19, 1, 6, 19, 1, 8, 19, 1, 9, 19, 1, 10, 19, 0, 11, 19, 0, 12, 19, 0, 13, 19, 0, 14, 19, 0, 15, 19, 1, 16, 19, 1, 17, 19, 1, 18, 19, 1, 19, 19, 1, 20, 19, 0, 21, 19, 0, 22, 19, 0, 23, 19, 0, 24, 19, 0, 25, 19, 0, 26, 19, 1, 27, 19, 1, 28, 19, 0, 29, 19, 0, 30, 19, 0, 31, 19, 0, 32, 19, 0, 33, 19, 1, 40, 19, 1, 41, 19, 0, 42, 19, 0, 43, 19, 0, 44, 19, 0, 45, 19, 0, 46, 19, 0, 47, 19, 0, 48, 19, 1, 49, 19, 1, 50, 19, 1, 51, 19, 1, 52, 19, 0, 53, 19, 0, 54, 19, 0, 55, 19, 0, 56, 19, 0, 57, 19, 0, 58, 19, 6, 59, 19, 6, 60, 19, 6, 61, 19, 1, 62, 19, 0, 63, 19, 6, 64, 19, 6, 65, 19, 1, 66, 19, 1, 69, 19, 1, 70, 19, 1, 71, 19, 0, 72, 19, 0, 73, 19, 0, 74, 19, 1, 75, 19, 1, 9, 20, 1, 10, 20, 0, 11, 20, 0, 12, 20, 0, 13, 20, 0, 14, 20, 0, 15, 20, 1, 19, 20, 1, 20, 20, 1, 21, 20, 1, 22, 20, 0, 23, 20, 0, 24, 20, 0, 25, 20, 1, 26, 20, 1, 27, 20, 1, 28, 20, 0, 29, 20, 0, 30, 20, 0, 31, 20, 0, 32, 20, 0, 33, 20, 1, 40, 20, 1, 41, 20, 1, 42, 20, 0, 43, 20, 0, 44, 20, 0, 45, 20, 0, 46, 20, 0, 47, 20, 1, 48, 20, 1, 51, 20, 1, 52, 20, 2, 53, 20, 1, 54, 20, 1, 55, 20, 1, 56, 20, 1, 57, 20, 1, 58, 20, 1, 59, 20, 6, 60, 20, 6, 61, 20, 0, 62, 20, 0, 63, 20, 0, 64, 20, 0, 65, 20, 0, 66, 20, 1, 67, 20, 1, 69, 20, 1, 70, 20, 0, 71, 20, 0, 72, 20, 0, 73, 20, 0, 74, 20, 0, 75, 20, 1, 76, 20, 1, 9, 21, 1, 10, 21, 1, 11, 21, 1, 12, 21, 0, 13, 21, 1, 14, 21, 1, 15, 21, 1, 21, 21, 1, 22, 21, 1, 23, 21, 0, 24, 21, 1, 25, 21, 1, 27, 21, 1, 28, 21, 1, 29, 21, 1, 30, 21, 1, 31, 21, 1, 32, 21, 1, 33, 21, 1, 41, 21, 1, 42, 21, 0, 43, 21, 0, 44, 21, 0, 45, 21, 0, 46, 21, 0, 47, 21, 1, 51, 21, 1, 52, 21, 3, 53, 21, 1, 60, 21, 1, 61, 21, 1, 62, 21, 0, 63, 21, 0, 64, 21, 0, 65, 21, 1, 66, 21, 0, 67, 21, 1, 68, 21, 1, 69, 21, 1, 70, 21, 1, 71, 21, 0, 72, 21, 0, 73, 21, 0, 74, 21, 0, 75, 21, 0, 76, 21, 1, 11, 22, 1, 12, 22, 1, 13, 22, 1, 21, 22, 1, 22, 22, 1, 23, 22, 2, 24, 22, 1, 25, 22, 1, 34, 22, 1, 35, 22, 1, 36, 22, 1, 37, 22, 1, 38, 22, 1, 39, 22, 1, 40, 22, 1, 41, 22, 1, 42, 22, 1, 43, 22, 1, 44, 22, 0, 45, 22, 1, 46, 22, 1, 47, 22, 1, 51, 22, 1, 52, 22, 3, 53, 22, 1, 61, 22, 1, 62, 22, 1, 63, 22, 0, 64, 22, 1, 65, 22, 0, 66, 22, 0, 67, 22, 0, 68, 22, 1, 70, 22, 1, 71, 22, 1, 72, 22, 0, 73, 22, 0, 74, 22, 0, 75, 22, 1, 76, 22, 1, 20, 23, 1, 21, 23, 1, 22, 23, 0, 23, 23, 0, 24, 23, 0, 25, 23, 1, 26, 23, 1, 34, 23, 1, 35, 23, 0, 36, 23, 0, 37, 23, 0, 38, 23, 0, 39, 23, 0, 40, 23, 1, 43, 23, 1, 44, 23, 1, 45, 23, 1, 48, 23, 1, 49, 23, 1, 50, 23, 1, 51, 23, 1, 52, 23, 3, 53, 23, 1, 54, 23, 1, 55, 23, 1, 56, 23, 1, 60, 23, 1, 61, 23, 1, 62, 23, 0, 63, 23, 0, 64, 23, 0, 65, 23, 0, 66, 23, 0, 67, 23, 1, 68, 23, 1, 71, 23, 1, 72, 23, 1, 73, 23, 0, 74, 23, 1, 75, 23, 1, 20, 24, 1, 21, 24, 0, 22, 24, 0, 23, 24, 0, 24, 24, 0, 25, 24, 0, 26, 24, 1, 27, 24, 1, 28, 24, 1, 29, 24, 1, 30, 24, 1, 31, 24, 1, 32, 24, 1, 33, 24, 1, 34, 24, 1, 35, 24, 0, 36, 24, 0, 37, 24, 0, 38, 24, 0, 39, 24, 0, 40, 24, 1, 48, 24, 1, 49, 24, 0, 50, 24, 0, 51, 24, 0, 52, 24, 0, 53, 24, 0, 54, 24, 0, 55, 24, 0, 56, 24, 1, 60, 24, 1, 61, 24, 0, 62, 24, 0, 63, 24, 0, 64, 24, 0, 65, 24, 0, 66, 24, 0, 67, 24, 0, 68, 24, 1, 72, 24, 1, 73, 24, 1, 74, 24, 1, 12, 25, 1, 13, 25, 1, 14, 25, 1, 15, 25, 1, 16, 25, 1, 17, 25, 1, 18, 25, 1, 19, 25, 1, 20, 25, 1, 21, 25, 0, 22, 25, 0, 23, 25, 0, 24, 25, 0, 25, 25, 0, 26, 25, 3, 27, 25, 3, 28, 25, 3, 29, 25, 3, 30, 25, 3, 31, 25, 3, 32, 25, 3, 33, 25, 3, 34, 25, 2, 35, 25, 0, 36, 25, 0, 37, 25, 0, 38, 25, 0, 39, 25, 0, 40, 25, 1, 41, 25, 1, 42, 25, 1, 43, 25, 1, 44, 25, 1, 45, 25, 1, 46, 25, 1, 47, 25, 1, 48, 25, 1, 49, 25, 0, 50, 25, 0, 51, 25, 0, 52, 25, 0, 53, 25, 0, 54, 25, 0, 55, 25, 0, 56, 25, 1, 57, 25, 1, 58, 25, 1, 59, 25, 1, 60, 25, 1, 61, 25, 1, 62, 25, 0, 63, 25, 0, 64, 25, 0, 65, 25, 0, 66, 25, 0, 67, 25, 1, 68, 25, 1, 12, 26, 1, 13, 26, 0, 14, 26, 0, 15, 26, 0, 16, 26, 0, 17, 26, 0, 18, 26, 3, 19, 26, 3, 20, 26, 2, 21, 26, 0, 22, 26, 0, 23, 26, 0, 24, 26, 0, 25, 26, 0, 26, 26, 1, 27, 26, 1, 28, 26, 1, 29, 26, 1, 30, 26, 1, 31, 26, 1, 32, 26, 1, 33, 26, 1, 34, 26, 1, 35, 26, 0, 36, 26, 0, 37, 26, 0, 38, 26, 0, 39, 26, 0, 40, 26, 3, 41, 26, 3, 42, 26, 3, 43, 26, 3, 44, 26, 3, 45, 26, 3, 46, 26, 3, 47, 26, 3, 48, 26, 2, 49, 26, 0, 50, 26, 0, 51, 26, 0, 52, 26, 0, 53, 26, 0, 54, 26, 0, 55, 26, 0, 56, 26, 2, 57, 26, 3, 58, 26, 3, 59, 26, 3, 60, 26, 3, 61, 26, 3, 62, 26, 3, 63, 26, 0, 64, 26, 3, 65, 26, 0, 66, 26, 1, 67, 26, 1, 12, 27, 1, 13, 27, 0, 14, 27, 0, 15, 27, 0, 16, 27, 0, 17, 27, 0, 18, 27, 1, 19, 27, 1, 20, 27, 1, 21, 27, 1, 22, 27, 0, 23, 27, 0, 24, 27, 0, 25, 27, 1, 26, 27, 1, 34, 27, 1, 35, 27, 1, 36, 27, 1, 37, 27, 1, 38, 27, 1, 39, 27, 1, 40, 27, 1, 41, 27, 1, 42, 27, 1, 43, 27, 1, 44, 27, 1, 45, 27, 1, 46, 27, 1, 47, 27, 1, 48, 27, 1, 49, 27, 0, 50, 27, 0, 51, 27, 0, 52, 27, 0, 53, 27, 0, 54, 27, 0, 55, 27, 0, 56, 27, 1, 57, 27, 1, 58, 27, 1, 59, 27, 1, 60, 27, 1, 61, 27, 1, 62, 27, 1, 63, 27, 1, 64, 27, 1, 65, 27, 1, 66, 27, 1, 12, 28, 1, 13, 28, 1, 14, 28, 1, 15, 28, 1, 16, 28, 1, 17, 28, 1, 18, 28, 1, 21, 28, 1, 22, 28, 1, 23, 28, 1, 24, 28, 1, 25, 28, 1, 48, 28, 1, 49, 28, 1, 50, 28, 1, 51, 28, 1, 52, 28, 1, 53, 28, 1, 54, 28, 1, 55, 28, 1, 56, 28, 1)
-}
-
-[node name="Level6" type="GridMap" parent="Levels" unique_id=202287074]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -36, 0)
-mesh_library = SubResource("MeshLibrary_r4kn7")
-cell_size = Vector3(1, 1, 1)
-data = {
-"cells": PackedInt32Array(3, 0, 1, 4, 0, 1, 5, 0, 1, 6, 0, 1, 7, 0, 1, 29, 0, 1, 30, 0, 1, 31, 0, 1, 32, 0, 1, 33, 0, 1, 34, 0, 1, 35, 0, 1, 37, 0, 1, 38, 0, 1, 39, 0, 1, 40, 0, 1, 41, 0, 1, 42, 0, 1, 43, 0, 1, 44, 0, 1, 49, 0, 1, 50, 0, 1, 51, 0, 1, 0, 1, 1, 1, 1, 1, 2, 1, 1, 3, 1, 1, 4, 1, 0, 5, 1, 0, 6, 1, 0, 7, 1, 1, 8, 1, 1, 9, 1, 1, 10, 1, 1, 19, 1, 1, 20, 1, 1, 21, 1, 1, 22, 1, 1, 23, 1, 1, 24, 1, 1, 25, 1, 1, 26, 1, 1, 27, 1, 1, 28, 1, 1, 29, 1, 1, 30, 1, 0, 31, 1, 0, 32, 1, 0, 33, 1, 0, 34, 1, 0, 35, 1, 1, 37, 1, 1, 38, 1, 0, 39, 1, 0, 40, 1, 0, 41, 1, 0, 42, 1, 0, 43, 1, 0, 44, 1, 1, 47, 1, 1, 48, 1, 1, 49, 1, 1, 50, 1, 0, 51, 1, 1, 52, 1, 1, 53, 1, 1, 0, 2, 1, 1, 2, 0, 2, 2, 0, 3, 2, 0, 4, 2, 0, 5, 2, 0, 6, 2, 0, 7, 2, 0, 8, 2, 0, 9, 2, 9, 10, 2, 1, 19, 2, 1, 20, 2, 0, 21, 2, 0, 22, 2, 0, 23, 2, 0, 24, 2, 0, 25, 2, 0, 26, 2, 2, 27, 2, 3, 28, 2, 3, 29, 2, 3, 30, 2, 0, 31, 2, 0, 32, 2, 0, 33, 2, 0, 34, 2, 0, 35, 2, 1, 37, 2, 1, 38, 2, 0, 39, 2, 0, 40, 2, 0, 41, 2, 0, 42, 2, 0, 43, 2, 0, 44, 2, 1, 47, 2, 1, 48, 2, 0, 49, 2, 0, 50, 2, 0, 51, 2, 0, 52, 2, 0, 53, 2, 1, 55, 2, 1, 56, 2, 1, 57, 2, 1, 58, 2, 1, 59, 2, 1, 60, 2, 1, 61, 2, 1, 0, 3, 1, 1, 3, 0, 2, 3, 0, 3, 3, 0, 4, 3, 0, 5, 3, 0, 6, 3, 0, 7, 3, 0, 8, 3, 0, 9, 3, 0, 10, 3, 1, 19, 3, 1, 20, 3, 0, 21, 3, 0, 22, 3, 0, 23, 3, 0, 24, 3, 0, 25, 3, 0, 26, 3, 1, 27, 3, 1, 28, 3, 1, 29, 3, 1, 30, 3, 0, 31, 3, 0, 32, 3, 0, 33, 3, 0, 34, 3, 0, 35, 3, 1, 37, 3, 1, 38, 3, 0, 39, 3, 0, 40, 3, 0, 41, 3, 0, 42, 3, 0, 43, 3, 0, 44, 3, 1, 46, 3, 1, 47, 3, 1, 48, 3, 0, 49, 3, 0, 50, 3, 0, 51, 3, 0, 52, 3, 0, 53, 3, 1, 54, 3, 1, 55, 3, 1, 56, 3, 0, 57, 3, 0, 58, 3, 0, 59, 3, 0, 60, 3, 0, 61, 3, 1, 0, 4, 1, 1, 4, 0, 2, 4, 0, 3, 4, 0, 4, 4, 0, 5, 4, 0, 6, 4, 0, 7, 4, 0, 8, 4, 0, 9, 4, 0, 10, 4, 1, 19, 4, 1, 20, 4, 1, 21, 4, 1, 22, 4, 1, 23, 4, 3, 24, 4, 1, 25, 4, 1, 26, 4, 1, 29, 4, 1, 30, 4, 1, 31, 4, 1, 32, 4, 1, 33, 4, 1, 34, 4, 1, 35, 4, 1, 37, 4, 1, 38, 4, 3, 39, 4, 1, 40, 4, 1, 41, 4, 1, 42, 4, 1, 43, 4, 1, 44, 4, 1, 46, 4, 1, 47, 4, 0, 48, 4, 0, 49, 4, 0, 50, 4, 0, 51, 4, 0, 52, 4, 0, 53, 4, 0, 54, 4, 1, 55, 4, 1, 56, 4, 0, 57, 4, 0, 58, 4, 0, 59, 4, 0, 60, 4, 0, 61, 4, 1, 66, 4, 1, 67, 4, 1, 68, 4, 1, 69, 4, 1, 70, 4, 1, 0, 5, 1, 1, 5, 3, 2, 5, 1, 3, 5, 1, 4, 5, 0, 5, 5, 0, 6, 5, 0, 7, 5, 1, 8, 5, 1, 9, 5, 1, 10, 5, 1, 22, 5, 1, 23, 5, 3, 24, 5, 1, 37, 5, 1, 38, 5, 2, 39, 5, 1, 40, 5, 0, 41, 5, 0, 42, 5, 0, 43, 5, 1, 44, 5, 1, 45, 5, 1, 46, 5, 1, 47, 5, 3, 48, 5, 0, 49, 5, 0, 50, 5, 0, 51, 5, 0, 52, 5, 0, 53, 5, 1, 54, 5, 1, 55, 5, 1, 56, 5, 0, 57, 5, 0, 58, 5, 0, 59, 5, 0, 60, 5, 0, 61, 5, 1, 66, 5, 1, 67, 5, 0, 68, 5, 0, 69, 5, 0, 70, 5, 1, 0, 6, 1, 1, 6, 3, 2, 6, 1, 3, 6, 1, 4, 6, 1, 5, 6, 1, 6, 6, 1, 7, 6, 1, 22, 6, 1, 23, 6, 3, 24, 6, 1, 37, 6, 1, 38, 6, 0, 39, 6, 0, 40, 6, 0, 41, 6, 0, 42, 6, 0, 43, 6, 0, 44, 6, 0, 45, 6, 1, 46, 6, 1, 47, 6, 3, 48, 6, 0, 49, 6, 0, 50, 6, 0, 51, 6, 0, 52, 6, 0, 53, 6, 1, 55, 6, 1, 56, 6, 1, 57, 6, 3, 58, 6, 1, 59, 6, 1, 60, 6, 1, 61, 6, 1, 66, 6, 1, 67, 6, 0, 68, 6, 0, 69, 6, 0, 70, 6, 1, 0, 7, 1, 1, 7, 3, 2, 7, 1, 22, 7, 1, 23, 7, 3, 24, 7, 1, 37, 7, 1, 38, 7, 0, 39, 7, 0, 40, 7, 0, 41, 7, 0, 42, 7, 0, 43, 7, 0, 44, 7, 0, 45, 7, 1, 46, 7, 1, 47, 7, 3, 48, 7, 1, 49, 7, 1, 50, 7, 0, 51, 7, 1, 52, 7, 1, 53, 7, 1, 54, 7, 1, 55, 7, 1, 56, 7, 1, 57, 7, 3, 58, 7, 1, 66, 7, 1, 67, 7, 3, 68, 7, 0, 69, 7, 0, 70, 7, 1, 0, 8, 1, 1, 8, 2, 2, 8, 1, 3, 8, 1, 4, 8, 1, 5, 8, 1, 6, 8, 1, 7, 8, 1, 8, 8, 1, 9, 8, 1, 10, 8, 1, 11, 8, 1, 12, 8, 1, 13, 8, 1, 14, 8, 1, 15, 8, 1, 16, 8, 1, 17, 8, 1, 18, 8, 1, 19, 8, 1, 20, 8, 1, 21, 8, 1, 22, 8, 1, 23, 8, 2, 24, 8, 1, 37, 8, 1, 38, 8, 0, 39, 8, 0, 40, 8, 0, 41, 8, 0, 42, 8, 0, 43, 8, 0, 44, 8, 0, 45, 8, 1, 46, 8, 1, 47, 8, 3, 48, 8, 1, 49, 8, 1, 50, 8, 1, 51, 8, 1, 53, 8, 1, 54, 8, 1, 55, 8, 0, 56, 8, 1, 57, 8, 3, 58, 8, 1, 66, 8, 1, 67, 8, 3, 68, 8, 1, 69, 8, 1, 70, 8, 1, 0, 9, 1, 1, 9, 0, 2, 9, 0, 3, 9, 0, 4, 9, 0, 5, 9, 0, 6, 9, 3, 7, 9, 3, 8, 9, 3, 9, 9, 3, 10, 9, 3, 11, 9, 3, 12, 9, 3, 13, 9, 3, 14, 9, 2, 15, 9, 0, 16, 9, 0, 17, 9, 0, 18, 9, 0, 19, 9, 0, 20, 9, 0, 21, 9, 0, 22, 9, 0, 23, 9, 0, 24, 9, 1, 37, 9, 1, 38, 9, 1, 39, 9, 3, 40, 9, 0, 41, 9, 0, 42, 9, 0, 43, 9, 1, 44, 9, 1, 45, 9, 1, 46, 9, 1, 47, 9, 2, 48, 9, 0, 49, 9, 1, 50, 9, 1, 52, 9, 1, 53, 9, 1, 54, 9, 0, 55, 9, 0, 56, 9, 0, 57, 9, 2, 58, 9, 1, 66, 9, 1, 67, 9, 3, 68, 9, 1, 0, 10, 1, 1, 10, 0, 2, 10, 0, 3, 10, 0, 4, 10, 0, 5, 10, 0, 6, 10, 1, 7, 10, 1, 8, 10, 1, 9, 10, 1, 10, 10, 1, 11, 10, 1, 12, 10, 1, 13, 10, 1, 14, 10, 1, 15, 10, 0, 16, 10, 0, 17, 10, 0, 18, 10, 0, 19, 10, 0, 20, 10, 0, 21, 10, 0, 22, 10, 0, 23, 10, 0, 24, 10, 1, 38, 10, 1, 39, 10, 3, 40, 10, 1, 41, 10, 1, 42, 10, 1, 43, 10, 1, 45, 10, 1, 46, 10, 1, 47, 10, 0, 48, 10, 0, 49, 10, 0, 50, 10, 1, 51, 10, 1, 52, 10, 1, 53, 10, 0, 54, 10, 0, 55, 10, 0, 56, 10, 0, 57, 10, 0, 58, 10, 1, 66, 10, 1, 67, 10, 3, 68, 10, 1, 0, 11, 1, 1, 11, 0, 2, 11, 0, 3, 11, 0, 4, 11, 0, 5, 11, 0, 6, 11, 1, 9, 11, 1, 10, 11, 1, 11, 11, 1, 12, 11, 1, 13, 11, 1, 14, 11, 1, 15, 11, 0, 16, 11, 0, 17, 11, 0, 18, 11, 0, 19, 11, 0, 20, 11, 0, 21, 11, 0, 22, 11, 0, 23, 11, 0, 24, 11, 1, 29, 11, 1, 30, 11, 1, 31, 11, 1, 32, 11, 1, 33, 11, 1, 35, 11, 1, 36, 11, 1, 37, 11, 1, 38, 11, 1, 39, 11, 2, 40, 11, 1, 41, 11, 1, 42, 11, 1, 43, 11, 1, 44, 11, 1, 45, 11, 1, 46, 11, 0, 47, 11, 0, 48, 11, 0, 49, 11, 0, 50, 11, 0, 51, 11, 2, 52, 11, 3, 53, 11, 3, 54, 11, 0, 55, 11, 0, 56, 11, 0, 57, 11, 1, 58, 11, 1, 61, 11, 1, 62, 11, 1, 63, 11, 1, 64, 11, 1, 65, 11, 1, 66, 11, 1, 67, 11, 3, 68, 11, 1, 0, 12, 1, 1, 12, 0, 2, 12, 0, 3, 12, 0, 4, 12, 0, 5, 12, 0, 6, 12, 1, 7, 12, 1, 8, 12, 1, 9, 12, 1, 10, 12, 0, 11, 12, 0, 12, 12, 0, 13, 12, 1, 14, 12, 1, 15, 12, 1, 16, 12, 1, 17, 12, 1, 18, 12, 1, 19, 12, 3, 20, 12, 1, 21, 12, 1, 22, 12, 1, 23, 12, 1, 24, 12, 1, 28, 12, 1, 29, 12, 1, 30, 12, 0, 31, 12, 0, 32, 12, 0, 33, 12, 1, 34, 12, 1, 35, 12, 1, 36, 12, 0, 37, 12, 0, 38, 12, 0, 39, 12, 0, 40, 12, 0, 41, 12, 0, 42, 12, 0, 43, 12, 2, 44, 12, 3, 45, 12, 3, 46, 12, 0, 47, 12, 0, 48, 12, 0, 49, 12, 0, 50, 12, 1, 51, 12, 1, 52, 12, 1, 53, 12, 1, 54, 12, 1, 55, 12, 0, 56, 12, 1, 57, 12, 1, 58, 12, 1, 59, 12, 1, 60, 12, 1, 61, 12, 1, 62, 12, 0, 63, 12, 0, 64, 12, 0, 65, 12, 1, 66, 12, 1, 67, 12, 2, 68, 12, 1, 0, 13, 1, 1, 13, 1, 2, 13, 1, 3, 13, 1, 4, 13, 1, 5, 13, 1, 6, 13, 1, 7, 13, 1, 8, 13, 0, 9, 13, 0, 10, 13, 0, 11, 13, 0, 12, 13, 0, 13, 13, 0, 14, 13, 0, 15, 13, 1, 16, 13, 1, 17, 13, 1, 18, 13, 1, 19, 13, 2, 20, 13, 1, 21, 13, 1, 22, 13, 1, 23, 13, 1, 24, 13, 1, 25, 13, 1, 26, 13, 1, 27, 13, 1, 28, 13, 1, 29, 13, 0, 30, 13, 0, 31, 13, 0, 32, 13, 0, 33, 13, 0, 34, 13, 1, 35, 13, 1, 36, 13, 0, 37, 13, 0, 38, 13, 0, 39, 13, 0, 40, 13, 0, 41, 13, 0, 42, 13, 0, 43, 13, 1, 44, 13, 1, 45, 13, 0, 46, 13, 0, 47, 13, 0, 48, 13, 0, 49, 13, 0, 50, 13, 2, 51, 13, 3, 52, 13, 3, 53, 13, 3, 54, 13, 3, 55, 13, 3, 56, 13, 3, 57, 13, 3, 58, 13, 3, 59, 13, 0, 60, 13, 0, 61, 13, 0, 62, 13, 0, 63, 13, 0, 64, 13, 0, 65, 13, 0, 66, 13, 0, 67, 13, 0, 68, 13, 1, 7, 14, 1, 8, 14, 0, 9, 14, 0, 10, 14, 0, 11, 14, 0, 12, 14, 0, 13, 14, 0, 14, 14, 0, 15, 14, 3, 16, 14, 2, 17, 14, 0, 18, 14, 0, 19, 14, 0, 20, 14, 0, 21, 14, 3, 22, 14, 2, 23, 14, 0, 24, 14, 0, 25, 14, 0, 26, 14, 0, 27, 14, 1, 28, 14, 1, 29, 14, 0, 30, 14, 0, 31, 14, 0, 32, 14, 0, 33, 14, 0, 34, 14, 1, 35, 14, 1, 36, 14, 0, 37, 14, 0, 38, 14, 0, 39, 14, 8, 40, 14, 0, 41, 14, 0, 42, 14, 0, 43, 14, 1, 44, 14, 1, 45, 14, 1, 46, 14, 0, 47, 14, 0, 48, 14, 0, 49, 14, 1, 50, 14, 1, 51, 14, 1, 52, 14, 1, 53, 14, 1, 54, 14, 1, 55, 14, 1, 56, 14, 1, 57, 14, 1, 58, 14, 1, 59, 14, 0, 60, 14, 0, 61, 14, 0, 62, 14, 0, 63, 14, 0, 64, 14, 0, 65, 14, 0, 66, 14, 0, 67, 14, 0, 68, 14, 1, 7, 15, 1, 8, 15, 0, 9, 15, 0, 10, 15, 0, 11, 15, 0, 12, 15, 0, 13, 15, 0, 14, 15, 0, 15, 15, 1, 16, 15, 1, 17, 15, 0, 18, 15, 0, 19, 15, 0, 20, 15, 0, 21, 15, 1, 22, 15, 1, 23, 15, 0, 24, 15, 0, 25, 15, 0, 26, 15, 0, 27, 15, 2, 28, 15, 3, 29, 15, 0, 30, 15, 0, 31, 15, 0, 32, 15, 0, 33, 15, 0, 34, 15, 1, 35, 15, 1, 36, 15, 0, 37, 15, 0, 38, 15, 0, 39, 15, 0, 40, 15, 0, 41, 15, 0, 42, 15, 0, 43, 15, 1, 45, 15, 1, 46, 15, 1, 47, 15, 0, 48, 15, 1, 49, 15, 1, 58, 15, 1, 59, 15, 0, 60, 15, 0, 61, 15, 0, 62, 15, 0, 63, 15, 0, 64, 15, 0, 65, 15, 0, 66, 15, 0, 67, 15, 0, 68, 15, 1, 2, 16, 1, 3, 16, 1, 4, 16, 1, 5, 16, 1, 6, 16, 1, 7, 16, 1, 8, 16, 1, 9, 16, 1, 10, 16, 0, 11, 16, 0, 12, 16, 0, 13, 16, 1, 14, 16, 1, 15, 16, 1, 16, 16, 1, 17, 16, 2, 18, 16, 1, 19, 16, 1, 20, 16, 1, 21, 16, 1, 22, 16, 1, 23, 16, 1, 24, 16, 1, 25, 16, 1, 26, 16, 3, 27, 16, 1, 28, 16, 1, 29, 16, 1, 30, 16, 0, 31, 16, 0, 32, 16, 0, 33, 16, 1, 34, 16, 1, 35, 16, 1, 36, 16, 1, 37, 16, 1, 38, 16, 2, 39, 16, 1, 40, 16, 1, 41, 16, 1, 42, 16, 1, 43, 16, 1, 46, 16, 1, 47, 16, 2, 48, 16, 1, 56, 16, 1, 57, 16, 1, 58, 16, 1, 59, 16, 1, 60, 16, 1, 61, 16, 1, 62, 16, 0, 63, 16, 0, 64, 16, 0, 65, 16, 1, 66, 16, 1, 67, 16, 1, 68, 16, 1, 2, 17, 1, 3, 17, 0, 4, 17, 0, 5, 17, 0, 6, 17, 0, 7, 17, 1, 9, 17, 1, 10, 17, 1, 11, 17, 1, 12, 17, 1, 13, 17, 1, 14, 17, 1, 15, 17, 0, 16, 17, 0, 17, 17, 0, 18, 17, 0, 19, 17, 0, 20, 17, 1, 21, 17, 1, 22, 17, 1, 23, 17, 1, 24, 17, 1, 25, 17, 1, 26, 17, 2, 27, 17, 1, 28, 17, 1, 29, 17, 1, 30, 17, 1, 31, 17, 1, 32, 17, 1, 33, 17, 1, 37, 17, 1, 38, 17, 3, 39, 17, 1, 46, 17, 1, 47, 17, 3, 48, 17, 1, 54, 17, 1, 55, 17, 1, 56, 17, 1, 57, 17, 0, 58, 17, 1, 59, 17, 1, 60, 17, 1, 61, 17, 1, 62, 17, 1, 63, 17, 1, 64, 17, 2, 65, 17, 1, 72, 17, 1, 73, 17, 1, 74, 17, 1, 75, 17, 1, 76, 17, 1, 77, 17, 1, 78, 17, 1, 2, 18, 1, 3, 18, 0, 4, 18, 0, 5, 18, 0, 6, 18, 0, 7, 18, 1, 8, 18, 1, 9, 18, 1, 10, 18, 1, 11, 18, 1, 12, 18, 1, 13, 18, 1, 14, 18, 1, 15, 18, 0, 16, 18, 0, 17, 18, 0, 18, 18, 0, 19, 18, 0, 20, 18, 3, 21, 18, 3, 22, 18, 3, 23, 18, 3, 24, 18, 3, 25, 18, 2, 26, 18, 0, 27, 18, 0, 28, 18, 0, 29, 18, 1, 30, 18, 1, 31, 18, 1, 32, 18, 1, 33, 18, 1, 34, 18, 1, 35, 18, 1, 36, 18, 1, 37, 18, 1, 38, 18, 0, 39, 18, 1, 40, 18, 1, 46, 18, 1, 47, 18, 3, 48, 18, 1, 49, 18, 1, 53, 18, 1, 54, 18, 1, 55, 18, 0, 56, 18, 0, 57, 18, 0, 58, 18, 0, 59, 18, 0, 60, 18, 1, 61, 18, 1, 63, 18, 1, 64, 18, 3, 65, 18, 1, 66, 18, 1, 67, 18, 1, 72, 18, 1, 73, 18, 0, 74, 18, 0, 75, 18, 0, 76, 18, 0, 77, 18, 0, 78, 18, 1, 2, 19, 1, 3, 19, 0, 4, 19, 0, 5, 19, 0, 6, 19, 0, 7, 19, 3, 8, 19, 3, 9, 19, 3, 10, 19, 3, 11, 19, 3, 12, 19, 3, 13, 19, 3, 14, 19, 2, 15, 19, 0, 16, 19, 0, 17, 19, 0, 18, 19, 0, 19, 19, 0, 20, 19, 1, 21, 19, 1, 22, 19, 1, 23, 19, 1, 24, 19, 0, 25, 19, 0, 26, 19, 0, 27, 19, 0, 28, 19, 0, 29, 19, 0, 30, 19, 0, 31, 19, 3, 32, 19, 3, 33, 19, 3, 34, 19, 3, 35, 19, 3, 36, 19, 2, 37, 19, 0, 38, 19, 0, 39, 19, 0, 40, 19, 1, 45, 19, 1, 46, 19, 1, 47, 19, 3, 48, 19, 0, 49, 19, 1, 50, 19, 1, 51, 19, 1, 52, 19, 1, 53, 19, 1, 54, 19, 0, 55, 19, 0, 56, 19, 0, 57, 19, 0, 58, 19, 0, 59, 19, 0, 60, 19, 0, 61, 19, 1, 62, 19, 1, 63, 19, 1, 64, 19, 0, 65, 19, 0, 66, 19, 0, 67, 19, 1, 68, 19, 1, 69, 19, 1, 70, 19, 1, 71, 19, 1, 72, 19, 1, 73, 19, 0, 74, 19, 0, 75, 19, 0, 76, 19, 0, 77, 19, 0, 78, 19, 1, 2, 20, 1, 3, 20, 1, 4, 20, 2, 5, 20, 1, 6, 20, 1, 7, 20, 1, 8, 20, 1, 9, 20, 1, 10, 20, 1, 11, 20, 1, 12, 20, 1, 13, 20, 1, 14, 20, 1, 15, 20, 1, 16, 20, 1, 17, 20, 1, 18, 20, 1, 19, 20, 1, 20, 20, 1, 23, 20, 1, 24, 20, 0, 25, 20, 0, 26, 20, 0, 27, 20, 0, 28, 20, 0, 29, 20, 0, 30, 20, 0, 31, 20, 1, 32, 20, 1, 33, 20, 1, 34, 20, 1, 35, 20, 1, 36, 20, 1, 37, 20, 0, 38, 20, 0, 39, 20, 0, 40, 20, 1, 45, 20, 1, 46, 20, 0, 47, 20, 0, 48, 20, 0, 49, 20, 0, 50, 20, 0, 51, 20, 2, 52, 20, 3, 53, 20, 3, 54, 20, 0, 55, 20, 0, 56, 20, 0, 57, 20, 0, 58, 20, 0, 59, 20, 0, 60, 20, 0, 61, 20, 1, 62, 20, 1, 63, 20, 0, 64, 20, 0, 65, 20, 0, 66, 20, 0, 67, 20, 0, 68, 20, 2, 69, 20, 3, 70, 20, 3, 71, 20, 3, 72, 20, 3, 73, 20, 0, 74, 20, 0, 75, 20, 0, 76, 20, 0, 77, 20, 0, 78, 20, 1, 3, 21, 1, 4, 21, 3, 5, 21, 1, 23, 21, 1, 24, 21, 0, 25, 21, 0, 26, 21, 0, 27, 21, 0, 28, 21, 0, 29, 21, 0, 30, 21, 0, 31, 21, 1, 36, 21, 1, 37, 21, 1, 38, 21, 1, 39, 21, 2, 40, 21, 1, 44, 21, 1, 45, 21, 1, 46, 21, 0, 47, 21, 0, 48, 21, 0, 49, 21, 0, 50, 21, 0, 51, 21, 1, 52, 21, 1, 53, 21, 0, 54, 21, 0, 55, 21, 0, 56, 21, 0, 57, 21, 0, 58, 21, 0, 59, 21, 0, 60, 21, 0, 61, 21, 0, 62, 21, 2, 63, 21, 0, 64, 21, 0, 65, 21, 0, 66, 21, 0, 67, 21, 0, 68, 21, 1, 69, 21, 1, 70, 21, 1, 71, 21, 1, 72, 21, 1, 73, 21, 2, 74, 21, 1, 75, 21, 1, 76, 21, 1, 77, 21, 1, 78, 21, 1, 2, 22, 1, 3, 22, 1, 4, 22, 3, 5, 22, 1, 6, 22, 1, 23, 22, 1, 24, 22, 1, 25, 22, 1, 26, 22, 0, 27, 22, 0, 28, 22, 0, 29, 22, 2, 30, 22, 1, 31, 22, 1, 38, 22, 1, 39, 22, 3, 40, 22, 1, 44, 22, 1, 45, 22, 0, 46, 22, 0, 47, 22, 0, 48, 22, 0, 49, 22, 0, 50, 22, 0, 51, 22, 0, 52, 22, 1, 53, 22, 1, 54, 22, 0, 55, 22, 0, 56, 22, 0, 57, 22, 0, 58, 22, 0, 59, 22, 0, 60, 22, 0, 61, 22, 1, 62, 22, 1, 63, 22, 0, 64, 22, 0, 65, 22, 0, 66, 22, 0, 67, 22, 0, 68, 22, 1, 72, 22, 1, 73, 22, 3, 74, 22, 1, 1, 23, 1, 2, 23, 1, 3, 23, 0, 4, 23, 0, 5, 23, 0, 6, 23, 1, 7, 23, 1, 25, 23, 1, 26, 23, 1, 27, 23, 1, 28, 23, 1, 29, 23, 3, 30, 23, 1, 38, 23, 1, 39, 23, 3, 40, 23, 1, 44, 23, 1, 45, 23, 1, 46, 23, 0, 47, 23, 0, 48, 23, 0, 49, 23, 0, 50, 23, 0, 51, 23, 1, 52, 23, 1, 53, 23, 1, 54, 23, 0, 55, 23, 0, 56, 23, 0, 57, 23, 0, 58, 23, 0, 59, 23, 0, 60, 23, 0, 61, 23, 1, 62, 23, 1, 63, 23, 1, 64, 23, 0, 65, 23, 0, 66, 23, 0, 67, 23, 1, 68, 23, 1, 69, 23, 1, 70, 23, 1, 71, 23, 1, 72, 23, 1, 73, 23, 3, 74, 23, 1, 75, 23, 1, 76, 23, 1, 77, 23, 1, 78, 23, 1, 1, 24, 1, 2, 24, 0, 3, 24, 0, 4, 24, 0, 5, 24, 0, 6, 24, 0, 7, 24, 1, 17, 24, 1, 18, 24, 1, 19, 24, 1, 20, 24, 1, 21, 24, 1, 22, 24, 1, 23, 24, 1, 28, 24, 1, 29, 24, 3, 30, 24, 1, 31, 24, 1, 32, 24, 1, 33, 24, 1, 34, 24, 1, 38, 24, 1, 39, 24, 3, 40, 24, 1, 45, 24, 1, 46, 24, 0, 47, 24, 0, 48, 24, 0, 49, 24, 0, 50, 24, 0, 51, 24, 1, 53, 24, 1, 54, 24, 1, 55, 24, 0, 56, 24, 0, 57, 24, 0, 58, 24, 0, 59, 24, 0, 60, 24, 1, 61, 24, 1, 63, 24, 1, 64, 24, 1, 65, 24, 1, 66, 24, 1, 67, 24, 1, 69, 24, 1, 70, 24, 0, 71, 24, 0, 72, 24, 0, 73, 24, 0, 74, 24, 0, 75, 24, 0, 76, 24, 0, 77, 24, 0, 78, 24, 1, 1, 25, 1, 2, 25, 0, 3, 25, 0, 4, 25, 0, 5, 25, 0, 6, 25, 0, 7, 25, 1, 17, 25, 1, 18, 25, 0, 19, 25, 0, 20, 25, 0, 21, 25, 0, 22, 25, 0, 23, 25, 1, 28, 25, 1, 29, 25, 0, 30, 25, 0, 31, 25, 0, 32, 25, 0, 33, 25, 0, 34, 25, 1, 38, 25, 1, 39, 25, 3, 40, 25, 1, 41, 25, 1, 42, 25, 1, 45, 25, 1, 46, 25, 1, 47, 25, 1, 48, 25, 0, 49, 25, 1, 50, 25, 1, 51, 25, 1, 54, 25, 1, 55, 25, 1, 56, 25, 1, 57, 25, 0, 58, 25, 1, 59, 25, 1, 60, 25, 1, 69, 25, 1, 70, 25, 0, 71, 25, 0, 72, 25, 0, 73, 25, 0, 74, 25, 0, 75, 25, 0, 76, 25, 0, 77, 25, 0, 78, 25, 1, 1, 26, 1, 2, 26, 0, 3, 26, 0, 4, 26, 0, 5, 26, 0, 6, 26, 0, 7, 26, 1, 17, 26, 1, 18, 26, 0, 19, 26, 0, 20, 26, 0, 21, 26, 0, 22, 26, 0, 23, 26, 1, 24, 26, 1, 25, 26, 1, 26, 26, 1, 27, 26, 1, 28, 26, 1, 29, 26, 0, 30, 26, 0, 31, 26, 0, 32, 26, 0, 33, 26, 0, 34, 26, 1, 38, 26, 1, 39, 26, 0, 40, 26, 0, 41, 26, 0, 42, 26, 1, 47, 26, 1, 48, 26, 1, 49, 26, 1, 56, 26, 1, 57, 26, 1, 58, 26, 1, 69, 26, 1, 70, 26, 0, 71, 26, 0, 72, 26, 0, 73, 26, 0, 74, 26, 0, 75, 26, 0, 76, 26, 0, 77, 26, 0, 78, 26, 1, 1, 27, 1, 2, 27, 1, 3, 27, 0, 4, 27, 0, 5, 27, 0, 6, 27, 1, 7, 27, 1, 17, 27, 1, 18, 27, 0, 19, 27, 0, 20, 27, 0, 21, 27, 0, 22, 27, 0, 23, 27, 3, 24, 27, 3, 25, 27, 3, 26, 27, 3, 27, 27, 3, 28, 27, 2, 29, 27, 0, 30, 27, 0, 31, 27, 0, 32, 27, 0, 33, 27, 0, 34, 27, 1, 38, 27, 1, 39, 27, 0, 40, 27, 0, 41, 27, 0, 42, 27, 1, 69, 27, 1, 70, 27, 1, 71, 27, 1, 72, 27, 1, 73, 27, 1, 74, 27, 1, 75, 27, 1, 76, 27, 1, 77, 27, 1, 78, 27, 1, 2, 28, 1, 3, 28, 1, 4, 28, 1, 5, 28, 1, 6, 28, 1, 17, 28, 1, 18, 28, 1, 19, 28, 1, 20, 28, 1, 21, 28, 1, 22, 28, 1, 23, 28, 1, 24, 28, 1, 25, 28, 1, 26, 28, 1, 27, 28, 1, 28, 28, 1, 29, 28, 1, 30, 28, 1, 31, 28, 1, 32, 28, 1, 33, 28, 1, 34, 28, 1, 38, 28, 1, 39, 28, 1, 40, 28, 1, 41, 28, 1, 42, 28, 1)
-}
-
-[node name="Level7" type="GridMap" parent="Levels" unique_id=575662351]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -42, 0)
-mesh_library = SubResource("MeshLibrary_r4kn7")
-cell_size = Vector3(1, 1, 1)
-data = {
-"cells": PackedInt32Array(20, 0, 1, 21, 0, 1, 22, 0, 1, 23, 0, 1, 24, 0, 1, 41, 0, 1, 42, 0, 1, 43, 0, 1, 51, 0, 1, 52, 0, 1, 53, 0, 1, 54, 0, 1, 55, 0, 1, 56, 0, 1, 20, 1, 1, 21, 1, 0, 22, 1, 0, 23, 1, 0, 24, 1, 1, 31, 1, 1, 32, 1, 1, 33, 1, 1, 34, 1, 1, 35, 1, 1, 36, 1, 1, 37, 1, 1, 38, 1, 1, 39, 1, 1, 40, 1, 1, 41, 1, 1, 42, 1, 0, 43, 1, 1, 44, 1, 1, 45, 1, 1, 46, 1, 1, 47, 1, 1, 48, 1, 1, 49, 1, 1, 50, 1, 1, 51, 1, 1, 52, 1, 0, 53, 1, 0, 54, 1, 0, 55, 1, 0, 56, 1, 1, 57, 1, 1, 58, 1, 1, 59, 1, 1, 60, 1, 1, 61, 1, 1, 62, 1, 1, 63, 1, 1, 64, 1, 1, 65, 1, 1, 66, 1, 1, 67, 1, 1, 68, 1, 1, 69, 1, 1, 70, 1, 1, 71, 1, 1, 13, 2, 1, 14, 2, 1, 15, 2, 1, 16, 2, 1, 17, 2, 1, 18, 2, 1, 20, 2, 1, 21, 2, 0, 22, 2, 0, 23, 2, 0, 24, 2, 1, 27, 2, 1, 28, 2, 1, 29, 2, 1, 30, 2, 1, 31, 2, 1, 32, 2, 0, 33, 2, 0, 34, 2, 0, 35, 2, 0, 36, 2, 0, 37, 2, 3, 38, 2, 3, 39, 2, 2, 40, 2, 0, 41, 2, 0, 42, 2, 0, 43, 2, 0, 44, 2, 0, 45, 2, 2, 46, 2, 3, 47, 2, 3, 48, 2, 3, 49, 2, 3, 50, 2, 3, 51, 2, 3, 52, 2, 0, 53, 2, 0, 54, 2, 0, 55, 2, 0, 56, 2, 2, 57, 2, 3, 58, 2, 3, 59, 2, 3, 60, 2, 3, 61, 2, 3, 62, 2, 3, 63, 2, 3, 64, 2, 0, 65, 2, 0, 66, 2, 0, 67, 2, 0, 68, 2, 0, 69, 2, 0, 70, 2, 0, 71, 2, 1, 13, 3, 1, 14, 3, 0, 15, 3, 0, 16, 3, 0, 17, 3, 0, 18, 3, 1, 20, 3, 1, 21, 3, 0, 22, 3, 0, 23, 3, 0, 24, 3, 1, 26, 3, 1, 27, 3, 1, 28, 3, 0, 29, 3, 0, 30, 3, 1, 31, 3, 1, 32, 3, 0, 33, 3, 0, 34, 3, 0, 35, 3, 0, 36, 3, 0, 37, 3, 1, 38, 3, 1, 39, 3, 0, 40, 3, 0, 41, 3, 0, 42, 3, 0, 43, 3, 0, 44, 3, 0, 45, 3, 0, 46, 3, 1, 47, 3, 1, 48, 3, 1, 49, 3, 1, 50, 3, 1, 51, 3, 1, 52, 3, 2, 53, 3, 1, 54, 3, 1, 55, 3, 1, 56, 3, 1, 57, 3, 1, 58, 3, 1, 59, 3, 1, 60, 3, 1, 61, 3, 1, 62, 3, 1, 63, 3, 1, 64, 3, 0, 65, 3, 0, 66, 3, 0, 67, 3, 0, 68, 3, 0, 69, 3, 0, 70, 3, 0, 71, 3, 1, 13, 4, 1, 14, 4, 0, 15, 4, 0, 16, 4, 0, 17, 4, 0, 18, 4, 1, 20, 4, 1, 21, 4, 0, 22, 4, 0, 23, 4, 0, 24, 4, 1, 25, 4, 1, 26, 4, 1, 27, 4, 0, 28, 4, 0, 29, 4, 0, 30, 4, 0, 31, 4, 2, 32, 4, 0, 33, 4, 0, 34, 4, 0, 35, 4, 0, 36, 4, 0, 37, 4, 1, 38, 4, 1, 39, 4, 0, 40, 4, 0, 41, 4, 0, 42, 4, 0, 43, 4, 0, 44, 4, 0, 45, 4, 0, 46, 4, 1, 47, 4, 1, 51, 4, 1, 52, 4, 3, 53, 4, 1, 63, 4, 1, 64, 4, 0, 65, 4, 0, 66, 4, 0, 67, 4, 0, 68, 4, 0, 69, 4, 0, 70, 4, 0, 71, 4, 1, 13, 5, 1, 14, 5, 1, 15, 5, 1, 16, 5, 1, 17, 5, 3, 18, 5, 1, 20, 5, 1, 21, 5, 1, 22, 5, 1, 23, 5, 3, 24, 5, 1, 25, 5, 1, 26, 5, 0, 27, 5, 0, 28, 5, 0, 29, 5, 0, 30, 5, 0, 31, 5, 1, 32, 5, 1, 33, 5, 1, 34, 5, 1, 35, 5, 1, 36, 5, 1, 37, 5, 1, 38, 5, 0, 39, 5, 0, 40, 5, 0, 41, 5, 0, 42, 5, 0, 43, 5, 0, 44, 5, 0, 45, 5, 0, 46, 5, 0, 47, 5, 1, 51, 5, 1, 52, 5, 3, 53, 5, 1, 63, 5, 1, 64, 5, 0, 65, 5, 0, 66, 5, 0, 67, 5, 0, 68, 5, 0, 69, 5, 0, 70, 5, 0, 71, 5, 1, 10, 6, 1, 11, 6, 1, 12, 6, 1, 13, 6, 1, 14, 6, 1, 16, 6, 1, 17, 6, 3, 18, 6, 1, 22, 6, 1, 23, 6, 3, 24, 6, 1, 25, 6, 1, 26, 6, 1, 27, 6, 0, 28, 6, 0, 29, 6, 0, 30, 6, 3, 31, 6, 3, 32, 6, 3, 33, 6, 3, 34, 6, 3, 35, 6, 3, 36, 6, 3, 37, 6, 3, 38, 6, 2, 39, 6, 0, 40, 6, 0, 41, 6, 0, 42, 6, 0, 43, 6, 0, 44, 6, 0, 45, 6, 0, 46, 6, 1, 47, 6, 1, 51, 6, 1, 52, 6, 3, 53, 6, 1, 55, 6, 1, 56, 6, 1, 57, 6, 1, 58, 6, 1, 59, 6, 1, 60, 6, 1, 61, 6, 1, 62, 6, 1, 63, 6, 1, 64, 6, 1, 65, 6, 1, 66, 6, 1, 67, 6, 1, 68, 6, 1, 69, 6, 1, 70, 6, 1, 71, 6, 1, 10, 7, 1, 11, 7, 0, 12, 7, 0, 13, 7, 0, 14, 7, 1, 16, 7, 1, 17, 7, 3, 18, 7, 1, 21, 7, 1, 22, 7, 1, 23, 7, 2, 24, 7, 1, 25, 7, 1, 26, 7, 1, 27, 7, 1, 28, 7, 0, 29, 7, 1, 30, 7, 1, 31, 7, 1, 32, 7, 1, 33, 7, 1, 34, 7, 1, 35, 7, 1, 36, 7, 1, 37, 7, 1, 38, 7, 1, 39, 7, 0, 40, 7, 0, 41, 7, 0, 42, 7, 0, 43, 7, 0, 44, 7, 0, 45, 7, 0, 46, 7, 1, 49, 7, 1, 50, 7, 1, 51, 7, 1, 52, 7, 3, 53, 7, 1, 55, 7, 1, 56, 7, 0, 57, 7, 0, 58, 7, 0, 59, 7, 0, 60, 7, 0, 61, 7, 0, 62, 7, 1, 67, 7, 1, 68, 7, 1, 69, 7, 1, 70, 7, 1, 10, 8, 1, 11, 8, 0, 12, 8, 0, 13, 8, 0, 14, 8, 1, 16, 8, 1, 17, 8, 3, 18, 8, 1, 21, 8, 1, 22, 8, 0, 23, 8, 0, 24, 8, 0, 25, 8, 0, 26, 8, 0, 27, 8, 1, 28, 8, 2, 29, 8, 1, 30, 8, 1, 31, 8, 1, 32, 8, 1, 38, 8, 1, 39, 8, 1, 40, 8, 0, 41, 8, 0, 42, 8, 0, 43, 8, 0, 44, 8, 0, 45, 8, 1, 46, 8, 1, 49, 8, 1, 50, 8, 0, 51, 8, 0, 52, 8, 0, 53, 8, 1, 55, 8, 1, 56, 8, 0, 57, 8, 0, 58, 8, 0, 59, 8, 0, 60, 8, 0, 61, 8, 0, 62, 8, 1, 63, 8, 1, 64, 8, 1, 65, 8, 1, 66, 8, 1, 67, 8, 1, 68, 8, 0, 69, 8, 0, 70, 8, 1, 10, 9, 1, 11, 9, 1, 12, 9, 1, 13, 9, 3, 14, 9, 1, 16, 9, 1, 17, 9, 3, 18, 9, 1, 21, 9, 1, 22, 9, 0, 23, 9, 0, 24, 9, 0, 25, 9, 0, 26, 9, 0, 27, 9, 2, 28, 9, 3, 29, 9, 0, 30, 9, 0, 31, 9, 0, 32, 9, 1, 33, 9, 1, 39, 9, 1, 40, 9, 3, 41, 9, 1, 42, 9, 0, 43, 9, 1, 44, 9, 1, 45, 9, 1, 49, 9, 1, 50, 9, 0, 51, 9, 0, 52, 9, 0, 53, 9, 1, 55, 9, 1, 56, 9, 0, 57, 9, 0, 58, 9, 0, 59, 9, 0, 60, 9, 0, 61, 9, 0, 62, 9, 3, 63, 9, 3, 64, 9, 3, 65, 9, 3, 66, 9, 3, 67, 9, 2, 68, 9, 0, 69, 9, 0, 70, 9, 1, 2, 10, 1, 3, 10, 1, 4, 10, 1, 12, 10, 1, 13, 10, 2, 14, 10, 1, 15, 10, 1, 16, 10, 1, 17, 10, 2, 18, 10, 1, 21, 10, 1, 22, 10, 0, 23, 10, 0, 24, 10, 0, 25, 10, 0, 26, 10, 0, 27, 10, 1, 28, 10, 0, 29, 10, 0, 30, 10, 0, 31, 10, 0, 32, 10, 0, 33, 10, 1, 39, 10, 1, 40, 10, 3, 41, 10, 1, 42, 10, 1, 43, 10, 1, 49, 10, 1, 50, 10, 1, 51, 10, 1, 52, 10, 1, 53, 10, 1, 55, 10, 1, 56, 10, 0, 57, 10, 0, 58, 10, 0, 59, 10, 0, 60, 10, 0, 61, 10, 0, 62, 10, 1, 63, 10, 1, 64, 10, 1, 65, 10, 1, 66, 10, 1, 67, 10, 1, 68, 10, 0, 69, 10, 0, 70, 10, 1, 1, 11, 1, 2, 11, 1, 3, 11, 0, 4, 11, 1, 5, 11, 1, 6, 11, 1, 7, 11, 1, 8, 11, 1, 9, 11, 1, 10, 11, 1, 11, 11, 1, 12, 11, 1, 13, 11, 0, 14, 11, 0, 15, 11, 0, 16, 11, 0, 17, 11, 0, 18, 11, 1, 21, 11, 1, 22, 11, 1, 23, 11, 1, 24, 11, 1, 25, 11, 3, 26, 11, 1, 27, 11, 1, 28, 11, 0, 29, 11, 0, 30, 11, 0, 31, 11, 0, 32, 11, 0, 33, 11, 1, 37, 11, 1, 38, 11, 1, 39, 11, 1, 40, 11, 2, 41, 11, 1, 45, 11, 1, 46, 11, 1, 47, 11, 1, 48, 11, 1, 49, 11, 1, 50, 11, 1, 51, 11, 1, 52, 11, 1, 55, 11, 1, 56, 11, 1, 57, 11, 1, 58, 11, 2, 59, 11, 1, 60, 11, 1, 61, 11, 1, 62, 11, 1, 66, 11, 1, 67, 11, 1, 68, 11, 3, 69, 11, 1, 70, 11, 1, 0, 12, 1, 1, 12, 1, 2, 12, 0, 3, 12, 0, 4, 12, 0, 5, 12, 3, 6, 12, 3, 7, 12, 3, 8, 12, 3, 9, 12, 3, 10, 12, 3, 11, 12, 3, 12, 12, 2, 13, 12, 0, 14, 12, 0, 15, 12, 0, 16, 12, 0, 17, 12, 0, 18, 12, 1, 23, 12, 1, 24, 12, 1, 25, 12, 3, 26, 12, 1, 27, 12, 1, 28, 12, 0, 29, 12, 0, 30, 12, 0, 31, 12, 0, 32, 12, 0, 33, 12, 1, 37, 12, 1, 38, 12, 0, 39, 12, 0, 40, 12, 0, 41, 12, 1, 45, 12, 1, 46, 12, 0, 47, 12, 0, 48, 12, 0, 49, 12, 0, 50, 12, 0, 51, 12, 0, 52, 12, 1, 56, 12, 1, 57, 12, 1, 58, 12, 0, 59, 12, 1, 60, 12, 1, 64, 12, 1, 65, 12, 1, 66, 12, 1, 67, 12, 0, 68, 12, 2, 69, 12, 1, 0, 13, 1, 1, 13, 9, 2, 13, 0, 3, 13, 0, 4, 13, 0, 5, 13, 0, 6, 13, 1, 7, 13, 1, 8, 13, 1, 9, 13, 1, 10, 13, 1, 11, 13, 1, 12, 13, 1, 13, 13, 0, 14, 13, 0, 15, 13, 0, 16, 13, 0, 17, 13, 0, 18, 13, 1, 22, 13, 1, 23, 13, 1, 24, 13, 0, 25, 13, 2, 26, 13, 1, 27, 13, 1, 28, 13, 1, 29, 13, 0, 30, 13, 0, 31, 13, 0, 32, 13, 1, 33, 13, 1, 37, 13, 1, 38, 13, 0, 39, 13, 0, 40, 13, 0, 41, 13, 1, 45, 13, 1, 46, 13, 0, 47, 13, 0, 48, 13, 0, 49, 13, 0, 50, 13, 0, 51, 13, 0, 52, 13, 1, 55, 13, 1, 56, 13, 1, 57, 13, 0, 58, 13, 0, 59, 13, 0, 60, 13, 1, 61, 13, 1, 63, 13, 1, 64, 13, 1, 65, 13, 0, 66, 13, 0, 67, 13, 0, 68, 13, 0, 69, 13, 1, 0, 14, 1, 1, 14, 1, 2, 14, 0, 3, 14, 0, 4, 14, 0, 5, 14, 1, 6, 14, 1, 7, 14, 1, 8, 14, 1, 9, 14, 1, 10, 14, 1, 11, 14, 1, 12, 14, 1, 13, 14, 2, 14, 14, 1, 15, 14, 1, 16, 14, 1, 17, 14, 3, 18, 14, 1, 21, 14, 1, 22, 14, 1, 23, 14, 0, 24, 14, 0, 25, 14, 0, 26, 14, 1, 27, 14, 1, 28, 14, 1, 29, 14, 1, 30, 14, 1, 31, 14, 1, 32, 14, 1, 37, 14, 1, 38, 14, 0, 39, 14, 8, 40, 14, 0, 41, 14, 1, 42, 14, 1, 43, 14, 1, 44, 14, 1, 45, 14, 1, 46, 14, 0, 47, 14, 0, 48, 14, 0, 49, 14, 0, 50, 14, 0, 51, 14, 0, 52, 14, 1, 55, 14, 1, 56, 14, 0, 57, 14, 0, 58, 14, 0, 59, 14, 0, 60, 14, 0, 61, 14, 1, 62, 14, 1, 63, 14, 1, 64, 14, 0, 65, 14, 0, 66, 14, 0, 67, 14, 0, 68, 14, 0, 69, 14, 1, 70, 14, 1, 1, 15, 1, 2, 15, 1, 3, 15, 0, 4, 15, 1, 5, 15, 1, 7, 15, 1, 8, 15, 0, 9, 15, 0, 10, 15, 0, 11, 15, 1, 12, 15, 1, 13, 15, 3, 14, 15, 1, 16, 15, 1, 17, 15, 3, 18, 15, 1, 20, 15, 1, 21, 15, 1, 22, 15, 0, 23, 15, 0, 24, 15, 0, 25, 15, 0, 26, 15, 0, 27, 15, 1, 37, 15, 1, 38, 15, 0, 39, 15, 0, 40, 15, 0, 41, 15, 2, 42, 15, 3, 43, 15, 3, 44, 15, 3, 45, 15, 3, 46, 15, 0, 47, 15, 0, 48, 15, 0, 49, 15, 0, 50, 15, 0, 51, 15, 0, 52, 15, 1, 54, 15, 1, 55, 15, 1, 56, 15, 0, 57, 15, 0, 58, 15, 0, 59, 15, 0, 60, 15, 1, 61, 15, 1, 62, 15, 1, 63, 15, 0, 64, 15, 0, 65, 15, 0, 66, 15, 0, 67, 15, 0, 68, 15, 0, 69, 15, 0, 70, 15, 1, 71, 15, 1, 2, 16, 1, 3, 16, 1, 4, 16, 1, 7, 16, 1, 8, 16, 0, 9, 16, 0, 10, 16, 0, 11, 16, 1, 12, 16, 1, 13, 16, 3, 14, 16, 1, 16, 16, 1, 17, 16, 3, 18, 16, 1, 20, 16, 1, 21, 16, 0, 22, 16, 0, 23, 16, 0, 24, 16, 0, 25, 16, 0, 26, 16, 1, 27, 16, 1, 37, 16, 1, 38, 16, 1, 39, 16, 1, 40, 16, 2, 41, 16, 1, 42, 16, 1, 43, 16, 1, 44, 16, 1, 45, 16, 1, 46, 16, 1, 47, 16, 1, 48, 16, 1, 49, 16, 1, 50, 16, 1, 51, 16, 1, 52, 16, 1, 54, 16, 1, 55, 16, 0, 56, 16, 0, 57, 16, 0, 58, 16, 0, 59, 16, 0, 60, 16, 1, 61, 16, 1, 62, 16, 1, 63, 16, 1, 64, 16, 0, 65, 16, 0, 66, 16, 0, 67, 16, 0, 68, 16, 0, 69, 16, 0, 70, 16, 0, 71, 16, 1, 7, 17, 1, 8, 17, 0, 9, 17, 0, 10, 17, 0, 11, 17, 1, 12, 17, 1, 13, 17, 3, 14, 17, 1, 16, 17, 1, 17, 17, 3, 18, 17, 1, 20, 17, 1, 21, 17, 1, 22, 17, 0, 23, 17, 0, 24, 17, 0, 25, 17, 0, 26, 17, 1, 27, 17, 1, 28, 17, 1, 29, 17, 1, 30, 17, 1, 38, 17, 1, 39, 17, 1, 40, 17, 3, 41, 17, 1, 42, 17, 1, 43, 17, 1, 44, 17, 1, 45, 17, 1, 46, 17, 1, 53, 17, 1, 54, 17, 1, 55, 17, 0, 56, 17, 0, 57, 17, 0, 58, 17, 0, 59, 17, 2, 60, 17, 3, 61, 17, 3, 62, 17, 3, 63, 17, 3, 64, 17, 3, 65, 17, 0, 66, 17, 0, 67, 17, 0, 68, 17, 0, 69, 17, 0, 70, 17, 1, 71, 17, 1, 7, 18, 1, 8, 18, 0, 9, 18, 0, 10, 18, 0, 11, 18, 1, 12, 18, 1, 13, 18, 3, 14, 18, 1, 16, 18, 1, 17, 18, 2, 18, 18, 1, 19, 18, 1, 20, 18, 1, 21, 18, 1, 22, 18, 0, 23, 18, 0, 24, 18, 0, 25, 18, 0, 26, 18, 0, 27, 18, 3, 28, 18, 2, 29, 18, 0, 30, 18, 1, 31, 18, 1, 32, 18, 1, 33, 18, 1, 34, 18, 1, 35, 18, 1, 36, 18, 1, 37, 18, 1, 38, 18, 1, 39, 18, 0, 40, 18, 0, 41, 18, 0, 42, 18, 0, 43, 18, 0, 44, 18, 0, 45, 18, 0, 46, 18, 1, 47, 18, 1, 48, 18, 1, 49, 18, 1, 50, 18, 1, 51, 18, 1, 52, 18, 1, 53, 18, 1, 54, 18, 0, 55, 18, 0, 56, 18, 0, 57, 18, 0, 58, 18, 1, 59, 18, 1, 60, 18, 1, 61, 18, 1, 62, 18, 1, 63, 18, 1, 64, 18, 1, 65, 18, 2, 66, 18, 0, 67, 18, 1, 68, 18, 0, 69, 18, 1, 70, 18, 1, 7, 19, 1, 8, 19, 3, 9, 19, 1, 10, 19, 1, 11, 19, 1, 12, 19, 1, 13, 19, 3, 14, 19, 1, 15, 19, 1, 16, 19, 1, 17, 19, 0, 18, 19, 0, 19, 19, 0, 20, 19, 0, 21, 19, 1, 22, 19, 1, 23, 19, 0, 24, 19, 0, 25, 19, 0, 26, 19, 1, 27, 19, 1, 28, 19, 0, 29, 19, 0, 30, 19, 0, 31, 19, 3, 32, 19, 3, 33, 19, 3, 34, 19, 3, 35, 19, 3, 36, 19, 3, 37, 19, 3, 38, 19, 2, 39, 19, 0, 40, 19, 0, 41, 19, 0, 42, 19, 0, 43, 19, 0, 44, 19, 0, 45, 19, 0, 46, 19, 2, 47, 19, 3, 48, 19, 3, 49, 19, 3, 50, 19, 3, 51, 19, 3, 52, 19, 3, 53, 19, 3, 54, 19, 3, 55, 19, 0, 56, 19, 1, 57, 19, 1, 58, 19, 1, 64, 19, 1, 65, 19, 3, 66, 19, 1, 67, 19, 1, 68, 19, 1, 69, 19, 1, 74, 19, 1, 75, 19, 1, 76, 19, 1, 7, 20, 1, 8, 20, 3, 9, 20, 1, 11, 20, 1, 12, 20, 0, 13, 20, 0, 14, 20, 0, 15, 20, 1, 16, 20, 1, 17, 20, 0, 18, 20, 0, 19, 20, 0, 20, 20, 0, 21, 20, 3, 22, 20, 3, 23, 20, 2, 24, 20, 0, 25, 20, 1, 26, 20, 1, 27, 20, 1, 28, 20, 0, 29, 20, 0, 30, 20, 0, 31, 20, 1, 32, 20, 1, 33, 20, 1, 34, 20, 1, 35, 20, 1, 36, 20, 1, 37, 20, 1, 38, 20, 1, 39, 20, 0, 40, 20, 0, 41, 20, 0, 42, 20, 0, 43, 20, 0, 44, 20, 0, 45, 20, 0, 46, 20, 1, 47, 20, 1, 48, 20, 1, 49, 20, 1, 50, 20, 1, 51, 20, 1, 52, 20, 1, 53, 20, 1, 54, 20, 1, 55, 20, 1, 56, 20, 1, 63, 20, 1, 64, 20, 1, 65, 20, 3, 66, 20, 1, 73, 20, 1, 74, 20, 1, 75, 20, 0, 76, 20, 1, 77, 20, 1, 7, 21, 1, 8, 21, 3, 9, 21, 1, 11, 21, 1, 12, 21, 0, 13, 21, 0, 14, 21, 0, 15, 21, 1, 16, 21, 1, 17, 21, 0, 18, 21, 0, 19, 21, 0, 20, 21, 0, 21, 21, 1, 22, 21, 1, 23, 21, 1, 24, 21, 1, 25, 21, 1, 27, 21, 1, 28, 21, 1, 29, 21, 0, 30, 21, 1, 31, 21, 1, 34, 21, 1, 35, 21, 1, 36, 21, 1, 37, 21, 1, 38, 21, 1, 39, 21, 2, 40, 21, 1, 41, 21, 1, 42, 21, 1, 43, 21, 1, 44, 21, 2, 45, 21, 1, 46, 21, 1, 54, 21, 1, 55, 21, 1, 56, 21, 1, 62, 21, 1, 63, 21, 1, 64, 21, 0, 65, 21, 3, 66, 21, 1, 72, 21, 1, 73, 21, 1, 74, 21, 0, 75, 21, 0, 76, 21, 0, 77, 21, 1, 78, 21, 1, 7, 22, 1, 8, 22, 3, 9, 22, 1, 11, 22, 1, 12, 22, 1, 13, 22, 1, 14, 22, 1, 15, 22, 1, 16, 22, 1, 17, 22, 1, 18, 22, 1, 19, 22, 1, 20, 22, 2, 21, 22, 1, 27, 22, 1, 28, 22, 0, 29, 22, 0, 30, 22, 0, 31, 22, 1, 32, 22, 1, 33, 22, 1, 34, 22, 1, 35, 22, 0, 36, 22, 0, 37, 22, 0, 38, 22, 0, 39, 22, 0, 40, 22, 1, 42, 22, 1, 43, 22, 1, 44, 22, 3, 45, 22, 1, 53, 22, 1, 54, 22, 1, 55, 22, 0, 56, 22, 1, 57, 22, 1, 61, 22, 1, 62, 22, 1, 63, 22, 0, 64, 22, 0, 65, 22, 0, 66, 22, 1, 67, 22, 1, 72, 22, 1, 73, 22, 0, 74, 22, 0, 75, 22, 0, 76, 22, 0, 77, 22, 0, 78, 22, 1, 5, 23, 1, 6, 23, 1, 7, 23, 1, 8, 23, 2, 9, 23, 1, 10, 23, 1, 11, 23, 1, 12, 23, 1, 13, 23, 1, 14, 23, 1, 15, 23, 1, 16, 23, 1, 17, 23, 1, 18, 23, 1, 19, 23, 1, 20, 23, 3, 21, 23, 1, 22, 23, 1, 23, 23, 1, 24, 23, 1, 25, 23, 1, 26, 23, 1, 27, 23, 1, 28, 23, 1, 29, 23, 0, 30, 23, 2, 31, 23, 3, 32, 23, 3, 33, 23, 3, 34, 23, 3, 35, 23, 0, 36, 23, 0, 37, 23, 0, 38, 23, 0, 39, 23, 0, 40, 23, 1, 42, 23, 1, 43, 23, 0, 44, 23, 0, 45, 23, 1, 51, 23, 1, 52, 23, 1, 53, 23, 1, 54, 23, 0, 55, 23, 0, 56, 23, 0, 57, 23, 1, 61, 23, 1, 62, 23, 0, 63, 23, 0, 64, 23, 0, 65, 23, 0, 66, 23, 0, 67, 23, 1, 68, 23, 1, 69, 23, 1, 70, 23, 1, 71, 23, 1, 72, 23, 1, 73, 23, 1, 74, 23, 0, 75, 23, 0, 76, 23, 0, 77, 23, 1, 78, 23, 1, 5, 24, 1, 6, 24, 0, 7, 24, 0, 8, 24, 0, 9, 24, 0, 10, 24, 0, 11, 24, 0, 12, 24, 3, 13, 24, 3, 14, 24, 3, 15, 24, 3, 16, 24, 3, 17, 24, 3, 18, 24, 3, 19, 24, 2, 20, 24, 0, 21, 24, 0, 22, 24, 0, 23, 24, 0, 24, 24, 0, 25, 24, 0, 26, 24, 0, 27, 24, 0, 28, 24, 1, 29, 24, 1, 30, 24, 1, 31, 24, 1, 32, 24, 1, 33, 24, 1, 34, 24, 1, 35, 24, 0, 36, 24, 0, 37, 24, 0, 38, 24, 0, 39, 24, 0, 40, 24, 1, 42, 24, 1, 43, 24, 0, 44, 24, 0, 45, 24, 1, 50, 24, 1, 51, 24, 1, 52, 24, 0, 53, 24, 1, 54, 24, 0, 55, 24, 0, 56, 24, 0, 57, 24, 1, 58, 24, 1, 59, 24, 1, 60, 24, 1, 61, 24, 1, 62, 24, 1, 63, 24, 0, 64, 24, 0, 65, 24, 0, 66, 24, 2, 67, 24, 3, 68, 24, 3, 69, 24, 3, 70, 24, 3, 71, 24, 3, 72, 24, 3, 73, 24, 3, 74, 24, 3, 75, 24, 0, 76, 24, 1, 77, 24, 1, 5, 25, 1, 6, 25, 0, 7, 25, 0, 8, 25, 0, 9, 25, 0, 10, 25, 0, 11, 25, 0, 12, 25, 1, 13, 25, 1, 14, 25, 1, 15, 25, 1, 16, 25, 1, 17, 25, 1, 18, 25, 1, 19, 25, 1, 20, 25, 0, 21, 25, 0, 22, 25, 0, 23, 25, 0, 24, 25, 0, 25, 25, 0, 26, 25, 0, 27, 25, 0, 28, 25, 1, 34, 25, 1, 35, 25, 1, 36, 25, 1, 37, 25, 1, 38, 25, 1, 39, 25, 1, 40, 25, 1, 42, 25, 1, 43, 25, 1, 44, 25, 1, 45, 25, 1, 50, 25, 1, 51, 25, 0, 52, 25, 0, 53, 25, 0, 54, 25, 0, 55, 25, 0, 56, 25, 0, 57, 25, 0, 58, 25, 3, 59, 25, 3, 60, 25, 3, 61, 25, 3, 62, 25, 3, 63, 25, 2, 64, 25, 0, 65, 25, 1, 66, 25, 1, 67, 25, 1, 68, 25, 1, 69, 25, 1, 70, 25, 1, 71, 25, 1, 72, 25, 1, 73, 25, 1, 74, 25, 1, 75, 25, 1, 76, 25, 1, 5, 26, 1, 6, 26, 0, 7, 26, 0, 8, 26, 0, 9, 26, 0, 10, 26, 0, 11, 26, 0, 12, 26, 1, 19, 26, 1, 20, 26, 0, 21, 26, 0, 22, 26, 0, 23, 26, 0, 24, 26, 0, 25, 26, 0, 26, 26, 0, 27, 26, 0, 28, 26, 1, 50, 26, 1, 51, 26, 1, 52, 26, 0, 53, 26, 1, 54, 26, 0, 55, 26, 0, 56, 26, 0, 57, 26, 1, 58, 26, 1, 59, 26, 1, 60, 26, 1, 61, 26, 1, 62, 26, 1, 63, 26, 1, 64, 26, 1, 65, 26, 1, 5, 27, 1, 6, 27, 1, 7, 27, 1, 8, 27, 1, 9, 27, 1, 10, 27, 1, 11, 27, 1, 12, 27, 1, 19, 27, 1, 20, 27, 1, 21, 27, 1, 22, 27, 1, 23, 27, 1, 24, 27, 1, 25, 27, 1, 26, 27, 1, 27, 27, 1, 28, 27, 1, 51, 27, 1, 52, 27, 1, 53, 27, 1, 54, 27, 1, 55, 27, 0, 56, 27, 1, 57, 27, 1, 54, 28, 1, 55, 28, 1, 56, 28, 1)
-}
-
-[node name="Level8" type="GridMap" parent="Levels" unique_id=573253142]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -48, 0)
-mesh_library = SubResource("MeshLibrary_r4kn7")
-cell_size = Vector3(1, 1, 1)
-data = {
-"cells": PackedInt32Array(19, 0, 1, 20, 0, 1, 21, 0, 1, 22, 0, 1, 23, 0, 1, 24, 0, 1, 25, 0, 1, 26, 0, 1, 29, 0, 1, 30, 0, 1, 31, 0, 1, 32, 0, 1, 33, 0, 1, 34, 0, 1, 35, 0, 1, 36, 0, 1, 37, 0, 1, 57, 0, 1, 58, 0, 1, 59, 0, 1, 60, 0, 1, 61, 0, 1, 62, 0, 1, 19, 1, 1, 20, 1, 0, 21, 1, 0, 22, 1, 0, 23, 1, 0, 24, 1, 0, 25, 1, 0, 26, 1, 1, 29, 1, 1, 30, 1, 0, 31, 1, 0, 32, 1, 0, 33, 1, 0, 34, 1, 0, 35, 1, 0, 36, 1, 0, 37, 1, 1, 57, 1, 1, 58, 1, 0, 59, 1, 0, 60, 1, 0, 61, 1, 0, 62, 1, 1, 68, 1, 1, 69, 1, 1, 70, 1, 1, 71, 1, 1, 72, 1, 1, 73, 1, 1, 74, 1, 1, 75, 1, 1, 19, 2, 1, 20, 2, 0, 21, 2, 0, 22, 2, 0, 23, 2, 0, 24, 2, 0, 25, 2, 0, 26, 2, 1, 29, 2, 1, 30, 2, 0, 31, 2, 0, 32, 2, 0, 33, 2, 0, 34, 2, 0, 35, 2, 0, 36, 2, 0, 37, 2, 1, 48, 2, 1, 49, 2, 1, 50, 2, 1, 51, 2, 1, 52, 2, 1, 53, 2, 1, 54, 2, 1, 55, 2, 1, 56, 2, 1, 57, 2, 1, 58, 2, 0, 59, 2, 0, 60, 2, 0, 61, 2, 0, 62, 2, 1, 68, 2, 1, 69, 2, 0, 70, 2, 0, 71, 2, 0, 72, 2, 0, 73, 2, 0, 74, 2, 0, 75, 2, 1, 3, 3, 1, 4, 3, 1, 5, 3, 1, 6, 3, 1, 7, 3, 1, 8, 3, 1, 9, 3, 1, 19, 3, 1, 20, 3, 0, 21, 3, 0, 22, 3, 0, 23, 3, 0, 24, 3, 0, 25, 3, 0, 26, 3, 1, 29, 3, 1, 30, 3, 0, 31, 3, 0, 32, 3, 0, 33, 3, 0, 34, 3, 0, 35, 3, 0, 36, 3, 0, 37, 3, 1, 39, 3, 1, 40, 3, 1, 41, 3, 1, 42, 3, 1, 43, 3, 1, 44, 3, 1, 45, 3, 1, 46, 3, 1, 47, 3, 1, 48, 3, 1, 49, 3, 0, 50, 3, 0, 51, 3, 0, 52, 3, 0, 53, 3, 0, 54, 3, 0, 55, 3, 0, 56, 3, 0, 57, 3, 1, 58, 3, 1, 59, 3, 3, 60, 3, 1, 61, 3, 1, 62, 3, 1, 68, 3, 1, 69, 3, 0, 70, 3, 0, 71, 3, 0, 72, 3, 0, 73, 3, 0, 74, 3, 0, 75, 3, 1, 3, 4, 1, 4, 4, 0, 5, 4, 0, 6, 4, 0, 7, 4, 0, 8, 4, 0, 9, 4, 1, 10, 4, 1, 11, 4, 1, 12, 4, 1, 13, 4, 1, 14, 4, 1, 15, 4, 1, 16, 4, 1, 17, 4, 1, 19, 4, 1, 20, 4, 1, 21, 4, 3, 22, 4, 1, 23, 4, 1, 24, 4, 1, 25, 4, 1, 26, 4, 1, 29, 4, 1, 30, 4, 1, 31, 4, 1, 32, 4, 3, 33, 4, 1, 34, 4, 1, 35, 4, 1, 36, 4, 1, 37, 4, 1, 39, 4, 1, 40, 4, 0, 41, 4, 0, 42, 4, 0, 43, 4, 0, 44, 4, 0, 45, 4, 0, 46, 4, 3, 47, 4, 3, 48, 4, 2, 49, 4, 0, 50, 4, 0, 51, 4, 0, 52, 4, 0, 53, 4, 0, 54, 4, 0, 55, 4, 0, 56, 4, 0, 57, 4, 1, 58, 4, 1, 59, 4, 2, 60, 4, 1, 61, 4, 1, 62, 4, 1, 63, 4, 1, 64, 4, 1, 65, 4, 1, 66, 4, 1, 68, 4, 1, 69, 4, 1, 70, 4, 1, 71, 4, 3, 72, 4, 1, 73, 4, 1, 74, 4, 1, 75, 4, 1, 3, 5, 1, 4, 5, 0, 5, 5, 0, 6, 5, 0, 7, 5, 0, 8, 5, 0, 9, 5, 3, 10, 5, 2, 11, 5, 0, 12, 5, 0, 13, 5, 0, 14, 5, 0, 15, 5, 0, 16, 5, 0, 17, 5, 1, 20, 5, 1, 21, 5, 3, 22, 5, 1, 27, 5, 1, 28, 5, 1, 29, 5, 1, 30, 5, 0, 31, 5, 1, 32, 5, 2, 33, 5, 1, 35, 5, 1, 36, 5, 1, 37, 5, 1, 38, 5, 1, 39, 5, 1, 40, 5, 0, 41, 5, 0, 42, 5, 0, 43, 5, 0, 44, 5, 0, 45, 5, 0, 46, 5, 1, 47, 5, 1, 48, 5, 1, 49, 5, 0, 50, 5, 0, 51, 5, 0, 52, 5, 0, 53, 5, 0, 54, 5, 0, 55, 5, 0, 56, 5, 0, 57, 5, 2, 58, 5, 0, 59, 5, 0, 60, 5, 0, 61, 5, 0, 62, 5, 0, 63, 5, 0, 64, 5, 0, 65, 5, 0, 66, 5, 1, 70, 5, 1, 71, 5, 3, 72, 5, 1, 3, 6, 1, 4, 6, 1, 5, 6, 1, 6, 6, 1, 7, 6, 1, 8, 6, 1, 9, 6, 1, 10, 6, 1, 11, 6, 0, 12, 6, 0, 13, 6, 0, 14, 6, 0, 15, 6, 0, 16, 6, 0, 17, 6, 1, 20, 6, 1, 21, 6, 3, 22, 6, 1, 23, 6, 1, 24, 6, 1, 27, 6, 1, 28, 6, 0, 29, 6, 0, 30, 6, 0, 31, 6, 0, 32, 6, 0, 33, 6, 1, 34, 6, 1, 35, 6, 1, 36, 6, 0, 37, 6, 1, 38, 6, 0, 39, 6, 1, 40, 6, 1, 41, 6, 1, 42, 6, 1, 43, 6, 1, 44, 6, 1, 45, 6, 1, 46, 6, 1, 48, 6, 1, 49, 6, 0, 50, 6, 0, 51, 6, 0, 52, 6, 0, 53, 6, 0, 54, 6, 0, 55, 6, 0, 56, 6, 0, 57, 6, 1, 58, 6, 0, 59, 6, 0, 60, 6, 0, 61, 6, 0, 62, 6, 0, 63, 6, 0, 64, 6, 0, 65, 6, 0, 66, 6, 1, 70, 6, 1, 71, 6, 3, 72, 6, 1, 10, 7, 1, 11, 7, 0, 12, 7, 0, 13, 7, 0, 14, 7, 0, 15, 7, 0, 16, 7, 0, 17, 7, 1, 20, 7, 1, 21, 7, 2, 22, 7, 1, 23, 7, 0, 24, 7, 1, 25, 7, 1, 26, 7, 1, 27, 7, 1, 28, 7, 0, 29, 7, 0, 30, 7, 0, 31, 7, 0, 32, 7, 0, 33, 7, 1, 34, 7, 1, 35, 7, 0, 36, 7, 0, 37, 7, 0, 38, 7, 0, 39, 7, 0, 40, 7, 1, 48, 7, 1, 49, 7, 1, 50, 7, 3, 51, 7, 1, 52, 7, 1, 53, 7, 1, 54, 7, 1, 55, 7, 1, 56, 7, 1, 57, 7, 1, 58, 7, 0, 59, 7, 0, 60, 7, 0, 61, 7, 0, 62, 7, 0, 63, 7, 0, 64, 7, 0, 65, 7, 0, 66, 7, 1, 70, 7, 1, 71, 7, 3, 72, 7, 1, 10, 8, 1, 11, 8, 0, 12, 8, 0, 13, 8, 0, 14, 8, 0, 15, 8, 0, 16, 8, 0, 17, 8, 1, 20, 8, 1, 21, 8, 0, 22, 8, 0, 23, 8, 0, 24, 8, 0, 25, 8, 0, 26, 8, 2, 27, 8, 0, 28, 8, 0, 29, 8, 0, 30, 8, 0, 31, 8, 0, 32, 8, 0, 33, 8, 0, 34, 8, 3, 35, 8, 2, 36, 8, 0, 37, 8, 0, 38, 8, 0, 39, 8, 1, 40, 8, 1, 49, 8, 1, 50, 8, 3, 51, 8, 1, 57, 8, 1, 58, 8, 0, 59, 8, 0, 60, 8, 0, 61, 8, 0, 62, 8, 0, 63, 8, 0, 64, 8, 0, 65, 8, 0, 66, 8, 1, 70, 8, 1, 71, 8, 3, 72, 8, 1, 10, 9, 1, 11, 9, 1, 12, 9, 1, 13, 9, 1, 14, 9, 3, 15, 9, 1, 16, 9, 1, 17, 9, 1, 19, 9, 1, 20, 9, 1, 21, 9, 0, 22, 9, 0, 23, 9, 0, 24, 9, 0, 25, 9, 0, 26, 9, 1, 27, 9, 1, 28, 9, 0, 29, 9, 0, 30, 9, 0, 31, 9, 0, 32, 9, 0, 33, 9, 1, 34, 9, 1, 35, 9, 1, 36, 9, 1, 37, 9, 0, 38, 9, 3, 39, 9, 1, 49, 9, 1, 50, 9, 3, 51, 9, 1, 54, 9, 1, 55, 9, 1, 56, 9, 1, 57, 9, 1, 58, 9, 1, 59, 9, 1, 60, 9, 1, 61, 9, 1, 62, 9, 1, 63, 9, 3, 64, 9, 1, 65, 9, 1, 66, 9, 1, 67, 9, 1, 68, 9, 1, 69, 9, 1, 70, 9, 1, 71, 9, 2, 72, 9, 1, 13, 10, 1, 14, 10, 2, 15, 10, 0, 16, 10, 1, 17, 10, 1, 19, 10, 1, 20, 10, 0, 21, 10, 0, 22, 10, 0, 23, 10, 0, 24, 10, 0, 25, 10, 0, 26, 10, 0, 27, 10, 1, 28, 10, 0, 29, 10, 0, 30, 10, 0, 31, 10, 0, 32, 10, 0, 33, 10, 1, 36, 10, 1, 37, 10, 1, 38, 10, 3, 39, 10, 1, 49, 10, 1, 50, 10, 3, 51, 10, 1, 54, 10, 1, 55, 10, 0, 56, 10, 0, 57, 10, 0, 58, 10, 1, 61, 10, 1, 62, 10, 1, 63, 10, 2, 64, 10, 0, 65, 10, 2, 66, 10, 3, 67, 10, 3, 68, 10, 0, 69, 10, 0, 70, 10, 0, 71, 10, 0, 72, 10, 1, 12, 11, 1, 13, 11, 1, 14, 11, 0, 15, 11, 0, 16, 11, 0, 17, 11, 1, 18, 11, 1, 19, 11, 1, 20, 11, 1, 21, 11, 0, 22, 11, 0, 23, 11, 0, 24, 11, 0, 25, 11, 0, 26, 11, 1, 27, 11, 1, 28, 11, 1, 29, 11, 1, 30, 11, 0, 31, 11, 1, 32, 11, 1, 33, 11, 1, 36, 11, 1, 37, 11, 1, 38, 11, 2, 39, 11, 1, 40, 11, 1, 41, 11, 1, 49, 11, 1, 50, 11, 2, 51, 11, 1, 52, 11, 1, 53, 11, 4, 54, 11, 4, 55, 11, 4, 56, 11, 7, 57, 11, 0, 58, 11, 1, 60, 11, 1, 61, 11, 1, 62, 11, 0, 63, 11, 0, 64, 11, 0, 65, 11, 0, 66, 11, 1, 67, 11, 1, 68, 11, 0, 69, 11, 0, 70, 11, 0, 71, 11, 0, 72, 11, 1, 73, 11, 1, 74, 11, 1, 75, 11, 1, 76, 11, 1, 77, 11, 1, 78, 11, 1, 12, 12, 1, 13, 12, 0, 14, 12, 0, 15, 12, 0, 16, 12, 0, 17, 12, 0, 18, 12, 1, 20, 12, 1, 21, 12, 0, 22, 12, 0, 23, 12, 0, 24, 12, 0, 25, 12, 0, 26, 12, 1, 27, 12, 1, 28, 12, 1, 29, 12, 1, 30, 12, 1, 31, 12, 1, 36, 12, 1, 37, 12, 0, 38, 12, 0, 39, 12, 0, 40, 12, 0, 41, 12, 1, 49, 12, 1, 50, 12, 0, 51, 12, 0, 52, 12, 0, 53, 12, 7, 54, 12, 7, 55, 12, 7, 56, 12, 7, 57, 12, 0, 58, 12, 1, 59, 12, 1, 60, 12, 1, 61, 12, 0, 62, 12, 0, 63, 12, 0, 64, 12, 0, 65, 12, 0, 66, 12, 0, 67, 12, 2, 68, 12, 3, 69, 12, 3, 70, 12, 3, 71, 12, 3, 72, 12, 3, 73, 12, 3, 74, 12, 3, 75, 12, 0, 76, 12, 0, 77, 12, 0, 78, 12, 1, 12, 13, 1, 13, 13, 1, 14, 13, 0, 15, 13, 0, 16, 13, 0, 17, 13, 1, 18, 13, 1, 20, 13, 1, 21, 13, 1, 22, 13, 1, 23, 13, 0, 24, 13, 3, 25, 13, 3, 26, 13, 2, 27, 13, 0, 28, 13, 1, 29, 13, 1, 31, 13, 1, 32, 13, 1, 33, 13, 1, 34, 13, 1, 35, 13, 1, 36, 13, 1, 37, 13, 0, 38, 13, 0, 39, 13, 0, 40, 13, 0, 41, 13, 1, 42, 13, 1, 43, 13, 1, 44, 13, 1, 45, 13, 1, 46, 13, 1, 47, 13, 1, 48, 13, 1, 49, 13, 1, 50, 13, 0, 51, 13, 0, 52, 13, 0, 53, 13, 7, 54, 13, 7, 55, 13, 7, 56, 13, 0, 57, 13, 0, 58, 13, 3, 59, 13, 3, 60, 13, 2, 61, 13, 0, 62, 13, 0, 63, 13, 0, 64, 13, 0, 65, 13, 0, 66, 13, 1, 67, 13, 1, 68, 13, 1, 69, 13, 1, 70, 13, 1, 71, 13, 1, 72, 13, 1, 73, 13, 1, 74, 13, 1, 75, 13, 0, 76, 13, 0, 77, 13, 0, 78, 13, 1, 4, 14, 4, 13, 14, 1, 14, 14, 0, 15, 14, 0, 16, 14, 0, 17, 14, 0, 18, 14, 1, 22, 14, 1, 23, 14, 1, 24, 14, 1, 25, 14, 1, 26, 14, 0, 27, 14, 0, 28, 14, 0, 29, 14, 1, 30, 14, 1, 31, 14, 1, 32, 14, 0, 33, 14, 3, 34, 14, 3, 35, 14, 3, 36, 14, 2, 37, 14, 0, 38, 14, 0, 39, 14, 8, 40, 14, 0, 41, 14, 2, 42, 14, 3, 43, 14, 3, 44, 14, 3, 45, 14, 3, 46, 14, 3, 47, 14, 3, 48, 14, 3, 49, 14, 3, 50, 14, 0, 51, 14, 0, 52, 14, 0, 53, 14, 0, 54, 14, 7, 55, 14, 1, 56, 14, 1, 57, 14, 1, 58, 14, 1, 59, 14, 1, 60, 14, 1, 61, 14, 1, 62, 14, 0, 63, 14, 0, 64, 14, 0, 65, 14, 0, 66, 14, 1, 69, 14, 1, 70, 14, 1, 71, 14, 1, 74, 14, 1, 75, 14, 0, 76, 14, 0, 77, 14, 0, 78, 14, 1, 3, 15, 4, 4, 15, 4, 5, 15, 4, 13, 15, 1, 14, 15, 1, 15, 15, 0, 16, 15, 0, 17, 15, 3, 18, 15, 1, 25, 15, 1, 26, 15, 1, 27, 15, 0, 28, 15, 3, 29, 15, 2, 30, 15, 0, 31, 15, 0, 32, 15, 0, 33, 15, 0, 34, 15, 0, 35, 15, 1, 36, 15, 1, 37, 15, 0, 38, 15, 0, 39, 15, 0, 40, 15, 0, 41, 15, 1, 42, 15, 1, 43, 15, 1, 44, 15, 1, 45, 15, 1, 46, 15, 1, 47, 15, 1, 48, 15, 1, 49, 15, 1, 50, 15, 0, 51, 15, 0, 52, 15, 0, 53, 15, 0, 54, 15, 2, 55, 15, 3, 56, 15, 3, 57, 15, 3, 58, 15, 3, 59, 15, 3, 60, 15, 3, 61, 15, 3, 62, 15, 3, 63, 15, 3, 64, 15, 0, 65, 15, 1, 66, 15, 1, 68, 15, 1, 69, 15, 1, 70, 15, 0, 71, 15, 1, 72, 15, 1, 74, 15, 1, 75, 15, 1, 76, 15, 1, 77, 15, 1, 78, 15, 1, 3, 16, 4, 4, 16, 4, 5, 16, 4, 14, 16, 1, 15, 16, 1, 16, 16, 1, 17, 16, 3, 18, 16, 1, 19, 16, 1, 20, 16, 1, 21, 16, 1, 22, 16, 1, 23, 16, 1, 24, 16, 1, 25, 16, 1, 26, 16, 1, 27, 16, 1, 28, 16, 1, 29, 16, 1, 30, 16, 0, 31, 16, 0, 32, 16, 0, 33, 16, 0, 34, 16, 0, 35, 16, 1, 36, 16, 1, 37, 16, 1, 38, 16, 1, 39, 16, 1, 40, 16, 1, 41, 16, 1, 49, 16, 1, 50, 16, 1, 51, 16, 1, 52, 16, 2, 53, 16, 1, 54, 16, 1, 55, 16, 1, 56, 16, 1, 57, 16, 1, 58, 16, 1, 59, 16, 1, 60, 16, 1, 61, 16, 1, 62, 16, 2, 63, 16, 1, 64, 16, 1, 65, 16, 1, 66, 16, 1, 67, 16, 1, 68, 16, 1, 69, 16, 0, 70, 16, 0, 71, 16, 0, 72, 16, 1, 73, 16, 1, 4, 17, 4, 5, 17, 4, 16, 17, 1, 17, 17, 3, 18, 17, 1, 19, 17, 0, 20, 17, 3, 21, 17, 3, 22, 17, 3, 23, 17, 3, 24, 17, 3, 25, 17, 3, 26, 17, 3, 27, 17, 3, 28, 17, 2, 29, 17, 0, 30, 17, 0, 31, 17, 0, 32, 17, 0, 33, 17, 0, 34, 17, 0, 35, 17, 0, 36, 17, 1, 51, 17, 1, 52, 17, 3, 53, 17, 1, 60, 17, 1, 61, 17, 1, 62, 17, 0, 63, 17, 2, 64, 17, 3, 65, 17, 3, 66, 17, 3, 67, 17, 3, 68, 17, 0, 69, 17, 0, 70, 17, 0, 71, 17, 0, 72, 17, 0, 73, 17, 1, 16, 18, 1, 17, 18, 2, 18, 18, 0, 19, 18, 0, 20, 18, 0, 21, 18, 1, 22, 18, 1, 23, 18, 1, 24, 18, 1, 25, 18, 1, 26, 18, 1, 27, 18, 1, 28, 18, 1, 29, 18, 1, 30, 18, 0, 31, 18, 0, 32, 18, 0, 33, 18, 0, 34, 18, 0, 35, 18, 1, 36, 18, 1, 51, 18, 1, 52, 18, 3, 53, 18, 1, 59, 18, 1, 60, 18, 1, 61, 18, 0, 62, 18, 0, 63, 18, 0, 64, 18, 1, 65, 18, 1, 66, 18, 1, 67, 18, 1, 68, 18, 1, 69, 18, 0, 70, 18, 0, 71, 18, 0, 72, 18, 2, 73, 18, 1, 3, 19, 1, 4, 19, 1, 5, 19, 1, 6, 19, 1, 7, 19, 1, 8, 19, 1, 9, 19, 1, 16, 19, 1, 17, 19, 0, 18, 19, 0, 19, 19, 0, 20, 19, 0, 21, 19, 0, 22, 19, 1, 29, 19, 1, 30, 19, 0, 31, 19, 0, 32, 19, 0, 33, 19, 0, 34, 19, 0, 35, 19, 1, 40, 19, 1, 41, 19, 1, 42, 19, 1, 43, 19, 1, 44, 19, 1, 45, 19, 1, 46, 19, 1, 47, 19, 1, 48, 19, 1, 49, 19, 1, 51, 19, 1, 52, 19, 3, 53, 19, 1, 59, 19, 1, 60, 19, 0, 61, 19, 0, 62, 19, 0, 63, 19, 0, 64, 19, 0, 65, 19, 1, 66, 19, 1, 68, 19, 1, 69, 19, 1, 70, 19, 0, 71, 19, 1, 72, 19, 3, 73, 19, 1, 3, 20, 1, 4, 20, 0, 5, 20, 0, 6, 20, 0, 7, 20, 0, 8, 20, 0, 9, 20, 1, 10, 20, 1, 11, 20, 1, 12, 20, 1, 13, 20, 1, 14, 20, 1, 15, 20, 1, 16, 20, 1, 17, 20, 1, 18, 20, 0, 19, 20, 0, 20, 20, 0, 21, 20, 1, 22, 20, 1, 29, 20, 1, 30, 20, 1, 31, 20, 1, 32, 20, 0, 33, 20, 1, 34, 20, 1, 35, 20, 1, 40, 20, 1, 41, 20, 0, 42, 20, 0, 43, 20, 0, 44, 20, 0, 45, 20, 0, 46, 20, 0, 47, 20, 0, 48, 20, 0, 49, 20, 1, 50, 20, 1, 51, 20, 1, 52, 20, 3, 53, 20, 1, 54, 20, 1, 55, 20, 1, 56, 20, 1, 57, 20, 1, 58, 20, 1, 59, 20, 1, 60, 20, 1, 61, 20, 0, 62, 20, 0, 63, 20, 0, 64, 20, 0, 65, 20, 0, 66, 20, 1, 69, 20, 1, 70, 20, 1, 71, 20, 1, 72, 20, 3, 73, 20, 1, 3, 21, 1, 4, 21, 0, 5, 21, 0, 6, 21, 0, 7, 21, 0, 8, 21, 0, 9, 21, 3, 10, 21, 3, 11, 21, 3, 12, 21, 3, 13, 21, 3, 14, 21, 3, 15, 21, 3, 16, 21, 3, 17, 21, 3, 18, 21, 2, 19, 21, 0, 20, 21, 1, 21, 21, 1, 31, 21, 1, 32, 21, 1, 33, 21, 1, 40, 21, 1, 41, 21, 0, 42, 21, 0, 43, 21, 0, 44, 21, 0, 45, 21, 0, 46, 21, 0, 47, 21, 0, 48, 21, 0, 49, 21, 1, 50, 21, 1, 51, 21, 0, 52, 21, 0, 53, 21, 0, 54, 21, 0, 55, 21, 0, 56, 21, 0, 57, 21, 2, 58, 21, 3, 59, 21, 3, 60, 21, 3, 61, 21, 3, 62, 21, 0, 63, 21, 3, 64, 21, 0, 65, 21, 1, 66, 21, 1, 71, 21, 1, 72, 21, 3, 73, 21, 1, 3, 22, 1, 4, 22, 0, 5, 22, 0, 6, 22, 0, 7, 22, 0, 8, 22, 0, 9, 22, 1, 10, 22, 1, 11, 22, 1, 12, 22, 1, 13, 22, 1, 14, 22, 1, 15, 22, 1, 16, 22, 1, 17, 22, 1, 18, 22, 1, 19, 22, 2, 20, 22, 1, 21, 22, 1, 22, 22, 1, 23, 22, 1, 40, 22, 1, 41, 22, 0, 42, 22, 0, 43, 22, 0, 44, 22, 0, 45, 22, 0, 46, 22, 0, 47, 22, 0, 48, 22, 0, 49, 22, 3, 50, 22, 2, 51, 22, 0, 52, 22, 0, 53, 22, 0, 54, 22, 0, 55, 22, 0, 56, 22, 0, 57, 22, 1, 58, 22, 1, 59, 22, 1, 60, 22, 1, 61, 22, 1, 62, 22, 1, 63, 22, 1, 64, 22, 2, 65, 22, 1, 71, 22, 1, 72, 22, 3, 73, 22, 1, 3, 23, 1, 4, 23, 1, 5, 23, 1, 6, 23, 1, 7, 23, 1, 8, 23, 1, 9, 23, 1, 18, 23, 1, 19, 23, 3, 20, 23, 0, 21, 23, 0, 22, 23, 0, 23, 23, 1, 24, 23, 1, 40, 23, 1, 41, 23, 0, 42, 23, 0, 43, 23, 0, 44, 23, 0, 45, 23, 0, 46, 23, 0, 47, 23, 0, 48, 23, 0, 49, 23, 1, 50, 23, 1, 51, 23, 0, 52, 23, 0, 53, 23, 0, 54, 23, 0, 55, 23, 0, 56, 23, 0, 57, 23, 1, 62, 23, 1, 63, 23, 1, 64, 23, 3, 65, 23, 1, 66, 23, 1, 67, 23, 1, 68, 23, 1, 69, 23, 1, 70, 23, 1, 71, 23, 1, 72, 23, 3, 73, 23, 1, 18, 24, 1, 19, 24, 0, 20, 24, 0, 21, 24, 0, 22, 24, 0, 23, 24, 0, 24, 24, 1, 33, 24, 1, 34, 24, 1, 35, 24, 1, 36, 24, 1, 37, 24, 1, 38, 24, 1, 39, 24, 1, 40, 24, 1, 41, 24, 1, 42, 24, 1, 43, 24, 1, 44, 24, 1, 45, 24, 1, 46, 24, 1, 47, 24, 1, 48, 24, 1, 49, 24, 1, 50, 24, 1, 51, 24, 0, 52, 24, 0, 53, 24, 0, 54, 24, 0, 55, 24, 0, 56, 24, 0, 57, 24, 1, 62, 24, 1, 63, 24, 0, 64, 24, 0, 65, 24, 0, 66, 24, 0, 67, 24, 0, 68, 24, 1, 69, 24, 1, 70, 24, 0, 71, 24, 0, 72, 24, 0, 73, 24, 1, 18, 25, 1, 19, 25, 0, 20, 25, 0, 21, 25, 0, 22, 25, 0, 23, 25, 0, 24, 25, 1, 25, 25, 1, 26, 25, 1, 27, 25, 1, 28, 25, 1, 29, 25, 1, 30, 25, 1, 31, 25, 1, 32, 25, 1, 33, 25, 1, 34, 25, 0, 35, 25, 0, 36, 25, 0, 37, 25, 0, 38, 25, 0, 39, 25, 0, 40, 25, 9, 41, 25, 1, 50, 25, 1, 51, 25, 1, 52, 25, 1, 53, 25, 1, 54, 25, 1, 55, 25, 1, 56, 25, 1, 57, 25, 1, 62, 25, 1, 63, 25, 0, 64, 25, 0, 65, 25, 0, 66, 25, 0, 67, 25, 0, 68, 25, 1, 69, 25, 1, 70, 25, 0, 71, 25, 0, 72, 25, 0, 73, 25, 1, 18, 26, 1, 19, 26, 0, 20, 26, 0, 21, 26, 0, 22, 26, 0, 23, 26, 0, 24, 26, 2, 25, 26, 3, 26, 26, 3, 27, 26, 3, 28, 26, 3, 29, 26, 3, 30, 26, 3, 31, 26, 3, 32, 26, 3, 33, 26, 3, 34, 26, 0, 35, 26, 0, 36, 26, 0, 37, 26, 0, 38, 26, 0, 39, 26, 0, 40, 26, 0, 41, 26, 1, 62, 26, 1, 63, 26, 0, 64, 26, 0, 65, 26, 0, 66, 26, 0, 67, 26, 0, 68, 26, 1, 69, 26, 1, 70, 26, 0, 71, 26, 0, 72, 26, 0, 73, 26, 1, 18, 27, 1, 19, 27, 1, 20, 27, 0, 21, 27, 0, 22, 27, 0, 23, 27, 1, 24, 27, 1, 25, 27, 1, 26, 27, 1, 27, 27, 1, 28, 27, 1, 29, 27, 1, 30, 27, 1, 31, 27, 1, 32, 27, 1, 33, 27, 1, 34, 27, 0, 35, 27, 0, 36, 27, 0, 37, 27, 0, 38, 27, 0, 39, 27, 0, 40, 27, 0, 41, 27, 1, 62, 27, 1, 63, 27, 1, 64, 27, 1, 65, 27, 1, 66, 27, 1, 67, 27, 1, 68, 27, 1, 69, 27, 1, 70, 27, 1, 71, 27, 1, 72, 27, 1, 73, 27, 1, 19, 28, 1, 20, 28, 1, 21, 28, 1, 22, 28, 1, 23, 28, 1, 33, 28, 1, 34, 28, 1, 35, 28, 1, 36, 28, 1, 37, 28, 1, 38, 28, 1, 39, 28, 1, 40, 28, 1, 41, 28, 1)
-}
-
-[node name="Level9" type="GridMap" parent="Levels" unique_id=252043595]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -54, 0)
-mesh_library = SubResource("MeshLibrary_r4kn7")
-cell_size = Vector3(1, 1, 1)
-data = {
-"cells": PackedInt32Array(60, 0, 1, 61, 0, 1, 62, 0, 1, 63, 0, 1, 64, 0, 1, 65, 0, 1, 66, 0, 1, 3, 1, 1, 4, 1, 1, 5, 1, 1, 6, 1, 1, 7, 1, 1, 8, 1, 1, 9, 1, 1, 10, 1, 1, 11, 1, 1, 36, 1, 1, 37, 1, 1, 38, 1, 1, 39, 1, 1, 40, 1, 1, 41, 1, 1, 42, 1, 1, 45, 1, 1, 46, 1, 1, 47, 1, 1, 50, 1, 1, 51, 1, 1, 52, 1, 1, 53, 1, 1, 54, 1, 1, 55, 1, 1, 56, 1, 1, 57, 1, 1, 60, 1, 1, 61, 1, 0, 62, 1, 0, 63, 1, 0, 64, 1, 0, 65, 1, 0, 66, 1, 1, 3, 2, 1, 4, 2, 0, 5, 2, 0, 6, 2, 0, 7, 2, 0, 8, 2, 0, 9, 2, 0, 10, 2, 0, 11, 2, 1, 17, 2, 1, 18, 2, 1, 19, 2, 1, 20, 2, 1, 21, 2, 1, 22, 2, 1, 23, 2, 1, 24, 2, 1, 27, 2, 4, 28, 2, 4, 31, 2, 4, 32, 2, 4, 36, 2, 1, 37, 2, 0, 38, 2, 0, 39, 2, 0, 40, 2, 0, 41, 2, 0, 42, 2, 1, 44, 2, 1, 45, 2, 1, 46, 2, 0, 47, 2, 1, 48, 2, 1, 49, 2, 1, 50, 2, 1, 51, 2, 0, 52, 2, 0, 53, 2, 0, 54, 2, 0, 55, 2, 0, 56, 2, 0, 57, 2, 1, 60, 2, 1, 61, 2, 0, 62, 2, 0, 63, 2, 0, 64, 2, 0, 65, 2, 0, 66, 2, 1, 3, 3, 1, 4, 3, 0, 5, 3, 0, 6, 3, 0, 7, 3, 0, 8, 3, 0, 9, 3, 0, 10, 3, 0, 11, 3, 1, 12, 3, 1, 13, 3, 1, 14, 3, 1, 15, 3, 1, 16, 3, 1, 17, 3, 1, 18, 3, 0, 19, 3, 0, 20, 3, 0, 21, 3, 0, 22, 3, 0, 23, 3, 0, 24, 3, 1, 25, 3, 1, 26, 3, 1, 27, 3, 4, 28, 3, 4, 29, 3, 4, 30, 3, 4, 31, 3, 4, 32, 3, 4, 33, 3, 4, 36, 3, 1, 37, 3, 0, 38, 3, 0, 39, 3, 0, 40, 3, 0, 41, 3, 0, 42, 3, 1, 43, 3, 1, 44, 3, 1, 45, 3, 0, 46, 3, 0, 47, 3, 0, 48, 3, 2, 49, 3, 3, 50, 3, 3, 51, 3, 0, 52, 3, 0, 53, 3, 0, 54, 3, 0, 55, 3, 0, 56, 3, 0, 57, 3, 1, 60, 3, 1, 61, 3, 0, 62, 3, 0, 63, 3, 0, 64, 3, 0, 65, 3, 0, 66, 3, 1, 3, 4, 1, 4, 4, 0, 5, 4, 0, 6, 4, 0, 7, 4, 0, 8, 4, 0, 9, 4, 0, 10, 4, 0, 11, 4, 3, 12, 4, 3, 13, 4, 3, 14, 4, 3, 15, 4, 3, 16, 4, 3, 17, 4, 2, 18, 4, 0, 19, 4, 0, 20, 4, 0, 21, 4, 0, 22, 4, 0, 23, 4, 0, 24, 4, 3, 25, 4, 3, 26, 4, 3, 27, 4, 2, 28, 4, 4, 29, 4, 4, 30, 4, 4, 31, 4, 4, 32, 4, 4, 33, 4, 4, 36, 4, 1, 37, 4, 0, 38, 4, 0, 39, 4, 0, 40, 4, 0, 41, 4, 0, 42, 4, 1, 43, 4, 1, 44, 4, 0, 45, 4, 0, 46, 4, 0, 47, 4, 0, 48, 4, 0, 49, 4, 1, 50, 4, 1, 51, 4, 1, 52, 4, 1, 53, 4, 1, 54, 4, 1, 55, 4, 1, 56, 4, 1, 57, 4, 1, 60, 4, 1, 61, 4, 1, 62, 4, 1, 63, 4, 3, 64, 4, 1, 65, 4, 1, 66, 4, 1, 1, 5, 1, 2, 5, 1, 3, 5, 1, 4, 5, 2, 5, 5, 1, 6, 5, 1, 7, 5, 1, 8, 5, 1, 9, 5, 1, 10, 5, 1, 11, 5, 1, 12, 5, 1, 13, 5, 1, 14, 5, 1, 15, 5, 1, 16, 5, 1, 17, 5, 1, 18, 5, 0, 19, 5, 0, 20, 5, 0, 21, 5, 0, 22, 5, 0, 23, 5, 0, 24, 5, 1, 25, 5, 1, 26, 5, 1, 27, 5, 1, 28, 5, 4, 29, 5, 4, 30, 5, 4, 31, 5, 4, 32, 5, 4, 33, 5, 4, 36, 5, 1, 37, 5, 1, 38, 5, 1, 39, 5, 1, 40, 5, 1, 41, 5, 3, 42, 5, 1, 43, 5, 1, 44, 5, 1, 45, 5, 0, 46, 5, 0, 47, 5, 0, 48, 5, 1, 49, 5, 1, 62, 5, 1, 63, 5, 3, 64, 5, 1, 1, 6, 1, 2, 6, 0, 3, 6, 0, 4, 6, 0, 5, 6, 0, 6, 6, 0, 7, 6, 1, 17, 6, 1, 18, 6, 0, 19, 6, 0, 20, 6, 0, 21, 6, 0, 22, 6, 0, 23, 6, 0, 24, 6, 1, 28, 6, 4, 29, 6, 4, 30, 6, 4, 31, 6, 4, 32, 6, 4, 33, 6, 4, 40, 6, 1, 41, 6, 3, 42, 6, 1, 44, 6, 1, 45, 6, 1, 46, 6, 0, 47, 6, 3, 48, 6, 1, 62, 6, 1, 63, 6, 3, 64, 6, 1, 68, 6, 1, 69, 6, 1, 70, 6, 1, 71, 6, 1, 72, 6, 1, 73, 6, 1, 74, 6, 1, 75, 6, 1, 1, 7, 1, 2, 7, 0, 3, 7, 0, 4, 7, 0, 5, 7, 0, 6, 7, 0, 7, 7, 1, 17, 7, 1, 18, 7, 2, 19, 7, 1, 20, 7, 1, 21, 7, 1, 22, 7, 1, 23, 7, 1, 24, 7, 1, 28, 7, 1, 29, 7, 4, 30, 7, 4, 31, 7, 4, 32, 7, 4, 40, 7, 1, 41, 7, 2, 42, 7, 1, 43, 7, 1, 44, 7, 1, 45, 7, 1, 46, 7, 1, 47, 7, 3, 48, 7, 1, 55, 7, 1, 56, 7, 1, 57, 7, 1, 58, 7, 1, 59, 7, 1, 60, 7, 1, 61, 7, 1, 62, 7, 1, 63, 7, 2, 64, 7, 1, 65, 7, 1, 66, 7, 1, 67, 7, 1, 68, 7, 1, 69, 7, 0, 70, 7, 0, 71, 7, 0, 72, 7, 0, 73, 7, 0, 74, 7, 0, 75, 7, 1, 1, 8, 1, 2, 8, 0, 3, 8, 0, 4, 8, 0, 5, 8, 0, 6, 8, 0, 7, 8, 1, 17, 8, 1, 18, 8, 3, 19, 8, 1, 28, 8, 1, 29, 8, 3, 30, 8, 1, 39, 8, 1, 40, 8, 1, 41, 8, 0, 42, 8, 1, 43, 8, 0, 44, 8, 1, 45, 8, 1, 46, 8, 1, 47, 8, 3, 48, 8, 1, 55, 8, 1, 56, 8, 0, 57, 8, 0, 58, 8, 0, 59, 8, 0, 60, 8, 0, 61, 8, 0, 62, 8, 0, 63, 8, 0, 64, 8, 0, 65, 8, 2, 66, 8, 3, 67, 8, 3, 68, 8, 3, 69, 8, 0, 70, 8, 0, 71, 8, 0, 72, 8, 0, 73, 8, 0, 74, 8, 0, 75, 8, 1, 1, 9, 1, 2, 9, 3, 3, 9, 1, 4, 9, 1, 5, 9, 1, 6, 9, 1, 7, 9, 1, 17, 9, 1, 18, 9, 3, 19, 9, 1, 28, 9, 1, 29, 9, 2, 30, 9, 1, 31, 9, 1, 32, 9, 1, 33, 9, 1, 34, 9, 1, 35, 9, 1, 36, 9, 1, 39, 9, 1, 40, 9, 0, 41, 9, 0, 42, 9, 0, 43, 9, 0, 44, 9, 0, 45, 9, 1, 46, 9, 1, 47, 9, 2, 48, 9, 1, 49, 9, 1, 50, 9, 1, 51, 9, 1, 52, 9, 1, 53, 9, 1, 54, 9, 1, 55, 9, 1, 56, 9, 0, 57, 9, 0, 58, 9, 0, 59, 9, 0, 60, 9, 0, 61, 9, 0, 62, 9, 0, 63, 9, 0, 64, 9, 0, 65, 9, 1, 66, 9, 1, 67, 9, 1, 68, 9, 1, 69, 9, 0, 70, 9, 0, 71, 9, 0, 72, 9, 0, 73, 9, 0, 74, 9, 0, 75, 9, 1, 0, 10, 1, 1, 10, 1, 2, 10, 2, 3, 10, 1, 4, 10, 1, 5, 10, 1, 6, 10, 1, 7, 10, 1, 17, 10, 1, 18, 10, 3, 19, 10, 1, 21, 10, 1, 22, 10, 1, 23, 10, 1, 24, 10, 1, 25, 10, 1, 28, 10, 1, 29, 10, 0, 30, 10, 0, 31, 10, 0, 32, 10, 0, 33, 10, 0, 34, 10, 0, 35, 10, 0, 36, 10, 1, 39, 10, 1, 40, 10, 1, 41, 10, 0, 42, 10, 1, 43, 10, 0, 44, 10, 1, 45, 10, 1, 46, 10, 1, 47, 10, 0, 48, 10, 0, 49, 10, 2, 50, 10, 3, 51, 10, 3, 52, 10, 0, 53, 10, 0, 54, 10, 0, 55, 10, 2, 56, 10, 0, 57, 10, 0, 58, 10, 0, 59, 10, 0, 60, 10, 0, 61, 10, 0, 62, 10, 0, 63, 10, 0, 64, 10, 0, 65, 10, 1, 68, 10, 1, 69, 10, 1, 70, 10, 1, 71, 10, 1, 72, 10, 1, 73, 10, 1, 74, 10, 1, 75, 10, 1, 0, 11, 1, 1, 11, 0, 2, 11, 0, 3, 11, 0, 4, 11, 0, 5, 11, 0, 6, 11, 0, 7, 11, 1, 17, 11, 1, 18, 11, 3, 19, 11, 1, 21, 11, 1, 22, 11, 0, 23, 11, 0, 24, 11, 0, 25, 11, 1, 28, 11, 1, 29, 11, 0, 30, 11, 0, 31, 11, 0, 32, 11, 0, 33, 11, 0, 34, 11, 0, 35, 11, 0, 36, 11, 1, 40, 11, 1, 41, 11, 3, 42, 11, 1, 43, 11, 1, 44, 11, 1, 45, 11, 1, 46, 11, 0, 47, 11, 0, 48, 11, 0, 49, 11, 0, 50, 11, 1, 51, 11, 1, 52, 11, 0, 53, 11, 0, 54, 11, 0, 55, 11, 1, 56, 11, 1, 57, 11, 1, 58, 11, 1, 59, 11, 1, 60, 11, 1, 61, 11, 3, 62, 11, 1, 63, 11, 1, 64, 11, 1, 65, 11, 1, 0, 12, 1, 1, 12, 0, 2, 12, 0, 3, 12, 0, 4, 12, 0, 5, 12, 0, 6, 12, 0, 7, 12, 1, 16, 12, 1, 17, 12, 1, 18, 12, 0, 19, 12, 1, 20, 12, 1, 21, 12, 1, 22, 12, 0, 23, 12, 0, 24, 12, 0, 25, 12, 1, 26, 12, 1, 27, 12, 1, 28, 12, 1, 29, 12, 0, 30, 12, 0, 31, 12, 0, 32, 12, 0, 33, 12, 0, 34, 12, 0, 35, 12, 0, 36, 12, 1, 37, 12, 1, 38, 12, 1, 39, 12, 1, 40, 12, 1, 41, 12, 2, 42, 12, 1, 43, 12, 1, 45, 12, 1, 46, 12, 0, 47, 12, 0, 48, 12, 0, 49, 12, 0, 50, 12, 0, 51, 12, 1, 52, 12, 1, 53, 12, 1, 54, 12, 1, 55, 12, 1, 60, 12, 1, 61, 12, 3, 62, 12, 1, 0, 13, 1, 1, 13, 0, 2, 13, 0, 3, 13, 0, 4, 13, 0, 5, 13, 0, 6, 13, 0, 7, 13, 1, 15, 13, 1, 16, 13, 1, 17, 13, 0, 18, 13, 0, 19, 13, 0, 20, 13, 1, 21, 13, 1, 22, 13, 0, 23, 13, 0, 24, 13, 0, 25, 13, 3, 26, 13, 3, 27, 13, 3, 28, 13, 2, 29, 13, 0, 30, 13, 0, 31, 13, 0, 32, 13, 0, 33, 13, 0, 34, 13, 0, 35, 13, 0, 36, 13, 3, 37, 13, 2, 38, 13, 0, 39, 13, 0, 40, 13, 0, 41, 13, 0, 42, 13, 0, 43, 13, 1, 45, 13, 1, 46, 13, 0, 47, 13, 0, 48, 13, 0, 49, 13, 0, 50, 13, 0, 51, 13, 1, 60, 13, 1, 61, 13, 3, 62, 13, 1, 0, 14, 1, 1, 14, 1, 2, 14, 1, 3, 14, 1, 4, 14, 1, 5, 14, 3, 6, 14, 1, 7, 14, 1, 15, 14, 1, 16, 14, 9, 17, 14, 0, 18, 14, 0, 19, 14, 0, 20, 14, 0, 21, 14, 1, 22, 14, 1, 23, 14, 1, 24, 14, 1, 25, 14, 1, 26, 14, 1, 27, 14, 1, 28, 14, 1, 29, 14, 1, 30, 14, 1, 31, 14, 1, 32, 14, 1, 33, 14, 1, 34, 14, 1, 35, 14, 1, 36, 14, 1, 37, 14, 1, 38, 14, 0, 39, 14, 8, 40, 14, 0, 41, 14, 0, 42, 14, 0, 43, 14, 1, 45, 14, 1, 46, 14, 0, 47, 14, 0, 48, 14, 0, 49, 14, 0, 50, 14, 0, 51, 14, 1, 58, 14, 1, 59, 14, 1, 60, 14, 1, 61, 14, 3, 62, 14, 1, 4, 15, 1, 5, 15, 3, 6, 15, 1, 11, 15, 1, 12, 15, 1, 13, 15, 1, 14, 15, 1, 15, 15, 1, 16, 15, 1, 17, 15, 0, 18, 15, 0, 19, 15, 0, 20, 15, 1, 21, 15, 1, 25, 15, 1, 26, 15, 1, 27, 15, 1, 28, 15, 1, 29, 15, 1, 37, 15, 1, 38, 15, 0, 39, 15, 0, 40, 15, 0, 41, 15, 0, 42, 15, 0, 43, 15, 1, 44, 15, 1, 45, 15, 1, 46, 15, 0, 47, 15, 0, 48, 15, 0, 49, 15, 0, 50, 15, 1, 51, 15, 1, 56, 15, 1, 57, 15, 1, 58, 15, 1, 59, 15, 0, 60, 15, 1, 61, 15, 2, 62, 15, 1, 2, 16, 1, 3, 16, 1, 4, 16, 1, 5, 16, 2, 6, 16, 1, 11, 16, 1, 12, 16, 0, 13, 16, 0, 14, 16, 0, 15, 16, 0, 16, 16, 1, 17, 16, 1, 18, 16, 0, 19, 16, 1, 20, 16, 1, 23, 16, 1, 24, 16, 1, 25, 16, 1, 26, 16, 0, 27, 16, 0, 28, 16, 0, 29, 16, 1, 30, 16, 1, 31, 16, 1, 37, 16, 1, 38, 16, 0, 39, 16, 0, 40, 16, 0, 41, 16, 0, 42, 16, 0, 43, 16, 3, 44, 16, 3, 45, 16, 3, 46, 16, 2, 47, 16, 0, 48, 16, 0, 49, 16, 0, 50, 16, 1, 51, 16, 1, 52, 16, 1, 53, 16, 1, 54, 16, 1, 55, 16, 1, 56, 16, 1, 57, 16, 0, 58, 16, 0, 59, 16, 0, 60, 16, 0, 61, 16, 0, 62, 16, 1, 63, 16, 1, 64, 16, 1, 65, 16, 1, 66, 16, 1, 67, 16, 1, 68, 16, 1, 69, 16, 1, 70, 16, 1, 71, 16, 1, 72, 16, 1, 73, 16, 1, 74, 16, 1, 75, 16, 1, 76, 16, 1, 2, 17, 1, 3, 17, 0, 4, 17, 0, 5, 17, 0, 6, 17, 1, 7, 17, 1, 8, 17, 1, 9, 17, 1, 10, 17, 1, 11, 17, 1, 12, 17, 0, 13, 17, 0, 14, 17, 0, 15, 17, 0, 16, 17, 1, 17, 17, 1, 18, 17, 1, 19, 17, 1, 23, 17, 1, 24, 17, 0, 25, 17, 0, 26, 17, 0, 27, 17, 0, 28, 17, 0, 29, 17, 0, 30, 17, 0, 31, 17, 1, 34, 17, 1, 35, 17, 1, 36, 17, 1, 37, 17, 1, 38, 17, 2, 39, 17, 1, 40, 17, 1, 41, 17, 2, 42, 17, 1, 43, 17, 1, 44, 17, 1, 45, 17, 1, 46, 17, 1, 47, 17, 0, 48, 17, 0, 49, 17, 0, 50, 17, 2, 51, 17, 3, 52, 17, 3, 53, 17, 3, 54, 17, 3, 55, 17, 3, 56, 17, 3, 57, 17, 0, 58, 17, 0, 59, 17, 0, 60, 17, 0, 61, 17, 0, 62, 17, 2, 63, 17, 3, 64, 17, 3, 65, 17, 3, 66, 17, 3, 67, 17, 3, 68, 17, 3, 69, 17, 3, 70, 17, 3, 71, 17, 0, 72, 17, 0, 73, 17, 0, 74, 17, 0, 75, 17, 0, 76, 17, 1, 2, 18, 1, 3, 18, 0, 4, 18, 0, 5, 18, 0, 6, 18, 2, 7, 18, 3, 8, 18, 3, 9, 18, 3, 10, 18, 3, 11, 18, 3, 12, 18, 0, 13, 18, 0, 14, 18, 0, 15, 18, 0, 16, 18, 1, 17, 18, 1, 18, 18, 1, 19, 18, 1, 20, 18, 1, 21, 18, 1, 22, 18, 1, 23, 18, 1, 24, 18, 0, 25, 18, 0, 26, 18, 0, 27, 18, 0, 28, 18, 0, 29, 18, 0, 30, 18, 0, 31, 18, 1, 32, 18, 1, 33, 18, 1, 34, 18, 1, 35, 18, 0, 36, 18, 0, 37, 18, 0, 38, 18, 0, 39, 18, 1, 40, 18, 1, 41, 18, 3, 42, 18, 1, 46, 18, 1, 47, 18, 1, 48, 18, 2, 49, 18, 1, 50, 18, 1, 51, 18, 1, 52, 18, 1, 53, 18, 1, 54, 18, 1, 55, 18, 1, 56, 18, 0, 57, 18, 0, 58, 18, 0, 59, 18, 0, 60, 18, 0, 61, 18, 0, 62, 18, 0, 63, 18, 1, 64, 18, 1, 65, 18, 1, 66, 18, 1, 67, 18, 1, 68, 18, 1, 69, 18, 1, 70, 18, 1, 71, 18, 0, 72, 18, 0, 73, 18, 0, 74, 18, 0, 75, 18, 0, 76, 18, 1, 2, 19, 1, 3, 19, 0, 4, 19, 0, 5, 19, 0, 6, 19, 1, 7, 19, 1, 8, 19, 1, 9, 19, 1, 10, 19, 1, 11, 19, 1, 12, 19, 1, 13, 19, 1, 14, 19, 2, 15, 19, 1, 16, 19, 1, 17, 19, 0, 18, 19, 3, 19, 19, 3, 20, 19, 3, 21, 19, 3, 22, 19, 3, 23, 19, 2, 24, 19, 0, 25, 19, 0, 26, 19, 0, 27, 19, 0, 28, 19, 0, 29, 19, 0, 30, 19, 0, 31, 19, 3, 32, 19, 3, 33, 19, 3, 34, 19, 2, 35, 19, 0, 36, 19, 0, 37, 19, 0, 38, 19, 0, 39, 19, 1, 40, 19, 1, 41, 19, 3, 42, 19, 1, 47, 19, 1, 48, 19, 3, 49, 19, 1, 55, 19, 1, 56, 19, 1, 57, 19, 0, 58, 19, 0, 59, 19, 0, 60, 19, 0, 61, 19, 0, 62, 19, 2, 63, 19, 3, 64, 19, 3, 65, 19, 0, 66, 19, 0, 67, 19, 0, 68, 19, 0, 69, 19, 0, 70, 19, 2, 71, 19, 0, 72, 19, 0, 73, 19, 0, 74, 19, 0, 75, 19, 0, 76, 19, 1, 2, 20, 1, 3, 20, 0, 4, 20, 0, 5, 20, 0, 6, 20, 3, 7, 20, 3, 8, 20, 3, 9, 20, 3, 10, 20, 3, 11, 20, 3, 12, 20, 3, 13, 20, 3, 14, 20, 2, 15, 20, 0, 16, 20, 0, 17, 20, 0, 18, 20, 0, 19, 20, 0, 20, 20, 1, 21, 20, 1, 22, 20, 1, 23, 20, 1, 24, 20, 1, 25, 20, 1, 26, 20, 0, 27, 20, 0, 28, 20, 0, 29, 20, 1, 30, 20, 2, 31, 20, 1, 32, 20, 1, 33, 20, 1, 34, 20, 1, 35, 20, 1, 36, 20, 3, 37, 20, 1, 38, 20, 1, 39, 20, 1, 40, 20, 1, 41, 20, 3, 42, 20, 1, 47, 20, 1, 48, 20, 3, 49, 20, 1, 50, 20, 1, 56, 20, 1, 57, 20, 0, 58, 20, 0, 59, 20, 0, 60, 20, 0, 61, 20, 0, 62, 20, 1, 63, 20, 1, 64, 20, 1, 65, 20, 0, 66, 20, 0, 67, 20, 0, 68, 20, 0, 69, 20, 0, 70, 20, 1, 71, 20, 1, 72, 20, 1, 73, 20, 1, 74, 20, 1, 75, 20, 1, 76, 20, 1, 2, 21, 1, 3, 21, 1, 4, 21, 1, 5, 21, 1, 6, 21, 1, 7, 21, 1, 8, 21, 1, 9, 21, 1, 10, 21, 1, 11, 21, 1, 12, 21, 1, 13, 21, 1, 14, 21, 1, 15, 21, 0, 16, 21, 0, 17, 21, 0, 18, 21, 0, 19, 21, 0, 20, 21, 1, 21, 21, 1, 25, 21, 1, 26, 21, 1, 27, 21, 1, 28, 21, 1, 29, 21, 1, 30, 21, 3, 31, 21, 1, 35, 21, 1, 36, 21, 3, 37, 21, 1, 40, 21, 1, 41, 21, 3, 42, 21, 1, 47, 21, 1, 48, 21, 3, 49, 21, 0, 50, 21, 1, 51, 21, 1, 56, 21, 1, 57, 21, 2, 58, 21, 1, 59, 21, 0, 60, 21, 1, 61, 21, 1, 62, 21, 1, 64, 21, 1, 65, 21, 1, 66, 21, 1, 67, 21, 1, 68, 21, 1, 69, 21, 1, 70, 21, 1, 13, 22, 1, 14, 22, 0, 15, 22, 0, 16, 22, 0, 17, 22, 0, 18, 22, 0, 19, 22, 0, 20, 22, 0, 21, 22, 1, 29, 22, 1, 30, 22, 3, 31, 22, 1, 35, 22, 1, 36, 22, 2, 37, 22, 1, 38, 22, 1, 39, 22, 1, 40, 22, 1, 41, 22, 3, 42, 22, 1, 46, 22, 1, 47, 22, 1, 48, 22, 0, 49, 22, 0, 50, 22, 0, 51, 22, 1, 52, 22, 1, 54, 22, 1, 55, 22, 1, 56, 22, 1, 57, 22, 3, 58, 22, 1, 59, 22, 1, 60, 22, 1, 61, 22, 1, 13, 23, 1, 14, 23, 1, 15, 23, 0, 16, 23, 0, 17, 23, 0, 18, 23, 0, 19, 23, 0, 20, 23, 1, 21, 23, 1, 29, 23, 1, 30, 23, 3, 31, 23, 1, 35, 23, 1, 36, 23, 0, 37, 23, 0, 38, 23, 0, 39, 23, 0, 40, 23, 0, 41, 23, 0, 42, 23, 1, 46, 23, 1, 47, 23, 0, 48, 23, 0, 49, 23, 0, 50, 23, 0, 51, 23, 0, 52, 23, 1, 54, 23, 1, 55, 23, 0, 56, 23, 0, 57, 23, 0, 58, 23, 0, 59, 23, 0, 60, 23, 0, 61, 23, 1, 14, 24, 1, 15, 24, 0, 16, 24, 0, 17, 24, 0, 18, 24, 0, 19, 24, 0, 20, 24, 1, 29, 24, 1, 30, 24, 3, 31, 24, 1, 35, 24, 1, 36, 24, 0, 37, 24, 0, 38, 24, 0, 39, 24, 0, 40, 24, 0, 41, 24, 0, 42, 24, 1, 43, 24, 1, 44, 24, 1, 45, 24, 1, 46, 24, 1, 47, 24, 1, 48, 24, 0, 49, 24, 0, 50, 24, 0, 51, 24, 1, 52, 24, 1, 54, 24, 1, 55, 24, 0, 56, 24, 0, 57, 24, 0, 58, 24, 0, 59, 24, 0, 60, 24, 0, 61, 24, 1, 14, 25, 1, 15, 25, 1, 16, 25, 1, 17, 25, 0, 18, 25, 1, 19, 25, 1, 20, 25, 1, 28, 25, 1, 29, 25, 1, 30, 25, 3, 31, 25, 1, 32, 25, 1, 35, 25, 1, 36, 25, 0, 37, 25, 0, 38, 25, 0, 39, 25, 0, 40, 25, 0, 41, 25, 0, 42, 25, 2, 43, 25, 3, 44, 25, 0, 45, 25, 0, 46, 25, 0, 47, 25, 1, 48, 25, 1, 49, 25, 0, 50, 25, 1, 51, 25, 1, 54, 25, 1, 55, 25, 1, 56, 25, 1, 57, 25, 1, 58, 25, 1, 59, 25, 1, 60, 25, 1, 61, 25, 1, 16, 26, 1, 17, 26, 1, 18, 26, 1, 28, 26, 1, 29, 26, 0, 30, 26, 0, 31, 26, 0, 32, 26, 1, 35, 26, 1, 36, 26, 1, 37, 26, 1, 38, 26, 1, 39, 26, 1, 40, 26, 1, 41, 26, 1, 42, 26, 1, 43, 26, 1, 44, 26, 0, 45, 26, 0, 46, 26, 0, 47, 26, 1, 48, 26, 1, 49, 26, 1, 50, 26, 1, 28, 27, 1, 29, 27, 0, 30, 27, 0, 31, 27, 0, 32, 27, 1, 43, 27, 1, 44, 27, 1, 45, 27, 1, 46, 27, 1, 47, 27, 1, 28, 28, 1, 29, 28, 1, 30, 28, 1, 31, 28, 1, 32, 28, 1)
-}
diff --git a/demo/scenes/generator.gd b/demo/scenes/generator.gd
deleted file mode 100644
index 42387b8..0000000
--- a/demo/scenes/generator.gd
+++ /dev/null
@@ -1,136 +0,0 @@
-@tool
-extends Node3D
-class_name DungeonGenerator
-
-const T_NOTHING := 0
-const T_FLOOR := 1
-const T_WALL := 2
-const T_DOOR := 3
-const T_CORRIDOR := 4
-const T_LIQUID := 5
-const T_BRIDGE := 6
-const T_STAIRS_UP := 7
-const T_STAIRS_DOWN := 8
-
-const L_NONE := 0
-const L_WATER := 1
-const L_LAVA := 2
-const L_CHASM := 3
-const L_BRIMSTONE := 4
-
-# Match the tile IDs exposed by MeshLibraryBuilder.
-const TILE_FLOOR := 0
-const TILE_WALL := 1
-const TILE_DOOR := 2
-const TILE_CORRIDOR := 3
-const TILE_WATER := 4
-const TILE_LAVA := 5
-const TILE_BRIMSTONE := 6
-const TILE_BRIDGE := 7
-const TILE_STAIRS_UP := 8
-const TILE_STAIRS_DOWN := 9
-
-@export var base_seed: int = 2321
-@export var depth_start: int = 20
-@export var level_count: int = 10
-@export var level_spacing: float = 6.0
-@export var generate: bool = false:
- set(value):
- if value and Engine.is_editor_hint():
- generate_dungeon()
- generate = false
-@export var clear_levels: bool = false:
- set(value):
- if value and Engine.is_editor_hint():
- _clear_levels()
- clear_levels = false
-
-@onready var levels_root: Node3D = %Levels
-
-var _mesh_library: MeshLibrary
-
-# Populate one level's GridMap from the grid dict. Returns the count of
-# chasm cells that were deliberately skipped (for reporting).
-func _populate_level(grid_map: GridMap, grid: Dictionary) -> int:
- var w: int = grid["width"]
- var h: int = grid["height"]
- var terrain: PackedByteArray = grid["terrain"]
- var liquid: PackedByteArray = grid["liquid"]
-
- var chasm_cells := 0
- for y in range(h):
- for x in range(w):
- var idx := y * w + x
- var t: int = terrain[idx]
- var liq: int = liquid[idx]
- # Chasm liquid renders as an actual see-through pit.
- if t == T_LIQUID and liq == L_CHASM:
- chasm_cells += 1
- continue
- var tile_id := _tile_for(t, liq)
- if tile_id == -1:
- continue # T_NOTHING or other empty — leave unrendered
- # GridMap coords: (X, Y, Z). We want dungeon x → X, dungeon y → Z.
- grid_map.set_cell_item(Vector3i(x, 0, y), tile_id)
- return chasm_cells
-
-func _tile_for(terrain: int, liquid: int) -> int:
- match terrain:
- T_FLOOR: return TILE_FLOOR
- T_CORRIDOR: return TILE_CORRIDOR
- T_DOOR: return TILE_DOOR
- T_WALL: return TILE_WALL
- T_BRIDGE: return TILE_BRIDGE
- T_STAIRS_UP: return TILE_STAIRS_UP
- T_STAIRS_DOWN: return TILE_STAIRS_DOWN
- T_LIQUID:
- match liquid:
- L_WATER: return TILE_WATER
- L_LAVA: return TILE_LAVA
- L_BRIMSTONE: return TILE_BRIMSTONE
- L_CHASM: return -1 # empty cell — see through to level below
- _: return TILE_WATER
- _: return -1 # T_NOTHING or unknown
-
-func _clear_levels() -> void:
- if levels_root == null:
- levels_root = %Levels
- for child in levels_root.get_children():
- levels_root.remove_child(child)
- child.queue_free()
-
-func generate_dungeon():
- if levels_root == null:
- levels_root = %Levels
- if levels_root.get_child_count() > 0:
- push_warning("Levels already generated — toggle clear_levels first to regenerate.")
- return
-
- _mesh_library = MeshLibraryBuilder.build()
-
- var edited_root: Node = get_tree().edited_scene_root if Engine.is_editor_hint() else null
- var total_chasm_cells := 0
- for level_index in range(level_count):
- var level_seed := base_seed + level_index
- var depth := depth_start + level_index
- var gen := BrogueGen.new()
- var grid: Dictionary = gen.generate(level_seed, depth)
- gen.free()
-
- var grid_map := GridMap.new()
- grid_map.name = "Level%d" % level_index
- grid_map.mesh_library = _mesh_library
- grid_map.cell_size = Vector3(1, 1, 1)
- grid_map.position.y = -level_index * level_spacing
- levels_root.add_child(grid_map)
- if edited_root:
- grid_map.owner = edited_root
-
- var chasm_cells := _populate_level(grid_map, grid)
- total_chasm_cells += chasm_cells
-
- print("Level %d: seed=%d depth=%d rooms=%d machines=%d chasms=%d"
- % [level_index, level_seed, depth,
- (grid["rooms"] as Array).size(),
- (grid["machines"] as Array).size(),
- chasm_cells])
\ No newline at end of file
diff --git a/demo/scenes/generator.gd.uid b/demo/scenes/generator.gd.uid
deleted file mode 100644
index 1e709c3..0000000
--- a/demo/scenes/generator.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://ox0s7xjdj3lw
diff --git a/demo/scripts/arcade/arcade_scene.gd b/demo/scripts/arcade/arcade_scene.gd
deleted file mode 100644
index 26e7ef9..0000000
--- a/demo/scripts/arcade/arcade_scene.gd
+++ /dev/null
@@ -1,233 +0,0 @@
-extends Node3D
-
-# Arcade main scene. Owns:
-# - current depth
-# - map generation + rendering
-# - player spawning
-# - enemy spawning (proportional to room size)
-# - level clear detection (player steps onto stairs_down)
-# - death / restart
-# Emits signals for the HUD.
-
-signal depth_changed(depth: int, total: int)
-signal enemies_changed(remaining: int, total: int)
-signal game_over(won: bool)
-
-const T_STAIRS_DOWN := 8
-
-@export var base_seed: int = 25
-@export var start_depth: int = 1
-@export var total_depths: int = 10 # win after clearing this many
-# Enemy count per room scales with depth. At depth 1, dungeons are sparse;
-# by depth 10 they're thick.
-@export var enemy_density_base: float = 0.04
-@export var enemy_density_per_depth: float = 0.015
-@export var enemy_min_per_room: int = 0
-@export var enemy_max_per_room: int = 3
-
-@onready var world_root: Node3D = $World
-@onready var player: CharacterBody3D = $Player
-@onready var hud: Node = $HUD
-
-var _current_depth := 1
-var _mesh_library: MeshLibrary
-var _grid_map: GridMap = null
-var _grid: Dictionary
-var _enemies: Array[Node3D] = []
-var _rng := RandomNumberGenerator.new()
-var _transitioning := false
-
-func _ready() -> void:
- _rng.randomize()
- _mesh_library = MeshLibraryBuilder.build(true)
- player.died.connect(_on_player_died)
- player.hp_changed.connect(_on_player_hp)
- _current_depth = start_depth
- _build_level()
-
-func _process(_delta: float) -> void:
- if _transitioning:
- return
- _check_stairs_down()
-
-# --- level lifecycle ---
-
-func _build_level() -> void:
- # Clear old level.
- if _grid_map:
- _grid_map.queue_free()
- for e in _enemies:
- if is_instance_valid(e):
- e.queue_free()
- _enemies.clear()
-
- # Generate.
- var gen := BrogueGen.new()
- _grid = gen.generate(base_seed + _current_depth, _current_depth)
- gen.free()
-
- _grid_map = GridMap.new()
- _grid_map.mesh_library = _mesh_library
- _grid_map.cell_size = Vector3(1, 1, 1)
- world_root.add_child(_grid_map)
- _populate_grid_map(_grid_map, _grid)
-
- # Spawn player at up-stair.
- var up: Vector2i = _grid["stairs_up"] as Vector2i
- if up.x >= 0:
- player.global_position = GridUtil.grid_to_world(up, 1.1)
- player.velocity = Vector3.ZERO
- # Reset facing so each level starts neutral.
- player.rotation.y = 0.0
-
- # Spawn enemies.
- _spawn_enemies()
-
- print("Arcade: built depth %d — %d rooms, %d enemies, up=%s down=%s" % [
- _current_depth, (_grid["rooms"] as Array).size(), _enemies.size(),
- _grid["stairs_up"], _grid["stairs_down"],
- ])
- depth_changed.emit(_current_depth, total_depths)
- enemies_changed.emit(_enemies.size(), _enemies.size())
-
-func _populate_grid_map(gm: GridMap, grid: Dictionary) -> void:
- var w: int = grid["width"]
- var h: int = grid["height"]
- var terrain: PackedByteArray = grid["terrain"]
- var liquid: PackedByteArray = grid["liquid"]
- for y in range(h):
- for x in range(w):
- var idx := y * w + x
- var t: int = terrain[idx]
- var liq: int = liquid[idx]
- if t == GridUtil.T_LIQUID and liq == GridUtil.L_CHASM:
- continue
- var tile := _tile_for(t, liq)
- if tile < 0:
- continue
- gm.set_cell_item(Vector3i(x, 0, y), tile)
-
-func _tile_for(terrain: int, liquid: int) -> int:
- match terrain:
- GridUtil.T_FLOOR: return MeshLibraryBuilder.TILE_FLOOR
- GridUtil.T_CORRIDOR: return MeshLibraryBuilder.TILE_CORRIDOR
- GridUtil.T_DOOR: return MeshLibraryBuilder.TILE_DOOR
- GridUtil.T_WALL: return MeshLibraryBuilder.TILE_WALL
- GridUtil.T_BRIDGE: return MeshLibraryBuilder.TILE_BRIDGE
- GridUtil.T_STAIRS_UP: return MeshLibraryBuilder.TILE_STAIRS_UP
- GridUtil.T_STAIRS_DOWN: return MeshLibraryBuilder.TILE_STAIRS_DOWN
- GridUtil.T_LIQUID:
- match liquid:
- GridUtil.L_WATER: return MeshLibraryBuilder.TILE_WATER
- GridUtil.L_LAVA: return MeshLibraryBuilder.TILE_LAVA
- GridUtil.L_BRIMSTONE: return MeshLibraryBuilder.TILE_BRIMSTONE
- _: return MeshLibraryBuilder.TILE_WATER
- _: return -1
-
-# --- enemy spawning ---
-
-func _spawn_enemies() -> void:
- var player_grid := GridUtil.world_to_grid(player.global_position)
- var rooms: Array = _grid["rooms"]
- for room in rooms:
- var cells: Array = room["cells"]
- if cells.size() < 4:
- continue
- var density := enemy_density_base + enemy_density_per_depth * (_current_depth - 1)
- var target := int(round(cells.size() * density))
- target = clamp(target, enemy_min_per_room, enemy_max_per_room)
- # Room containing the player gets at most 1 enemy, placed far from them.
- var room_has_player := false
- for c in cells:
- if c == player_grid:
- room_has_player = true
- break
- if room_has_player:
- target = min(target, 1)
- for i in range(target):
- var cell: Vector2i = cells[_rng.randi() % cells.size()]
- if room_has_player and cell.distance_to(player_grid) < 4.0:
- continue
- _spawn_enemy_at(cell, cells)
-
-func _spawn_enemy_at(cell: Vector2i, room_cells: Array) -> void:
- var enemy := CharacterBody3D.new()
- enemy.set_script(load("res://scripts/arcade/enemy.gd"))
-
- # Visible body: a red box above the floor.
- var mesh := MeshInstance3D.new()
- var box := BoxMesh.new()
- box.size = Vector3(0.6, 1.4, 0.6)
- mesh.mesh = box
- var mat := StandardMaterial3D.new()
- mat.albedo_color = Color(0.80, 0.20, 0.20)
- box.material = mat
- mesh.position = Vector3(0, 0.7, 0)
- enemy.add_child(mesh)
-
- # Collision shape.
- var col := CollisionShape3D.new()
- var shape := CapsuleShape3D.new()
- shape.radius = 0.3
- shape.height = 1.4
- col.shape = shape
- col.position = Vector3(0, 0.7, 0)
- enemy.add_child(col)
-
- world_root.add_child(enemy)
- enemy.global_position = GridUtil.grid_to_world(cell, 0.1)
- enemy.grid_ref = _grid
- enemy.set_room(room_cells)
- enemy.player_ref = player
- enemy.died.connect(_on_enemy_died.bind(enemy))
- _enemies.append(enemy)
-
-# --- transitions ---
-
-func _check_stairs_down() -> void:
- var pg := GridUtil.world_to_grid(player.global_position)
- var w: int = _grid["width"]
- var terrain: PackedByteArray = _grid["terrain"]
- var idx := pg.y * w + pg.x
- if idx < 0 or idx >= terrain.size():
- return
- if terrain[idx] == T_STAIRS_DOWN:
- _advance_depth()
-
-func _advance_depth() -> void:
- if _transitioning:
- return
- _transitioning = true
- _current_depth += 1
- if _current_depth > start_depth + total_depths - 1:
- game_over.emit(true)
- return
- call_deferred("_build_level")
- call_deferred("_clear_transition_flag")
-
-func _clear_transition_flag() -> void:
- _transitioning = false
-
-func _on_player_died() -> void:
- game_over.emit(false)
-
-func _on_player_hp(_cur: int, _max: int) -> void:
- # HUD listens directly; no-op here.
- pass
-
-func _on_enemy_died(enemy: Node3D) -> void:
- _enemies.erase(enemy)
- var total_spawned := _enemies.size()
- # We emit remaining = enemies.size; total isn't tracked separately for
- # this MVP. HUD shows alive count.
- enemies_changed.emit(_enemies.size(), total_spawned)
-
-# Called by HUD restart button.
-func restart_from_start() -> void:
- _transitioning = true
- _current_depth = start_depth
- player.hp = player.max_hp
- player.hp_changed.emit(player.hp, player.max_hp)
- player._alive = true
- call_deferred("_build_level")
- call_deferred("_clear_transition_flag")
diff --git a/demo/scripts/arcade/arcade_scene.gd.uid b/demo/scripts/arcade/arcade_scene.gd.uid
deleted file mode 100644
index 1d274bc..0000000
--- a/demo/scripts/arcade/arcade_scene.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://3hwwo1hwe12
diff --git a/demo/scripts/arcade/enemy.gd b/demo/scripts/arcade/enemy.gd
deleted file mode 100644
index 814e0db..0000000
--- a/demo/scripts/arcade/enemy.gd
+++ /dev/null
@@ -1,174 +0,0 @@
-class_name ArcadeEnemy
-extends CharacterBody3D
-
-# Three-state FSM:
-# WANDER: pick a random passable cell in this enemy's room, walk there,
-# occasionally idle.
-# CHASE: move toward last-known player grid cell via BFS.
-# ATTACK: within melee range — swing, tick cooldown.
-#
-# Transitions:
-# WANDER -> CHASE when player has LOS AND distance ≤ sight_radius
-# CHASE -> WANDER when player has been out of sight for lose_sight_turns
-# CHASE -> ATTACK when close enough to hit
-# ATTACK -> CHASE when out of range
-
-signal died
-
-enum State { WANDER, CHASE, ATTACK }
-
-@export var hp := 8
-@export var move_speed := 3.0
-@export var sight_radius := 10.0
-@export var melee_range := 1.4
-@export var melee_damage := 2
-@export var melee_cooldown := 0.9
-@export var lose_sight_seconds := 3.0
-
-# Set by spawner on instantiate.
-var grid_ref: Dictionary = {}
-var room_cells: Array = [] # Array[Vector2i]
-var player_ref: Node3D = null
-var rng := RandomNumberGenerator.new()
-
-var _state := State.WANDER
-var _path_target: Vector2i = Vector2i(-1, -1)
-var _repath_timer := 0.0
-var _attack_timer := 0.0
-var _lose_sight_timer := 0.0
-var _alive := true
-
-func _ready() -> void:
- rng.randomize()
- _pick_wander_target()
-
-func set_room(cells: Array) -> void:
- room_cells = cells
- _pick_wander_target()
-
-func _physics_process(delta: float) -> void:
- if not _alive:
- return
- if _attack_timer > 0.0:
- _attack_timer -= delta
-
- _update_state(delta)
-
- match _state:
- State.WANDER: _do_wander(delta)
- State.CHASE: _do_chase(delta)
- State.ATTACK: _do_attack(delta)
-
- # Gravity always.
- if not is_on_floor():
- velocity.y -= 22.0 * delta
- else:
- velocity.y = max(velocity.y, 0.0)
-
- move_and_slide()
-
-# --- state machine ---
-
-func _update_state(delta: float) -> void:
- if player_ref == null:
- return
- var my_grid := GridUtil.world_to_grid(global_position)
- var p_grid := GridUtil.world_to_grid(player_ref.global_position)
- var dist := float(my_grid.distance_to(p_grid))
- var can_see := dist <= sight_radius and GridUtil.has_los(grid_ref, my_grid, p_grid)
-
- match _state:
- State.WANDER:
- if can_see:
- _state = State.CHASE
- _path_target = p_grid
- _repath_timer = 0.0
- State.CHASE:
- if can_see:
- _lose_sight_timer = 0.0
- _path_target = p_grid
- else:
- _lose_sight_timer += delta
- if _lose_sight_timer >= lose_sight_seconds:
- _state = State.WANDER
- _pick_wander_target()
- return
- # Close enough to swing?
- var world_dist := global_position.distance_to(player_ref.global_position)
- if world_dist <= melee_range:
- _state = State.ATTACK
- State.ATTACK:
- var world_dist2 := global_position.distance_to(player_ref.global_position)
- if world_dist2 > melee_range * 1.3:
- _state = State.CHASE
- if not can_see:
- _lose_sight_timer += delta
- if _lose_sight_timer >= lose_sight_seconds:
- _state = State.WANDER
- _pick_wander_target()
-
-# --- behaviors ---
-
-func _do_wander(delta: float) -> void:
- _repath_timer -= delta
- if _path_target == Vector2i(-1, -1) or _at_target():
- if _repath_timer <= 0.0:
- _repath_timer = rng.randf_range(0.8, 2.0)
- _pick_wander_target()
- _step_toward(_path_target, move_speed * 0.5)
-
-func _do_chase(delta: float) -> void:
- _repath_timer -= delta
- if _repath_timer <= 0.0:
- _repath_timer = 0.25 # repath 4×/sec
- _step_toward(_path_target, move_speed)
-
-func _do_attack(_delta: float) -> void:
- velocity.x = 0.0
- velocity.z = 0.0
- if _attack_timer <= 0.0:
- _attack_timer = melee_cooldown
- if player_ref and player_ref.has_method("take_damage"):
- player_ref.take_damage(melee_damage, self)
-
-# --- helpers ---
-
-func _pick_wander_target() -> void:
- if room_cells.is_empty():
- _path_target = GridUtil.world_to_grid(global_position)
- return
- _path_target = GridUtil.random_passable(grid_ref, room_cells, rng)
-
-func _at_target() -> bool:
- var my_grid := GridUtil.world_to_grid(global_position)
- return my_grid == _path_target
-
-func _step_toward(target: Vector2i, speed: float) -> void:
- if target == Vector2i(-1, -1):
- velocity.x = 0.0
- velocity.z = 0.0
- return
- var my_grid := GridUtil.world_to_grid(global_position)
- var nxt := GridUtil.next_step_toward(grid_ref, my_grid, target)
- if nxt == my_grid:
- velocity.x = 0.0
- velocity.z = 0.0
- return
- var target_world := GridUtil.grid_to_world(nxt, global_position.y)
- var dir := (target_world - global_position)
- dir.y = 0
- dir = dir.normalized()
- velocity.x = dir.x * speed
- velocity.z = dir.z * speed
- # Face movement direction so we look right visually.
- if dir.length() > 0.1:
- look_at(global_position + dir, Vector3.UP)
-
-func take_damage(amount: int, _source: Node) -> void:
- if not _alive:
- return
- hp -= amount
- if hp <= 0:
- _alive = false
- died.emit()
- queue_free()
diff --git a/demo/scripts/arcade/enemy.gd.uid b/demo/scripts/arcade/enemy.gd.uid
deleted file mode 100644
index fd87213..0000000
--- a/demo/scripts/arcade/enemy.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://dr7jelri306sd
diff --git a/demo/scripts/arcade/grid_util.gd b/demo/scripts/arcade/grid_util.gd
deleted file mode 100644
index e11c641..0000000
--- a/demo/scripts/arcade/grid_util.gd
+++ /dev/null
@@ -1,139 +0,0 @@
-class_name GridUtil
-extends RefCounted
-
-# Static helpers for the arcade game. Single 79×29 grid per level; the
-# generator dict is the source of truth. World↔grid mapping uses 1-unit
-# cells (matches demo_fps.gd GridMap cell_size).
-#
-# Pathfinding is a plain 4-connected BFS, tiny code, fast enough at this
-# scale (<1 ms for a full grid flood on a laptop). For an arcade game we
-# recompute per-enemy every ~200ms, not every frame.
-
-const T_FLOOR := 1
-const T_WALL := 2
-const T_DOOR := 3
-const T_CORRIDOR := 4
-const T_LIQUID := 5
-const T_BRIDGE := 6
-const T_STAIRS_UP := 7
-const T_STAIRS_DOWN := 8
-
-const L_WATER := 1
-const L_LAVA := 2
-const L_CHASM := 3
-const L_BRIMSTONE := 4
-
-const CELL_SIZE := 1.0
-
-# --- coordinate mapping ---
-
-static func grid_to_world(g: Vector2i, y: float = 0.0) -> Vector3:
- return Vector3(float(g.x), y, float(g.y))
-
-static func world_to_grid(w: Vector3) -> Vector2i:
- return Vector2i(int(floor(w.x + 0.5)), int(floor(w.z + 0.5)))
-
-# --- passability ---
-
-# A cell is passable if an enemy (or player, for pathfinding intent) can
-# step on it. Corridors, floor, doors, bridges, stairs, water count.
-# Walls, chasms, lava, brimstone do not.
-static func is_passable(grid: Dictionary, pos: Vector2i) -> bool:
- var w: int = grid["width"]
- var h: int = grid["height"]
- if pos.x < 0 or pos.y < 0 or pos.x >= w or pos.y >= h:
- return false
- var idx := pos.y * w + pos.x
- var t: int = (grid["terrain"] as PackedByteArray)[idx]
- if t == T_LIQUID:
- var liq: int = (grid["liquid"] as PackedByteArray)[idx]
- return liq == L_WATER # water walkable, lava/chasm/brimstone not
- return t == T_FLOOR or t == T_CORRIDOR or t == T_DOOR \
- or t == T_BRIDGE or t == T_STAIRS_UP or t == T_STAIRS_DOWN
-
-# --- BFS pathfinding ---
-
-# Returns the next grid step from `from` toward `to`, or from itself if no
-# path exists or we're already there. 4-connected, uniform cost.
-static func next_step_toward(grid: Dictionary, from: Vector2i, to: Vector2i) -> Vector2i:
- if from == to:
- return from
- if not is_passable(grid, from) or not is_passable(grid, to):
- return from
-
- var w: int = grid["width"]
- var h: int = grid["height"]
- var came_from := {} # Vector2i → Vector2i
- came_from[from] = from
- var queue: Array[Vector2i] = [from]
- var found := false
- var dirs := [Vector2i(1,0), Vector2i(-1,0), Vector2i(0,1), Vector2i(0,-1)]
-
- while queue.size() > 0 and not found:
- var cur: Vector2i = queue.pop_front()
- for d in dirs:
- var nxt: Vector2i = cur + d
- if came_from.has(nxt): continue
- if not is_passable(grid, nxt): continue
- came_from[nxt] = cur
- if nxt == to:
- found = true
- break
- queue.push_back(nxt)
-
- if not found:
- return from
-
- # Walk back from to → find cell whose came_from is `from`
- var cur: Vector2i = to
- while came_from[cur] != from:
- cur = came_from[cur]
- return cur
-
-# True if there's an unobstructed line from a → b for enemy LOS purposes.
-# Walks the Bresenham ray; any non-passable cell (wall / chasm / lava)
-# blocks sight. Doors are transparent for LOS.
-static func has_los(grid: Dictionary, a: Vector2i, b: Vector2i) -> bool:
- var dx := absi(b.x - a.x)
- var dy := absi(b.y - a.y)
- var sx := 1 if a.x < b.x else -1
- var sy := 1 if a.y < b.y else -1
- var err := dx - dy
- var x := a.x
- var y := a.y
- while true:
- if Vector2i(x, y) == b: return true
- # Skip blocking check at start cell.
- if not (x == a.x and y == a.y):
- if not _transparent(grid, Vector2i(x, y)):
- return false
- var e2 := 2 * err
- if e2 > -dy:
- err -= dy
- x += sx
- if e2 < dx:
- err += dx
- y += sy
- return false
-
-static func _transparent(grid: Dictionary, pos: Vector2i) -> bool:
- var w: int = grid["width"]
- var h: int = grid["height"]
- if pos.x < 0 or pos.y < 0 or pos.x >= w or pos.y >= h:
- return false
- var idx := pos.y * w + pos.x
- var t: int = (grid["terrain"] as PackedByteArray)[idx]
- # Walls block; everything else is transparent for LOS.
- return t != T_WALL and t != 0
-
-# Pick a random passable cell in the given list of cells. Returns (-1,-1)
-# if none are passable. Used by enemy wander target selection.
-static func random_passable(grid: Dictionary, cells: Array, rng: RandomNumberGenerator) -> Vector2i:
- if cells.is_empty():
- return Vector2i(-1, -1)
- var shuffled := cells.duplicate()
- shuffled.shuffle()
- for c in shuffled:
- if is_passable(grid, c):
- return c
- return Vector2i(-1, -1)
diff --git a/demo/scripts/arcade/grid_util.gd.uid b/demo/scripts/arcade/grid_util.gd.uid
deleted file mode 100644
index 2ff28a7..0000000
--- a/demo/scripts/arcade/grid_util.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://buo0sbr01qcm5
diff --git a/demo/scripts/arcade/hud.gd b/demo/scripts/arcade/hud.gd
deleted file mode 100644
index 30b72e0..0000000
--- a/demo/scripts/arcade/hud.gd
+++ /dev/null
@@ -1,57 +0,0 @@
-extends CanvasLayer
-
-# HP bar, depth label, enemy count, death / win overlay.
-
-@onready var hp_bar: ColorRect = $HPBar/Fill
-@onready var hp_label: Label = $HPBar/Label
-@onready var depth_label: Label = $InfoLabel
-@onready var overlay: Control = $EndOverlay
-@onready var overlay_title: Label = $EndOverlay/Title
-@onready var overlay_hint: Label = $EndOverlay/Hint
-
-const HP_COLOR_HIGH := Color(0.32, 0.82, 0.36)
-const HP_COLOR_LOW := Color(0.88, 0.28, 0.22)
-const HP_BAR_MAX_WIDTH := 200.0
-
-var _game_over := false
-var _scene_ref: Node = null
-
-func _ready() -> void:
- overlay.hide()
- _scene_ref = get_parent()
- _scene_ref.depth_changed.connect(_on_depth)
- _scene_ref.enemies_changed.connect(_on_enemies)
- _scene_ref.game_over.connect(_on_game_over)
- # Player is a sibling; @onready on parent isn't set yet when we fire.
- var p: Node = get_node("../Player")
- p.hp_changed.connect(_on_hp)
-
-func _unhandled_input(event: InputEvent) -> void:
- if _game_over and event is InputEventKey and event.pressed and event.keycode == KEY_R:
- _game_over = false
- overlay.hide()
- _scene_ref.restart_from_start()
-
-func _on_hp(current: int, maximum: int) -> void:
- var frac := 0.0 if maximum <= 0 else float(current) / float(maximum)
- hp_bar.size.x = HP_BAR_MAX_WIDTH * frac
- hp_bar.color = HP_COLOR_LOW.lerp(HP_COLOR_HIGH, frac)
- hp_label.text = "HP %d / %d" % [current, maximum]
-
-func _on_depth(depth: int, total: int) -> void:
- depth_label.text = "Depth %d / %d" % [depth, total]
-
-func _on_enemies(remaining: int, _total: int) -> void:
- depth_label.text = "%s Enemies: %d" % [depth_label.text.split(" ")[0], remaining]
-
-func _on_game_over(won: bool) -> void:
- _game_over = true
- overlay.show()
- if won:
- overlay_title.text = "YOU WIN"
- overlay_title.modulate = Color(0.35, 0.90, 0.45)
- else:
- overlay_title.text = "YOU DIED"
- overlay_title.modulate = Color(0.92, 0.28, 0.28)
- overlay_hint.text = "Press R to restart"
- Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
diff --git a/demo/scripts/arcade/hud.gd.uid b/demo/scripts/arcade/hud.gd.uid
deleted file mode 100644
index 54e67dc..0000000
--- a/demo/scripts/arcade/hud.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://dd7ud5jyshij5
diff --git a/demo/scripts/arcade/player_arcade.gd b/demo/scripts/arcade/player_arcade.gd
deleted file mode 100644
index 81a589c..0000000
--- a/demo/scripts/arcade/player_arcade.gd
+++ /dev/null
@@ -1,118 +0,0 @@
-class_name PlayerArcade
-extends CharacterBody3D
-
-# Arcade player: same FPS controller as the demo, plus HP and a melee
-# attack on left-click. Emits hp_changed and died signals; arcade_scene
-# listens and updates the HUD / restart logic.
-
-signal hp_changed(current: int, maximum: int)
-signal died
-signal attacked(hit_point: Vector3, hit_body: Node)
-
-@export var speed := 6.0
-@export var run_multiplier := 1.8
-@export var jump_velocity := 6.0
-@export var gravity := 22.0
-@export var mouse_sensitivity := 0.002
-@export var max_hp := 20
-@export var melee_range := 1.6
-@export var melee_damage := 6
-@export var melee_cooldown := 0.35
-
-@onready var camera: Camera3D = $Camera3D
-
-var hp := 20
-var _pitch := 0.0
-var _captured := false
-var _melee_timer := 0.0
-var _alive := true
-
-func _ready() -> void:
- hp = max_hp
- hp_changed.emit(hp, max_hp)
- _capture()
-
-func _capture() -> void:
- Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
- _captured = true
-
-func _release() -> void:
- Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
- _captured = false
-
-func _unhandled_input(event: InputEvent) -> void:
- if not _alive:
- return
- if event is InputEventMouseMotion and _captured:
- var m := event as InputEventMouseMotion
- rotation.y -= m.relative.x * mouse_sensitivity
- _pitch -= m.relative.y * mouse_sensitivity
- _pitch = clamp(_pitch, deg_to_rad(-85.0), deg_to_rad(85.0))
- camera.rotation.x = _pitch
- elif event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
- _release()
- elif event is InputEventMouseButton and event.pressed:
- if not _captured:
- _capture()
- elif event.button_index == MOUSE_BUTTON_LEFT:
- _try_attack()
-
-func _physics_process(delta: float) -> void:
- if not _alive:
- return
- if _melee_timer > 0.0:
- _melee_timer -= delta
-
- if not is_on_floor():
- velocity.y -= gravity * delta
-
- var input := Vector2.ZERO
- if Input.is_key_pressed(KEY_W): input.y -= 1.0
- if Input.is_key_pressed(KEY_S): input.y += 1.0
- if Input.is_key_pressed(KEY_A): input.x -= 1.0
- if Input.is_key_pressed(KEY_D): input.x += 1.0
- input = input.normalized()
-
- var s := speed
- if Input.is_key_pressed(KEY_SHIFT):
- s *= run_multiplier
-
- var forward := -transform.basis.z
- var right := transform.basis.x
- var horiz := (forward * -input.y + right * input.x) * s
- velocity.x = horiz.x
- velocity.z = horiz.z
-
- if is_on_floor() and Input.is_key_pressed(KEY_SPACE):
- velocity.y = jump_velocity
-
- move_and_slide()
-
-# Deal damage to any enemy within melee_range in front of the camera.
-func _try_attack() -> void:
- if _melee_timer > 0.0:
- return
- _melee_timer = melee_cooldown
-
- var origin := camera.global_position
- var dir := -camera.global_transform.basis.z
- var space := get_world_3d().direct_space_state
- var query := PhysicsRayQueryParameters3D.create(origin, origin + dir * melee_range)
- query.exclude = [self]
- var hit := space.intersect_ray(query)
- if hit.is_empty():
- return
- var body := hit.get("collider") as Node
- if body and body.has_method("take_damage"):
- body.take_damage(melee_damage, self)
- attacked.emit(hit.get("position", Vector3.ZERO), body)
-
-# Called by enemies.
-func take_damage(amount: int, _source: Node) -> void:
- if not _alive:
- return
- hp = max(0, hp - amount)
- hp_changed.emit(hp, max_hp)
- if hp <= 0:
- _alive = false
- died.emit()
diff --git a/demo/scripts/arcade/player_arcade.gd.uid b/demo/scripts/arcade/player_arcade.gd.uid
deleted file mode 100644
index 75507a2..0000000
--- a/demo/scripts/arcade/player_arcade.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://dxyvug2vl2jjq
diff --git a/demo/scripts/bake_dungeon.gd b/demo/scripts/bake_dungeon.gd
deleted file mode 100644
index 96ea634..0000000
--- a/demo/scripts/bake_dungeon.gd
+++ /dev/null
@@ -1,135 +0,0 @@
-extends SceneTree
-
-# Usage:
-# godot --headless --path demo --script scripts/bake_dungeon.gd -- SEED DEPTH OUT_DIR
-#
-# Shells out to bin/genesis --emit=json and bakes the result into two sibling
-# assets under OUT_DIR:
-# - mesh_library.tres (tile catalog, same content MLB.build()
-# produces at runtime — shareable across bakes)
-# - dungeon_seed_depth.tscn (PackedScene with a GridMap referencing
-# the MeshLibrary, populated per the
-# generated dungeon)
-#
-# Override the genesis binary path with the GENESIS_BIN env var; default is
-# "../bin/genesis" relative to the Godot project.
-
-const MESH_LIB_NAME := "mesh_library.tres"
-const MLB = preload("res://scripts/mesh_library_builder.gd")
-
-func _init() -> void:
- var args := OS.get_cmdline_user_args()
- if args.size() != 3:
- push_error("usage: -- SEED DEPTH OUT_DIR")
- quit(1)
- return
-
- var seed := int(args[0])
- var depth := int(args[1])
- var out_dir := args[2]
-
- var bin_path := OS.get_environment("GENESIS_BIN")
- if bin_path == "":
- bin_path = ProjectSettings.globalize_path("res://") + "../bin/genesis"
- bin_path = bin_path.simplify_path()
-
- if not DirAccess.dir_exists_absolute(out_dir):
- var err := DirAccess.make_dir_recursive_absolute(out_dir)
- if err != OK:
- push_error("cannot create %s (err %d)" % [out_dir, err])
- quit(1)
- return
-
- var stdout: Array = []
- var exit_code := OS.execute(bin_path, [
- "--seed", str(seed), "--depth", str(depth), "--emit=json",
- ], stdout, true)
- if exit_code != 0:
- push_error("genesis exited %d (bin=%s)" % [exit_code, bin_path])
- quit(1)
- return
-
- var grid: Dictionary = JSON.parse_string(stdout[0])
- if grid.is_empty():
- push_error("genesis produced invalid JSON")
- quit(1)
- return
-
- # Step 1 — MeshLibrary.tres.
- var lib_path := "%s/%s" % [out_dir, MESH_LIB_NAME]
- var save_err := MLB.save_resource(lib_path, true)
- if save_err != OK:
- push_error("failed to save %s (err %d)" % [lib_path, save_err])
- quit(1)
- return
-
- # Step 2 — PackedScene with populated GridMap.
- var lib: MeshLibrary = load(lib_path)
- var root := Node3D.new()
- root.name = "Dungeon_seed%d_depth%d" % [seed, depth]
- var gm := GridMap.new()
- gm.name = "GridMap"
- gm.mesh_library = lib
- gm.cell_size = Vector3(1, 1, 1)
- root.add_child(gm)
- gm.owner = root
-
- var cells_set := _populate(gm, grid)
-
- var packed := PackedScene.new()
- var pack_err := packed.pack(root)
- if pack_err != OK:
- push_error("failed to pack scene (err %d)" % pack_err)
- quit(1)
- return
-
- var scn_path := "%s/dungeon_seed%d_depth%d.tscn" % [out_dir, seed, depth]
- var scn_err := ResourceSaver.save(packed, scn_path)
- if scn_err != OK:
- push_error("failed to save %s (err %d)" % [scn_path, scn_err])
- quit(1)
- return
-
- print("wrote %s + %s — %d cells" % [scn_path, lib_path, cells_set])
- quit(0)
-
-# Decode the base64 layers and stamp tile IDs into the GridMap. Mirrors
-# arcade_scene.gd _populate_grid_map so the CLI produces the same geometry
-# as the in-engine path for the same seed/depth. Chasms stay as empty cells.
-func _populate(gm: GridMap, grid: Dictionary) -> int:
- var w := int(grid["width"])
- var h := int(grid["height"])
- var terrain := Marshalls.base64_to_raw(grid["terrain"])
- var liquid := Marshalls.base64_to_raw(grid["liquid"])
-
- var count := 0
- for y in range(h):
- for x in range(w):
- var idx := y * w + x
- var t := terrain[idx]
- var liq := liquid[idx]
- if t == 5 and liq == 3: # T_LIQUID + L_CHASM
- continue
- var tile := _tile_for(t, liq)
- if tile < 0:
- continue
- gm.set_cell_item(Vector3i(x, 0, y), tile)
- count += 1
- return count
-
-func _tile_for(terrain: int, liquid: int) -> int:
- match terrain:
- 1: return MLB.TILE_FLOOR
- 4: return MLB.TILE_CORRIDOR
- 3: return MLB.TILE_DOOR
- 2: return MLB.TILE_WALL
- 6: return MLB.TILE_BRIDGE
- 7: return MLB.TILE_STAIRS_UP
- 8: return MLB.TILE_STAIRS_DOWN
- 5:
- match liquid:
- 1: return MLB.TILE_WATER
- 2: return MLB.TILE_LAVA
- 4: return MLB.TILE_BRIMSTONE
- _: return MLB.TILE_WATER
- _: return -1
diff --git a/demo/scripts/bake_dungeon.gd.uid b/demo/scripts/bake_dungeon.gd.uid
deleted file mode 100644
index d96616a..0000000
--- a/demo/scripts/bake_dungeon.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://y0qm8301m7w6
diff --git a/demo/scripts/blobber_party.gd b/demo/scripts/blobber_party.gd
new file mode 100644
index 0000000..191c7e9
--- /dev/null
+++ b/demo/scripts/blobber_party.gd
@@ -0,0 +1,219 @@
+class_name BlobberParty
+extends Node3D
+
+# Discrete cell-step party controller (Wizardry / M&M style).
+# Reads cell3d_t bytes straight out of the dungeon Dictionary to query wall
+# flags; no collision shape needed. Stair cells trigger a level switch.
+
+signal level_changed(new_level: int)
+signal stepped(cell: Vector2i, level: int)
+signal blocked(face: int)
+
+const FACE_N := 0
+const FACE_E := 1
+const FACE_S := 2
+const FACE_W := 3
+
+# cell3d_t byte offsets (see src/blobber/cell3d.h)
+const OFF_FLOOR := 0
+const OFF_WALLS := 2 # walls[0..3]
+
+# b_floor_t values
+const FT_VOID := 0
+const FT_STONE := 1
+const FT_STAIR_UP := 5
+const FT_STAIR_DOWN := 6
+
+@export var step_duration: float = 0.18
+@export var turn_duration: float = 0.14
+@export var eye_height_ratio: float = 0.55 # fraction of cell_size above floor
+
+var cell: Vector2i = Vector2i.ZERO
+var level: int = 0
+var facing: int = FACE_N
+
+var _cells: PackedByteArray
+var _width: int = 0
+var _height: int = 0
+var _n_levels: int = 1
+var _cell_size: float = 3.0
+var _cell_stride: int = 12
+var _levels_meta: Array = []
+var _busy: bool = false
+
+func setup(dungeon: Dictionary) -> void:
+ _cells = dungeon.get("cells", PackedByteArray())
+ var dims: Vector3i = dungeon.get("dimensions", Vector3i(79, 1, 29))
+ _width = dims.x
+ _n_levels = dims.y
+ _height = dims.z
+ _cell_size = float(dungeon.get("cell_size", 3.0))
+ _cell_stride = int(dungeon.get("cell_stride", 12))
+ _levels_meta = dungeon.get("levels", [])
+
+ var entry := _entry_cell()
+ cell = entry
+ level = 0
+ facing = FACE_N
+ _snap_to_cell(true)
+
+# --- Public controls -------------------------------------------------------
+
+func try_step_forward() -> void:
+ _try_step(_facing_delta(facing))
+
+func try_step_back() -> void:
+ _try_step(-_facing_delta(facing))
+
+func try_step_left() -> void:
+ _try_step(_facing_delta((facing + 3) % 4))
+
+func try_step_right() -> void:
+ _try_step(_facing_delta((facing + 1) % 4))
+
+func try_turn_left() -> void:
+ if _busy: return
+ facing = (facing + 3) % 4
+ _tween_rotation()
+
+func try_turn_right() -> void:
+ if _busy: return
+ facing = (facing + 1) % 4
+ _tween_rotation()
+
+func try_use_stair() -> void:
+ if _busy: return
+ var f := _cell_floor(level, cell.x, cell.y)
+ if f == FT_STAIR_DOWN and level + 1 < _n_levels:
+ _change_level(level + 1)
+ elif f == FT_STAIR_UP and level > 0:
+ _change_level(level - 1)
+
+# --- Input -----------------------------------------------------------------
+
+func _unhandled_key_input(event: InputEvent) -> void:
+ if not (event is InputEventKey) or not event.pressed or event.echo:
+ return
+ var key: int = event.keycode
+ match key:
+ KEY_W, KEY_UP: try_step_forward()
+ KEY_S, KEY_DOWN: try_step_back()
+ KEY_A: try_step_left()
+ KEY_D: try_step_right()
+ KEY_Q, KEY_LEFT: try_turn_left()
+ KEY_E, KEY_RIGHT: try_turn_right()
+ KEY_SPACE, KEY_ENTER, KEY_PERIOD, KEY_GREATER, KEY_LESS: try_use_stair()
+
+# --- Movement internals ----------------------------------------------------
+
+func _try_step(delta: Vector2i) -> void:
+ if _busy: return
+ var face: int = _delta_to_face(delta)
+ if face < 0:
+ return
+ if _cell_wall(level, cell.x, cell.y, face) != 0: # W_NONE == 0
+ blocked.emit(face)
+ return
+ var nx := cell.x + delta.x
+ var ny := cell.y + delta.y
+ if nx < 0 or ny < 0 or nx >= _width or ny >= _height:
+ return
+ var dest_floor := _cell_floor(level, nx, ny)
+ if dest_floor == FT_VOID:
+ return
+ cell = Vector2i(nx, ny)
+ stepped.emit(cell, level)
+ _tween_position()
+
+func _change_level(new_level: int) -> void:
+ # Land on the paired stair on the adjacent level. Plan guarantees the XYs
+ # match, so we keep our cell coordinates.
+ level = new_level
+ level_changed.emit(level)
+ _snap_to_cell(true)
+
+# --- Cell byte reads -------------------------------------------------------
+
+func _cell_index(lvl: int, x: int, y: int) -> int:
+ return ((lvl * _height + y) * _width + x) * _cell_stride
+
+func _cell_floor(lvl: int, x: int, y: int) -> int:
+ return _cells[_cell_index(lvl, x, y) + OFF_FLOOR]
+
+func _cell_wall(lvl: int, x: int, y: int, face: int) -> int:
+ return _cells[_cell_index(lvl, x, y) + OFF_WALLS + face]
+
+# --- Geometry --------------------------------------------------------------
+
+func _facing_delta(f: int) -> Vector2i:
+ match f:
+ FACE_N: return Vector2i(0, -1)
+ FACE_E: return Vector2i(1, 0)
+ FACE_S: return Vector2i(0, 1)
+ FACE_W: return Vector2i(-1, 0)
+ return Vector2i.ZERO
+
+func _delta_to_face(d: Vector2i) -> int:
+ if d == Vector2i(0, -1): return FACE_N
+ if d == Vector2i(1, 0): return FACE_E
+ if d == Vector2i(0, 1): return FACE_S
+ if d == Vector2i(-1, 0): return FACE_W
+ return -1
+
+func _cell_world_position(lvl: int, x: int, y: int) -> Vector3:
+ var s := _cell_size
+ return Vector3(
+ (float(x) + 0.5) * s,
+ -float(lvl) * s - s * (1.0 - eye_height_ratio),
+ (float(y) + 0.5) * s)
+
+func _facing_yaw(f: int) -> float:
+ # Camera default forward = -Z. N (-y → -z) = 0. Then +x is -PI/2, etc.
+ match f:
+ FACE_N: return 0.0
+ FACE_E: return -PI * 0.5
+ FACE_S: return PI
+ FACE_W: return PI * 0.5
+ return 0.0
+
+func _snap_to_cell(reset_rotation: bool) -> void:
+ position = _cell_world_position(level, cell.x, cell.y)
+ if reset_rotation:
+ rotation.y = _facing_yaw(facing)
+
+func _tween_position() -> void:
+ var target := _cell_world_position(level, cell.x, cell.y)
+ _busy = true
+ var tw := create_tween()
+ tw.tween_property(self, "position", target, step_duration)
+ tw.finished.connect(func(): _busy = false)
+
+func _tween_rotation() -> void:
+ var target_yaw := _facing_yaw(facing)
+ # Take the shortest angular path.
+ var cur := rotation.y
+ var diff := wrapf(target_yaw - cur, -PI, PI)
+ target_yaw = cur + diff
+ _busy = true
+ var tw := create_tween()
+ tw.tween_property(self, "rotation:y", target_yaw, turn_duration)
+ tw.finished.connect(func(): _busy = false)
+
+# --- Entry point -----------------------------------------------------------
+
+func _entry_cell() -> Vector2i:
+ if _levels_meta.size() > 0:
+ var lv0: Dictionary = _levels_meta[0]
+ var su: Vector2i = lv0.get("stairs_up", Vector2i(-1, -1))
+ if su.x >= 0:
+ return su
+ var sd: Vector2i = lv0.get("stairs_down", Vector2i(-1, -1))
+ if sd.x >= 0:
+ return sd
+ # Fallback: first standable cell on level 0.
+ for y in _height:
+ for x in _width:
+ var f := _cell_floor(0, x, y)
+ if f != FT_VOID:
+ return Vector2i(x, y)
+ return Vector2i.ZERO
diff --git a/demo/scripts/blobber_party.gd.uid b/demo/scripts/blobber_party.gd.uid
new file mode 100644
index 0000000..2a7e764
--- /dev/null
+++ b/demo/scripts/blobber_party.gd.uid
@@ -0,0 +1 @@
+uid://buibf58bfnceo
diff --git a/demo/scripts/demo_3d.gd b/demo/scripts/demo_3d.gd
deleted file mode 100644
index 385918d..0000000
--- a/demo/scripts/demo_3d.gd
+++ /dev/null
@@ -1,122 +0,0 @@
-extends Node3D
-
-# Demo_3D: renders multiple stacked levels of Brogue dungeons using GridMap.
-# Chasm cells are rendered as empty (no tile), so looking down through one
-# reveals the floor of the level below.
-
-# Must match src/gen/grid.h
-const T_NOTHING := 0
-const T_FLOOR := 1
-const T_WALL := 2
-const T_DOOR := 3
-const T_CORRIDOR := 4
-const T_LIQUID := 5
-const T_BRIDGE := 6
-const T_STAIRS_UP := 7
-const T_STAIRS_DOWN := 8
-
-const L_NONE := 0
-const L_WATER := 1
-const L_LAVA := 2
-const L_CHASM := 3
-const L_BRIMSTONE := 4
-
-# Match the tile IDs exposed by MeshLibraryBuilder.
-const TILE_FLOOR := 0
-const TILE_WALL := 1
-const TILE_DOOR := 2
-const TILE_CORRIDOR := 3
-const TILE_WATER := 4
-const TILE_LAVA := 5
-const TILE_BRIMSTONE := 6
-const TILE_BRIDGE := 7
-const TILE_STAIRS_UP := 8
-const TILE_STAIRS_DOWN := 9
-
-@export var base_seed: int = 2321
-@export var depth_start: int = 20
-@export var level_count: int = 10
-@export var level_spacing: float = 6.0
-
-@onready var levels_root: Node3D = $Levels
-@onready var camera: Camera3D = $FlyCamera
-
-var _mesh_library: MeshLibrary
-
-func _ready() -> void:
- _mesh_library = MeshLibraryBuilder.build()
-
- var total_chasm_cells := 0
- for level_index in range(level_count):
- var level_seed := base_seed + level_index
- var depth := depth_start + level_index
- var gen := BrogueGen.new()
- var grid: Dictionary = gen.generate(level_seed, depth)
- gen.free()
-
- var grid_map := GridMap.new()
- grid_map.name = "Level%d" % level_index
- grid_map.mesh_library = _mesh_library
- grid_map.cell_size = Vector3(1, 1, 1)
- grid_map.position.y = -level_index * level_spacing
- levels_root.add_child(grid_map)
-
- var chasm_cells := _populate_level(grid_map, grid)
- total_chasm_cells += chasm_cells
-
- print("Level %d: seed=%d depth=%d rooms=%d machines=%d chasms=%d"
- % [level_index, level_seed, depth,
- (grid["rooms"] as Array).size(),
- (grid["machines"] as Array).size(),
- chasm_cells])
-
- # Position the camera above level 0's grid center, tilted down.
- var grid_w := 79
- var grid_h := 29
- camera.position = Vector3(grid_w * 0.5, 14, grid_h * 0.5 + 18)
- camera.rotation_degrees = Vector3(-35, 0, 0)
-
- print("Demo3D ready. %d levels, %d chasm cells total." % [level_count, total_chasm_cells])
-
-# Populate one level's GridMap from the grid dict. Returns the count of
-# chasm cells that were deliberately skipped (for reporting).
-func _populate_level(grid_map: GridMap, grid: Dictionary) -> int:
- var w: int = grid["width"]
- var h: int = grid["height"]
- var terrain: PackedByteArray = grid["terrain"]
- var liquid: PackedByteArray = grid["liquid"]
-
- var chasm_cells := 0
- for y in range(h):
- for x in range(w):
- var idx := y * w + x
- var t: int = terrain[idx]
- var liq: int = liquid[idx]
- # Chasm liquid renders as an actual see-through pit.
- if t == T_LIQUID and liq == L_CHASM:
- chasm_cells += 1
- continue
- var tile_id := _tile_for(t, liq)
- if tile_id == -1:
- continue # T_NOTHING or other empty — leave unrendered
- # GridMap coords: (X, Y, Z). We want dungeon x → X, dungeon y → Z.
- grid_map.set_cell_item(Vector3i(x, 0, y), tile_id)
- return chasm_cells
-
-func _tile_for(terrain: int, liquid: int) -> int:
- match terrain:
- T_FLOOR: return TILE_FLOOR
- T_CORRIDOR: return TILE_CORRIDOR
- T_DOOR: return TILE_DOOR
- T_WALL: return TILE_WALL
- T_BRIDGE: return TILE_BRIDGE
- T_STAIRS_UP: return TILE_STAIRS_UP
- T_STAIRS_DOWN: return TILE_STAIRS_DOWN
- T_LIQUID:
- match liquid:
- L_WATER: return TILE_WATER
- L_LAVA: return TILE_LAVA
- L_BRIMSTONE: return TILE_BRIMSTONE
- L_CHASM: return -1 # empty cell — see through to level below
- _: return TILE_WATER
- _: return -1 # T_NOTHING or unknown
diff --git a/demo/scripts/demo_3d.gd.uid b/demo/scripts/demo_3d.gd.uid
deleted file mode 100644
index f1e9e51..0000000
--- a/demo/scripts/demo_3d.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://brsi02a7ei24j
diff --git a/demo/scripts/demo_fps.gd b/demo/scripts/demo_fps.gd
deleted file mode 100644
index cca94ae..0000000
--- a/demo/scripts/demo_fps.gd
+++ /dev/null
@@ -1,143 +0,0 @@
-extends Node3D
-
-# First-person demo. Generates a grid of chunks (each a 79×29 dungeon) times
-# level_count stacked layers. Default is 1×1 chunks × 3 levels. Bump
-# chunks_x / chunks_y to stress-test the renderer with larger worlds.
-
-const T_NOTHING := 0
-const T_FLOOR := 1
-const T_WALL := 2
-const T_DOOR := 3
-const T_CORRIDOR := 4
-const T_LIQUID := 5
-const T_BRIDGE := 6
-const T_STAIRS_UP := 7
-const T_STAIRS_DOWN := 8
-
-const L_WATER := 1
-const L_LAVA := 2
-const L_CHASM := 3
-const L_BRIMSTONE := 4
-
-const TILE_FLOOR := 0
-const TILE_WALL := 1
-const TILE_DOOR := 2
-const TILE_CORRIDOR := 3
-const TILE_WATER := 4
-const TILE_LAVA := 5
-const TILE_BRIMSTONE := 6
-const TILE_BRIDGE := 7
-const TILE_STAIRS_UP := 8
-const TILE_STAIRS_DOWN := 9
-
-const CHUNK_W := 79
-const CHUNK_H := 29
-
-@export var base_seed: int = 2028
-@export var depth_start: int = 20
-@export var level_count: int = 3
-@export var level_spacing: float = 6.0
-@export var chunks_x: int = 1
-@export var chunks_y: int = 1
-@export var player_spawn_height: float = 1.1
-
-@onready var levels_root: Node3D = $Levels
-@onready var player: CharacterBody3D = $Player
-@onready var fps_overlay: Node = $FPSOverlay if has_node("FPSOverlay") else null
-
-var _mesh_library: MeshLibrary
-
-func _ready() -> void:
- var t0 := Time.get_ticks_msec()
- _mesh_library = MeshLibraryBuilder.build(true)
-
- var total_cells := 0
- var total_chunks := 0
- var spawn_world := Vector3.ZERO
- var spawn_found := false
-
- for level_index in range(level_count):
- for cy in range(chunks_y):
- for cx in range(chunks_x):
- var seed := base_seed \
- + level_index * 1000 \
- + cy * chunks_x + cx
- var depth := depth_start + level_index
- var gen := BrogueGen.new()
- var grid: Dictionary = gen.generate(seed, depth)
- gen.free()
-
- var grid_map := GridMap.new()
- grid_map.name = "L%d_C%d_%d" % [level_index, cx, cy]
- grid_map.mesh_library = _mesh_library
- grid_map.cell_size = Vector3(1, 1, 1)
- grid_map.position = Vector3(
- cx * CHUNK_W,
- -level_index * level_spacing,
- cy * CHUNK_H
- )
- levels_root.add_child(grid_map)
-
- var cells := _populate_level(grid_map, grid)
- total_cells += cells
- total_chunks += 1
-
- # First up-stair on level 0, chunk (0,0) is the spawn point.
- if not spawn_found and level_index == 0 and cx == 0 and cy == 0:
- var up: Vector2i = grid["stairs_up"] as Vector2i
- if up.x >= 0:
- spawn_world = Vector3(up.x, player_spawn_height, up.y)
- spawn_found = true
-
- if not spawn_found:
- spawn_world = Vector3(CHUNK_W * 0.5, player_spawn_height, CHUNK_H * 0.5)
- player.position = spawn_world
-
- var build_ms := Time.get_ticks_msec() - t0
- var info := "%d chunks (%dx%d x %d lvls) %d placed cells build=%d ms" % [
- total_chunks, chunks_x, chunks_y, level_count, total_cells, build_ms,
- ]
- print(info)
- print("Player spawned at %s" % spawn_world)
- if fps_overlay and fps_overlay.has_method("set_subtitle"):
- fps_overlay.set_subtitle(info)
-
-# Place every non-chasm cell into the GridMap. Returns the number of cells
-# actually placed (excludes T_NOTHING and chasms).
-func _populate_level(grid_map: GridMap, grid: Dictionary) -> int:
- var w: int = grid["width"]
- var h: int = grid["height"]
- var terrain: PackedByteArray = grid["terrain"]
- var liquid: PackedByteArray = grid["liquid"]
-
- var placed := 0
- for y in range(h):
- for x in range(w):
- var idx := y * w + x
- var t: int = terrain[idx]
- var liq: int = liquid[idx]
- if t == T_LIQUID and liq == L_CHASM:
- continue
- var tile_id := _tile_for(t, liq)
- if tile_id == -1:
- continue
- grid_map.set_cell_item(Vector3i(x, 0, y), tile_id)
- placed += 1
- return placed
-
-func _tile_for(terrain: int, liquid: int) -> int:
- match terrain:
- T_FLOOR: return TILE_FLOOR
- T_CORRIDOR: return TILE_CORRIDOR
- T_DOOR: return TILE_DOOR
- T_WALL: return TILE_WALL
- T_BRIDGE: return TILE_BRIDGE
- T_STAIRS_UP: return TILE_STAIRS_UP
- T_STAIRS_DOWN: return TILE_STAIRS_DOWN
- T_LIQUID:
- match liquid:
- L_WATER: return TILE_WATER
- L_LAVA: return TILE_LAVA
- L_BRIMSTONE: return TILE_BRIMSTONE
- _: return TILE_WATER
- _: return -1
diff --git a/demo/scripts/demo_fps.gd.uid b/demo/scripts/demo_fps.gd.uid
deleted file mode 100644
index fa37eab..0000000
--- a/demo/scripts/demo_fps.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://dvoatkhtgji2u
diff --git a/demo/scripts/dungeon_builder.gd b/demo/scripts/dungeon_builder.gd
new file mode 100644
index 0000000..b93c159
--- /dev/null
+++ b/demo/scripts/dungeon_builder.gd
@@ -0,0 +1,107 @@
+extends Node3D
+
+# Blobber dungeon builder. Calls BrogueGen.generate_dungeon() and assembles
+# per-material MeshInstance3D children from the returned mesh surface arrays.
+# Material ids mirror src/mesh/material_ids.h.
+#
+# If `party_path` is set, the builder also feeds the generated Dictionary to
+# the party controller so it lands on the entry stair with fresh cell data.
+
+const BlobberPartyScript := preload("res://scripts/blobber_party.gd")
+
+const MAT_STONE_FLOOR := 0
+const MAT_STONE_CEILING := 1
+const MAT_STONE_WALL := 2
+const MAT_DOOR_FLOOR := 3
+const MAT_STAIR_UP := 4
+const MAT_STAIR_DOWN := 5
+const MAT_WATER := 6
+const MAT_LAVA := 7
+const MAT_BRIDGE := 8
+const MAT_CAVE_FLOOR := 9
+const MAT_CAVE_CEILING := 10
+const MAT_CAVE_WALL := 11
+
+@export var seed_value: int = 42
+@export var num_levels: int = 1
+@export var depth: int = 1
+@export var regenerate_on_ready: bool = true
+@export var party_path: NodePath
+
+var _materials: Dictionary = {}
+var _mesh_parent: Node3D
+
+func _ready() -> void:
+ _materials = _build_materials()
+ if regenerate_on_ready:
+ regenerate()
+
+func regenerate() -> void:
+ if _mesh_parent and is_instance_valid(_mesh_parent):
+ _mesh_parent.queue_free()
+ _mesh_parent = Node3D.new()
+ _mesh_parent.name = "Meshes"
+ add_child(_mesh_parent)
+
+ var gen := BrogueGen.new()
+ var dungeon: Dictionary = gen.generate_dungeon(seed_value, num_levels, depth)
+ gen.free()
+
+ if dungeon.is_empty():
+ push_error("generate_dungeon returned empty dictionary")
+ return
+
+ var meshes: Array = dungeon.get("meshes", [])
+ for entry_v in meshes:
+ var entry: Dictionary = entry_v
+ var material_id: int = entry.get("material", -1)
+ var arrays: Array = entry.get("arrays", [])
+ if material_id < 0 or arrays.is_empty():
+ continue
+
+ var mesh := ArrayMesh.new()
+ mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
+ mesh.surface_set_material(0, _materials.get(material_id, _fallback_material()))
+
+ var mi := MeshInstance3D.new()
+ mi.mesh = mesh
+ mi.name = "Surface_%d" % material_id
+ _mesh_parent.add_child(mi)
+
+ var dims: Vector3i = dungeon.get("dimensions", Vector3i(79, 1, 29))
+ var levels: Array = dungeon.get("levels", [])
+ print("Dungeon built: seed=%d dims=%s surfaces=%d levels=%d" %
+ [seed_value, dims, meshes.size(), levels.size()])
+
+ if party_path != NodePath(""):
+ var party := get_node_or_null(party_path)
+ if party and party.has_method("setup"):
+ party.setup(dungeon)
+
+# Distinct albedo colors per material for instant visual debugging.
+# Later PRs replace these with proper StandardMaterial3D + textures.
+func _build_materials() -> Dictionary:
+ var m := {}
+ m[MAT_STONE_FLOOR] = _flat(Color(0.45, 0.42, 0.38))
+ m[MAT_STONE_CEILING] = _flat(Color(0.30, 0.28, 0.26))
+ m[MAT_STONE_WALL] = _flat(Color(0.55, 0.50, 0.45))
+ m[MAT_DOOR_FLOOR] = _flat(Color(0.62, 0.38, 0.18))
+ m[MAT_STAIR_UP] = _flat(Color(0.20, 0.75, 0.40))
+ m[MAT_STAIR_DOWN] = _flat(Color(0.85, 0.25, 0.20))
+ m[MAT_WATER] = _flat(Color(0.15, 0.40, 0.75))
+ m[MAT_LAVA] = _flat(Color(0.95, 0.40, 0.10))
+ m[MAT_BRIDGE] = _flat(Color(0.50, 0.35, 0.20))
+ m[MAT_CAVE_FLOOR] = _flat(Color(0.35, 0.32, 0.28))
+ m[MAT_CAVE_CEILING] = _flat(Color(0.22, 0.20, 0.18))
+ m[MAT_CAVE_WALL] = _flat(Color(0.42, 0.38, 0.34))
+ return m
+
+func _flat(c: Color) -> StandardMaterial3D:
+ var sm := StandardMaterial3D.new()
+ sm.albedo_color = c
+ sm.roughness = 0.85
+ sm.metallic = 0.0
+ return sm
+
+func _fallback_material() -> StandardMaterial3D:
+ return _flat(Color.MAGENTA)
diff --git a/demo/scripts/dungeon_builder.gd.uid b/demo/scripts/dungeon_builder.gd.uid
new file mode 100644
index 0000000..a55a014
--- /dev/null
+++ b/demo/scripts/dungeon_builder.gd.uid
@@ -0,0 +1 @@
+uid://b2r6hnyvt7ef7
diff --git a/demo/scripts/export_map.gd b/demo/scripts/export_map.gd
index fc29d1f..f4e1190 100644
--- a/demo/scripts/export_map.gd
+++ b/demo/scripts/export_map.gd
@@ -1,10 +1,16 @@
extends SceneTree
-# Usage:
+# Usage (preferred):
+# godot --headless --path demo --script scripts/export_map.gd -- \
+# --generator brogue|blobber --seed N --depth N [--levels N] --out PATH.map
+#
+# Legacy positional form (brogue only):
# godot --headless --path demo --script scripts/export_map.gd -- SEED DEPTH [LEVELS] OUT.map
#
-# Generates N dungeons (seeds SEED..SEED+N-1, depths DEPTH..DEPTH+N-1),
-# stacks them vertically, and writes one Standard Quake .map file.
+# Generates N dungeons (seeds SEED..SEED+N-1, depths DEPTH..DEPTH+N-1 for
+# brogue; single seed multi-level for blobber), stacks them vertically, and
+# writes one Standard Quake .map file with typed FuncGodot entities for
+# water, lava, stairs, doors, and a player spawn.
#
# Chasms on non-bottom levels are real holes — no floor brush, and the
# level below has its ceiling drilled out at the same (x,y) so the shaft
@@ -19,6 +25,7 @@ const TEXTURE := "__TB_empty"
const FLOOR_TOP := 0
const WATER_TOP := -32
+const LAVA_TOP := -32
const CHASM_TOP := -128
const PIT_BOTTOM := CHASM_TOP - WALL_THICKNESS # = -192
const WALL_TOP := HEIGHT + WALL_THICKNESS # = 192
@@ -41,42 +48,32 @@ const T_STAIRS_DOWN := 8
# liquid_t
const L_WATER := 1
+const L_LAVA := 2
const L_CHASM := 3
-enum Kind { EMPTY, WALL, FLOOR, WATER, CHASM }
+enum Kind { EMPTY, WALL, FLOOR, WATER, LAVA, CHASM }
func _init() -> void:
- var args := OS.get_cmdline_user_args()
- var seed: int
- var depth: int
- var levels: int
- var out_path: String
- match args.size():
- 3:
- seed = int(args[0])
- depth = int(args[1])
- levels = 1
- out_path = args[2]
- 4:
- seed = int(args[0])
- depth = int(args[1])
- levels = int(args[2])
- out_path = args[3]
- _:
- push_error("usage: -- SEED DEPTH [LEVELS] OUT.map")
- quit(1)
- return
+ var parsed := _parse_args(OS.get_cmdline_user_args())
+ if parsed.is_empty():
+ quit(1)
+ return
+ var generator: String = parsed["generator"]
+ var seed_value: int = parsed["seed"]
+ var depth: int = parsed["depth"]
+ var levels: int = parsed["levels"]
+ var out_path: String = parsed["out"]
if levels < 1:
push_error("levels must be >= 1")
quit(1)
return
- var grids: Array = []
- for k in range(levels):
- var gen := BrogueGen.new()
- grids.append(gen.generate(seed + k, depth + k))
- gen.free()
+ var grids: Array = _generate_grids(generator, seed_value, depth, levels)
+ if grids.is_empty():
+ push_error("generator '%s' returned no grids" % generator)
+ quit(1)
+ return
var f := FileAccess.open(out_path, FileAccess.WRITE)
if f == null:
@@ -84,25 +81,126 @@ func _init() -> void:
quit(1)
return
- var brush_count := _write_map(f, grids, seed, depth)
+ var stats := _write_map(f, grids, seed_value, depth, generator)
f.close()
- print("wrote %s — %d levels, %d brushes" % [out_path, levels, brush_count])
+ print("wrote %s — generator=%s levels=%d brushes=%d entities=%d" % [
+ out_path, generator, levels, stats["brushes"], stats["entities"],
+ ])
quit(0)
-func _write_map(f: FileAccess, grids: Array, seed: int, depth: int) -> int:
+func _parse_args(args: PackedStringArray) -> Dictionary:
+ # Accept either --flag form or legacy positional form.
+ if args.size() >= 1 and args[0].begins_with("--"):
+ return _parse_flag_args(args)
+ return _parse_positional_args(args)
+
+func _parse_flag_args(args: PackedStringArray) -> Dictionary:
+ var out := {
+ "generator": "brogue",
+ "seed": 0,
+ "depth": 1,
+ "levels": 1,
+ "out": "",
+ }
+ var seen_seed := false
+ var seen_depth := false
+ var seen_out := false
+ var i := 0
+ while i < args.size():
+ var a: String = args[i]
+ match a:
+ "--generator":
+ i += 1
+ out["generator"] = args[i] if i < args.size() else ""
+ "--seed":
+ i += 1
+ if i < args.size():
+ out["seed"] = int(args[i]); seen_seed = true
+ "--depth":
+ i += 1
+ if i < args.size():
+ out["depth"] = int(args[i]); seen_depth = true
+ "--levels":
+ i += 1
+ if i < args.size():
+ out["levels"] = int(args[i])
+ "--out":
+ i += 1
+ if i < args.size():
+ out["out"] = args[i]; seen_out = true
+ _:
+ push_error("unknown arg: %s" % a)
+ return {}
+ i += 1
+ if not seen_seed or not seen_depth or not seen_out:
+ push_error("usage: --generator brogue|blobber --seed N --depth N [--levels N] --out PATH")
+ return {}
+ if out["generator"] != "brogue" and out["generator"] != "blobber":
+ push_error("--generator must be 'brogue' or 'blobber'")
+ return {}
+ return out
+
+func _parse_positional_args(args: PackedStringArray) -> Dictionary:
+ match args.size():
+ 3:
+ return {
+ "generator": "brogue",
+ "seed": int(args[0]),
+ "depth": int(args[1]),
+ "levels": 1,
+ "out": args[2],
+ }
+ 4:
+ return {
+ "generator": "brogue",
+ "seed": int(args[0]),
+ "depth": int(args[1]),
+ "levels": int(args[2]),
+ "out": args[3],
+ }
+ _:
+ push_error("usage: --generator brogue|blobber --seed N --depth N [--levels N] --out PATH (or: SEED DEPTH [LEVELS] OUT.map)")
+ return {}
+
+func _generate_grids(generator: String, seed_value: int, depth: int, levels: int) -> Array:
+ match generator:
+ "brogue":
+ var out: Array = []
+ for k in range(levels):
+ var gen := BrogueGen.new()
+ out.append(gen.generate(seed_value + k, depth + k))
+ gen.free()
+ return out
+ "blobber":
+ var gen := BrogueGen.new()
+ var slices_v: Variant = gen.generate_2d_slices(seed_value, levels, depth)
+ gen.free()
+ if slices_v == null:
+ return []
+ var slices: Array = slices_v
+ return slices
+ return []
+
+func _write_map(f: FileAccess, grids: Array, seed_value: int, depth: int, generator: String) -> Dictionary:
f.store_string("// Game: FuncGodot\n")
f.store_string("// Format: Valve\n")
- f.store_string("// Generated by brogue-genesis — seed: %d, depth: %d, levels: %d\n" % [
- seed, depth, grids.size(),
+ f.store_string("// Generated by brogue-genesis — generator: %s, seed: %d, depth: %d, levels: %d\n" % [
+ generator, seed_value, depth, grids.size(),
])
+
+ var stats := {"brushes": 0, "entities": 0}
+ _emit_worldspawn(f, grids, stats)
+ _emit_liquid_entities(f, grids, stats)
+ _emit_point_entities(f, grids, stats)
+ return stats
+
+# --- Worldspawn: static geometry only (floors/walls/ceilings/bridges/stairs floors). ---
+func _emit_worldspawn(f: FileAccess, grids: Array, stats: Dictionary) -> void:
f.store_string("{\n")
f.store_string("\"classname\" \"worldspawn\"\n")
- var count := 0
var w: int = grids[0]["width"]
var h: int = grids[0]["height"]
- # Per-(x,y) flag: "some level above has a chasm here, so our ceiling
- # is drilled out so the shaft stays open". Accumulates top-down.
var chasm_above := PackedByteArray()
chasm_above.resize(w * h)
@@ -110,14 +208,12 @@ func _write_map(f: FileAccess, grids: Array, seed: int, depth: int) -> int:
var grid: Dictionary = grids[k]
var is_bottom := (k == grids.size() - 1)
var z_offset := -k * LEVEL_SPACING
- count += _write_level(f, grid, z_offset, chasm_above, is_bottom)
- # Update chasm_above for the next level using THIS level's chasms.
+ stats["brushes"] += _write_level_worldspawn(f, grid, z_offset, chasm_above, is_bottom)
_propagate_chasms(grid, chasm_above)
f.store_string("}\n")
- return count
-func _write_level(f: FileAccess, grid: Dictionary, z_off: int,
+func _write_level_worldspawn(f: FileAccess, grid: Dictionary, z_off: int,
chasm_above: PackedByteArray, is_bottom: bool) -> int:
var w: int = grid["width"]
var h: int = grid["height"]
@@ -127,7 +223,8 @@ func _write_level(f: FileAccess, grid: Dictionary, z_off: int,
var count := 0
var ts := TILE_SIZE
- # Pass 1 — floors / walls, row-merged by kind.
+ # Pass 1 — floors / walls / chasm pit-floors, row-merged.
+ # Water & lava are skipped here; they are emitted as entities.
for gy in range(h):
var run_start := 0
var run_kind := _kind(terrain, liquid, w, 0, gy)
@@ -148,9 +245,6 @@ func _write_level(f: FileAccess, grid: Dictionary, z_off: int,
Kind.FLOOR:
count += _emit_box(f, x0, y0, z_off + PIT_BOTTOM,
x1, y1, z_off + FLOOR_TOP)
- Kind.WATER:
- count += _emit_box(f, x0, y0, z_off + PIT_BOTTOM,
- x1, y1, z_off + WATER_TOP)
Kind.CHASM:
# Non-bottom chasms are real holes — no floor. Bottom
# chasms get a local pit floor so the player doesn't
@@ -187,6 +281,124 @@ func _write_level(f: FileAccess, grid: Dictionary, z_off: int,
return count
+# --- Liquid entities: one func_water / func_lava per row-run, per level. ---
+func _emit_liquid_entities(f: FileAccess, grids: Array, stats: Dictionary) -> void:
+ for k in range(grids.size()):
+ var grid: Dictionary = grids[k]
+ var z_off := -k * LEVEL_SPACING
+ var counts := _write_level_liquid_entities(f, grid, z_off)
+ stats["brushes"] += counts["brushes"]
+ stats["entities"] += counts["entities"]
+
+func _write_level_liquid_entities(f: FileAccess, grid: Dictionary, z_off: int) -> Dictionary:
+ var w: int = grid["width"]
+ var h: int = grid["height"]
+ var terrain: PackedByteArray = grid["terrain"]
+ var liquid: PackedByteArray = grid["liquid"]
+
+ var brushes := 0
+ var entities := 0
+ var ts := TILE_SIZE
+
+ for gy in range(h):
+ var run_start := 0
+ var run_kind := _kind(terrain, liquid, w, 0, gy)
+ for gx in range(1, w + 1):
+ var cur: int = Kind.EMPTY
+ if gx < w:
+ cur = _kind(terrain, liquid, w, gx, gy)
+ if cur == run_kind and gx < w:
+ continue
+ var x0 := run_start * ts
+ var x1 := gx * ts
+ var y0 := gy * ts
+ var y1 := (gy + 1) * ts
+ match run_kind:
+ Kind.WATER:
+ brushes += _emit_solid_entity_box(f, "func_water",
+ x0, y0, z_off + PIT_BOTTOM, x1, y1, z_off + WATER_TOP)
+ entities += 1
+ Kind.LAVA:
+ brushes += _emit_solid_entity_box(f, "func_lava",
+ x0, y0, z_off + PIT_BOTTOM, x1, y1, z_off + LAVA_TOP)
+ entities += 1
+ _:
+ pass
+ run_start = gx
+ run_kind = cur
+
+ return {"brushes": brushes, "entities": entities}
+
+# --- Point entities: stairs, doors, and a single player spawn. ---
+func _emit_point_entities(f: FileAccess, grids: Array, stats: Dictionary) -> void:
+ var player_start_origin: Vector3i = Vector3i(-1, -1, -1)
+
+ for k in range(grids.size()):
+ var grid: Dictionary = grids[k]
+ var z_off := -k * LEVEL_SPACING
+ var w: int = grid["width"]
+ var h: int = grid["height"]
+ var terrain: PackedByteArray = grid["terrain"]
+ var ts := TILE_SIZE
+ for gy in range(h):
+ for gx in range(w):
+ var idx := gy * w + gx
+ var t: int = terrain[idx]
+ var ox := gx * ts + ts / 2
+ var oy := gy * ts + ts / 2
+ var oz := z_off + FLOOR_TOP
+ match t:
+ T_STAIRS_UP:
+ _emit_point_entity(f, "point_stair_up", Vector3i(ox, oy, oz))
+ stats["entities"] += 1
+ if k == 0 and player_start_origin.x < 0:
+ player_start_origin = Vector3i(ox, oy, oz + 32)
+ T_STAIRS_DOWN:
+ _emit_point_entity(f, "point_stair_down", Vector3i(ox, oy, oz))
+ stats["entities"] += 1
+ T_DOOR:
+ var angle := _door_angle(terrain, w, h, gx, gy)
+ _emit_point_entity(f, "point_door", Vector3i(ox, oy, oz),
+ {"angle": str(angle)})
+ stats["entities"] += 1
+ _:
+ pass
+
+ # Fallback: no stairs-up on level 0 — pick first floor cell.
+ if player_start_origin.x < 0 and grids.size() > 0:
+ var grid: Dictionary = grids[0]
+ var w: int = grid["width"]
+ var h: int = grid["height"]
+ var terrain: PackedByteArray = grid["terrain"]
+ var ts := TILE_SIZE
+ for gy in range(h):
+ for gx in range(w):
+ var idx := gy * w + gx
+ var t: int = terrain[idx]
+ if t == T_FLOOR or t == T_CORRIDOR or t == T_BRIDGE:
+ player_start_origin = Vector3i(
+ gx * ts + ts / 2, gy * ts + ts / 2, FLOOR_TOP + 32)
+ break
+ if player_start_origin.x >= 0:
+ break
+
+ if player_start_origin.x >= 0:
+ _emit_point_entity(f, "point_player_start", player_start_origin)
+ stats["entities"] += 1
+
+# Angle (0/90/180/270, Quake convention: 0=east) derived from which
+# adjacent cell is open — door faces into the open corridor/room.
+func _door_angle(terrain: PackedByteArray, w: int, _h: int, x: int, y: int) -> int:
+ var east_open := x + 1 < w and _is_passable(terrain[y * w + (x + 1)])
+ var west_open := x - 1 >= 0 and _is_passable(terrain[y * w + (x - 1)])
+ if east_open or west_open:
+ return 0 # door axis along E/W; value doesn't matter much for now
+ return 90 # default to N/S axis
+
+func _is_passable(t: int) -> bool:
+ return t == T_FLOOR or t == T_CORRIDOR or t == T_BRIDGE \
+ or t == T_DOOR or t == T_STAIRS_UP or t == T_STAIRS_DOWN
+
# Record any chasm cells at this level into the running mask so the NEXT
# level knows to drill its ceiling there.
func _propagate_chasms(grid: Dictionary, chasm_above: PackedByteArray) -> void:
@@ -200,7 +412,8 @@ func _propagate_chasms(grid: Dictionary, chasm_above: PackedByteArray) -> void:
if terrain[idx] == T_LIQUID and liquid[idx] == L_CHASM:
chasm_above[idx] = 1
-# Draw kind for floor/wall emission.
+# Draw kind for floor/wall emission. Water and lava are distinct so the
+# main switch can route them to separate entity emitters.
func _kind(terrain: PackedByteArray, liquid: PackedByteArray,
w: int, x: int, y: int) -> int:
var idx := y * w + x
@@ -212,7 +425,8 @@ func _kind(terrain: PackedByteArray, liquid: PackedByteArray,
var liq: int = liquid[idx]
if liq == L_CHASM: return Kind.CHASM
if liq == L_WATER: return Kind.WATER
- return Kind.FLOOR # lava / brimstone sit at floor level
+ if liq == L_LAVA: return Kind.LAVA
+ return Kind.FLOOR
_: return Kind.FLOOR
# Ceiling emitted when the cell is floor-like AND no chasm sits above it.
@@ -234,25 +448,38 @@ func _emit_box(f: FileAccess, x0: int, y0: int, z0: int,
return 0
f.store_string("{\n")
# Valve 220 texture axes per face normal (Quake convention).
- # X-facing walls: U=Y, V=-Z. Y-facing walls: U=X, V=-Z. Z-facing: U=X, V=-Y.
var ax_x := "[ 0 1 0 0 ] [ 0 0 -1 0 ]"
var ax_y := "[ 1 0 0 0 ] [ 0 0 -1 0 ]"
var ax_z := "[ 1 0 0 0 ] [ 0 -1 0 0 ]"
- # -X
_face(f, x0, y0, z0, x0, y1, z0, x0, y0, z1, ax_x)
- # +X
_face(f, x1, y0, z0, x1, y0, z1, x1, y1, z0, ax_x)
- # -Y
_face(f, x0, y0, z1, x1, y0, z1, x0, y0, z0, ax_y)
- # +Y
_face(f, x0, y1, z0, x1, y1, z0, x0, y1, z1, ax_y)
- # -Z
_face(f, x0, y0, z0, x1, y0, z0, x0, y1, z0, ax_z)
- # +Z
_face(f, x0, y0, z1, x0, y1, z1, x1, y0, z1, ax_z)
f.store_string("}\n")
return 1
+# Solid entity wrapping a single axis-aligned box (one brush per entity).
+func _emit_solid_entity_box(f: FileAccess, classname: String,
+ x0: int, y0: int, z0: int, x1: int, y1: int, z1: int) -> int:
+ if x0 >= x1 or y0 >= y1 or z0 >= z1:
+ return 0
+ f.store_string("{\n")
+ f.store_string("\"classname\" \"%s\"\n" % classname)
+ var emitted := _emit_box(f, x0, y0, z0, x1, y1, z1)
+ f.store_string("}\n")
+ return emitted
+
+func _emit_point_entity(f: FileAccess, classname: String, origin: Vector3i,
+ extra: Dictionary = {}) -> void:
+ f.store_string("{\n")
+ f.store_string("\"classname\" \"%s\"\n" % classname)
+ f.store_string("\"origin\" \"%d %d %d\"\n" % [origin.x, origin.y, origin.z])
+ for key in extra.keys():
+ f.store_string("\"%s\" \"%s\"\n" % [key, extra[key]])
+ f.store_string("}\n")
+
func _face(f: FileAccess, x1: int, y1: int, z1: int,
x2: int, y2: int, z2: int,
x3: int, y3: int, z3: int, axes: String) -> void:
diff --git a/demo/scripts/export_tb_config.gd b/demo/scripts/export_tb_config.gd
new file mode 100644
index 0000000..8f94056
--- /dev/null
+++ b/demo/scripts/export_tb_config.gd
@@ -0,0 +1,97 @@
+extends SceneTree
+
+# Usage:
+# godot --headless --path demo --script scripts/export_tb_config.gd -- OUT_DIR
+#
+# Writes a complete TrenchBroom game config to OUT_DIR:
+# OUT_DIR/GameConfig.cfg — game definition (name, formats, texture dirs)
+# OUT_DIR/FuncGodot.fgd — entity definitions, generated from
+# res://data/fgd/brogue_fgd.tres via
+# FuncGodotFGDFile.build_class_text().
+#
+# Skips the Inspector's "Export FGD" button entirely. Safe to re-run after
+# any change to brogue_fgd.tres or entity resources under data/entities/.
+
+const GAME_CONFIG := """{
+\t\"version\": 9,
+\t\"name\": \"brogue-genesis\",
+\t\"icon\": \"icon.png\",
+\t\"fileformats\": [
+\t\t{ \"format\": \"Valve\", \"initialmap\": \"initial_valve.map\" },
+\t\t{ \"format\": \"Standard\", \"initialmap\": \"initial_standard.map\" }
+\t],
+\t\"filesystem\": {
+\t\t\"searchpath\": \".\",
+\t\t\"packageformat\": { \"extension\": \".zip\", \"format\": \"zip\" }
+\t},
+\t\"materials\": {
+\t\t\"root\": \"textures\",
+\t\t\"extensions\": [\".bmp\", \".jpeg\", \".jpg\", \".png\", \".tga\", \".webp\"],
+\t\t\"excludes\": [ \"*_albedo\", \"*_ao\", \"*_emission\", \"*_height\", \"*_metallic\", \"*_normal\", \"*_orm\", \"*_roughness\", \"*_sss\" ],
+\t\t\"palette\": \"textures/palette.lmp\",
+\t\t\"attribute\": \"wad\"
+\t},
+\t\"entities\": {
+\t\t\"definitions\": [ \"FuncGodot.fgd\" ],
+\t\t\"defaultcolor\": \"0.6 0.6 0.6 1.0\",
+\t\t\"scale\": 32
+\t},
+\t\"tags\": {
+\t\t\"brush\": [],
+\t\t\"brushface\": [
+\t\t\t{ \"name\": \"Clip\", \"attribs\": [ \"transparent\" ], \"match\": \"material\", \"pattern\": \"clip\" },
+\t\t\t{ \"name\": \"Skip\", \"attribs\": [ \"transparent\" ], \"match\": \"material\", \"pattern\": \"skip\" },
+\t\t\t{ \"name\": \"Origin\", \"attribs\": [ \"transparent\" ], \"match\": \"material\", \"pattern\": \"origin\" }
+\t\t]
+\t},
+\t\"faceattribs\": {
+\t\t\"defaults\": { \"scale\": [1.0, 1.0] },
+\t\t\"contentflags\": [],
+\t\t\"surfaceflags\": []
+\t}
+}
+"""
+
+func _init() -> void:
+ var args := OS.get_cmdline_user_args()
+ if args.size() != 1:
+ push_error("usage: -- OUT_DIR")
+ quit(1)
+ return
+ var out_dir: String = args[0]
+
+ if not DirAccess.dir_exists_absolute(out_dir):
+ if DirAccess.make_dir_recursive_absolute(out_dir) != OK:
+ push_error("failed to create %s" % out_dir)
+ quit(1)
+ return
+
+ var cfg_path := out_dir.path_join("GameConfig.cfg")
+ var cfg_file := FileAccess.open(cfg_path, FileAccess.WRITE)
+ if cfg_file == null:
+ push_error("cannot open %s for write" % cfg_path)
+ quit(1)
+ return
+ cfg_file.store_string(GAME_CONFIG)
+ cfg_file.close()
+
+ var fgd: Resource = load("res://data/fgd/brogue_fgd.tres")
+ if fgd == null:
+ push_error("failed to load res://data/fgd/brogue_fgd.tres")
+ quit(1)
+ return
+
+ var fgd_text: String = fgd.build_class_text(1) # 1 == TRENCHBROOM
+ var fgd_path := out_dir.path_join("FuncGodot.fgd")
+ var fgd_file := FileAccess.open(fgd_path, FileAccess.WRITE)
+ if fgd_file == null:
+ push_error("cannot open %s for write" % fgd_path)
+ quit(1)
+ return
+ fgd_file.store_string(fgd_text)
+ fgd_file.close()
+
+ print("synced TrenchBroom config to %s" % out_dir)
+ print(" GameConfig.cfg %d bytes" % GAME_CONFIG.length())
+ print(" FuncGodot.fgd %d bytes" % fgd_text.length())
+ quit(0)
diff --git a/demo/scripts/export_tb_config.gd.uid b/demo/scripts/export_tb_config.gd.uid
new file mode 100644
index 0000000..7d64187
--- /dev/null
+++ b/demo/scripts/export_tb_config.gd.uid
@@ -0,0 +1 @@
+uid://daojwolkldxxo
diff --git a/demo/scripts/fps_overlay.gd b/demo/scripts/fps_overlay.gd
deleted file mode 100644
index 0c8fe10..0000000
--- a/demo/scripts/fps_overlay.gd
+++ /dev/null
@@ -1,18 +0,0 @@
-extends CanvasLayer
-
-# Cheap FPS + stats overlay. Attach as a child of any scene.
-# The parent scene can call set_subtitle(text) to add a line below the FPS.
-
-@onready var label: Label = $Label
-var _subtitle := ""
-
-func set_subtitle(text: String) -> void:
- _subtitle = text
-
-func _process(_delta: float) -> void:
- var fps := Engine.get_frames_per_second()
- var frame_ms := 0.0 if fps <= 0 else 1000.0 / fps
- var txt := "FPS: %d %.2f ms" % [fps, frame_ms]
- if _subtitle != "":
- txt += "\n" + _subtitle
- label.text = txt
diff --git a/demo/scripts/fps_overlay.gd.uid b/demo/scripts/fps_overlay.gd.uid
deleted file mode 100644
index a5e0511..0000000
--- a/demo/scripts/fps_overlay.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://chq0nb6xd2e5w
diff --git a/demo/scripts/game_root.gd b/demo/scripts/game_root.gd
deleted file mode 100644
index e70b082..0000000
--- a/demo/scripts/game_root.gd
+++ /dev/null
@@ -1,3 +0,0 @@
-@warning_ignore("empty_file")
-extends Node3D
-
diff --git a/demo/scripts/game_root.gd.uid b/demo/scripts/game_root.gd.uid
deleted file mode 100644
index b93906a..0000000
--- a/demo/scripts/game_root.gd.uid
+++ /dev/null
@@ -1 +0,0 @@
-uid://yh36kisxu2q3
diff --git a/demo/stuff/dungeon_seed52_depth100.tscn b/demo/stuff/dungeon_seed52_depth100.tscn
deleted file mode 100644
index f21413a..0000000
--- a/demo/stuff/dungeon_seed52_depth100.tscn
+++ /dev/null
@@ -1,12 +0,0 @@
-[gd_scene format=3 uid="uid://dl6n21j5322xq"]
-
-[ext_resource type="MeshLibrary" uid="uid://u7pnhcouyiss" path="res://stuff/mesh_library.tres" id="1_i4dsk"]
-
-[node name="Dungeon_seed52_depth100" type="Node3D" unique_id=204893552]
-
-[node name="GridMap" type="GridMap" parent="." unique_id=2028227728]
-mesh_library = ExtResource("1_i4dsk")
-cell_size = Vector3(1, 1, 1)
-data = {
-"cells": PackedInt32Array(8, 0, 1, 9, 0, 1, 10, 0, 1, 47, 0, 1, 48, 0, 1, 49, 0, 1, 50, 0, 1, 51, 0, 1, 52, 0, 1, 53, 0, 1, 55, 0, 1, 56, 0, 1, 57, 0, 1, 58, 0, 1, 59, 0, 1, 60, 0, 1, 61, 0, 1, 62, 0, 1, 63, 0, 1, 64, 0, 1, 65, 0, 1, 66, 0, 1, 67, 0, 1, 68, 0, 1, 69, 0, 1, 7, 1, 1, 8, 1, 1, 9, 1, 0, 10, 1, 1, 11, 1, 1, 41, 1, 1, 42, 1, 1, 43, 1, 1, 44, 1, 1, 45, 1, 1, 47, 1, 1, 48, 1, 0, 49, 1, 0, 50, 1, 0, 51, 1, 0, 52, 1, 0, 53, 1, 1, 54, 1, 1, 55, 1, 1, 56, 1, 0, 57, 1, 0, 58, 1, 0, 59, 1, 0, 60, 1, 0, 61, 1, 1, 62, 1, 1, 63, 1, 9, 64, 1, 0, 65, 1, 0, 66, 1, 0, 67, 1, 0, 68, 1, 0, 69, 1, 1, 6, 2, 1, 7, 2, 1, 8, 2, 0, 9, 2, 0, 10, 2, 0, 11, 2, 1, 12, 2, 1, 40, 2, 1, 41, 2, 1, 42, 2, 0, 43, 2, 0, 44, 2, 0, 45, 2, 1, 46, 2, 1, 47, 2, 1, 48, 2, 0, 49, 2, 0, 50, 2, 0, 51, 2, 0, 52, 2, 0, 53, 2, 2, 54, 2, 3, 55, 2, 3, 56, 2, 0, 57, 2, 0, 58, 2, 0, 59, 2, 0, 60, 2, 0, 61, 2, 1, 62, 2, 1, 63, 2, 0, 64, 2, 0, 65, 2, 0, 66, 2, 0, 67, 2, 0, 68, 2, 0, 69, 2, 1, 6, 3, 1, 7, 3, 0, 8, 3, 0, 9, 3, 0, 10, 3, 0, 11, 3, 0, 12, 3, 1, 13, 3, 1, 14, 3, 1, 15, 3, 1, 16, 3, 1, 17, 3, 1, 18, 3, 1, 19, 3, 1, 20, 3, 1, 40, 3, 1, 41, 3, 0, 42, 3, 0, 43, 3, 0, 44, 3, 0, 45, 3, 0, 46, 3, 1, 47, 3, 1, 48, 3, 0, 49, 3, 0, 50, 3, 0, 51, 3, 0, 52, 3, 0, 53, 3, 1, 54, 3, 1, 55, 3, 1, 56, 3, 0, 57, 3, 0, 58, 3, 0, 59, 3, 0, 60, 3, 0, 61, 3, 1, 62, 3, 1, 63, 3, 0, 64, 3, 0, 65, 3, 0, 66, 3, 0, 67, 3, 0, 68, 3, 0, 69, 3, 1, 6, 4, 1, 7, 4, 1, 8, 4, 0, 9, 4, 0, 10, 4, 0, 11, 4, 3, 12, 4, 3, 13, 4, 3, 14, 4, 3, 15, 4, 2, 16, 4, 0, 17, 4, 0, 18, 4, 0, 19, 4, 0, 20, 4, 1, 21, 4, 1, 22, 4, 1, 23, 4, 1, 24, 4, 1, 25, 4, 1, 26, 4, 1, 27, 4, 1, 28, 4, 1, 29, 4, 1, 30, 4, 1, 31, 4, 1, 32, 4, 1, 33, 4, 1, 40, 4, 1, 41, 4, 0, 42, 4, 0, 43, 4, 0, 44, 4, 0, 45, 4, 0, 46, 4, 2, 47, 4, 3, 48, 4, 0, 49, 4, 0, 50, 4, 0, 51, 4, 0, 52, 4, 0, 53, 4, 1, 55, 4, 1, 56, 4, 0, 57, 4, 0, 58, 4, 0, 59, 4, 0, 60, 4, 0, 61, 4, 1, 62, 4, 1, 63, 4, 1, 64, 4, 1, 65, 4, 1, 66, 4, 3, 67, 4, 1, 68, 4, 1, 69, 4, 1, 7, 5, 1, 8, 5, 1, 9, 5, 0, 10, 5, 1, 11, 5, 1, 12, 5, 1, 13, 5, 1, 14, 5, 1, 15, 5, 1, 16, 5, 0, 17, 5, 0, 18, 5, 0, 19, 5, 0, 20, 5, 3, 21, 5, 3, 22, 5, 3, 23, 5, 3, 24, 5, 3, 25, 5, 3, 26, 5, 3, 27, 5, 2, 28, 5, 0, 29, 5, 0, 30, 5, 0, 31, 5, 0, 32, 5, 0, 33, 5, 1, 34, 5, 1, 35, 5, 1, 36, 5, 1, 37, 5, 1, 38, 5, 1, 39, 5, 1, 40, 5, 1, 41, 5, 0, 42, 5, 0, 43, 5, 0, 44, 5, 0, 45, 5, 0, 46, 5, 1, 47, 5, 1, 48, 5, 1, 49, 5, 1, 50, 5, 2, 51, 5, 1, 52, 5, 1, 53, 5, 1, 55, 5, 1, 56, 5, 2, 57, 5, 1, 58, 5, 1, 59, 5, 1, 60, 5, 1, 61, 5, 1, 65, 5, 1, 66, 5, 3, 67, 5, 1, 68, 5, 1, 69, 5, 1, 70, 5, 1, 71, 5, 1, 8, 6, 1, 9, 6, 1, 10, 6, 1, 15, 6, 1, 16, 6, 0, 17, 6, 0, 18, 6, 0, 19, 6, 0, 20, 6, 1, 21, 6, 1, 22, 6, 1, 23, 6, 1, 24, 6, 1, 25, 6, 1, 26, 6, 1, 27, 6, 1, 28, 6, 0, 29, 6, 0, 30, 6, 0, 31, 6, 0, 32, 6, 0, 33, 6, 2, 34, 6, 3, 35, 6, 3, 36, 6, 3, 37, 6, 3, 38, 6, 3, 39, 6, 3, 40, 6, 3, 41, 6, 3, 42, 6, 0, 43, 6, 0, 44, 6, 0, 45, 6, 2, 46, 6, 1, 49, 6, 1, 50, 6, 3, 51, 6, 1, 55, 6, 1, 56, 6, 3, 57, 6, 1, 64, 6, 1, 65, 6, 1, 66, 6, 2, 67, 6, 1, 68, 6, 0, 69, 6, 0, 70, 6, 0, 71, 6, 1, 72, 6, 1, 73, 6, 1, 74, 6, 1, 15, 7, 1, 16, 7, 0, 17, 7, 0, 18, 7, 0, 19, 7, 0, 20, 7, 1, 27, 7, 1, 28, 7, 1, 29, 7, 1, 30, 7, 3, 31, 7, 1, 32, 7, 1, 33, 7, 1, 34, 7, 1, 35, 7, 1, 36, 7, 1, 37, 7, 1, 38, 7, 1, 39, 7, 1, 40, 7, 1, 41, 7, 1, 42, 7, 1, 43, 7, 1, 44, 7, 1, 45, 7, 3, 46, 7, 1, 49, 7, 1, 50, 7, 3, 51, 7, 1, 52, 7, 1, 53, 7, 1, 55, 7, 1, 56, 7, 3, 57, 7, 1, 58, 7, 1, 59, 7, 1, 64, 7, 1, 65, 7, 0, 66, 7, 0, 67, 7, 0, 68, 7, 0, 69, 7, 0, 70, 7, 0, 71, 7, 0, 72, 7, 0, 73, 7, 0, 74, 7, 1, 15, 8, 1, 16, 8, 1, 17, 8, 1, 18, 8, 1, 19, 8, 1, 20, 8, 1, 28, 8, 1, 29, 8, 1, 30, 8, 2, 31, 8, 0, 32, 8, 1, 33, 8, 1, 34, 8, 1, 44, 8, 1, 45, 8, 3, 46, 8, 1, 49, 8, 1, 50, 8, 0, 51, 8, 0, 52, 8, 0, 53, 8, 1, 55, 8, 1, 56, 8, 3, 57, 8, 0, 58, 8, 0, 59, 8, 1, 60, 8, 1, 64, 8, 1, 65, 8, 0, 66, 8, 0, 67, 8, 0, 68, 8, 0, 69, 8, 0, 70, 8, 0, 71, 8, 0, 72, 8, 0, 73, 8, 0, 74, 8, 1, 27, 9, 1, 28, 9, 1, 29, 9, 0, 30, 9, 0, 31, 9, 0, 32, 9, 0, 33, 9, 0, 34, 9, 1, 35, 9, 1, 44, 9, 1, 45, 9, 3, 46, 9, 1, 49, 9, 1, 50, 9, 0, 51, 9, 0, 52, 9, 0, 53, 9, 1, 55, 9, 1, 56, 9, 0, 57, 9, 0, 58, 9, 0, 59, 9, 0, 60, 9, 1, 61, 9, 1, 62, 9, 1, 64, 9, 1, 65, 9, 0, 66, 9, 0, 67, 9, 0, 68, 9, 0, 69, 9, 0, 70, 9, 0, 71, 9, 0, 72, 9, 0, 73, 9, 0, 74, 9, 1, 14, 10, 1, 15, 10, 1, 16, 10, 1, 17, 10, 1, 18, 10, 1, 19, 10, 1, 20, 10, 1, 21, 10, 1, 22, 10, 1, 23, 10, 1, 24, 10, 1, 25, 10, 1, 26, 10, 1, 27, 10, 1, 28, 10, 0, 29, 10, 0, 30, 10, 0, 31, 10, 0, 32, 10, 0, 33, 10, 0, 34, 10, 0, 35, 10, 1, 44, 10, 1, 45, 10, 3, 46, 10, 1, 49, 10, 1, 50, 10, 0, 51, 10, 0, 52, 10, 0, 53, 10, 1, 55, 10, 1, 56, 10, 0, 57, 10, 0, 58, 10, 0, 59, 10, 0, 60, 10, 0, 61, 10, 0, 62, 10, 1, 63, 10, 1, 64, 10, 1, 65, 10, 1, 66, 10, 1, 67, 10, 1, 68, 10, 0, 69, 10, 0, 70, 10, 0, 71, 10, 1, 72, 10, 1, 73, 10, 1, 74, 10, 1, 5, 11, 1, 6, 11, 1, 7, 11, 1, 8, 11, 1, 9, 11, 1, 10, 11, 1, 11, 11, 1, 12, 11, 1, 13, 11, 1, 14, 11, 1, 15, 11, 0, 16, 11, 0, 17, 11, 0, 18, 11, 0, 19, 11, 3, 20, 11, 3, 21, 11, 3, 22, 11, 3, 23, 11, 3, 24, 11, 3, 25, 11, 3, 26, 11, 3, 27, 11, 2, 28, 11, 0, 29, 11, 0, 30, 11, 0, 31, 11, 0, 32, 11, 0, 33, 11, 0, 34, 11, 0, 35, 11, 1, 36, 11, 1, 37, 11, 1, 38, 11, 1, 39, 11, 1, 40, 11, 1, 41, 11, 1, 42, 11, 1, 43, 11, 1, 44, 11, 1, 45, 11, 3, 46, 11, 1, 47, 11, 1, 48, 11, 1, 49, 11, 1, 50, 11, 0, 51, 11, 0, 52, 11, 0, 53, 11, 1, 55, 11, 1, 56, 11, 1, 57, 11, 0, 58, 11, 0, 59, 11, 0, 60, 11, 0, 61, 11, 0, 62, 11, 0, 63, 11, 1, 67, 11, 1, 68, 11, 1, 69, 11, 3, 70, 11, 1, 71, 11, 1, 5, 12, 1, 6, 12, 0, 7, 12, 0, 8, 12, 0, 9, 12, 3, 10, 12, 3, 11, 12, 3, 12, 12, 3, 13, 12, 3, 14, 12, 2, 15, 12, 0, 16, 12, 0, 17, 12, 0, 18, 12, 0, 19, 12, 1, 20, 12, 1, 21, 12, 1, 22, 12, 1, 23, 12, 1, 24, 12, 1, 25, 12, 1, 26, 12, 1, 27, 12, 0, 28, 12, 0, 29, 12, 0, 30, 12, 0, 31, 12, 0, 32, 12, 0, 33, 12, 0, 34, 12, 0, 35, 12, 0, 36, 12, 3, 37, 12, 2, 38, 12, 0, 39, 12, 0, 40, 12, 0, 41, 12, 2, 42, 12, 0, 43, 12, 0, 44, 12, 0, 45, 12, 0, 46, 12, 0, 47, 12, 0, 48, 12, 1, 49, 12, 1, 50, 12, 1, 51, 12, 1, 52, 12, 1, 53, 12, 1, 56, 12, 1, 57, 12, 1, 58, 12, 0, 59, 12, 1, 60, 12, 1, 61, 12, 0, 62, 12, 1, 63, 12, 1, 65, 12, 1, 66, 12, 1, 67, 12, 1, 68, 12, 1, 69, 12, 2, 70, 12, 1, 74, 12, 1, 75, 12, 1, 76, 12, 1, 77, 12, 1, 78, 12, 1, 5, 13, 1, 6, 13, 0, 7, 13, 0, 8, 13, 0, 9, 13, 1, 10, 13, 1, 11, 13, 1, 12, 13, 1, 13, 13, 1, 14, 13, 1, 15, 13, 1, 16, 13, 1, 17, 13, 2, 18, 13, 1, 19, 13, 1, 26, 13, 1, 27, 13, 1, 28, 13, 0, 29, 13, 0, 30, 13, 0, 31, 13, 0, 32, 13, 0, 33, 13, 0, 34, 13, 0, 35, 13, 1, 36, 13, 1, 37, 13, 1, 38, 13, 0, 39, 13, 0, 40, 13, 0, 41, 13, 1, 42, 13, 0, 43, 13, 0, 44, 13, 0, 45, 13, 0, 46, 13, 0, 47, 13, 0, 48, 13, 1, 57, 13, 1, 58, 13, 2, 59, 13, 1, 60, 13, 1, 61, 13, 1, 62, 13, 1, 65, 13, 1, 66, 13, 0, 67, 13, 0, 68, 13, 0, 69, 13, 0, 70, 13, 1, 71, 13, 1, 72, 13, 1, 73, 13, 1, 74, 13, 1, 75, 13, 0, 76, 13, 0, 77, 13, 0, 78, 13, 1, 5, 14, 1, 6, 14, 0, 7, 14, 0, 8, 14, 0, 9, 14, 1, 16, 14, 1, 17, 14, 3, 18, 14, 1, 27, 14, 1, 28, 14, 0, 29, 14, 0, 30, 14, 0, 31, 14, 0, 32, 14, 0, 33, 14, 0, 34, 14, 0, 35, 14, 1, 37, 14, 1, 38, 14, 0, 39, 14, 8, 40, 14, 0, 41, 14, 1, 42, 14, 1, 43, 14, 1, 44, 14, 1, 45, 14, 1, 46, 14, 1, 47, 14, 1, 48, 14, 1, 57, 14, 1, 58, 14, 3, 59, 14, 1, 65, 14, 1, 66, 14, 0, 67, 14, 0, 68, 14, 0, 69, 14, 0, 70, 14, 3, 71, 14, 3, 72, 14, 3, 73, 14, 3, 74, 14, 2, 75, 14, 0, 76, 14, 0, 77, 14, 0, 78, 14, 1, 5, 15, 1, 6, 15, 1, 7, 15, 1, 8, 15, 1, 9, 15, 1, 16, 15, 1, 17, 15, 3, 18, 15, 1, 27, 15, 1, 28, 15, 1, 29, 15, 0, 30, 15, 0, 31, 15, 0, 32, 15, 0, 33, 15, 0, 34, 15, 1, 35, 15, 1, 37, 15, 1, 38, 15, 0, 39, 15, 0, 40, 15, 0, 41, 15, 1, 49, 15, 1, 50, 15, 1, 51, 15, 1, 52, 15, 1, 57, 15, 1, 58, 15, 3, 59, 15, 1, 65, 15, 1, 66, 15, 0, 67, 15, 0, 68, 15, 0, 69, 15, 0, 70, 15, 1, 71, 15, 1, 72, 15, 1, 73, 15, 1, 74, 15, 1, 75, 15, 3, 76, 15, 1, 77, 15, 1, 78, 15, 1, 16, 16, 1, 17, 16, 3, 18, 16, 1, 28, 16, 1, 29, 16, 1, 30, 16, 1, 31, 16, 0, 32, 16, 1, 33, 16, 1, 34, 16, 1, 37, 16, 1, 38, 16, 1, 39, 16, 1, 40, 16, 1, 41, 16, 1, 49, 16, 1, 50, 16, 0, 51, 16, 0, 52, 16, 1, 55, 16, 1, 56, 16, 1, 57, 16, 1, 58, 16, 3, 59, 16, 1, 60, 16, 1, 65, 16, 1, 66, 16, 1, 67, 16, 1, 68, 16, 1, 69, 16, 1, 70, 16, 1, 74, 16, 1, 75, 16, 3, 76, 16, 1, 16, 17, 1, 17, 17, 3, 18, 17, 1, 19, 17, 1, 30, 17, 1, 31, 17, 1, 32, 17, 1, 33, 17, 1, 34, 17, 1, 35, 17, 1, 36, 17, 1, 49, 17, 1, 50, 17, 0, 51, 17, 0, 52, 17, 1, 55, 17, 1, 56, 17, 0, 57, 17, 0, 58, 17, 0, 59, 17, 0, 60, 17, 1, 74, 17, 1, 75, 17, 3, 76, 17, 1, 3, 18, 1, 4, 18, 1, 5, 18, 1, 6, 18, 1, 7, 18, 1, 8, 18, 1, 9, 18, 1, 10, 18, 1, 11, 18, 1, 12, 18, 1, 13, 18, 1, 14, 18, 1, 15, 18, 1, 16, 18, 1, 17, 18, 3, 18, 18, 0, 19, 18, 1, 20, 18, 1, 21, 18, 1, 22, 18, 1, 23, 18, 1, 24, 18, 1, 25, 18, 1, 26, 18, 1, 27, 18, 1, 28, 18, 1, 29, 18, 1, 30, 18, 1, 31, 18, 1, 32, 18, 1, 33, 18, 0, 34, 18, 0, 35, 18, 0, 36, 18, 1, 37, 18, 1, 38, 18, 1, 39, 18, 1, 49, 18, 1, 50, 18, 3, 51, 18, 1, 52, 18, 1, 55, 18, 1, 56, 18, 0, 57, 18, 0, 58, 18, 0, 59, 18, 0, 60, 18, 1, 71, 18, 1, 72, 18, 1, 73, 18, 1, 74, 18, 1, 75, 18, 2, 76, 18, 1, 77, 18, 1, 3, 19, 1, 4, 19, 0, 5, 19, 0, 6, 19, 0, 7, 19, 3, 8, 19, 3, 9, 19, 3, 10, 19, 3, 11, 19, 3, 12, 19, 3, 13, 19, 3, 14, 19, 3, 15, 19, 3, 16, 19, 2, 17, 19, 0, 18, 19, 0, 19, 19, 0, 20, 19, 2, 21, 19, 3, 22, 19, 3, 23, 19, 3, 24, 19, 3, 25, 19, 3, 26, 19, 3, 27, 19, 3, 28, 19, 3, 29, 19, 3, 30, 19, 0, 31, 19, 0, 32, 19, 0, 33, 19, 0, 34, 19, 0, 35, 19, 0, 36, 19, 0, 37, 19, 0, 38, 19, 0, 39, 19, 1, 49, 19, 1, 50, 19, 3, 51, 19, 1, 55, 19, 1, 56, 19, 0, 57, 19, 0, 58, 19, 0, 59, 19, 0, 60, 19, 1, 71, 19, 1, 72, 19, 0, 73, 19, 0, 74, 19, 0, 75, 19, 0, 76, 19, 0, 77, 19, 1, 3, 20, 1, 4, 20, 0, 5, 20, 0, 6, 20, 0, 7, 20, 1, 8, 20, 1, 9, 20, 1, 10, 20, 1, 11, 20, 1, 12, 20, 1, 13, 20, 1, 14, 20, 1, 15, 20, 1, 16, 20, 1, 17, 20, 1, 18, 20, 0, 19, 20, 1, 20, 20, 1, 21, 20, 1, 22, 20, 1, 23, 20, 1, 24, 20, 1, 25, 20, 1, 26, 20, 1, 27, 20, 1, 28, 20, 1, 29, 20, 1, 30, 20, 0, 31, 20, 0, 32, 20, 0, 33, 20, 0, 34, 20, 0, 35, 20, 0, 36, 20, 0, 37, 20, 0, 38, 20, 0, 39, 20, 1, 47, 20, 1, 48, 20, 1, 49, 20, 1, 50, 20, 3, 51, 20, 1, 55, 20, 1, 56, 20, 1, 57, 20, 1, 58, 20, 1, 59, 20, 1, 60, 20, 1, 71, 20, 1, 72, 20, 0, 73, 20, 0, 74, 20, 0, 75, 20, 0, 76, 20, 0, 77, 20, 1, 3, 21, 1, 4, 21, 1, 5, 21, 1, 6, 21, 2, 7, 21, 1, 17, 21, 1, 18, 21, 1, 19, 21, 1, 29, 21, 1, 30, 21, 0, 31, 21, 0, 32, 21, 0, 33, 21, 0, 34, 21, 0, 35, 21, 0, 36, 21, 0, 37, 21, 0, 38, 21, 0, 39, 21, 1, 40, 21, 1, 41, 21, 1, 42, 21, 1, 43, 21, 1, 44, 21, 1, 45, 21, 1, 46, 21, 1, 47, 21, 1, 48, 21, 0, 49, 21, 1, 50, 21, 3, 51, 21, 1, 59, 21, 1, 60, 21, 1, 61, 21, 1, 62, 21, 1, 63, 21, 1, 64, 21, 1, 65, 21, 1, 66, 21, 1, 67, 21, 1, 68, 21, 1, 69, 21, 1, 70, 21, 1, 71, 21, 1, 72, 21, 0, 73, 21, 0, 74, 21, 0, 75, 21, 0, 76, 21, 0, 77, 21, 1, 4, 22, 1, 5, 22, 1, 6, 22, 3, 7, 22, 1, 8, 22, 1, 9, 22, 1, 10, 22, 1, 29, 22, 1, 30, 22, 1, 31, 22, 2, 32, 22, 1, 33, 22, 0, 34, 22, 0, 35, 22, 0, 36, 22, 2, 37, 22, 3, 38, 22, 3, 39, 22, 3, 40, 22, 3, 41, 22, 3, 42, 22, 3, 43, 22, 3, 44, 22, 3, 45, 22, 3, 46, 22, 0, 47, 22, 0, 48, 22, 0, 49, 22, 0, 50, 22, 2, 51, 22, 1, 59, 22, 1, 60, 22, 0, 61, 22, 0, 62, 22, 0, 63, 22, 0, 64, 22, 0, 65, 22, 2, 66, 22, 3, 67, 22, 3, 68, 22, 3, 69, 22, 3, 70, 22, 3, 71, 22, 3, 72, 22, 0, 73, 22, 0, 74, 22, 0, 75, 22, 0, 76, 22, 0, 77, 22, 1, 4, 23, 1, 5, 23, 0, 6, 23, 0, 7, 23, 0, 8, 23, 0, 9, 23, 0, 10, 23, 1, 30, 23, 1, 31, 23, 3, 32, 23, 1, 33, 23, 1, 34, 23, 1, 35, 23, 1, 36, 23, 1, 37, 23, 1, 38, 23, 1, 39, 23, 1, 40, 23, 1, 41, 23, 1, 42, 23, 1, 43, 23, 1, 44, 23, 1, 45, 23, 0, 46, 23, 0, 47, 23, 0, 48, 23, 0, 49, 23, 0, 50, 23, 0, 51, 23, 1, 52, 23, 1, 53, 23, 1, 54, 23, 1, 55, 23, 1, 56, 23, 1, 57, 23, 1, 58, 23, 1, 59, 23, 1, 60, 23, 0, 61, 23, 0, 62, 23, 0, 63, 23, 0, 64, 23, 0, 65, 23, 1, 66, 23, 1, 67, 23, 1, 68, 23, 1, 69, 23, 1, 70, 23, 1, 71, 23, 1, 72, 23, 1, 73, 23, 2, 74, 23, 1, 75, 23, 1, 76, 23, 1, 77, 23, 1, 4, 24, 1, 5, 24, 0, 6, 24, 0, 7, 24, 0, 8, 24, 0, 9, 24, 0, 10, 24, 1, 27, 24, 1, 28, 24, 1, 29, 24, 1, 30, 24, 1, 31, 24, 3, 32, 24, 1, 33, 24, 1, 34, 24, 1, 35, 24, 1, 43, 24, 1, 44, 24, 0, 45, 24, 0, 46, 24, 0, 47, 24, 0, 48, 24, 0, 49, 24, 0, 50, 24, 0, 51, 24, 0, 52, 24, 2, 53, 24, 3, 54, 24, 3, 55, 24, 3, 56, 24, 3, 57, 24, 3, 58, 24, 3, 59, 24, 3, 60, 24, 0, 61, 24, 0, 62, 24, 0, 63, 24, 0, 64, 24, 0, 65, 24, 1, 68, 24, 1, 69, 24, 1, 70, 24, 1, 71, 24, 1, 72, 24, 1, 73, 24, 3, 74, 24, 1, 75, 24, 1, 4, 25, 1, 5, 25, 0, 6, 25, 0, 7, 25, 0, 8, 25, 0, 9, 25, 0, 10, 25, 1, 27, 25, 1, 28, 25, 0, 29, 25, 0, 30, 25, 0, 31, 25, 0, 32, 25, 0, 33, 25, 0, 34, 25, 0, 35, 25, 1, 43, 25, 1, 44, 25, 1, 45, 25, 0, 46, 25, 0, 47, 25, 0, 48, 25, 0, 49, 25, 1, 50, 25, 0, 51, 25, 1, 52, 25, 1, 53, 25, 1, 54, 25, 1, 55, 25, 1, 56, 25, 1, 57, 25, 1, 58, 25, 1, 59, 25, 1, 60, 25, 0, 61, 25, 0, 62, 25, 0, 63, 25, 0, 64, 25, 0, 65, 25, 1, 68, 25, 1, 69, 25, 0, 70, 25, 0, 71, 25, 0, 72, 25, 0, 73, 25, 0, 74, 25, 0, 75, 25, 1, 4, 26, 1, 5, 26, 1, 6, 26, 1, 7, 26, 1, 8, 26, 1, 9, 26, 1, 10, 26, 1, 27, 26, 1, 28, 26, 0, 29, 26, 0, 30, 26, 0, 31, 26, 0, 32, 26, 0, 33, 26, 0, 34, 26, 0, 35, 26, 1, 44, 26, 1, 45, 26, 1, 46, 26, 0, 47, 26, 1, 48, 26, 1, 49, 26, 1, 50, 26, 1, 51, 26, 1, 59, 26, 1, 60, 26, 1, 61, 26, 1, 62, 26, 1, 63, 26, 1, 64, 26, 1, 65, 26, 1, 68, 26, 1, 69, 26, 0, 70, 26, 0, 71, 26, 0, 72, 26, 0, 73, 26, 0, 74, 26, 0, 75, 26, 1, 27, 27, 1, 28, 27, 0, 29, 27, 0, 30, 27, 0, 31, 27, 0, 32, 27, 0, 33, 27, 0, 34, 27, 0, 35, 27, 1, 45, 27, 1, 46, 27, 1, 47, 27, 1, 68, 27, 1, 69, 27, 0, 70, 27, 0, 71, 27, 0, 72, 27, 0, 73, 27, 0, 74, 27, 0, 75, 27, 1, 27, 28, 1, 28, 28, 1, 29, 28, 1, 30, 28, 1, 31, 28, 1, 32, 28, 1, 33, 28, 1, 34, 28, 1, 35, 28, 1, 68, 28, 1, 69, 28, 1, 70, 28, 1, 71, 28, 1, 72, 28, 1, 73, 28, 1, 74, 28, 1, 75, 28, 1)
-}
diff --git a/demo/stuff/mesh_library.tres b/demo/stuff/mesh_library.tres
deleted file mode 100644
index 10a6f2a..0000000
--- a/demo/stuff/mesh_library.tres
+++ /dev/null
@@ -1,192 +0,0 @@
-[gd_resource type="MeshLibrary" format=4 uid="uid://u7pnhcouyiss"]
-
-[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_mxq65"]
-albedo_color = Color(0.64, 0.54, 0.42, 1)
-roughness = 0.8
-
-[sub_resource type="BoxMesh" id="BoxMesh_nfq1i"]
-material = SubResource("StandardMaterial3D_mxq65")
-size = Vector3(1, 0.05, 1)
-
-[sub_resource type="BoxShape3D" id="BoxShape3D_c471q"]
-size = Vector3(1, 0.05, 1)
-
-[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_iuw7j"]
-albedo_color = Color(0.28, 0.28, 0.32, 1)
-roughness = 0.8
-
-[sub_resource type="BoxMesh" id="BoxMesh_7yf4u"]
-material = SubResource("StandardMaterial3D_iuw7j")
-size = Vector3(1, 3, 1)
-
-[sub_resource type="BoxShape3D" id="BoxShape3D_aewbc"]
-size = Vector3(1, 3, 1)
-
-[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_dpcan"]
-albedo_color = Color(0.72, 0.36, 0.2, 1)
-roughness = 0.8
-
-[sub_resource type="BoxMesh" id="BoxMesh_ba7m6"]
-material = SubResource("StandardMaterial3D_dpcan")
-size = Vector3(1, 0.8, 1)
-
-[sub_resource type="BoxShape3D" id="BoxShape3D_pqd2j"]
-size = Vector3(1, 0.8, 1)
-
-[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_7bx42"]
-albedo_color = Color(0.44, 0.37, 0.29, 1)
-roughness = 0.8
-
-[sub_resource type="BoxMesh" id="BoxMesh_q4frx"]
-material = SubResource("StandardMaterial3D_7bx42")
-size = Vector3(1, 0.05, 1)
-
-[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_n2rcg"]
-transparency = 1
-albedo_color = Color(0.18, 0.42, 0.78, 0.75)
-roughness = 0.8
-
-[sub_resource type="BoxMesh" id="BoxMesh_qowgg"]
-material = SubResource("StandardMaterial3D_n2rcg")
-size = Vector3(1, 0.05, 1)
-
-[sub_resource type="BoxShape3D" id="BoxShape3D_n4omc"]
-size = Vector3(1, 0.05, 1)
-
-[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_rdtaw"]
-albedo_color = Color(0.9, 0.3, 0.12, 1)
-roughness = 0.8
-emission_enabled = true
-emission = Color(1, 0.45, 0.15, 1)
-emission_energy_multiplier = 1.5
-
-[sub_resource type="BoxMesh" id="BoxMesh_bbjka"]
-material = SubResource("StandardMaterial3D_rdtaw")
-size = Vector3(1, 0.05, 1)
-
-[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_e4bfb"]
-albedo_color = Color(0.72, 0.5, 0.22, 1)
-roughness = 0.8
-emission_enabled = true
-emission = Color(0.95, 0.65, 0.25, 1)
-emission_energy_multiplier = 0.8
-
-[sub_resource type="BoxMesh" id="BoxMesh_r1428"]
-material = SubResource("StandardMaterial3D_e4bfb")
-size = Vector3(1, 0.05, 1)
-
-[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_04bo8"]
-albedo_color = Color(0.55, 0.38, 0.22, 1)
-roughness = 0.8
-
-[sub_resource type="BoxMesh" id="BoxMesh_qyws6"]
-material = SubResource("StandardMaterial3D_04bo8")
-size = Vector3(1, 0.1, 1)
-
-[sub_resource type="BoxShape3D" id="BoxShape3D_ym5kt"]
-size = Vector3(1, 0.1, 1)
-
-[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_nv4rv"]
-albedo_color = Color(0.74, 0.88, 0.94, 1)
-roughness = 0.8
-
-[sub_resource type="ArrayMesh" id="ArrayMesh_xoewy"]
-_surfaces = [{
-"aabb": AABB(-0.5, 0, -0.5, 1, 0.7, 1),
-"format": 34359738375,
-"material": SubResource("StandardMaterial3D_nv4rv"),
-"primitive": 3,
-"uv_scale": Vector4(0, 0, 0, 0),
-"vertex_count": 72,
-"vertex_data": PackedByteArray("AAAAvwAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAPzMzMz8AAAAAAAAAvwAAAAAAAAAAAAAAPzMzMz8AAAAAAAAAvzMzMz8AAAAAAAAAPwAAAAAAAAA/AAAAvwAAAAAAAAA/AAAAvzMzMz8AAAA/AAAAPwAAAAAAAAA/AAAAvzMzMz8AAAA/AAAAPzMzMz8AAAA/AAAAPwAAAAAAAAAAAAAAPwAAAAAAAAA/AAAAPzMzMz8AAAA/AAAAPwAAAAAAAAAAAAAAPzMzMz8AAAA/AAAAPzMzMz8AAAAAAAAAvwAAAAAAAAA/AAAAvwAAAAAAAAAAAAAAvzMzMz8AAAAAAAAAvwAAAAAAAAA/AAAAvzMzMz8AAAAAAAAAvzMzMz8AAAA/AAAAvzMzMz8AAAAAAAAAPzMzMz8AAAAAAAAAPzMzMz8AAAA/AAAAvzMzMz8AAAAAAAAAPzMzMz8AAAA/AAAAvzMzMz8AAAA/AAAAvwAAAAAAAAA/AAAAPwAAAAAAAAA/AAAAPwAAAAAAAAAAAAAAvwAAAAAAAAA/AAAAPwAAAAAAAAAAAAAAvwAAAAAAAAAAAAAAvwAAAAAAAAC/AAAAPwAAAAAAAAC/AAAAP0jhej4AAAC/AAAAvwAAAAAAAAC/AAAAP0jhej4AAAC/AAAAv0jhej4AAAC/AAAAPwAAAAAAAAAAAAAAvwAAAAAAAAAAAAAAv0jhej4AAAAAAAAAPwAAAAAAAAAAAAAAv0jhej4AAAAAAAAAP0jhej4AAAAAAAAAPwAAAAAAAAC/AAAAPwAAAAAAAAAAAAAAP0jhej4AAAAAAAAAPwAAAAAAAAC/AAAAP0jhej4AAAAAAAAAP0jhej4AAAC/AAAAvwAAAAAAAAAAAAAAvwAAAAAAAAC/AAAAv0jhej4AAAC/AAAAvwAAAAAAAAAAAAAAv0jhej4AAAC/AAAAv0jhej4AAAAAAAAAv0jhej4AAAC/AAAAP0jhej4AAAC/AAAAP0jhej4AAAAAAAAAv0jhej4AAAC/AAAAP0jhej4AAAAAAAAAv0jhej4AAAAAAAAAvwAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAPwAAAAAAAAC/AAAAvwAAAAAAAAAAAAAAPwAAAAAAAAC/AAAAvwAAAAAAAAC///////9/AID//////38AgP//////fwCA//////9/AID//////38AgP//////fwCA/3//f/9/AID/f/9//38AgP9//3//fwCA/3//f/9/AID/f/9//38AgP9//3//fwCA////f/9//7////9//3//v////3//f/+/////f/9//7////9//3//v////3//f/+/AAD/f/9//78AAP9//3//vwAA/3//f/+/AAD/f/9//78AAP9//3//vwAA/3//f/+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//38AAAAA/7//fwAAAAD/v/9/AAAAAP+//38AAAAA/7//fwAAAAD/v/9/AAAAAP+///////9/AID//////38AgP//////fwCA//////9/AID//////38AgP//////fwCA/3//f/9/AID/f/9//38AgP9//3//fwCA/3//f/9/AID/f/9//38AgP9//3//fwCA////f/9//7////9//3//v////3//f/+/////f/9//7////9//3//v////3//f/+/AAD/f/9//78AAP9//3//vwAA/3//f/+/AAD/f/9//78AAP9//3//vwAA/3//f/+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//38AAAAA/7//fwAAAAD/v/9/AAAAAP+//38AAAAA/7//fwAAAAD/v/9/AAAAAP+/")
-}]
-
-[sub_resource type="BoxShape3D" id="BoxShape3D_ksa0k"]
-size = Vector3(1, 0.7, 1)
-
-[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_wq4fl"]
-albedo_color = Color(0.2, 0.44, 0.8, 1)
-roughness = 0.8
-
-[sub_resource type="ArrayMesh" id="ArrayMesh_fk0ts"]
-_surfaces = [{
-"aabb": AABB(-0.5, 0, -0.5, 1, 0.7, 1),
-"format": 34359738375,
-"material": SubResource("StandardMaterial3D_wq4fl"),
-"primitive": 3,
-"uv_scale": Vector4(0, 0, 0, 0),
-"vertex_count": 72,
-"vertex_data": PackedByteArray("AAAAvwAAAAAAAAC/AAAAPwAAAAAAAAC/AAAAP0jhej4AAAC/AAAAvwAAAAAAAAC/AAAAP0jhej4AAAC/AAAAv0jhej4AAAC/AAAAPwAAAAAAAAAAAAAAvwAAAAAAAAAAAAAAv0jhej4AAAAAAAAAPwAAAAAAAAAAAAAAv0jhej4AAAAAAAAAP0jhej4AAAAAAAAAPwAAAAAAAAC/AAAAPwAAAAAAAAAAAAAAP0jhej4AAAAAAAAAPwAAAAAAAAC/AAAAP0jhej4AAAAAAAAAP0jhej4AAAC/AAAAvwAAAAAAAAAAAAAAvwAAAAAAAAC/AAAAv0jhej4AAAC/AAAAvwAAAAAAAAAAAAAAv0jhej4AAAC/AAAAv0jhej4AAAAAAAAAv0jhej4AAAC/AAAAP0jhej4AAAC/AAAAP0jhej4AAAAAAAAAv0jhej4AAAC/AAAAP0jhej4AAAAAAAAAv0jhej4AAAAAAAAAvwAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAPwAAAAAAAAC/AAAAvwAAAAAAAAAAAAAAPwAAAAAAAAC/AAAAvwAAAAAAAAC/AAAAvwAAAAAAAAAAAAAAPwAAAAAAAAAAAAAAPzMzMz8AAAAAAAAAvwAAAAAAAAAAAAAAPzMzMz8AAAAAAAAAvzMzMz8AAAAAAAAAPwAAAAAAAAA/AAAAvwAAAAAAAAA/AAAAvzMzMz8AAAA/AAAAPwAAAAAAAAA/AAAAvzMzMz8AAAA/AAAAPzMzMz8AAAA/AAAAPwAAAAAAAAAAAAAAPwAAAAAAAAA/AAAAPzMzMz8AAAA/AAAAPwAAAAAAAAAAAAAAPzMzMz8AAAA/AAAAPzMzMz8AAAAAAAAAvwAAAAAAAAA/AAAAvwAAAAAAAAAAAAAAvzMzMz8AAAAAAAAAvwAAAAAAAAA/AAAAvzMzMz8AAAAAAAAAvzMzMz8AAAA/AAAAvzMzMz8AAAAAAAAAPzMzMz8AAAAAAAAAPzMzMz8AAAA/AAAAvzMzMz8AAAAAAAAAPzMzMz8AAAA/AAAAvzMzMz8AAAA/AAAAvwAAAAAAAAA/AAAAPwAAAAAAAAA/AAAAPwAAAAAAAAAAAAAAvwAAAAAAAAA/AAAAPwAAAAAAAAAAAAAAvwAAAAAAAAAA//////9/AID//////38AgP//////fwCA//////9/AID//////38AgP//////fwCA/3//f/9/AID/f/9//38AgP9//3//fwCA/3//f/9/AID/f/9//38AgP9//3//fwCA////f/9//7////9//3//v////3//f/+/////f/9//7////9//3//v////3//f/+/AAD/f/9//78AAP9//3//vwAA/3//f/+/AAD/f/9//78AAP9//3//vwAA/3//f/+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//38AAAAA/7//fwAAAAD/v/9/AAAAAP+//38AAAAA/7//fwAAAAD/v/9/AAAAAP+///////9/AID//////38AgP//////fwCA//////9/AID//////38AgP//////fwCA/3//f/9/AID/f/9//38AgP9//3//fwCA/3//f/9/AID/f/9//38AgP9//3//fwCA////f/9//7////9//3//v////3//f/+/////f/9//7////9//3//v////3//f/+/AAD/f/9//78AAP9//3//vwAA/3//f/+/AAD/f/9//78AAP9//3//vwAA/3//f/+//3///wAA/7//f///AAD/v/9///8AAP+//3///wAA/7//f///AAD/v/9///8AAP+//38AAAAA/7//fwAAAAD/v/9/AAAAAP+//38AAAAA/7//fwAAAAD/v/9/AAAAAP+/")
-}]
-
-[resource]
-item/0/name = "Floor"
-item/0/mesh = SubResource("BoxMesh_nfq1i")
-item/0/mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.025, 0)
-item/0/mesh_cast_shadow = 1
-item/0/shapes = [SubResource("BoxShape3D_c471q"), Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.025, 0)]
-item/0/navigation_mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
-item/0/navigation_layers = 1
-item/1/name = "Wall"
-item/1/mesh = SubResource("BoxMesh_7yf4u")
-item/1/mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0)
-item/1/mesh_cast_shadow = 1
-item/1/shapes = [SubResource("BoxShape3D_aewbc"), Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 0)]
-item/1/navigation_mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
-item/1/navigation_layers = 1
-item/2/name = "Door"
-item/2/mesh = SubResource("BoxMesh_ba7m6")
-item/2/mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.4, 0)
-item/2/mesh_cast_shadow = 1
-item/2/shapes = [SubResource("BoxShape3D_pqd2j"), Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.4, 0)]
-item/2/navigation_mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
-item/2/navigation_layers = 1
-item/3/name = "Corridor"
-item/3/mesh = SubResource("BoxMesh_q4frx")
-item/3/mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.025, 0)
-item/3/mesh_cast_shadow = 1
-item/3/shapes = [SubResource("BoxShape3D_c471q"), Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.025, 0)]
-item/3/navigation_mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
-item/3/navigation_layers = 1
-item/4/name = "Water"
-item/4/mesh = SubResource("BoxMesh_qowgg")
-item/4/mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.025, 0)
-item/4/mesh_cast_shadow = 1
-item/4/shapes = [SubResource("BoxShape3D_n4omc"), Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.025, 0)]
-item/4/navigation_mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
-item/4/navigation_layers = 1
-item/5/name = "Lava"
-item/5/mesh = SubResource("BoxMesh_bbjka")
-item/5/mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.025, 0)
-item/5/mesh_cast_shadow = 1
-item/5/shapes = [SubResource("BoxShape3D_n4omc"), Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.025, 0)]
-item/5/navigation_mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
-item/5/navigation_layers = 1
-item/6/name = "Brimstone"
-item/6/mesh = SubResource("BoxMesh_r1428")
-item/6/mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.025, 0)
-item/6/mesh_cast_shadow = 1
-item/6/shapes = [SubResource("BoxShape3D_n4omc"), Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.025, 0)]
-item/6/navigation_mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
-item/6/navigation_layers = 1
-item/7/name = "Bridge"
-item/7/mesh = SubResource("BoxMesh_qyws6")
-item/7/mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.05, 0)
-item/7/mesh_cast_shadow = 1
-item/7/shapes = [SubResource("BoxShape3D_ym5kt"), Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.05, 0)]
-item/7/navigation_mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
-item/7/navigation_layers = 1
-item/8/name = "StairsUp"
-item/8/mesh = SubResource("ArrayMesh_xoewy")
-item/8/mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
-item/8/mesh_cast_shadow = 1
-item/8/shapes = [SubResource("BoxShape3D_ksa0k"), Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.35, 0)]
-item/8/navigation_mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
-item/8/navigation_layers = 1
-item/9/name = "StairsDown"
-item/9/mesh = SubResource("ArrayMesh_fk0ts")
-item/9/mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
-item/9/mesh_cast_shadow = 1
-item/9/shapes = [SubResource("BoxShape3D_ksa0k"), Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.35, 0)]
-item/9/navigation_mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
-item/9/navigation_layers = 1
diff --git a/godot/SConstruct b/godot/SConstruct
index 8030bd2..f5e2830 100644
--- a/godot/SConstruct
+++ b/godot/SConstruct
@@ -34,6 +34,9 @@ c_sources = [
"../src/gen/chokepoints.c",
"../src/gen/machines.c",
"../src/gen/blueprints_data.c",
+ "../src/blobber/dungeon.c",
+ "../src/blobber/pipeline.c",
+ "../src/mesh/face_mesh.c",
]
# Ensure C files compile with C99; don't inherit godot-cpp's C++ flags verbatim.
diff --git a/godot/src/brogue_gen.cpp b/godot/src/brogue_gen.cpp
index 6f9ef8a..fe8202b 100644
--- a/godot/src/brogue_gen.cpp
+++ b/godot/src/brogue_gen.cpp
@@ -18,9 +18,13 @@ extern "C" {
#include "gen/stairs.h"
#include "gen/chokepoints.h"
#include "gen/machines.h"
+#include "blobber/pipeline.h"
+#include "blobber/dungeon.h"
+#include "mesh/face_mesh.h"
}
#include "grid_to_dict.h"
+#include "dungeon_to_dict.h"
using namespace godot;
@@ -29,6 +33,10 @@ BrogueGen::~BrogueGen() = default;
void BrogueGen::_bind_methods() {
ClassDB::bind_method(D_METHOD("generate", "seed", "depth"), &BrogueGen::generate);
+ ClassDB::bind_method(D_METHOD("generate_dungeon", "seed", "num_levels", "depth"),
+ &BrogueGen::generate_dungeon);
+ ClassDB::bind_method(D_METHOD("generate_2d_slices", "seed", "num_levels", "depth"),
+ &BrogueGen::generate_2d_slices);
}
Dictionary BrogueGen::generate(int seed, int depth) {
@@ -66,3 +74,30 @@ Dictionary BrogueGen::generate(int seed, int depth) {
d["gate_sites"] = gates;
return d;
}
+
+Dictionary BrogueGen::generate_dungeon(int seed, int num_levels, int depth) {
+ if (num_levels < 1) num_levels = 1;
+ dungeon_t *dng = blobber_generate((uint64_t)seed, num_levels, depth);
+ if (!dng) return Dictionary();
+
+ mesh_build_t *mb = mesh_build_from_dungeon(dng, /*cell_size=*/3.0f);
+ if (!mb) {
+ dungeon_destroy(dng);
+ return Dictionary();
+ }
+
+ Dictionary d = dungeon_to_dictionary(dng, mb, seed, depth, num_levels);
+
+ mesh_build_destroy(mb);
+ dungeon_destroy(dng);
+ return d;
+}
+
+TypedArray BrogueGen::generate_2d_slices(int seed, int num_levels, int depth) {
+ if (num_levels < 1) num_levels = 1;
+ dungeon_t *dng = blobber_generate((uint64_t)seed, num_levels, depth);
+ if (!dng) return TypedArray();
+ TypedArray slices = dungeon_to_2d_slices(dng);
+ dungeon_destroy(dng);
+ return slices;
+}
diff --git a/godot/src/brogue_gen.h b/godot/src/brogue_gen.h
index ea2d9d2..88def2d 100644
--- a/godot/src/brogue_gen.h
+++ b/godot/src/brogue_gen.h
@@ -2,6 +2,7 @@
#include
#include
+#include
namespace godot {
@@ -20,6 +21,16 @@ public:
~BrogueGen();
Dictionary generate(int seed, int depth);
+
+ /* v2 API: 3D blobber dungeon. Returns a Dictionary with raw cell data +
+ per-material mesh surfaces. See dungeon_to_dict.h for the schema. */
+ Dictionary generate_dungeon(int seed, int num_levels, int depth);
+
+ /* Exporter bridge: returns an Array of per-level Dictionaries shaped like
+ the 2D generate() output ({width, height, terrain, liquid}), derived
+ from a blobber 3D dungeon. Lets the .map exporter consume either
+ generator uniformly. */
+ TypedArray generate_2d_slices(int seed, int num_levels, int depth);
};
} // namespace godot
diff --git a/godot/src/dungeon_to_dict.cpp b/godot/src/dungeon_to_dict.cpp
new file mode 100644
index 0000000..de2e260
--- /dev/null
+++ b/godot/src/dungeon_to_dict.cpp
@@ -0,0 +1,168 @@
+#include "dungeon_to_dict.h"
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+extern "C" {
+#include "blobber/cell3d.h"
+}
+
+using namespace godot;
+
+namespace {
+
+/* Match src/gen/grid.h terrain_t / liquid_t values. Kept local so
+ dungeon_to_dict stays independent of gen/grid.h at compile time. */
+constexpr uint8_t T_NOTHING = 0;
+constexpr uint8_t T_FLOOR = 1;
+constexpr uint8_t T_WALL = 2;
+constexpr uint8_t T_DOOR = 3;
+constexpr uint8_t T_LIQUID = 5;
+constexpr uint8_t T_BRIDGE = 6;
+constexpr uint8_t T_STAIRS_UP = 7;
+constexpr uint8_t T_STAIRS_DOWN= 8;
+
+constexpr uint8_t L_NONE = 0;
+constexpr uint8_t L_WATER = 1;
+constexpr uint8_t L_LAVA = 2;
+constexpr uint8_t L_CHASM = 3;
+
+/* Translate a cell3d_t's floor type into the brogue exporter's
+ (terrain, liquid) pair. Walls are represented by FT_VOID here — the
+ blobber grid surrounds rooms with solid voids rather than emitting a
+ T_WALL-equivalent cell, so VOID is what gives us the wall geometry. */
+void floor_to_2d(uint8_t b_floor, uint8_t &terrain, uint8_t &liquid) {
+ terrain = T_NOTHING;
+ liquid = L_NONE;
+ switch (b_floor) {
+ case FT_VOID: terrain = T_WALL; break;
+ case FT_STONE: terrain = T_FLOOR; break;
+ case FT_WATER: terrain = T_LIQUID; liquid = L_WATER; break;
+ case FT_LAVA: terrain = T_LIQUID; liquid = L_LAVA; break;
+ case FT_CHASM: terrain = T_LIQUID; liquid = L_CHASM; break;
+ case FT_STAIR_UP: terrain = T_STAIRS_UP; break;
+ case FT_STAIR_DOWN: terrain = T_STAIRS_DOWN; break;
+ case FT_BRIDGE: terrain = T_BRIDGE; break;
+ case FT_DOOR_SILL: terrain = T_DOOR; break;
+ default: terrain = T_NOTHING; break;
+ }
+}
+
+} // namespace
+
+static Array pack_group_arrays(const mesh_group_t *g) {
+ Array arrays;
+ arrays.resize(Mesh::ARRAY_MAX);
+
+ const size_t n = g->vert_count;
+ PackedVector3Array verts; verts.resize((int)n);
+ PackedVector3Array normals; normals.resize((int)n);
+ PackedVector2Array uvs; uvs.resize((int)n);
+
+ Vector3 *vp = verts.ptrw();
+ Vector3 *np = normals.ptrw();
+ Vector2 *tp = uvs.ptrw();
+ for (size_t i = 0; i < n; i++) {
+ const mesh_vertex_t *v = &g->verts[i];
+ vp[i] = Vector3(v->px, v->py, v->pz);
+ np[i] = Vector3(v->nx, v->ny, v->nz);
+ tp[i] = Vector2(v->u, v->v);
+ }
+ arrays[Mesh::ARRAY_VERTEX] = verts;
+ arrays[Mesh::ARRAY_NORMAL] = normals;
+ arrays[Mesh::ARRAY_TEX_UV] = uvs;
+ return arrays;
+}
+
+Dictionary godot::dungeon_to_dictionary(const dungeon_t *d, const mesh_build_t *mb,
+ int seed, int depth, int num_levels) {
+ Dictionary out;
+ out["seed"] = seed;
+ out["depth"] = depth;
+ out["num_levels"] = num_levels;
+ out["cell_size"] = mb->cell_size;
+ out["cell_stride"] = (int)sizeof(cell3d_t);
+ out["dimensions"] = Vector3i(BL_DCOLS, num_levels, BL_DROWS);
+
+ // Raw voxel cell data for future mutation. Order: z-major (level,y,x).
+ const size_t n_cells = (size_t)num_levels * BL_DROWS * BL_DCOLS;
+ PackedByteArray cells;
+ cells.resize((int)(n_cells * sizeof(cell3d_t)));
+ memcpy(cells.ptrw(), d->cells, n_cells * sizeof(cell3d_t));
+ out["cells"] = cells;
+
+ // Per-material mesh surfaces. Only materials with content are emitted.
+ Array meshes;
+ for (int m = 0; m < MAT_COUNT; m++) {
+ const mesh_group_t *g = &mb->groups[m];
+ if (g->vert_count == 0) continue;
+ Dictionary surf;
+ surf["material"] = m;
+ surf["tri_count"] = (int)(g->vert_count / 3);
+ surf["arrays"] = pack_group_arrays(g);
+ meshes.push_back(surf);
+ }
+ out["meshes"] = meshes;
+
+ // Per-level metadata.
+ Array levels;
+ for (int i = 0; i < d->n_levels; i++) {
+ const b_level_info_t *info = &d->levels[i];
+ Dictionary lvl;
+ lvl["index"] = i;
+ lvl["stairs_up"] = Vector2i(info->stairs_up_x, info->stairs_up_y);
+ lvl["stairs_down"] = Vector2i(info->stairs_down_x, info->stairs_down_y);
+ levels.push_back(lvl);
+ }
+ out["levels"] = levels;
+
+ // Plan reserved for PR 3+; keep the key present so consumers can rely on it.
+ Dictionary plan;
+ plan["stairs"] = Array();
+ plan["chasms"] = Array();
+ plan["waterfalls"] = Array();
+ plan["footprints"] = Array();
+ out["plan"] = plan;
+
+ return out;
+}
+
+TypedArray godot::dungeon_to_2d_slices(const dungeon_t *d) {
+ TypedArray slices;
+ if (!d) return slices;
+
+ const int w = BL_DCOLS;
+ const int h = BL_DROWS;
+ const int per_level = w * h;
+
+ for (int lvl = 0; lvl < d->n_levels; lvl++) {
+ PackedByteArray terrain; terrain.resize(per_level);
+ PackedByteArray liquid; liquid.resize(per_level);
+ uint8_t *tp = terrain.ptrw();
+ uint8_t *lp = liquid.ptrw();
+
+ const cell3d_t *base = &d->cells[lvl * per_level];
+ for (int i = 0; i < per_level; i++) {
+ uint8_t t, l;
+ floor_to_2d(base[i].floor, t, l);
+ tp[i] = t;
+ lp[i] = l;
+ }
+
+ Dictionary slice;
+ slice["width"] = w;
+ slice["height"] = h;
+ slice["terrain"] = terrain;
+ slice["liquid"] = liquid;
+ slices.push_back(slice);
+ }
+
+ return slices;
+}
diff --git a/godot/src/dungeon_to_dict.h b/godot/src/dungeon_to_dict.h
new file mode 100644
index 0000000..a155f00
--- /dev/null
+++ b/godot/src/dungeon_to_dict.h
@@ -0,0 +1,28 @@
+#pragma once
+
+#include
+#include
+
+extern "C" {
+#include "blobber/dungeon.h"
+#include "mesh/face_mesh.h"
+}
+
+namespace godot {
+
+/* Pack dungeon_t + mesh_build_t into a Dictionary suitable for return from
+ BrogueGen.generate_dungeon(). Keys: seed, num_levels, dimensions,
+ cell_size, cells (PackedByteArray of raw cell3d_t), meshes
+ (Array of per-material surface arrays), levels (metadata), plan (empty
+ for PR 1). */
+Dictionary dungeon_to_dictionary(const dungeon_t *d, const mesh_build_t *mb,
+ int seed, int depth, int num_levels);
+
+/* Flatten a 3D blobber dungeon into an Array of per-level 2D slices shaped
+ like legacy brogue's grid_to_dictionary() output: each entry is a
+ Dictionary with {width, height, terrain: PackedByteArray,
+ liquid: PackedByteArray}. Terrain / liquid values match the T_* / L_*
+ enums in src/gen/grid.h so the existing .map exporter can consume them. */
+TypedArray dungeon_to_2d_slices(const dungeon_t *d);
+
+} // namespace godot
diff --git a/src/blobber/cell3d.h b/src/blobber/cell3d.h
new file mode 100644
index 0000000..d0f728e
--- /dev/null
+++ b/src/blobber/cell3d.h
@@ -0,0 +1,82 @@
+#ifndef GENESIS_BLOBBER_CELL3D_H
+#define GENESIS_BLOBBER_CELL3D_H
+
+#include
+
+#define BL_DCOLS 79
+#define BL_DROWS 29
+
+/* Floor types are prefixed FT_ to avoid colliding with the legacy
+ gen/grid.h cell_flag_t values (F_IN_ROOM, F_BRIDGE flag, ...). */
+typedef enum {
+ FT_VOID = 0, /* fully solid \u2014 no cell you can stand in */
+ FT_STONE = 1,
+ FT_WATER = 2,
+ FT_LAVA = 3,
+ FT_CHASM = 4, /* no floor; fall through to cell below */
+ FT_STAIR_UP = 5,
+ FT_STAIR_DOWN= 6,
+ FT_BRIDGE = 7,
+ FT_DOOR_SILL = 8,
+} b_floor_t;
+
+typedef enum {
+ C_OPEN = 0, /* open to cell above (chasm ceiling / sky shaft) */
+ C_STONE = 1,
+ C_SKY = 2,
+} b_ceiling_t;
+
+typedef enum {
+ W_NONE = 0, /* open \u2014 party can pass */
+ W_SOLID = 1,
+ W_DOOR = 2,
+ W_SECRET = 3,
+ W_ARCH = 4, /* visible opening, decorative frame */
+} b_wall_t;
+
+typedef enum {
+ FACE_N = 0, /* \u2212y */
+ FACE_E = 1, /* +x */
+ FACE_S = 2, /* +y */
+ FACE_W = 3, /* \u2212x */
+} b_face_t;
+
+typedef enum {
+ S_VOID = 0,
+ S_ROOM = 1,
+ S_CORRIDOR = 2,
+ S_DOOR = 3,
+ S_CAVE = 4, /* rougher material; still blocky geometry */
+ S_MACHINE = 5,
+} b_style_t;
+
+typedef enum {
+ BF_IN_LOOP = 1u << 0,
+ BF_CHOKEPT = 1u << 1,
+ BF_IN_MACHINE = 1u << 2,
+ BF_MACHINE_GATE = 1u << 3,
+ BF_MACHINE_IMPREG = 1u << 4,
+ BF_WREATH = 1u << 5,
+} b_flag_t;
+
+typedef struct { /* 10 bytes */
+ uint8_t floor; /* b_floor_t */
+ uint8_t ceiling; /* b_ceiling_t */
+ uint8_t walls[4]; /* b_wall_t per b_face_t (N,E,S,W) */
+ uint8_t marker; /* MK_* from src/gen/grid.h (reused enum) */
+ uint8_t style; /* b_style_t */
+ uint16_t room_id; /* 0 = not in a room */
+ uint8_t machine_id;
+ uint8_t flags; /* b_flag_t bitfield */
+} cell3d_t;
+
+static inline int b_in_bounds(int x, int y) {
+ return x >= 0 && x < BL_DCOLS && y >= 0 && y < BL_DROWS;
+}
+
+static inline int b_floor_is_standable(uint8_t f) {
+ return f == FT_STONE || f == FT_BRIDGE || f == FT_DOOR_SILL
+ || f == FT_STAIR_UP || f == FT_STAIR_DOWN;
+}
+
+#endif
diff --git a/src/blobber/dungeon.c b/src/blobber/dungeon.c
new file mode 100644
index 0000000..f759740
--- /dev/null
+++ b/src/blobber/dungeon.c
@@ -0,0 +1,29 @@
+#include "dungeon.h"
+#include
+#include
+
+dungeon_t *dungeon_create(int n_levels) {
+ if (n_levels < 1) n_levels = 1;
+ dungeon_t *d = (dungeon_t *)calloc(1, sizeof(*d));
+ if (!d) return NULL;
+ d->n_levels = n_levels;
+ size_t n_cells = (size_t)n_levels * BL_DROWS * BL_DCOLS;
+ d->cells = (cell3d_t *)calloc(n_cells, sizeof(cell3d_t));
+ d->levels = (b_level_info_t *)calloc((size_t)n_levels, sizeof(b_level_info_t));
+ if (!d->cells || !d->levels) {
+ dungeon_destroy(d);
+ return NULL;
+ }
+ for (int i = 0; i < n_levels; i++) {
+ d->levels[i].stairs_up_x = d->levels[i].stairs_up_y = -1;
+ d->levels[i].stairs_down_x = d->levels[i].stairs_down_y = -1;
+ }
+ return d;
+}
+
+void dungeon_destroy(dungeon_t *d) {
+ if (!d) return;
+ free(d->cells);
+ free(d->levels);
+ free(d);
+}
diff --git a/src/blobber/dungeon.h b/src/blobber/dungeon.h
new file mode 100644
index 0000000..337625f
--- /dev/null
+++ b/src/blobber/dungeon.h
@@ -0,0 +1,28 @@
+#ifndef GENESIS_BLOBBER_DUNGEON_H
+#define GENESIS_BLOBBER_DUNGEON_H
+
+#include
+#include "cell3d.h"
+
+typedef struct {
+ int stairs_up_x, stairs_up_y; /* -1 if not set */
+ int stairs_down_x, stairs_down_y; /* -1 if not set */
+} b_level_info_t;
+
+typedef struct {
+ int n_levels;
+ cell3d_t *cells; /* contiguous: cells[(lvl*BL_DROWS + y)*BL_DCOLS + x] */
+ b_level_info_t *levels; /* length n_levels */
+} dungeon_t;
+
+dungeon_t *dungeon_create(int n_levels);
+void dungeon_destroy(dungeon_t *d);
+
+static inline cell3d_t *dungeon_cell(dungeon_t *d, int lvl, int x, int y) {
+ return &d->cells[(lvl * BL_DROWS + y) * BL_DCOLS + x];
+}
+static inline const cell3d_t *dungeon_cell_c(const dungeon_t *d, int lvl, int x, int y) {
+ return &d->cells[(lvl * BL_DROWS + y) * BL_DCOLS + x];
+}
+
+#endif
diff --git a/src/blobber/pipeline.c b/src/blobber/pipeline.c
new file mode 100644
index 0000000..7fc5309
--- /dev/null
+++ b/src/blobber/pipeline.c
@@ -0,0 +1,218 @@
+#include "pipeline.h"
+
+#include
+
+#include "../gen/grid.h"
+#include "../gen/rng.h"
+#include "../gen/events.h"
+#include "../gen/accretion.h"
+#include "../gen/loops.h"
+#include "../gen/walls.h"
+#include "../gen/stairs.h"
+
+/* Map a 2D terrain byte to its 3D floor type. */
+static uint8_t floor_from_terrain(uint8_t t, uint8_t liquid) {
+ switch (t) {
+ case T_FLOOR: return FT_STONE;
+ case T_CORRIDOR: return FT_STONE;
+ case T_DOOR: return FT_DOOR_SILL;
+ case T_STAIRS_UP: return FT_STAIR_UP;
+ case T_STAIRS_DOWN: return FT_STAIR_DOWN;
+ case T_BRIDGE: return FT_BRIDGE;
+ case T_LIQUID:
+ switch (liquid) {
+ case L_WATER: case L_BRIMSTONE: return FT_WATER;
+ case L_LAVA: return FT_LAVA;
+ case L_CHASM: return FT_CHASM;
+ default: return FT_WATER;
+ }
+ case T_WALL:
+ case T_NOTHING:
+ default:
+ return FT_VOID;
+ }
+}
+
+static uint8_t style_from_terrain(uint8_t t) {
+ switch (t) {
+ case T_FLOOR: return S_ROOM;
+ case T_CORRIDOR: return S_CORRIDOR;
+ case T_DOOR: return S_DOOR;
+ default: return S_VOID;
+ }
+}
+
+/* For each standable cell, if the 4-neighbor isn't standable, we have a solid
+ wall on that face. "Standable" in a 2D sense = any floor that isn't FT_VOID.
+ Chasms (F_CHASM) are walkable-over-but-fall-through; we still put walls
+ around them when the neighbor is solid, but they're a later-PR concern. */
+static void compute_face_walls(dungeon_t *d, int lvl) {
+ static const int dx[4] = { 0, 1, 0, -1 };
+ static const int dy[4] = {-1, 0, 1, 0 };
+ for (int y = 0; y < BL_DROWS; y++) {
+ for (int x = 0; x < BL_DCOLS; x++) {
+ cell3d_t *c = dungeon_cell(d, lvl, x, y);
+ if (c->floor == FT_VOID) continue;
+ for (int f = 0; f < 4; f++) {
+ int nx = x + dx[f];
+ int ny = y + dy[f];
+ int solid;
+ if (!b_in_bounds(nx, ny)) {
+ solid = 1;
+ } else {
+ const cell3d_t *n = dungeon_cell_c(d, lvl, nx, ny);
+ solid = (n->floor == FT_VOID);
+ }
+ c->walls[f] = solid ? W_SOLID : W_NONE;
+ }
+ }
+ }
+}
+
+/* Run the legacy 2D pipeline into a scratch grid_t. */
+static void gen_level_2d(grid_t g, uint64_t seed, int depth) {
+ rng_t rng;
+ rng_seed(&rng, seed);
+ grid_fill(g, T_NOTHING);
+
+ event_sink_t sink = { .fn = NULL, .ctx = NULL, .step = 0 };
+ (void)depth; /* lakes/machines deferred to later PRs */
+
+ accrete_rooms(g, &rng, &sink);
+ add_loops(g, &sink);
+ finish_walls(g, &sink);
+ place_stairs(g, &sink);
+}
+
+static void materialize_level(dungeon_t *d, int lvl, grid_t g) {
+ b_level_info_t *info = &d->levels[lvl];
+ for (int y = 0; y < BL_DROWS; y++) {
+ for (int x = 0; x < BL_DCOLS; x++) {
+ const cell_t *src = &g[y][x];
+ cell3d_t *dst = dungeon_cell(d, lvl, x, y);
+
+ dst->floor = floor_from_terrain(src->terrain, src->liquid);
+ dst->ceiling = C_STONE;
+ dst->marker = src->surface;
+ dst->style = style_from_terrain(src->terrain);
+ dst->room_id = src->room_id;
+ dst->machine_id = src->machine_id;
+ dst->flags = 0;
+
+ if (src->terrain == T_STAIRS_UP) {
+ info->stairs_up_x = x; info->stairs_up_y = y;
+ } else if (src->terrain == T_STAIRS_DOWN) {
+ info->stairs_down_x = x; info->stairs_down_y = y;
+ }
+ }
+ }
+ compute_face_walls(d, lvl);
+}
+
+/* Count cells that are FT_STONE (and optionally S_ROOM) on both levels. */
+static int count_shared_floor(const dungeon_t *d, int la, int lb, int require_room) {
+ int n = 0;
+ for (int y = 0; y < BL_DROWS; y++) {
+ for (int x = 0; x < BL_DCOLS; x++) {
+ const cell3d_t *ca = dungeon_cell_c(d, la, x, y);
+ const cell3d_t *cb = dungeon_cell_c(d, lb, x, y);
+ if (ca->floor != FT_STONE || cb->floor != FT_STONE) continue;
+ if (require_room && (ca->style != S_ROOM || cb->style != S_ROOM)) continue;
+ n++;
+ }
+ }
+ return n;
+}
+
+static int pick_nth_shared(const dungeon_t *d, int la, int lb, int require_room,
+ int target, int *ox, int *oy) {
+ int n = 0;
+ for (int y = 0; y < BL_DROWS; y++) {
+ for (int x = 0; x < BL_DCOLS; x++) {
+ const cell3d_t *ca = dungeon_cell_c(d, la, x, y);
+ const cell3d_t *cb = dungeon_cell_c(d, lb, x, y);
+ if (ca->floor != FT_STONE || cb->floor != FT_STONE) continue;
+ if (require_room && (ca->style != S_ROOM || cb->style != S_ROOM)) continue;
+ if (n == target) { *ox = x; *oy = y; return 1; }
+ n++;
+ }
+ }
+ return 0;
+}
+
+/* If (x,y) on level lvl currently holds expected_floor, reset it to FT_STONE.
+ Used to undo the per-level 2D stair placement before dropping an aligned
+ stair pair at a new XY. */
+static void clear_stair_cell(dungeon_t *d, int lvl, int x, int y, uint8_t expected_floor) {
+ if (x < 0 || y < 0 || !b_in_bounds(x, y)) return;
+ cell3d_t *c = dungeon_cell(d, lvl, x, y);
+ if (c->floor == expected_floor) c->floor = FT_STONE;
+}
+
+/* Replace each level's per-level 2D stair placement with aligned stair pairs
+ so level L's down-stair and level L+1's up-stair share the same XY. The
+ bottom level's down-stair is removed (no level below). The top level keeps
+ its existing up-stair as the party's entry point. Does nothing when
+ n_levels < 2. */
+static void link_stairs(dungeon_t *d, uint64_t seed) {
+ if (d->n_levels < 2) return;
+
+ rng_t rng;
+ rng_seed(&rng, seed ^ 0xB106B3D5B4B83A07ULL);
+
+ for (int lvl = 0; lvl < d->n_levels - 1; lvl++) {
+ clear_stair_cell(d, lvl, d->levels[lvl].stairs_down_x,
+ d->levels[lvl].stairs_down_y, FT_STAIR_DOWN);
+ clear_stair_cell(d, lvl + 1, d->levels[lvl + 1].stairs_up_x,
+ d->levels[lvl + 1].stairs_up_y, FT_STAIR_UP);
+
+ int sx = -1, sy = -1;
+ int n_rooms = count_shared_floor(d, lvl, lvl + 1, 1);
+ if (n_rooms > 0) {
+ int pick = (int)(rng_u32(&rng) % (uint32_t)n_rooms);
+ pick_nth_shared(d, lvl, lvl + 1, 1, pick, &sx, &sy);
+ } else {
+ int n_any = count_shared_floor(d, lvl, lvl + 1, 0);
+ if (n_any > 0) {
+ int pick = (int)(rng_u32(&rng) % (uint32_t)n_any);
+ pick_nth_shared(d, lvl, lvl + 1, 0, pick, &sx, &sy);
+ }
+ }
+ if (sx < 0) {
+ /* No shared standable cell — leave this boundary unlinked. */
+ d->levels[lvl].stairs_down_x = -1;
+ d->levels[lvl].stairs_down_y = -1;
+ d->levels[lvl + 1].stairs_up_x = -1;
+ d->levels[lvl + 1].stairs_up_y = -1;
+ continue;
+ }
+
+ dungeon_cell(d, lvl, sx, sy)->floor = FT_STAIR_DOWN;
+ dungeon_cell(d, lvl + 1, sx, sy)->floor = FT_STAIR_UP;
+ d->levels[lvl].stairs_down_x = sx;
+ d->levels[lvl].stairs_down_y = sy;
+ d->levels[lvl + 1].stairs_up_x = sx;
+ d->levels[lvl + 1].stairs_up_y = sy;
+ }
+
+ int last = d->n_levels - 1;
+ clear_stair_cell(d, last, d->levels[last].stairs_down_x,
+ d->levels[last].stairs_down_y, FT_STAIR_DOWN);
+ d->levels[last].stairs_down_x = -1;
+ d->levels[last].stairs_down_y = -1;
+}
+
+dungeon_t *blobber_generate(uint64_t seed, int n_levels, int depth) {
+ if (n_levels < 1) n_levels = 1;
+ dungeon_t *d = dungeon_create(n_levels);
+ if (!d) return NULL;
+
+ grid_t g;
+ for (int lvl = 0; lvl < n_levels; lvl++) {
+ uint64_t level_seed = seed + (uint64_t)lvl * 0x9E3779B97F4A7C15ULL;
+ gen_level_2d(g, level_seed, depth);
+ materialize_level(d, lvl, g);
+ }
+ link_stairs(d, seed);
+ return d;
+}
diff --git a/src/blobber/pipeline.h b/src/blobber/pipeline.h
new file mode 100644
index 0000000..3c83371
--- /dev/null
+++ b/src/blobber/pipeline.h
@@ -0,0 +1,20 @@
+#ifndef GENESIS_BLOBBER_PIPELINE_H
+#define GENESIS_BLOBBER_PIPELINE_H
+
+#include
+#include "dungeon.h"
+
+/* Generate a dungeon using the existing 2D pipeline under the hood, then
+ materialize into a 3D cell3d_t grid with per-face wall flags.
+
+ Each of the n_levels runs an independent 2D generation with seed +
+ level_index. When n_levels > 1, a post-pass replaces the per-level stair
+ placement with aligned pairs: level L's down-stair and level L+1's
+ up-stair share the same XY. The bottom level has no down-stair, the top
+ level keeps its up-stair as the party's entry point.
+
+ Returns a newly-allocated dungeon_t; caller must dungeon_destroy() it.
+ Returns NULL on allocation failure. */
+dungeon_t *blobber_generate(uint64_t seed, int n_levels, int depth);
+
+#endif
diff --git a/src/genesis3d_main.c b/src/genesis3d_main.c
new file mode 100644
index 0000000..8c021a6
--- /dev/null
+++ b/src/genesis3d_main.c
@@ -0,0 +1,111 @@
+#include
+#include
+#include
+
+#include "blobber/pipeline.h"
+#include "blobber/dungeon.h"
+#include "mesh/face_mesh.h"
+
+static void usage(FILE *f, const char *prog) {
+ fprintf(f,
+ "usage: %s [options]\n"
+ "\n"
+ "Generates a 3D blobber dungeon (cell3d grid + per-material mesh).\n"
+ "\n"
+ "options:\n"
+ " --seed N RNG seed (default 1234)\n"
+ " --levels N number of stacked levels (default 1)\n"
+ " --depth N dungeon depth bias, 1..26 (default 1)\n"
+ " --cell-size F world size of one cell in meters (default 3.0)\n"
+ " --obj PATH write an .obj dump of the mesh to PATH\n"
+ " --stats print tri/cell stats to stderr (default on)\n"
+ " --quiet suppress stats output\n"
+ " -h, --help show this help and exit\n",
+ prog);
+}
+
+int main(int argc, char **argv) {
+ uint64_t seed = 1234;
+ int n_levels = 1;
+ int depth = 1;
+ float cell_size = 3.0f;
+ const char *obj_path = NULL;
+ int want_stats = 1;
+
+ for (int i = 1; i < argc; i++) {
+ if (!strcmp(argv[i], "--seed") && i + 1 < argc) {
+ seed = strtoull(argv[++i], NULL, 10);
+ } else if (!strcmp(argv[i], "--levels") && i + 1 < argc) {
+ n_levels = atoi(argv[++i]);
+ if (n_levels < 1) n_levels = 1;
+ } else if (!strcmp(argv[i], "--depth") && i + 1 < argc) {
+ depth = atoi(argv[++i]);
+ if (depth < 1) depth = 1;
+ } else if (!strcmp(argv[i], "--cell-size") && i + 1 < argc) {
+ cell_size = (float)atof(argv[++i]);
+ } else if (!strcmp(argv[i], "--obj") && i + 1 < argc) {
+ obj_path = argv[++i];
+ } else if (!strcmp(argv[i], "--stats")) {
+ want_stats = 1;
+ } else if (!strcmp(argv[i], "--quiet")) {
+ want_stats = 0;
+ } else if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) {
+ usage(stdout, argv[0]);
+ return 0;
+ } else {
+ fprintf(stderr, "unknown arg: %s\n", argv[i]);
+ usage(stderr, argv[0]);
+ return 2;
+ }
+ }
+
+ dungeon_t *d = blobber_generate(seed, n_levels, depth);
+ if (!d) {
+ fprintf(stderr, "generation failed\n");
+ return 1;
+ }
+
+ mesh_build_t *mb = mesh_build_from_dungeon(d, cell_size);
+ if (!mb) {
+ fprintf(stderr, "mesh build failed\n");
+ dungeon_destroy(d);
+ return 1;
+ }
+
+ if (obj_path) {
+ if (!mesh_build_write_obj(mb, obj_path)) {
+ fprintf(stderr, "could not write %s\n", obj_path);
+ mesh_build_destroy(mb);
+ dungeon_destroy(d);
+ return 1;
+ }
+ fprintf(stderr, "wrote %s\n", obj_path);
+ }
+
+ if (want_stats) {
+ size_t total_verts = 0, total_tris = 0;
+ for (int m = 0; m < MAT_COUNT; m++) total_verts += mb->groups[m].vert_count;
+ total_tris = total_verts / 3;
+ int standable = 0;
+ for (int lvl = 0; lvl < d->n_levels; lvl++) {
+ for (int y = 0; y < BL_DROWS; y++) {
+ for (int x = 0; x < BL_DCOLS; x++) {
+ if (b_floor_is_standable(dungeon_cell_c(d, lvl, x, y)->floor))
+ standable++;
+ }
+ }
+ }
+ fprintf(stderr,
+ "seed=%llu levels=%d depth=%d cell=%.2f standable_cells=%d tris=%zu\n",
+ (unsigned long long)seed, n_levels, depth, (double)cell_size,
+ standable, total_tris);
+ for (int m = 0; m < MAT_COUNT; m++) {
+ if (mb->groups[m].vert_count == 0) continue;
+ fprintf(stderr, " mat %2d: %zu tris\n", m, mb->groups[m].vert_count / 3);
+ }
+ }
+
+ mesh_build_destroy(mb);
+ dungeon_destroy(d);
+ return 0;
+}
diff --git a/src/genesis_main.c b/src/genesis_main.c
index 6dbec6a..c50dcb6 100644
--- a/src/genesis_main.c
+++ b/src/genesis_main.c
@@ -148,6 +148,26 @@ static void print_summary(uint64_t seed, int depth, const run_stats_t *s) {
s->floor_cells, s->connected, s->unreached);
}
+static void usage(FILE *f, const char *prog) {
+ fprintf(f,
+ "usage: %s [options]\n"
+ "\n"
+ "Generates a single Brogue-style 2D level and prints an ASCII map.\n"
+ "\n"
+ "options:\n"
+ " --seed N RNG seed (default 1234)\n"
+ " --depth N dungeon depth 1..26, biases lakes/machines (default 1)\n"
+ " --verify assert connectivity after each pipeline stage\n"
+ " --mark-unreached print '!' on passable cells BFS cannot reach from start\n"
+ " --verbose stream generator events to stderr\n"
+ " --quiet suppress ASCII map (summary only)\n"
+ " --emit=json, --json emit grid JSON to stdout instead of ASCII\n"
+ " --stress N run N seeds (from --stress-base) and report failures\n"
+ " --stress-base N starting seed for --stress (default 1)\n"
+ " -h, --help show this help and exit\n",
+ prog);
+}
+
static int cmd_stress(int n, uint64_t base_seed, int depth) {
grid_t g;
int failures = 0;
@@ -193,6 +213,13 @@ int main(int argc, char **argv) {
g_mark_unreached = 1;
} else if (!strcmp(argv[i], "--emit=json") || !strcmp(argv[i], "--json")) {
g_emit_json = 1;
+ } else if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) {
+ usage(stdout, argv[0]);
+ return 0;
+ } else {
+ fprintf(stderr, "unknown arg: %s\n", argv[i]);
+ usage(stderr, argv[0]);
+ return 2;
}
}
diff --git a/src/mesh/face_mesh.c b/src/mesh/face_mesh.c
new file mode 100644
index 0000000..0b587fb
--- /dev/null
+++ b/src/mesh/face_mesh.c
@@ -0,0 +1,227 @@
+#include "face_mesh.h"
+
+#include
+#include
+#include
+
+/* ---- per-group dynamic array -------------------------------------------- */
+
+static int group_reserve(mesh_group_t *g, size_t extra) {
+ size_t need = g->vert_count + extra;
+ if (need <= g->capacity) return 1;
+ size_t cap = g->capacity ? g->capacity : 256;
+ while (cap < need) cap *= 2;
+ mesh_vertex_t *nv = (mesh_vertex_t *)realloc(g->verts, cap * sizeof(*nv));
+ if (!nv) return 0;
+ g->verts = nv;
+ g->capacity = cap;
+ return 1;
+}
+
+static void push_v(mesh_group_t *g,
+ float px, float py, float pz,
+ float nx, float ny, float nz,
+ float u, float v) {
+ mesh_vertex_t *mv = &g->verts[g->vert_count++];
+ mv->px = px; mv->py = py; mv->pz = pz;
+ mv->nx = nx; mv->ny = ny; mv->nz = nz;
+ mv->u = u; mv->v = v;
+}
+
+/* Emit a quad as two triangles with outward normal = (nx,ny,nz). Vertices are
+ passed in CCW order when viewed from the normal side. UV is a simple
+ 0..1 square mapped corner-to-corner. */
+static int emit_quad(mesh_group_t *g,
+ float x0, float y0, float z0,
+ float x1, float y1, float z1,
+ float x2, float y2, float z2,
+ float x3, float y3, float z3,
+ float nx, float ny, float nz) {
+ if (!group_reserve(g, 6)) return 0;
+ push_v(g, x0, y0, z0, nx, ny, nz, 0.0f, 0.0f);
+ push_v(g, x1, y1, z1, nx, ny, nz, 1.0f, 0.0f);
+ push_v(g, x2, y2, z2, nx, ny, nz, 1.0f, 1.0f);
+ push_v(g, x0, y0, z0, nx, ny, nz, 0.0f, 0.0f);
+ push_v(g, x2, y2, z2, nx, ny, nz, 1.0f, 1.0f);
+ push_v(g, x3, y3, z3, nx, ny, nz, 0.0f, 1.0f);
+ return 1;
+}
+
+/* ---- material selection -------------------------------------------------- */
+
+static int floor_material(const cell3d_t *c) {
+ switch (c->floor) {
+ case FT_DOOR_SILL: return MAT_DOOR_FLOOR;
+ case FT_STAIR_UP: return MAT_STAIR_UP;
+ case FT_STAIR_DOWN: return MAT_STAIR_DOWN;
+ case FT_BRIDGE: return MAT_BRIDGE;
+ case FT_WATER: return MAT_WATER;
+ case FT_LAVA: return MAT_LAVA;
+ default: break;
+ }
+ return (c->style == S_CAVE) ? MAT_CAVE_FLOOR : MAT_STONE_FLOOR;
+}
+static int ceiling_material(const cell3d_t *c) {
+ return (c->style == S_CAVE) ? MAT_CAVE_CEILING : MAT_STONE_CEILING;
+}
+static int wall_material(const cell3d_t *c) {
+ return (c->style == S_CAVE) ? MAT_CAVE_WALL : MAT_STONE_WALL;
+}
+
+/* ---- builder ------------------------------------------------------------- */
+
+mesh_build_t *mesh_build_from_dungeon(const dungeon_t *d, float cell_size) {
+ mesh_build_t *b = (mesh_build_t *)calloc(1, sizeof(*b));
+ if (!b) return NULL;
+ b->cell_size = cell_size;
+ b->n_levels = d->n_levels;
+ for (int m = 0; m < MAT_COUNT; m++) b->groups[m].material = m;
+
+ const float s = cell_size;
+
+ for (int lvl = 0; lvl < d->n_levels; lvl++) {
+ /* Level N sits at world-Y in [ -(lvl+1)*s , -lvl*s ]. Level 0 is at
+ the top; descending increases lvl and decreases Y. */
+ const float y_ceil = -(float)lvl * s;
+ const float y_floor = y_ceil - s;
+
+ for (int y = 0; y < BL_DROWS; y++) {
+ for (int x = 0; x < BL_DCOLS; x++) {
+ const cell3d_t *c = dungeon_cell_c(d, lvl, x, y);
+ if (c->floor == FT_VOID) continue;
+
+ const float x0 = (float)x * s;
+ const float x1 = x0 + s;
+ const float z0 = (float)y * s;
+ const float z1 = z0 + s;
+
+ /* Floor (skip chasms \u2014 open drop to level below). */
+ if (c->floor != FT_CHASM) {
+ mesh_group_t *g = &b->groups[floor_material(c)];
+ /* Normal +Y; CCW viewed from +Y means corners in order
+ (x0,z0) (x1,z0) (x1,z1) (x0,z1). */
+ emit_quad(g,
+ x0, y_floor, z0,
+ x1, y_floor, z0,
+ x1, y_floor, z1,
+ x0, y_floor, z1,
+ 0.0f, 1.0f, 0.0f);
+ }
+
+ /* Ceiling (skip when open to above \u2014 chasm-mirrored cell). */
+ if (c->ceiling != C_OPEN) {
+ mesh_group_t *g = &b->groups[ceiling_material(c)];
+ /* Normal \u2212Y; CCW viewed from \u2212Y = reverse winding. */
+ emit_quad(g,
+ x0, y_ceil, z1,
+ x1, y_ceil, z1,
+ x1, y_ceil, z0,
+ x0, y_ceil, z0,
+ 0.0f, -1.0f, 0.0f);
+ }
+
+ /* Wall faces. Each wall face sits at the cell boundary with
+ its visible normal pointing INTO the cell (so the party
+ standing in the cell sees it). Winding CCW from +normal. */
+ mesh_group_t *gw = &b->groups[wall_material(c)];
+
+ /* FACE_N (\u2212Z / north): wall plane at z = z0, normal +Z. */
+ if (c->walls[FACE_N] != W_NONE) {
+ emit_quad(gw,
+ x0, y_floor, z0,
+ x0, y_ceil, z0,
+ x1, y_ceil, z0,
+ x1, y_floor, z0,
+ 0.0f, 0.0f, 1.0f);
+ }
+ /* FACE_E (+X / east): wall plane at x = x1, normal \u2212X. */
+ if (c->walls[FACE_E] != W_NONE) {
+ emit_quad(gw,
+ x1, y_floor, z0,
+ x1, y_ceil, z0,
+ x1, y_ceil, z1,
+ x1, y_floor, z1,
+ -1.0f, 0.0f, 0.0f);
+ }
+ /* FACE_S (+Z / south): wall plane at z = z1, normal \u2212Z. */
+ if (c->walls[FACE_S] != W_NONE) {
+ emit_quad(gw,
+ x1, y_floor, z1,
+ x1, y_ceil, z1,
+ x0, y_ceil, z1,
+ x0, y_floor, z1,
+ 0.0f, 0.0f, -1.0f);
+ }
+ /* FACE_W (\u2212X / west): wall plane at x = x0, normal +X. */
+ if (c->walls[FACE_W] != W_NONE) {
+ emit_quad(gw,
+ x0, y_floor, z1,
+ x0, y_ceil, z1,
+ x0, y_ceil, z0,
+ x0, y_floor, z0,
+ 1.0f, 0.0f, 0.0f);
+ }
+ }
+ }
+ }
+ return b;
+}
+
+void mesh_build_destroy(mesh_build_t *b) {
+ if (!b) return;
+ for (int m = 0; m < MAT_COUNT; m++) free(b->groups[m].verts);
+ free(b);
+}
+
+static const char *material_name(int m) {
+ switch (m) {
+ case MAT_STONE_FLOOR: return "stone_floor";
+ case MAT_STONE_CEILING: return "stone_ceiling";
+ case MAT_STONE_WALL: return "stone_wall";
+ case MAT_DOOR_FLOOR: return "door_floor";
+ case MAT_STAIR_UP: return "stair_up";
+ case MAT_STAIR_DOWN: return "stair_down";
+ case MAT_WATER: return "water";
+ case MAT_LAVA: return "lava";
+ case MAT_BRIDGE: return "bridge";
+ case MAT_CAVE_FLOOR: return "cave_floor";
+ case MAT_CAVE_CEILING: return "cave_ceiling";
+ case MAT_CAVE_WALL: return "cave_wall";
+ default: return "unknown";
+ }
+}
+
+int mesh_build_write_obj(const mesh_build_t *b, const char *path) {
+ FILE *f = fopen(path, "w");
+ if (!f) return 0;
+ fprintf(f, "# brogue-genesis blobber dungeon mesh\n");
+ size_t v_offset = 1; /* .obj uses 1-based indices */
+ for (int m = 0; m < MAT_COUNT; m++) {
+ const mesh_group_t *g = &b->groups[m];
+ if (g->vert_count == 0) continue;
+ fprintf(f, "\no %s\n", material_name(m));
+ for (size_t i = 0; i < g->vert_count; i++) {
+ const mesh_vertex_t *v = &g->verts[i];
+ fprintf(f, "v %.4f %.4f %.4f\n", v->px, v->py, v->pz);
+ }
+ for (size_t i = 0; i < g->vert_count; i++) {
+ const mesh_vertex_t *v = &g->verts[i];
+ fprintf(f, "vn %.4f %.4f %.4f\n", v->nx, v->ny, v->nz);
+ }
+ for (size_t i = 0; i < g->vert_count; i++) {
+ const mesh_vertex_t *v = &g->verts[i];
+ fprintf(f, "vt %.4f %.4f\n", v->u, v->v);
+ }
+ /* Face list: every 3 verts = one triangle. */
+ for (size_t i = 0; i + 2 < g->vert_count; i += 3) {
+ size_t a = v_offset + i;
+ size_t b2 = v_offset + i + 1;
+ size_t c = v_offset + i + 2;
+ fprintf(f, "f %zu/%zu/%zu %zu/%zu/%zu %zu/%zu/%zu\n",
+ a, a, a, b2, b2, b2, c, c, c);
+ }
+ v_offset += g->vert_count;
+ }
+ fclose(f);
+ return 1;
+}
diff --git a/src/mesh/face_mesh.h b/src/mesh/face_mesh.h
new file mode 100644
index 0000000..99881cf
--- /dev/null
+++ b/src/mesh/face_mesh.h
@@ -0,0 +1,36 @@
+#ifndef GENESIS_MESH_FACE_MESH_H
+#define GENESIS_MESH_FACE_MESH_H
+
+#include
+#include
+#include "material_ids.h"
+#include "../blobber/dungeon.h"
+
+typedef struct {
+ float px, py, pz;
+ float nx, ny, nz;
+ float u, v;
+} mesh_vertex_t;
+
+typedef struct {
+ int material; /* mat_id_t */
+ mesh_vertex_t *verts; /* length = vert_count, tri-list (no index buffer) */
+ size_t vert_count;
+ size_t capacity;
+} mesh_group_t;
+
+typedef struct {
+ mesh_group_t groups[MAT_COUNT]; /* groups[m].vert_count == 0 means "no geometry" */
+ float cell_size;
+ int n_levels;
+} mesh_build_t;
+
+/* Build all meshes from the dungeon using the given cell size (world units).
+ Caller must mesh_build_destroy() to free. */
+mesh_build_t *mesh_build_from_dungeon(const dungeon_t *d, float cell_size);
+void mesh_build_destroy(mesh_build_t *b);
+
+/* Convenience: write a Wavefront .obj (one object per material) to path. */
+int mesh_build_write_obj(const mesh_build_t *b, const char *path);
+
+#endif
diff --git a/src/mesh/material_ids.h b/src/mesh/material_ids.h
new file mode 100644
index 0000000..86c32d8
--- /dev/null
+++ b/src/mesh/material_ids.h
@@ -0,0 +1,23 @@
+#ifndef GENESIS_MESH_MATERIAL_IDS_H
+#define GENESIS_MESH_MATERIAL_IDS_H
+
+/* Material ids are stable across the C core, the CLI .obj dump, and the
+ GDExtension Dictionary. Demo GDScript maps these to Godot Material
+ resources. Values are the group indices used by mesh_build_t. */
+typedef enum {
+ MAT_STONE_FLOOR = 0,
+ MAT_STONE_CEILING = 1,
+ MAT_STONE_WALL = 2,
+ MAT_DOOR_FLOOR = 3,
+ MAT_STAIR_UP = 4,
+ MAT_STAIR_DOWN = 5,
+ MAT_WATER = 6,
+ MAT_LAVA = 7,
+ MAT_BRIDGE = 8,
+ MAT_CAVE_FLOOR = 9,
+ MAT_CAVE_CEILING = 10,
+ MAT_CAVE_WALL = 11,
+ MAT_COUNT = 12
+} mat_id_t;
+
+#endif