diff --git a/.gitignore b/.gitignore
index d0e8a59..97a5cbc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,4 +19,4 @@ addons/godot-jolt/windows/~godot-jolt_windows-x64_editor.dll
addons/terrain_3d/bin/~libterrain.windows.debug.x86_64.dll
build
-
+dist
diff --git a/.gutconfig.json b/.gutconfig
similarity index 100%
rename from .gutconfig.json
rename to .gutconfig
diff --git a/README.md b/README.md
index 02648be..2472362 100644
--- a/README.md
+++ b/README.md
@@ -8,10 +8,9 @@ Download `Godot Engine` at https://godotengine.org/download
## Getting started
-1. Clone the repository
-2. Open Godot Engine
-3. Navigate to the repository and import the `project.godot`
-4. Start coding or run the project!
+1. Clone the repository and open `Godot Engine`
+2. Navigate to the repository and import the `project.godot`
+3. Start coding or run the project!
## Contributing
@@ -21,7 +20,7 @@ We welcome contributions from the community, feel free to submit pull requests,
## Community
-Connect with fellow contributors, ask questions and stay updated on the latest developments by joining our [Discord](https://discord.gg/tdmV3MxCn5).
+Connect, ask questions and stay updated on the latest developments by joining our [Discord](https://discord.gg/tdmV3MxCn5).
## License
diff --git a/addons/terrain_3d/README.md b/addons/terrain_3d/README.md
index 0a57570..429ccec 100644
--- a/addons/terrain_3d/README.md
+++ b/addons/terrain_3d/README.md
@@ -17,7 +17,7 @@ See [Project Status](https://terrain3d.readthedocs.io/en/stable/docs/project_sta
## Getting Started
-1. Read through our [documentation](https://terrain3d.readthedocs.io/en/stable/index.html), starting with [Installation](https://terrain3d.readthedocs.io/en/stable/docs/installation.html).
+1. Read the [Installation](https://terrain3d.readthedocs.io/en/stable/docs/installation.html) instructions, and the rest of the [documentation](https://terrain3d.readthedocs.io/en/stable/index.html).
2. For support, read [Getting Help](https://terrain3d.readthedocs.io/en/stable/docs/getting_help.html) or join our [Discord server](https://tokisan.com/discord).
diff --git a/addons/terrain_3d/bin/libterrain.android.debug.arm32.so b/addons/terrain_3d/bin/libterrain.android.debug.arm32.so
index eb7604d..2c1fd71 100644
Binary files a/addons/terrain_3d/bin/libterrain.android.debug.arm32.so and b/addons/terrain_3d/bin/libterrain.android.debug.arm32.so differ
diff --git a/addons/terrain_3d/bin/libterrain.android.debug.arm64.so b/addons/terrain_3d/bin/libterrain.android.debug.arm64.so
index 243cedd..61222b1 100644
Binary files a/addons/terrain_3d/bin/libterrain.android.debug.arm64.so and b/addons/terrain_3d/bin/libterrain.android.debug.arm64.so differ
diff --git a/addons/terrain_3d/bin/libterrain.android.release.arm32.so b/addons/terrain_3d/bin/libterrain.android.release.arm32.so
index 8b618bf..87d3121 100644
Binary files a/addons/terrain_3d/bin/libterrain.android.release.arm32.so and b/addons/terrain_3d/bin/libterrain.android.release.arm32.so differ
diff --git a/addons/terrain_3d/bin/libterrain.android.release.arm64.so b/addons/terrain_3d/bin/libterrain.android.release.arm64.so
index e56a265..6db9bd5 100644
Binary files a/addons/terrain_3d/bin/libterrain.android.release.arm64.so and b/addons/terrain_3d/bin/libterrain.android.release.arm64.so differ
diff --git a/addons/terrain_3d/bin/libterrain.ios.debug.universal.dylib b/addons/terrain_3d/bin/libterrain.ios.debug.universal.dylib
index 0852752..ffec4f2 100644
Binary files a/addons/terrain_3d/bin/libterrain.ios.debug.universal.dylib and b/addons/terrain_3d/bin/libterrain.ios.debug.universal.dylib differ
diff --git a/addons/terrain_3d/bin/libterrain.ios.release.universal.dylib b/addons/terrain_3d/bin/libterrain.ios.release.universal.dylib
index 96ae88f..af713b7 100644
Binary files a/addons/terrain_3d/bin/libterrain.ios.release.universal.dylib and b/addons/terrain_3d/bin/libterrain.ios.release.universal.dylib differ
diff --git a/addons/terrain_3d/bin/libterrain.linux.debug.x86_64.so b/addons/terrain_3d/bin/libterrain.linux.debug.x86_64.so
index d024aa5..21531f0 100644
Binary files a/addons/terrain_3d/bin/libterrain.linux.debug.x86_64.so and b/addons/terrain_3d/bin/libterrain.linux.debug.x86_64.so differ
diff --git a/addons/terrain_3d/bin/libterrain.linux.release.x86_64.so b/addons/terrain_3d/bin/libterrain.linux.release.x86_64.so
index 64d0f8c..7681cbc 100644
Binary files a/addons/terrain_3d/bin/libterrain.linux.release.x86_64.so and b/addons/terrain_3d/bin/libterrain.linux.release.x86_64.so differ
diff --git a/addons/terrain_3d/bin/libterrain.macos.debug.framework/libterrain.macos.debug b/addons/terrain_3d/bin/libterrain.macos.debug.framework/libterrain.macos.debug
index 8f1d52b..a5300c3 100644
Binary files a/addons/terrain_3d/bin/libterrain.macos.debug.framework/libterrain.macos.debug and b/addons/terrain_3d/bin/libterrain.macos.debug.framework/libterrain.macos.debug differ
diff --git a/addons/terrain_3d/bin/libterrain.macos.release.framework/libterrain.macos.release b/addons/terrain_3d/bin/libterrain.macos.release.framework/libterrain.macos.release
index f141b33..e8cbc9b 100644
Binary files a/addons/terrain_3d/bin/libterrain.macos.release.framework/libterrain.macos.release and b/addons/terrain_3d/bin/libterrain.macos.release.framework/libterrain.macos.release differ
diff --git a/addons/terrain_3d/bin/libterrain.windows.debug.x86_64.dll b/addons/terrain_3d/bin/libterrain.windows.debug.x86_64.dll
index b58b6f2..0bfd004 100644
Binary files a/addons/terrain_3d/bin/libterrain.windows.debug.x86_64.dll and b/addons/terrain_3d/bin/libterrain.windows.debug.x86_64.dll differ
diff --git a/addons/terrain_3d/bin/libterrain.windows.release.x86_64.dll b/addons/terrain_3d/bin/libterrain.windows.release.x86_64.dll
index 7b6d4f2..d4b066f 100644
Binary files a/addons/terrain_3d/bin/libterrain.windows.release.x86_64.dll and b/addons/terrain_3d/bin/libterrain.windows.release.x86_64.dll differ
diff --git a/addons/terrain_3d/editor/brushes/.gdignore b/addons/terrain_3d/brushes/.gdignore
similarity index 100%
rename from addons/terrain_3d/editor/brushes/.gdignore
rename to addons/terrain_3d/brushes/.gdignore
diff --git a/addons/terrain_3d/editor/brushes/acrylic1.exr b/addons/terrain_3d/brushes/acrylic1.exr
similarity index 100%
rename from addons/terrain_3d/editor/brushes/acrylic1.exr
rename to addons/terrain_3d/brushes/acrylic1.exr
diff --git a/addons/terrain_3d/editor/brushes/circle0.exr b/addons/terrain_3d/brushes/circle0.exr
similarity index 100%
rename from addons/terrain_3d/editor/brushes/circle0.exr
rename to addons/terrain_3d/brushes/circle0.exr
diff --git a/addons/terrain_3d/editor/brushes/circle1.exr b/addons/terrain_3d/brushes/circle1.exr
similarity index 100%
rename from addons/terrain_3d/editor/brushes/circle1.exr
rename to addons/terrain_3d/brushes/circle1.exr
diff --git a/addons/terrain_3d/editor/brushes/circle2.exr b/addons/terrain_3d/brushes/circle2.exr
similarity index 100%
rename from addons/terrain_3d/editor/brushes/circle2.exr
rename to addons/terrain_3d/brushes/circle2.exr
diff --git a/addons/terrain_3d/editor/brushes/circle3.exr b/addons/terrain_3d/brushes/circle3.exr
similarity index 100%
rename from addons/terrain_3d/editor/brushes/circle3.exr
rename to addons/terrain_3d/brushes/circle3.exr
diff --git a/addons/terrain_3d/editor/brushes/circle4.exr b/addons/terrain_3d/brushes/circle4.exr
similarity index 100%
rename from addons/terrain_3d/editor/brushes/circle4.exr
rename to addons/terrain_3d/brushes/circle4.exr
diff --git a/addons/terrain_3d/editor/brushes/hill1.exr b/addons/terrain_3d/brushes/hill1.exr
similarity index 100%
rename from addons/terrain_3d/editor/brushes/hill1.exr
rename to addons/terrain_3d/brushes/hill1.exr
diff --git a/addons/terrain_3d/editor/brushes/hill2.exr b/addons/terrain_3d/brushes/hill2.exr
similarity index 100%
rename from addons/terrain_3d/editor/brushes/hill2.exr
rename to addons/terrain_3d/brushes/hill2.exr
diff --git a/addons/terrain_3d/editor/brushes/mountain1.exr b/addons/terrain_3d/brushes/mountain1.exr
similarity index 100%
rename from addons/terrain_3d/editor/brushes/mountain1.exr
rename to addons/terrain_3d/brushes/mountain1.exr
diff --git a/addons/terrain_3d/editor/brushes/mountain2.exr b/addons/terrain_3d/brushes/mountain2.exr
similarity index 100%
rename from addons/terrain_3d/editor/brushes/mountain2.exr
rename to addons/terrain_3d/brushes/mountain2.exr
diff --git a/addons/terrain_3d/editor/brushes/mountain3.exr b/addons/terrain_3d/brushes/mountain3.exr
similarity index 100%
rename from addons/terrain_3d/editor/brushes/mountain3.exr
rename to addons/terrain_3d/brushes/mountain3.exr
diff --git a/addons/terrain_3d/editor/brushes/mountain4.exr b/addons/terrain_3d/brushes/mountain4.exr
similarity index 100%
rename from addons/terrain_3d/editor/brushes/mountain4.exr
rename to addons/terrain_3d/brushes/mountain4.exr
diff --git a/addons/terrain_3d/editor/brushes/peak1.exr b/addons/terrain_3d/brushes/peak1.exr
similarity index 100%
rename from addons/terrain_3d/editor/brushes/peak1.exr
rename to addons/terrain_3d/brushes/peak1.exr
diff --git a/addons/terrain_3d/editor/brushes/peak2.exr b/addons/terrain_3d/brushes/peak2.exr
similarity index 100%
rename from addons/terrain_3d/editor/brushes/peak2.exr
rename to addons/terrain_3d/brushes/peak2.exr
diff --git a/addons/terrain_3d/editor/brushes/peak3.exr b/addons/terrain_3d/brushes/peak3.exr
similarity index 100%
rename from addons/terrain_3d/editor/brushes/peak3.exr
rename to addons/terrain_3d/brushes/peak3.exr
diff --git a/addons/terrain_3d/editor/brushes/ring1.exr b/addons/terrain_3d/brushes/ring1.exr
similarity index 100%
rename from addons/terrain_3d/editor/brushes/ring1.exr
rename to addons/terrain_3d/brushes/ring1.exr
diff --git a/addons/terrain_3d/editor/brushes/smoke.exr b/addons/terrain_3d/brushes/smoke.exr
similarity index 100%
rename from addons/terrain_3d/editor/brushes/smoke.exr
rename to addons/terrain_3d/brushes/smoke.exr
diff --git a/addons/terrain_3d/editor/brushes/square1.exr b/addons/terrain_3d/brushes/square1.exr
similarity index 100%
rename from addons/terrain_3d/editor/brushes/square1.exr
rename to addons/terrain_3d/brushes/square1.exr
diff --git a/addons/terrain_3d/editor/brushes/square2.exr b/addons/terrain_3d/brushes/square2.exr
similarity index 100%
rename from addons/terrain_3d/editor/brushes/square2.exr
rename to addons/terrain_3d/brushes/square2.exr
diff --git a/addons/terrain_3d/editor/brushes/square3.exr b/addons/terrain_3d/brushes/square3.exr
similarity index 100%
rename from addons/terrain_3d/editor/brushes/square3.exr
rename to addons/terrain_3d/brushes/square3.exr
diff --git a/addons/terrain_3d/editor/brushes/square4.exr b/addons/terrain_3d/brushes/square4.exr
similarity index 100%
rename from addons/terrain_3d/editor/brushes/square4.exr
rename to addons/terrain_3d/brushes/square4.exr
diff --git a/addons/terrain_3d/editor/brushes/square5.exr b/addons/terrain_3d/brushes/square5.exr
similarity index 100%
rename from addons/terrain_3d/editor/brushes/square5.exr
rename to addons/terrain_3d/brushes/square5.exr
diff --git a/addons/terrain_3d/editor/brushes/stones.exr b/addons/terrain_3d/brushes/stones.exr
similarity index 100%
rename from addons/terrain_3d/editor/brushes/stones.exr
rename to addons/terrain_3d/brushes/stones.exr
diff --git a/addons/terrain_3d/editor/brushes/terrain1.exr b/addons/terrain_3d/brushes/terrain1.exr
similarity index 100%
rename from addons/terrain_3d/editor/brushes/terrain1.exr
rename to addons/terrain_3d/brushes/terrain1.exr
diff --git a/addons/terrain_3d/editor/brushes/terrain2.exr b/addons/terrain_3d/brushes/terrain2.exr
similarity index 100%
rename from addons/terrain_3d/editor/brushes/terrain2.exr
rename to addons/terrain_3d/brushes/terrain2.exr
diff --git a/addons/terrain_3d/editor/brushes/terrain3.exr b/addons/terrain_3d/brushes/terrain3.exr
similarity index 100%
rename from addons/terrain_3d/editor/brushes/terrain3.exr
rename to addons/terrain_3d/brushes/terrain3.exr
diff --git a/addons/terrain_3d/editor/brushes/terrain4.exr b/addons/terrain_3d/brushes/terrain4.exr
similarity index 100%
rename from addons/terrain_3d/editor/brushes/terrain4.exr
rename to addons/terrain_3d/brushes/terrain4.exr
diff --git a/addons/terrain_3d/editor/brushes/terrain5.exr b/addons/terrain_3d/brushes/terrain5.exr
similarity index 100%
rename from addons/terrain_3d/editor/brushes/terrain5.exr
rename to addons/terrain_3d/brushes/terrain5.exr
diff --git a/addons/terrain_3d/editor/brushes/terrain6.exr b/addons/terrain_3d/brushes/terrain6.exr
similarity index 100%
rename from addons/terrain_3d/editor/brushes/terrain6.exr
rename to addons/terrain_3d/brushes/terrain6.exr
diff --git a/addons/terrain_3d/editor/brushes/texture1.exr b/addons/terrain_3d/brushes/texture1.exr
similarity index 100%
rename from addons/terrain_3d/editor/brushes/texture1.exr
rename to addons/terrain_3d/brushes/texture1.exr
diff --git a/addons/terrain_3d/editor/brushes/texture2.exr b/addons/terrain_3d/brushes/texture2.exr
similarity index 100%
rename from addons/terrain_3d/editor/brushes/texture2.exr
rename to addons/terrain_3d/brushes/texture2.exr
diff --git a/addons/terrain_3d/editor/brushes/texture3.exr b/addons/terrain_3d/brushes/texture3.exr
similarity index 100%
rename from addons/terrain_3d/editor/brushes/texture3.exr
rename to addons/terrain_3d/brushes/texture3.exr
diff --git a/addons/terrain_3d/editor/brushes/texture4.exr b/addons/terrain_3d/brushes/texture4.exr
similarity index 100%
rename from addons/terrain_3d/editor/brushes/texture4.exr
rename to addons/terrain_3d/brushes/texture4.exr
diff --git a/addons/terrain_3d/editor/brushes/texture5.exr b/addons/terrain_3d/brushes/texture5.exr
similarity index 100%
rename from addons/terrain_3d/editor/brushes/texture5.exr
rename to addons/terrain_3d/brushes/texture5.exr
diff --git a/addons/terrain_3d/editor/brushes/vegetation1.exr b/addons/terrain_3d/brushes/vegetation1.exr
similarity index 100%
rename from addons/terrain_3d/editor/brushes/vegetation1.exr
rename to addons/terrain_3d/brushes/vegetation1.exr
diff --git a/addons/terrain_3d/editor/editor.gd b/addons/terrain_3d/editor.gd
similarity index 56%
rename from addons/terrain_3d/editor/editor.gd
rename to addons/terrain_3d/editor.gd
index 64a69d8..12fdf43 100644
--- a/addons/terrain_3d/editor/editor.gd
+++ b/addons/terrain_3d/editor.gd
@@ -4,23 +4,36 @@ extends EditorPlugin
# Includes
-const UI: Script = preload("res://addons/terrain_3d/editor/components/ui.gd")
-const RegionGizmo: Script = preload("res://addons/terrain_3d/editor/components/region_gizmo.gd")
-const TextureDock: Script = preload("res://addons/terrain_3d/editor/components/texture_dock.gd")
+const UI: Script = preload("res://addons/terrain_3d/src/ui.gd")
+const RegionGizmo: Script = preload("res://addons/terrain_3d/src/region_gizmo.gd")
+const ASSET_DOCK: String = "res://addons/terrain_3d/src/asset_dock.tscn"
+const PS_DOCK_POSITION: String = "terrain3d/config/dock_position"
+const PS_DOCK_PINNED: String = "terrain3d/config/dock_pinned"
var terrain: Terrain3D
var nav_region: NavigationRegion3D
var editor: Terrain3DEditor
var ui: Node # Terrain3DUI see Godot #75388
-var texture_dock: TextureDock
-var texture_dock_container: CustomControlContainer = CONTAINER_INSPECTOR_BOTTOM
-
-var visible: bool
var region_gizmo: RegionGizmo
+var visible: bool
var current_region_position: Vector2
var mouse_global_position: Vector3 = Vector3.ZERO
+enum DOCK_STATE {
+ HIDDEN = -1,
+ SIDEBAR = 0,
+ BOTTOM = 1,
+}
+var asset_dock: Control
+var dock_state: DOCK_STATE = -1
+var dock_position: DockSlot = DOCK_SLOT_RIGHT_BL
+
+# Track negative input (CTRL)
+var _negative_input: bool = false
+# Track state prior to pressing CTRL: -1 not tracked, 0 false, 1 true
+var _prev_enable_state: int = -1
+
func _enter_tree() -> void:
editor = Terrain3DEditor.new()
@@ -28,27 +41,38 @@ func _enter_tree() -> void:
ui.plugin = self
add_child(ui)
- texture_dock = TextureDock.new()
- texture_dock.hide()
- texture_dock.resource_changed.connect(_on_texture_dock_resource_changed)
- texture_dock.resource_inspected.connect(_on_texture_dock_resource_selected)
- texture_dock.resource_selected.connect(ui._on_setting_changed)
-
region_gizmo = RegionGizmo.new()
-
- add_control_to_container(texture_dock_container, texture_dock)
- texture_dock.get_parent().visibility_changed.connect(_on_texture_dock_visibility_changed)
+ scene_changed.connect(_on_scene_changed)
+
+ if ProjectSettings.has_setting(PS_DOCK_POSITION):
+ dock_position = ProjectSettings.get_setting(PS_DOCK_POSITION)
+ asset_dock = load(ASSET_DOCK).instantiate()
+ await asset_dock.ready
+ if ProjectSettings.has_setting(PS_DOCK_PINNED):
+ asset_dock.placement_pin.button_pressed = ProjectSettings.get_setting(PS_DOCK_PINNED)
+ asset_dock.placement_pin.toggled.connect(_on_asset_dock_pin_changed)
+ asset_dock.placement_option.selected = dock_position
+ asset_dock.placement_changed.connect(_on_asset_dock_placement_changed)
+ asset_dock.resource_changed.connect(_on_asset_dock_resource_changed)
+ asset_dock.resource_inspected.connect(_on_asset_dock_resource_inspected)
+ asset_dock.resource_selected.connect(_on_asset_dock_resource_selected)
+
func _exit_tree() -> void:
- remove_control_from_container(texture_dock_container, texture_dock)
- texture_dock.queue_free()
+ asset_dock.queue_free()
ui.queue_free()
editor.free()
-
+ scene_changed.disconnect(_on_scene_changed)
+
+
func _handles(p_object: Object) -> bool:
- return p_object is Terrain3D or p_object is NavigationRegion3D
+ if p_object is Terrain3D or p_object is NavigationRegion3D:
+ return true
+ if p_object is Terrain3DObjects or (p_object is Node3D and p_object.get_parent() is Terrain3DObjects):
+ return true
+ return false
func _edit(p_object: Object) -> void:
@@ -77,21 +101,37 @@ func _edit(p_object: Object) -> void:
nav_region = p_object
else:
nav_region = null
+
+ if p_object is Terrain3DObjects:
+ p_object.editor_setup(self)
+ elif p_object is Node3D and p_object.get_parent() is Terrain3DObjects:
+ p_object.get_parent().editor_setup(self)
- _update_visibility()
-
-func _make_visible(p_visible: bool) -> void:
+func _make_visible(p_visible: bool, p_redraw: bool = false) -> void:
visible = p_visible
- _update_visibility()
-
-
-func _update_visibility() -> void:
ui.set_visible(visible)
- texture_dock.set_visible(visible and terrain)
- if terrain:
- update_region_grid()
- region_gizmo.set_hidden(not visible or not terrain)
+ update_region_grid()
+
+ # Manage Asset Dock position and visibility
+ if visible and dock_state == DOCK_STATE.HIDDEN:
+ if dock_position < DOCK_SLOT_MAX:
+ add_control_to_dock(dock_position, asset_dock)
+ dock_state = DOCK_STATE.SIDEBAR
+ asset_dock.move_slider(true)
+ else:
+ add_control_to_bottom_panel(asset_dock, "Terrain3D")
+ make_bottom_panel_item_visible(asset_dock)
+ dock_state = DOCK_STATE.BOTTOM
+ asset_dock.move_slider(false)
+ elif not visible and dock_state != DOCK_STATE.HIDDEN:
+ var pinned: bool = false
+ if p_redraw or ( asset_dock.placement_pin and not asset_dock.placement_pin.button_pressed):
+ if dock_state == DOCK_STATE.SIDEBAR:
+ remove_control_from_docks(asset_dock)
+ else:
+ remove_control_from_bottom_panel(asset_dock)
+ dock_state = DOCK_STATE.HIDDEN
func _clear() -> void:
@@ -111,21 +151,39 @@ func _forward_3d_gui_input(p_viewport_camera: Camera3D, p_event: InputEvent) ->
if not is_terrain_valid():
return AFTER_GUI_INPUT_PASS
+ ## Track negative input (CTRL)
+ if p_event is InputEventKey and not p_event.echo and p_event.keycode == KEY_CTRL:
+ if p_event.is_pressed():
+ _negative_input = true
+ _prev_enable_state = int(ui.toolbar_settings.get_setting("enable"))
+ ui.toolbar_settings.set_setting("enable", false)
+ else:
+ _negative_input = false
+ ui.toolbar_settings.set_setting("enable", bool(_prev_enable_state))
+ _prev_enable_state = -1
+
## Handle mouse movement
if p_event is InputEventMouseMotion:
if Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT):
return AFTER_GUI_INPUT_PASS
- ## Get mouse location on terrain
+ if _prev_enable_state >= 0 and not Input.is_key_pressed(KEY_CTRL):
+ _negative_input = false
+ ui.toolbar_settings.set_setting("enable", bool(_prev_enable_state))
+ _prev_enable_state = -1
+ ## Setup for active camera & viewport
+
# Snap terrain to current camera
terrain.set_camera(p_viewport_camera)
-
+
# Detect if viewport is set to half_resolution
- # Structure is: Node3DEditorViewportContainer/Node3DEditorViewport/SubViewportContainer/SubViewport/Camera3D
+ # Structure is: Node3DEditorViewportContainer/Node3DEditorViewport(4)/SubViewportContainer/SubViewport/Camera3D
var editor_vpc: SubViewportContainer = p_viewport_camera.get_parent().get_parent()
var full_resolution: bool = false if editor_vpc.stretch_shrink == 2 else true
+ ## Get mouse location on terrain
+
# Project 2D mouse position to 3D position and direction
var mouse_pos: Vector2 = p_event.position if full_resolution else p_event.position/2
var camera_pos: Vector3 = p_viewport_camera.project_ray_origin(mouse_pos)
@@ -204,29 +262,22 @@ func _forward_3d_gui_input(p_viewport_camera: Camera3D, p_event: InputEvent) ->
func is_terrain_valid() -> bool:
- var valid: bool = false
- if is_instance_valid(terrain):
- valid = terrain.get_storage() != null
- return valid
-
-
-func update_texture_dock(p_args: Array) -> void:
- texture_dock.clear()
-
- if is_terrain_valid() and terrain.texture_list:
- var texture_count: int = terrain.texture_list.get_texture_count()
- for i in texture_count:
- var texture: Terrain3DTexture = terrain.texture_list.get_texture(i)
- texture_dock.add_item(texture)
-
- if texture_count < Terrain3DTextureList.MAX_TEXTURES:
- texture_dock.add_item()
+ if is_instance_valid(terrain) and terrain.get_storage():
+ return true
+ return false
+func _load_storage() -> void:
+ if terrain:
+ update_region_grid()
+
+
func update_region_grid() -> void:
- if !region_gizmo.get_node_3d():
+ if not region_gizmo:
return
-
+
+ region_gizmo.set_hidden(not visible)
+
if is_terrain_valid():
region_gizmo.show_rect = editor.get_tool() == Terrain3DEditor.REGION
region_gizmo.use_secondary_color = editor.get_operation() == Terrain3DEditor.SUBTRACT
@@ -242,41 +293,63 @@ func update_region_grid() -> void:
region_gizmo.grid = [Vector2i.ZERO]
-# Signal handlers
-
-
func _load_textures() -> void:
if terrain and terrain.texture_list:
- if not terrain.texture_list.textures_changed.is_connected(update_texture_dock):
- terrain.texture_list.textures_changed.connect(update_texture_dock)
- update_texture_dock(Array())
+ if not terrain.texture_list.textures_changed.is_connected(update_asset_dock):
+ terrain.texture_list.textures_changed.connect(update_asset_dock)
+ update_asset_dock()
-func _load_storage() -> void:
- if terrain:
- update_region_grid()
+func update_asset_dock(p_texture_list: Terrain3DTextureList = null) -> void:
+ asset_dock.clear()
+
+ if is_terrain_valid() and terrain.texture_list:
+ var texture_count: int = terrain.texture_list.get_texture_count()
+ for i in texture_count:
+ var texture: Terrain3DTexture = terrain.texture_list.get_texture(i)
+ asset_dock.add_item(texture)
+
+ if texture_count < Terrain3DTextureList.MAX_TEXTURES:
+ asset_dock.add_item()
-func _on_texture_dock_resource_changed(texture: Resource, index: int) -> void:
+func _on_asset_dock_pin_changed(toggled: bool) -> void:
+ ProjectSettings.set_setting(PS_DOCK_PINNED, toggled)
+ ProjectSettings.save()
+
+
+func _on_asset_dock_placement_changed(index: int) -> void:
+ dock_position = clamp(index, 0, DOCK_SLOT_MAX)
+ ProjectSettings.set_setting(PS_DOCK_POSITION, dock_position)
+ ProjectSettings.save()
+ _make_visible(false, true) # Hide to redraw
+ _make_visible(true)
+
+
+func _on_asset_dock_resource_changed(p_texture: Resource, p_index: int) -> void:
if is_terrain_valid():
# If removing last entry and its selected, clear inspector
- if not texture and index == texture_dock.get_selected_index() and \
- texture_dock.get_selected_index() == texture_dock.entries.size() - 2:
+ if not p_texture and p_index == asset_dock.get_selected_index() and \
+ asset_dock.get_selected_index() == asset_dock.entries.size() - 2:
get_editor_interface().inspect_object(null)
- terrain.get_texture_list().set_texture(index, texture)
- call_deferred("_load_storage")
+ terrain.get_texture_list().set_texture(p_index, p_texture)
-func _on_texture_dock_resource_selected(texture) -> void:
+func _on_asset_dock_resource_selected() -> void:
+ # If not on a texture painting tool, then switch to Paint
+ if editor.get_tool() != Terrain3DEditor.TEXTURE:
+ var paint_btn: Button = ui.toolbar.get_node_or_null("PaintBaseTexture")
+ if paint_btn:
+ paint_btn.set_pressed(true)
+ ui._on_tool_changed(Terrain3DEditor.TEXTURE, Terrain3DEditor.REPLACE)
+ ui._on_setting_changed()
+
+
+func _on_asset_dock_resource_inspected(texture: Resource) -> void:
get_editor_interface().inspect_object(texture, "", true)
-func _on_texture_dock_visibility_changed() -> void:
- if texture_dock.get_parent() != null:
- remove_control_from_container(texture_dock_container, texture_dock)
-
- if texture_dock.get_parent() == null:
- texture_dock_container = CONTAINER_INSPECTOR_BOTTOM
- if get_editor_interface().is_distraction_free_mode_enabled():
- texture_dock_container = CONTAINER_SPATIAL_EDITOR_SIDE_RIGHT
- add_control_to_container(texture_dock_container, texture_dock)
+func _on_scene_changed(scene_root: Node) -> void:
+ if scene_root:
+ for node in scene_root.find_children("", "Terrain3DObjects"):
+ node.editor_setup(self)
diff --git a/addons/terrain_3d/editor/components/tool_settings.gd b/addons/terrain_3d/editor/components/tool_settings.gd
deleted file mode 100644
index 1bfed2e..0000000
--- a/addons/terrain_3d/editor/components/tool_settings.gd
+++ /dev/null
@@ -1,460 +0,0 @@
-extends PanelContainer
-
-signal picking(type, callback)
-signal setting_changed
-
-enum Layout {
- HORIZONTAL,
- VERTICAL,
- GRID,
-}
-
-enum SettingType {
- CHECKBOX,
- SLIDER,
- DOUBLE_SLIDER,
- COLOR_SELECT,
- PICKER,
- POINT_PICKER,
-}
-
-const PointPicker: Script = preload("res://addons/terrain_3d/editor/components/point_picker.gd")
-const DEFAULT_BRUSH: String = "circle0.exr"
-const BRUSH_PATH: String = "res://addons/terrain_3d/editor/brushes"
-const PICKER_ICON: String = "res://addons/terrain_3d/icons/icon_picker.svg"
-
-const NONE: int = 0x0
-const ALLOW_LARGER: int = 0x1
-const ALLOW_SMALLER: int = 0x2
-const ALLOW_OUT_OF_BOUNDS: int = 0x3
-
-var brush_preview_material: ShaderMaterial
-
-var list: HBoxContainer
-var advanced_list: VBoxContainer
-var settings: Dictionary = {}
-
-
-func _ready() -> void:
- list = HBoxContainer.new()
- add_child(list, true)
-
- add_brushes(list)
-
- add_setting(SettingType.SLIDER, "size", 50, list, "m", 4, 200, 1, ALLOW_LARGER)
- add_setting(SettingType.SLIDER, "opacity", 10, list, "%", 1, 100)
- add_setting(SettingType.CHECKBOX, "enable", true, list)
-
- add_setting(SettingType.COLOR_SELECT, "color", Color.WHITE, list)
- add_setting(SettingType.PICKER, "color picker", Terrain3DEditor.COLOR, list)
-
- add_setting(SettingType.SLIDER, "roughness", 0, list, "%", -100, 100, 1)
- add_setting(SettingType.PICKER, "roughness picker", Terrain3DEditor.ROUGHNESS, list)
-
- add_setting(SettingType.SLIDER, "height", 50, list, "m", -500, 500, 0.1, ALLOW_OUT_OF_BOUNDS)
- add_setting(SettingType.PICKER, "height picker", Terrain3DEditor.HEIGHT, list)
- add_setting(SettingType.DOUBLE_SLIDER, "slope", 0, list, "°", 0, 180, 1)
-
- add_setting(SettingType.POINT_PICKER, "gradient_points", Terrain3DEditor.HEIGHT, list)
- add_setting(SettingType.CHECKBOX, "drawable", false, list)
-
- settings["drawable"].toggled.connect(_on_drawable_toggled)
-
- var spacer: Control = Control.new()
- spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
- list.add_child(spacer, true)
-
- ## Advanced Settings Menu
- advanced_list = create_submenu(list, "Advanced", Layout.VERTICAL)
- add_setting(SettingType.CHECKBOX, "automatic_regions", true, advanced_list)
- add_setting(SettingType.CHECKBOX, "align_to_view", true, advanced_list)
- add_setting(SettingType.CHECKBOX, "show_cursor_while_painting", true, advanced_list)
- advanced_list.add_child(HSeparator.new(), true)
- add_setting(SettingType.SLIDER, "gamma", 1.0, advanced_list, "γ", 0.1, 2.0, 0.01)
- add_setting(SettingType.SLIDER, "jitter", 50, advanced_list, "%", 0, 100)
-
-
-func create_submenu(p_parent: Control, p_button_name: String, p_layout: Layout) -> Container:
- var menu_button: Button = Button.new()
- menu_button.set_text(p_button_name)
- menu_button.set_toggle_mode(true)
- menu_button.set_v_size_flags(SIZE_SHRINK_CENTER)
- menu_button.connect("toggled", _on_show_submenu.bind(menu_button))
-
- var submenu: PopupPanel = PopupPanel.new()
- submenu.connect("popup_hide", menu_button.set_pressed_no_signal.bind(false))
- submenu.set("theme_override_styles/panel", get_theme_stylebox("panel", "PopupMenu"))
-
- var sublist: Container
- match(p_layout):
- Layout.GRID:
- sublist = GridContainer.new()
- Layout.VERTICAL:
- sublist = VBoxContainer.new()
- Layout.HORIZONTAL, _:
- sublist = HBoxContainer.new()
-
- p_parent.add_child(menu_button, true)
- menu_button.add_child(submenu, true)
- submenu.add_child(sublist, true)
-
- return sublist
-
-
-func _on_show_submenu(p_toggled: bool, p_button: Button) -> void:
- var popup: PopupPanel = p_button.get_child(0)
- var popup_pos: Vector2 = p_button.get_screen_transform().origin
- popup.set_visible(p_toggled)
- popup_pos.y -= popup.get_size().y
- popup.set_position(popup_pos)
-
-
-func add_brushes(p_parent: Control) -> void:
- var brush_list: GridContainer = create_submenu(p_parent, "Brush", Layout.GRID)
- brush_list.name = "BrushList"
-
- var brush_button_group: ButtonGroup = ButtonGroup.new()
- brush_button_group.connect("pressed", _on_setting_changed)
- var default_brush_btn: Button
-
- var dir: DirAccess = DirAccess.open(BRUSH_PATH)
- if dir:
- dir.list_dir_begin()
- var file_name = dir.get_next()
- while file_name != "":
- if !dir.current_is_dir() and file_name.ends_with(".exr"):
- var img: Image = Image.load_from_file(BRUSH_PATH + "/" + file_name)
- _black_to_alpha(img)
- var tex: ImageTexture = ImageTexture.create_from_image(img)
-
- var btn: Button = Button.new()
- btn.set_custom_minimum_size(Vector2.ONE * 100)
- btn.set_button_icon(tex)
- btn.set_meta("image", img)
- btn.set_expand_icon(true)
- btn.set_material(_get_brush_preview_material())
- btn.set_toggle_mode(true)
- btn.set_button_group(brush_button_group)
- btn.mouse_entered.connect(_on_brush_hover.bind(true, btn))
- btn.mouse_exited.connect(_on_brush_hover.bind(false, btn))
- brush_list.add_child(btn, true)
- if file_name == DEFAULT_BRUSH:
- default_brush_btn = btn
-
- var lbl: Label = Label.new()
- btn.add_child(lbl, true)
- lbl.text = file_name.get_basename()
- lbl.visible = false
- lbl.position.y = 70
- lbl.add_theme_color_override("font_shadow_color", Color.BLACK)
- lbl.add_theme_constant_override("shadow_offset_x", 1)
- lbl.add_theme_constant_override("shadow_offset_y", 1)
- lbl.add_theme_font_size_override("font_size", 16)
-
- file_name = dir.get_next()
-
- brush_list.columns = sqrt(brush_list.get_child_count()) + 2
-
- if not default_brush_btn:
- default_brush_btn = brush_button_group.get_buttons()[0]
- default_brush_btn.set_pressed(true)
-
- settings["brush"] = brush_button_group
-
- # Optionally erase the main brush button text and replace it with the texture
-# var select_brush_btn: Button = brush_list.get_parent().get_parent()
-# select_brush_btn.set_button_icon(default_brush_btn.get_button_icon())
-# select_brush_btn.set_custom_minimum_size(Vector2.ONE * 36)
-# select_brush_btn.set_icon_alignment(HORIZONTAL_ALIGNMENT_CENTER)
-# select_brush_btn.set_expand_icon(true)
-
-
-func _on_brush_hover(p_hovering: bool, p_button: Button) -> void:
- if p_button.get_child_count() > 0:
- var child = p_button.get_child(0)
- if child is Label:
- if p_hovering:
- child.visible = true
- else:
- child.visible = false
-
-
-func _on_pick(p_type: Terrain3DEditor.Tool) -> void:
- emit_signal("picking", p_type, _on_picked)
-
-
-func _on_picked(p_type: Terrain3DEditor.Tool, p_color: Color, p_global_position: Vector3) -> void:
- match p_type:
- Terrain3DEditor.HEIGHT:
- settings["height"].value = p_color.r
- Terrain3DEditor.COLOR:
- settings["color"].color = p_color
- Terrain3DEditor.ROUGHNESS:
- # 200... -.5 converts 0,1 to -100,100
- settings["roughness"].value = round(200 * (p_color.a - 0.5))
- _on_setting_changed()
-
-
-func _on_point_pick(p_type: Terrain3DEditor.Tool, p_name: String) -> void:
- assert(p_type == Terrain3DEditor.HEIGHT)
- emit_signal("picking", p_type, _on_point_picked.bind(p_name))
-
-
-func _on_point_picked(p_type: Terrain3DEditor.Tool, p_color: Color, p_global_position: Vector3, p_name: String) -> void:
- assert(p_type == Terrain3DEditor.HEIGHT)
-
- var point: Vector3 = p_global_position
- point.y = p_color.r
- settings[p_name].add_point(point)
- _on_setting_changed()
-
-
-func add_setting(p_type: SettingType, p_name: StringName, p_value: Variant, p_parent: Control,
- p_suffix: String = "", p_min_value: float = 0.0, p_max_value: float = 0.0, p_step: float = 1.0,
- p_flags: int = NONE) -> void:
-
- var container: HBoxContainer = HBoxContainer.new()
- var label: Label = Label.new()
- var control: Control
-
- container.set_v_size_flags(SIZE_EXPAND_FILL)
-
- match p_type:
- SettingType.SLIDER, SettingType.DOUBLE_SLIDER:
- label.set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER)
- label.set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER)
- label.set_custom_minimum_size(Vector2(32, 0))
- label.set_v_size_flags(SIZE_SHRINK_CENTER)
- label.set_text(p_name.capitalize() + ": ")
- container.add_child(label, true)
-
- var slider: Control
- if p_type == SettingType.SLIDER:
- control = EditorSpinSlider.new()
- control.set_flat(true)
- control.set_hide_slider(true)
- control.connect("value_changed", _on_setting_changed)
- control.set_max(p_max_value)
- control.set_min(p_min_value)
- control.set_step(p_step)
- control.set_value(p_value)
- control.set_suffix(p_suffix)
- control.set_v_size_flags(SIZE_SHRINK_CENTER)
-
- slider = HSlider.new()
- slider.share(control)
- if p_flags & ALLOW_LARGER:
- slider.set_allow_greater(true)
- if p_flags & ALLOW_SMALLER:
- slider.set_allow_lesser(true)
-
- else:
- control = Label.new()
- control.set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER)
- control.set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER)
- slider = DoubleSlider.new()
- slider.label = control
- slider.suffix = p_suffix
- slider.connect("value_changed", _on_setting_changed)
-
- control.set_custom_minimum_size(Vector2(75, 0))
- slider.set_max(p_max_value)
- slider.set_min(p_min_value)
- slider.set_step(p_step)
- slider.set_value(p_value)
- slider.set_v_size_flags(SIZE_SHRINK_CENTER)
- slider.set_h_size_flags(SIZE_SHRINK_END | SIZE_EXPAND)
- slider.set_custom_minimum_size(Vector2(100, 10))
-
- container.add_child(slider, true)
-
- SettingType.CHECKBOX:
- control = CheckBox.new()
- control.set_text(p_name.capitalize())
- control.set_pressed_no_signal(p_value)
- control.connect("pressed", _on_setting_changed)
-
- SettingType.COLOR_SELECT:
- control = ColorPickerButton.new()
- control.set_custom_minimum_size(Vector2(100, 10))
- control.color = Color.WHITE
- control.edit_alpha = false
- control.get_picker().set_color_mode(ColorPicker.MODE_HSV)
- control.connect("color_changed", _on_setting_changed)
-
- SettingType.PICKER:
- control = Button.new()
- control.icon = load(PICKER_ICON)
- control.tooltip_text = "Pick value from the Terrain"
- control.connect("pressed", _on_pick.bind(p_value))
-
- SettingType.POINT_PICKER:
- control = PointPicker.new()
- control.connect("pressed", _on_point_pick.bind(p_value, p_name))
- control.connect("value_changed", _on_setting_changed)
-
- container.add_child(control, true)
- p_parent.add_child(container, true)
-
- settings[p_name] = control
-
-
-func get_setting(p_setting: String) -> Variant:
- var object: Object = settings[p_setting]
- var value: Variant
- if object is Range:
- value = object.get_value()
- elif object is DoubleSlider:
- value = [object.get_min_value(), object.get_max_value()]
- elif object is ButtonGroup:
- var img: Image = object.get_pressed_button().get_meta("image")
- var tex: Texture2D = object.get_pressed_button().get_button_icon()
- value = [ img, tex ]
- elif object is CheckBox:
- value = object.is_pressed()
- elif object is ColorPickerButton:
- value = object.color
- elif object is PointPicker:
- value = object.get_points()
- return value
-
-
-func hide_settings(p_settings: PackedStringArray) -> void:
- for setting in settings.keys():
- var object: Object = settings[setting]
- if object is Control:
- object.get_parent().show()
-
- for setting in p_settings:
- if settings.has(setting):
- var object: Object = settings[setting]
- if object is Control:
- object.get_parent().hide()
-
-
-func _on_setting_changed(p_data: Variant = null) -> void:
- # If a button was clicked on a submenu
- if p_data is Button and p_data.get_parent().get_parent() is PopupPanel:
- if p_data.get_parent().name == "BrushList":
- # Optionally Set selected brush texture in main brush button
-# p_data.get_parent().get_parent().get_parent().set_button_icon(p_data.get_button_icon())
- # Hide popup
- p_data.get_parent().get_parent().set_visible(false)
- # Hide label
- if p_data.get_child_count() > 0:
- p_data.get_child(0).visible = false
-
- emit_signal("setting_changed")
-
-
-func _on_drawable_toggled(p_button_pressed: bool) -> void:
- if not p_button_pressed:
- settings["gradient_points"].clear()
-
-
-func _get_brush_preview_material() -> ShaderMaterial:
- if !brush_preview_material:
- brush_preview_material = ShaderMaterial.new()
-
- var shader: Shader = Shader.new()
- var code: String = "shader_type canvas_item;\n"
-
- code += "varying vec4 v_vertex_color;\n"
- code += "void vertex() {\n"
- code += " v_vertex_color = COLOR;\n"
- code += "}\n"
- code += "void fragment(){\n"
- code += " vec4 tex = texture(TEXTURE, UV);\n"
- code += " COLOR.a *= pow(tex.r, 0.666);\n"
- code += " COLOR.rgb = v_vertex_color.rgb;\n"
- code += "}\n"
-
- shader.set_code(code)
-
- brush_preview_material.set_shader(shader)
-
- return brush_preview_material
-
-
-func _black_to_alpha(p_image: Image) -> void:
- if p_image.get_format() != Image.FORMAT_RGBAF:
- p_image.convert(Image.FORMAT_RGBAF)
-
- for y in p_image.get_height():
- for x in p_image.get_width():
- var color: Color = p_image.get_pixel(x,y)
- color.a = color.get_luminance()
- p_image.set_pixel(x, y, color)
-
-
-#### Sub Class DoubleSlider
-
-class DoubleSlider extends Range:
-
- var label: Label
- var suffix: String
- var grabbed: bool = false
- var _max_value: float
-
-
- func _gui_input(p_event: InputEvent) -> void:
- if p_event is InputEventMouseButton:
- if p_event.get_button_index() == MOUSE_BUTTON_LEFT:
- grabbed = p_event.is_pressed()
- set_min_max(p_event.get_position().x)
-
- if p_event is InputEventMouseMotion:
- if grabbed:
- set_min_max(p_event.get_position().x)
-
-
- func _notification(p_what: int) -> void:
- if p_what == NOTIFICATION_RESIZED:
- pass
- if p_what == NOTIFICATION_DRAW:
- var bg: StyleBox = get_theme_stylebox("slider", "HSlider")
- var bg_height: float = bg.get_minimum_size().y
- draw_style_box(bg, Rect2(Vector2(0, (size.y - bg_height) / 2), Vector2(size.x, bg_height)))
-
- var grabber: Texture2D = get_theme_icon("grabber", "HSlider")
- var area: StyleBox = get_theme_stylebox("grabber_area", "HSlider")
- var h: float = size.y / 2 - grabber.get_size().y / 2
-
- var minpos: Vector2 = Vector2((min_value / _max_value) * size.x - grabber.get_size().x / 2, h)
- var maxpos: Vector2 = Vector2((max_value / _max_value) * size.x - grabber.get_size().x / 2, h)
-
- draw_style_box(area, Rect2(Vector2(minpos.x + grabber.get_size().x / 2, (size.y - bg_height) / 2), Vector2(maxpos.x - minpos.x, bg_height)))
-
- draw_texture(grabber, minpos)
- draw_texture(grabber, maxpos)
-
-
- func set_max(p_value: float) -> void:
- max_value = p_value
- if _max_value == 0:
- _max_value = max_value
- update_label()
-
-
- func set_min_max(p_xpos: float) -> void:
- var mid_value_normalized: float = ((max_value + min_value) / 2.0) / _max_value
- var mid_value: float = size.x * mid_value_normalized
- var min_active: bool = p_xpos < mid_value
- var xpos_ranged: float = snappedf((p_xpos / size.x) * _max_value, step)
-
- if min_active:
- min_value = xpos_ranged
- else:
- max_value = xpos_ranged
-
- min_value = clamp(min_value, 0, max_value - 10)
- max_value = clamp(max_value, min_value + 10, _max_value)
-
- update_label()
- emit_signal("setting_changed", value)
- queue_redraw()
-
-
- func update_label() -> void:
- if label:
- label.set_text(str(min_value) + suffix + "/" + str(max_value) + suffix)
diff --git a/addons/terrain_3d/extras/minimum.gdshader b/addons/terrain_3d/extras/minimum.gdshader
index dcc297e..1cedd32 100644
--- a/addons/terrain_3d/extras/minimum.gdshader
+++ b/addons/terrain_3d/extras/minimum.gdshader
@@ -1,4 +1,4 @@
-// This shader is the minimum needed to allow the terrain to function.
+// This shader is the minimum needed to allow the terrain to function, without any texturing.
shader_type spatial;
render_mode blend_mix,depth_draw_opaque,cull_back,diffuse_burley,specular_schlick_ggx;
@@ -12,8 +12,19 @@ uniform int _region_map_size = 16;
uniform int _region_map[256];
uniform vec2 _region_offsets[256];
uniform sampler2DArray _height_maps : repeat_disable;
+uniform usampler2DArray _control_maps : repeat_disable;
+uniform sampler2DArray _color_maps : source_color, filter_linear_mipmap_anisotropic, repeat_disable;
+uniform sampler2DArray _texture_array_albedo : source_color, filter_linear_mipmap_anisotropic, repeat_enable;
+uniform sampler2DArray _texture_array_normal : hint_normal, filter_linear_mipmap_anisotropic, repeat_enable;
-varying vec3 v_vertex; // World coordinate vertex location
+uniform float _texture_uv_scale_array[32];
+uniform float _texture_uv_rotation_array[32];
+uniform vec4 _texture_color_array[32];
+uniform uint _background_mode = 1u; // NONE = 0, FLAT = 1, NOISE = 2
+uniform uint _mouse_layer = 0x80000000u; // Layer 32
+
+varying flat vec2 v_uv_offset;
+varying flat vec2 v_uv2_offset;
////////////////////////
// Vertex
@@ -34,12 +45,17 @@ ivec3 get_region_uv(vec2 uv) {
// XY: (0 to 1) coordinates within a region
// Z: layer index used for texturearrays, -1 if not in a region
vec3 get_region_uv2(vec2 uv) {
- ivec2 pos = ivec2(floor(uv)) + (_region_map_size / 2);
+ // Vertex function added half a texel to UV2, to center the UV's. vertex(), fragment() and get_height()
+ // call this with reclaimed versions of UV2, so to keep the last row/column within the correct
+ // window, take back the half pixel before the floor().
+ ivec2 pos = ivec2(floor(uv - vec2(_region_texel_size * 0.5))) + (_region_map_size / 2);
int bounds = int(pos.x>=0 && pos.x<_region_map_size && pos.y>=0 && pos.y<_region_map_size);
int layer_index = _region_map[ pos.y * _region_map_size + pos.x ] * bounds - 1;
+ // The return value is still texel-centered.
return vec3(uv - _region_offsets[layer_index], float(layer_index));
}
+// 1 lookup
float get_height(vec2 uv) {
highp float height = 0.0;
vec3 region = get_region_uv2(uv);
@@ -51,23 +67,39 @@ float get_height(vec2 uv) {
void vertex() {
// Get vertex of flat plane in world coordinates and set world UV
- v_vertex = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
+ vec3 vertex = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
// UV coordinates in world space. Values are 0 to _region_size within regions
- UV = round(v_vertex.xz * _mesh_vertex_density);
+ UV = round(vertex.xz * _mesh_vertex_density);
- // UV coordinates in region space + texel offset. Values are 0 to 1 within regions
- UV2 = (UV + vec2(0.5)) * _region_texel_size;
+ // Discard vertices for Holes. 1 lookup
+ ivec3 region = get_region_uv(UV);
+ uint control = texelFetch(_control_maps, region, 0).r;
+ bool hole = bool(control >>2u & 0x1u);
+ // Show holes to all cameras except mouse camera (on exactly 1 layer)
+ if ( !(CAMERA_VISIBLE_LAYERS == _mouse_layer) &&
+ (hole || (_background_mode == 0u && region.z < 0)) ) {
+ VERTEX.x = 0./0.;
+ } else {
+ // UV coordinates in region space + texel offset. Values are 0 to 1 within regions
+ UV2 = (UV + vec2(0.5)) * _region_texel_size;
- // Get final vertex location and save it
- VERTEX.y = get_height(UV2);
- v_vertex = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
+ // Get final vertex location and save it
+ VERTEX.y = get_height(UV2);
+ }
+
+ // Transform UVs to local to avoid poor precision during varying interpolation.
+ v_uv_offset = MODEL_MATRIX[3].xz * _mesh_vertex_density;
+ UV -= v_uv_offset;
+ v_uv2_offset = v_uv_offset * _region_texel_size;
+ UV2 -= v_uv2_offset;
}
////////////////////////
// Fragment
////////////////////////
+// 3 lookups
vec3 get_normal(vec2 uv, out vec3 tangent, out vec3 binormal) {
// Get the height of the current vertex
float height = get_height(uv);
@@ -97,9 +129,13 @@ vec3 get_normal(vec2 uv, out vec3 tangent, out vec3 binormal) {
}
void fragment() {
- // Calculate Terrain Normals
+ // Recover UVs
+ vec2 uv = UV + v_uv_offset;
+ vec2 uv2 = UV2 + v_uv2_offset;
+
+ // Calculate Terrain Normals. 4 lookups
vec3 w_tangent, w_binormal;
- vec3 w_normal = get_normal(UV2, w_tangent, w_binormal);
+ vec3 w_normal = get_normal(uv2, w_tangent, w_binormal);
NORMAL = mat3(VIEW_MATRIX) * w_normal;
TANGENT = mat3(VIEW_MATRIX) * w_tangent;
BINORMAL = mat3(VIEW_MATRIX) * w_binormal;
@@ -107,3 +143,4 @@ void fragment() {
// Apply PBR
ALBEDO=vec3(.2);
}
+
diff --git a/addons/terrain_3d/extras/project_on_terrain3d.gd b/addons/terrain_3d/extras/project_on_terrain3d.gd
index c4f0321..6ef6d83 100644
--- a/addons/terrain_3d/extras/project_on_terrain3d.gd
+++ b/addons/terrain_3d/extras/project_on_terrain3d.gd
@@ -1,8 +1,9 @@
# This script is an addon for HungryProton's Scatter https://github.com/HungryProton/scatter
-# It allows Scatter to detect the terrain height from Terrain3D
+# It provides a `Project on Terrain3D` modifier, which allows Scatter
+# to detect the terrain height from Terrain3D without using collision.
# Copy this file into /addons/proton_scatter/src/modifiers
# Then uncomment everything below
-# In the editor, add this modifier, then set your Terrain3D node
+# In the editor, add this modifier to Scatter, then set your Terrain3D node
#@tool
#extends "base_modifier.gd"
diff --git a/addons/terrain_3d/icons/icon_brush.svg.import b/addons/terrain_3d/icons/icon_brush.svg.import
index 02c3201..3cb66ae 100644
--- a/addons/terrain_3d/icons/icon_brush.svg.import
+++ b/addons/terrain_3d/icons/icon_brush.svg.import
@@ -2,9 +2,10 @@
importer="texture"
type="CompressedTexture2D"
-uid="uid://cjrjfjmbyseol"
+uid="uid://dbby47aoknjeb"
path="res://.godot/imported/icon_brush.svg-8886426485f67abe2233686de39952ce.ctex"
metadata={
+"has_editor_variant": true,
"vram_texture": false
}
@@ -33,5 +34,5 @@ process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
-editor/scale_with_editor_scale=false
+editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false
diff --git a/addons/terrain_3d/icons/icon_color.svg.import b/addons/terrain_3d/icons/icon_color.svg.import
index f532bbc..27f1fce 100644
--- a/addons/terrain_3d/icons/icon_color.svg.import
+++ b/addons/terrain_3d/icons/icon_color.svg.import
@@ -2,9 +2,10 @@
importer="texture"
type="CompressedTexture2D"
-uid="uid://cnytnsydxqdn2"
+uid="uid://bump6ax7j3rp1"
path="res://.godot/imported/icon_color.svg-b5b52e67ad2f610c27688c5daeb9cd1c.ctex"
metadata={
+"has_editor_variant": true,
"vram_texture": false
}
@@ -33,5 +34,5 @@ process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
-editor/scale_with_editor_scale=false
+editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false
diff --git a/addons/terrain_3d/icons/icon_height_add.svg.import b/addons/terrain_3d/icons/icon_height_add.svg.import
index 3ab6671..e8c2611 100644
--- a/addons/terrain_3d/icons/icon_height_add.svg.import
+++ b/addons/terrain_3d/icons/icon_height_add.svg.import
@@ -2,9 +2,10 @@
importer="texture"
type="CompressedTexture2D"
-uid="uid://ca7l0radlfn5w"
+uid="uid://8ma1rbcinto3"
path="res://.godot/imported/icon_height_add.svg-2928bbcb35ef4816ead056c5bcf5bdbd.ctex"
metadata={
+"has_editor_variant": true,
"vram_texture": false
}
@@ -33,5 +34,5 @@ process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
-editor/scale_with_editor_scale=false
+editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false
diff --git a/addons/terrain_3d/icons/icon_height_div.svg.import b/addons/terrain_3d/icons/icon_height_div.svg.import
index 2573051..982fe22 100644
--- a/addons/terrain_3d/icons/icon_height_div.svg.import
+++ b/addons/terrain_3d/icons/icon_height_div.svg.import
@@ -2,9 +2,10 @@
importer="texture"
type="CompressedTexture2D"
-uid="uid://cunkwpgutguvo"
+uid="uid://5xwklmfyqaha"
path="res://.godot/imported/icon_height_div.svg-982f74e4859453a7d67caa2f6a71b056.ctex"
metadata={
+"has_editor_variant": true,
"vram_texture": false
}
@@ -33,5 +34,5 @@ process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
-editor/scale_with_editor_scale=false
+editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false
diff --git a/addons/terrain_3d/icons/icon_height_flat.svg.import b/addons/terrain_3d/icons/icon_height_flat.svg.import
index 1c01887..f5c5247 100644
--- a/addons/terrain_3d/icons/icon_height_flat.svg.import
+++ b/addons/terrain_3d/icons/icon_height_flat.svg.import
@@ -2,9 +2,10 @@
importer="texture"
type="CompressedTexture2D"
-uid="uid://cs2injw82o1il"
+uid="uid://cpfvqnfgrw4e"
path="res://.godot/imported/icon_height_flat.svg-e58f6a7038e84631a5f56866c4c671e0.ctex"
metadata={
+"has_editor_variant": true,
"vram_texture": false
}
@@ -33,5 +34,5 @@ process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
-editor/scale_with_editor_scale=false
+editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false
diff --git a/addons/terrain_3d/icons/icon_height_mul.svg.import b/addons/terrain_3d/icons/icon_height_mul.svg.import
index 66a84ee..cfa978c 100644
--- a/addons/terrain_3d/icons/icon_height_mul.svg.import
+++ b/addons/terrain_3d/icons/icon_height_mul.svg.import
@@ -2,9 +2,10 @@
importer="texture"
type="CompressedTexture2D"
-uid="uid://tvaw0pb2bdhu"
+uid="uid://bkqdmfjtug1c7"
path="res://.godot/imported/icon_height_mul.svg-b6b666e20be820f5aa48e7410648290c.ctex"
metadata={
+"has_editor_variant": true,
"vram_texture": false
}
@@ -33,5 +34,5 @@ process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
-editor/scale_with_editor_scale=false
+editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false
diff --git a/addons/terrain_3d/icons/icon_height_slope.svg.import b/addons/terrain_3d/icons/icon_height_slope.svg.import
index defce65..118de23 100644
--- a/addons/terrain_3d/icons/icon_height_slope.svg.import
+++ b/addons/terrain_3d/icons/icon_height_slope.svg.import
@@ -2,9 +2,10 @@
importer="texture"
type="CompressedTexture2D"
-uid="uid://c3xbj3i7emxir"
+uid="uid://wkm8llh72h1m"
path="res://.godot/imported/icon_height_slope.svg-2a8181e8d9f9b74739d6f4a9e62f040d.ctex"
metadata={
+"has_editor_variant": true,
"vram_texture": false
}
@@ -33,5 +34,5 @@ process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
-editor/scale_with_editor_scale=false
+editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false
diff --git a/addons/terrain_3d/icons/icon_height_smooth.svg.import b/addons/terrain_3d/icons/icon_height_smooth.svg.import
index a308e01..0a3e974 100644
--- a/addons/terrain_3d/icons/icon_height_smooth.svg.import
+++ b/addons/terrain_3d/icons/icon_height_smooth.svg.import
@@ -2,9 +2,10 @@
importer="texture"
type="CompressedTexture2D"
-uid="uid://ub12g3jkvivd"
+uid="uid://fhvnyvqmygfs"
path="res://.godot/imported/icon_height_smooth.svg-83cfb47d64fb9579b212027a5aa50672.ctex"
metadata={
+"has_editor_variant": true,
"vram_texture": false
}
@@ -33,5 +34,5 @@ process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
-editor/scale_with_editor_scale=false
+editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false
diff --git a/addons/terrain_3d/icons/icon_height_sub.svg.import b/addons/terrain_3d/icons/icon_height_sub.svg.import
index 9208500..01a10cd 100644
--- a/addons/terrain_3d/icons/icon_height_sub.svg.import
+++ b/addons/terrain_3d/icons/icon_height_sub.svg.import
@@ -2,9 +2,10 @@
importer="texture"
type="CompressedTexture2D"
-uid="uid://vyw5rhfpbjku"
+uid="uid://cbtqb5fq02yx1"
path="res://.godot/imported/icon_height_sub.svg-f01f73a219b6c1858d4bd958d01e8130.ctex"
metadata={
+"has_editor_variant": true,
"vram_texture": false
}
@@ -33,5 +34,5 @@ process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
-editor/scale_with_editor_scale=false
+editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false
diff --git a/addons/terrain_3d/icons/icon_holes.svg.import b/addons/terrain_3d/icons/icon_holes.svg.import
index cd00a66..d8a42b7 100644
--- a/addons/terrain_3d/icons/icon_holes.svg.import
+++ b/addons/terrain_3d/icons/icon_holes.svg.import
@@ -2,9 +2,10 @@
importer="texture"
type="CompressedTexture2D"
-uid="uid://bqktati6gi08q"
+uid="uid://4qj0x1rs0a83"
path="res://.godot/imported/icon_holes.svg-fadd8eef4df4cdc393621d5ff25aa8e3.ctex"
metadata={
+"has_editor_variant": true,
"vram_texture": false
}
@@ -33,5 +34,5 @@ process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
-editor/scale_with_editor_scale=false
+editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false
diff --git a/addons/terrain_3d/icons/icon_map_add.svg.import b/addons/terrain_3d/icons/icon_map_add.svg.import
index 84445b4..385706b 100644
--- a/addons/terrain_3d/icons/icon_map_add.svg.import
+++ b/addons/terrain_3d/icons/icon_map_add.svg.import
@@ -2,9 +2,10 @@
importer="texture"
type="CompressedTexture2D"
-uid="uid://js5u88icy324"
+uid="uid://dkgbiictluyh8"
path="res://.godot/imported/icon_map_add.svg-a13cebbb261c5138d4ca5cbb5df24202.ctex"
metadata={
+"has_editor_variant": true,
"vram_texture": false
}
@@ -33,5 +34,5 @@ process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
-editor/scale_with_editor_scale=false
+editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false
diff --git a/addons/terrain_3d/icons/icon_map_remove.svg.import b/addons/terrain_3d/icons/icon_map_remove.svg.import
index 952a8a0..41f4442 100644
--- a/addons/terrain_3d/icons/icon_map_remove.svg.import
+++ b/addons/terrain_3d/icons/icon_map_remove.svg.import
@@ -2,9 +2,10 @@
importer="texture"
type="CompressedTexture2D"
-uid="uid://c1o6vv5wx3w65"
+uid="uid://bavktgaibu05s"
path="res://.godot/imported/icon_map_remove.svg-bf5a269f9171f7027b6de1785cc63713.ctex"
metadata={
+"has_editor_variant": true,
"vram_texture": false
}
@@ -33,5 +34,5 @@ process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
-editor/scale_with_editor_scale=false
+editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false
diff --git a/addons/terrain_3d/icons/icon_multimesh.svg.import b/addons/terrain_3d/icons/icon_multimesh.svg.import
index 32cf127..66664c4 100644
--- a/addons/terrain_3d/icons/icon_multimesh.svg.import
+++ b/addons/terrain_3d/icons/icon_multimesh.svg.import
@@ -2,9 +2,10 @@
importer="texture"
type="CompressedTexture2D"
-uid="uid://xg3l0exmqch3"
+uid="uid://c5004ol65cqti"
path="res://.godot/imported/icon_multimesh.svg-9447b6c5fe1ee9d406fd55dd3c63e196.ctex"
metadata={
+"has_editor_variant": true,
"vram_texture": false
}
@@ -33,5 +34,5 @@ process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
-editor/scale_with_editor_scale=false
+editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false
diff --git a/addons/terrain_3d/icons/icon_navigation.svg.import b/addons/terrain_3d/icons/icon_navigation.svg.import
index ef0bf8e..7afe437 100644
--- a/addons/terrain_3d/icons/icon_navigation.svg.import
+++ b/addons/terrain_3d/icons/icon_navigation.svg.import
@@ -2,9 +2,10 @@
importer="texture"
type="CompressedTexture2D"
-uid="uid://c0sprba3li1x4"
+uid="uid://dewr4ro7nrg7y"
path="res://.godot/imported/icon_navigation.svg-35e49ee3c403c103a0079d4156b0d168.ctex"
metadata={
+"has_editor_variant": true,
"vram_texture": false
}
@@ -33,5 +34,5 @@ process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
-editor/scale_with_editor_scale=false
+editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false
diff --git a/addons/terrain_3d/icons/icon_picker.svg.import b/addons/terrain_3d/icons/icon_picker.svg.import
index 7ad1ca7..6ba7564 100644
--- a/addons/terrain_3d/icons/icon_picker.svg.import
+++ b/addons/terrain_3d/icons/icon_picker.svg.import
@@ -2,9 +2,10 @@
importer="texture"
type="CompressedTexture2D"
-uid="uid://516qh3duc33g"
+uid="uid://iiocbe4njv5s"
path="res://.godot/imported/icon_picker.svg-9f872a162ed3e0053283f4bf299ac645.ctex"
metadata={
+"has_editor_variant": true,
"vram_texture": false
}
@@ -33,5 +34,5 @@ process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
-editor/scale_with_editor_scale=false
+editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false
diff --git a/addons/terrain_3d/icons/icon_picker_checked.svg.import b/addons/terrain_3d/icons/icon_picker_checked.svg.import
index 66d885c..568696e 100644
--- a/addons/terrain_3d/icons/icon_picker_checked.svg.import
+++ b/addons/terrain_3d/icons/icon_picker_checked.svg.import
@@ -2,9 +2,10 @@
importer="texture"
type="CompressedTexture2D"
-uid="uid://wor7v0l4vc6t"
+uid="uid://73eyg81dnj2e"
path="res://.godot/imported/icon_picker_checked.svg-4e271ac1a29c979a28440c683998675e.ctex"
metadata={
+"has_editor_variant": true,
"vram_texture": false
}
@@ -33,5 +34,5 @@ process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
-editor/scale_with_editor_scale=false
+editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false
diff --git a/addons/terrain_3d/icons/icon_spray.svg.import b/addons/terrain_3d/icons/icon_spray.svg.import
index bc68921..ba57122 100644
--- a/addons/terrain_3d/icons/icon_spray.svg.import
+++ b/addons/terrain_3d/icons/icon_spray.svg.import
@@ -2,9 +2,10 @@
importer="texture"
type="CompressedTexture2D"
-uid="uid://bx2wxkso083ht"
+uid="uid://28jdhtmo0tcm"
path="res://.godot/imported/icon_spray.svg-d9864c87d5d420aa9f80c0d3fdc80e87.ctex"
metadata={
+"has_editor_variant": true,
"vram_texture": false
}
@@ -33,5 +34,5 @@ process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
-editor/scale_with_editor_scale=false
+editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false
diff --git a/addons/terrain_3d/icons/icon_terrain3d.svg.import b/addons/terrain_3d/icons/icon_terrain3d.svg.import
index 5622659..09f496b 100644
--- a/addons/terrain_3d/icons/icon_terrain3d.svg.import
+++ b/addons/terrain_3d/icons/icon_terrain3d.svg.import
@@ -2,9 +2,10 @@
importer="texture"
type="CompressedTexture2D"
-uid="uid://b03x10iou1m5"
+uid="uid://cyfcx5hwray0d"
path="res://.godot/imported/icon_terrain3d.svg-39252bb986e607c413d93e00ee31a619.ctex"
metadata={
+"has_editor_variant": true,
"vram_texture": false
}
@@ -33,5 +34,5 @@ process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
-editor/scale_with_editor_scale=false
+editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false
diff --git a/addons/terrain_3d/icons/icon_terrain_layer_material.svg.import b/addons/terrain_3d/icons/icon_terrain_layer_material.svg.import
index b96ea64..130b5fe 100644
--- a/addons/terrain_3d/icons/icon_terrain_layer_material.svg.import
+++ b/addons/terrain_3d/icons/icon_terrain_layer_material.svg.import
@@ -2,9 +2,10 @@
importer="texture"
type="CompressedTexture2D"
-uid="uid://gt2l3qdaivl2"
+uid="uid://1bsjjcq0cv08"
path="res://.godot/imported/icon_terrain_layer_material.svg-f3d447e84f51556fc60c5f0bd3f57443.ctex"
metadata={
+"has_editor_variant": true,
"vram_texture": false
}
@@ -33,5 +34,5 @@ process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
-editor/scale_with_editor_scale=false
+editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false
diff --git a/addons/terrain_3d/icons/icon_terrain_material.svg.import b/addons/terrain_3d/icons/icon_terrain_material.svg.import
index a7e3e96..ca135d4 100644
--- a/addons/terrain_3d/icons/icon_terrain_material.svg.import
+++ b/addons/terrain_3d/icons/icon_terrain_material.svg.import
@@ -2,9 +2,10 @@
importer="texture"
type="CompressedTexture2D"
-uid="uid://ccywk15q2ad8q"
+uid="uid://hsoj3vbpdg6d"
path="res://.godot/imported/icon_terrain_material.svg-ea0cde03d29920e042a4043982714cbe.ctex"
metadata={
+"has_editor_variant": true,
"vram_texture": false
}
@@ -33,5 +34,5 @@ process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
-editor/scale_with_editor_scale=false
+editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false
diff --git a/addons/terrain_3d/icons/icon_wetness.svg.import b/addons/terrain_3d/icons/icon_wetness.svg.import
index 367b1a0..336742c 100644
--- a/addons/terrain_3d/icons/icon_wetness.svg.import
+++ b/addons/terrain_3d/icons/icon_wetness.svg.import
@@ -2,9 +2,10 @@
importer="texture"
type="CompressedTexture2D"
-uid="uid://bv4f7kijr12yr"
+uid="uid://cidqhk7wrkcl7"
path="res://.godot/imported/icon_wetness.svg-6c67b1e4e9435c1aa106bee56f000e06.ctex"
metadata={
+"has_editor_variant": true,
"vram_texture": false
}
@@ -33,5 +34,5 @@ process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
-editor/scale_with_editor_scale=false
+editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false
diff --git a/addons/terrain_3d/plugin.cfg b/addons/terrain_3d/plugin.cfg
index 19f799a..d485abf 100644
--- a/addons/terrain_3d/plugin.cfg
+++ b/addons/terrain_3d/plugin.cfg
@@ -3,5 +3,5 @@
name="Terrain3D"
description="A high performance, editable terrain system for Godot 4."
author="Cory Petkovsek & Roope Palmroos"
-version="0.9.1"
-script="editor/editor.gd"
+version="0.9.2-dev"
+script="editor.gd"
diff --git a/addons/terrain_3d/editor/components/texture_dock.gd b/addons/terrain_3d/src/asset_dock.gd
similarity index 73%
rename from addons/terrain_3d/editor/components/texture_dock.gd
rename to addons/terrain_3d/src/asset_dock.gd
index 0d64dc9..6cbbb1b 100644
--- a/addons/terrain_3d/editor/components/texture_dock.gd
+++ b/addons/terrain_3d/src/asset_dock.gd
@@ -1,6 +1,7 @@
+@tool
extends PanelContainer
-
+signal placement_changed(index: int)
signal resource_changed(resource: Resource, index: int)
signal resource_inspected(resource: Resource)
signal resource_selected
@@ -8,36 +9,55 @@ signal resource_selected
var list: ListContainer
var entries: Array[ListEntry]
var selected_index: int = 0
+var focus_style: StyleBox
+
+@onready var placement_option: OptionButton = $VBox/PlacementHBox/Options
+@onready var placement_pin: Button = $VBox/PlacementHBox/Pinned
+@onready var size_slider: HSlider = $VBox/SizeSlider
-func _init() -> void:
+func _ready() -> void:
+ placement_option.item_selected.connect(_on_placement_selected)
+ size_slider.value_changed.connect(_on_slider_changed)
+
list = ListContainer.new()
-
- var root: VBoxContainer = VBoxContainer.new()
- var scroll: ScrollContainer = ScrollContainer.new()
- var label: Label = Label.new()
-
- label.set_text("Textures")
- label.set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER)
- label.set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER)
- scroll.set_v_size_flags(SIZE_EXPAND_FILL)
- scroll.set_h_size_flags(SIZE_EXPAND_FILL)
list.set_v_size_flags(SIZE_EXPAND_FILL)
list.set_h_size_flags(SIZE_EXPAND_FILL)
-
- scroll.add_child(list)
- root.add_child(label)
- root.add_child(scroll)
- add_child(root)
-
- set_custom_minimum_size(Vector2(256, 448))
+ $VBox/ScrollContainer.add_child(list)
-
-func _ready() -> void:
- get_child(0).get_child(0).set("theme_override_styles/normal", get_theme_stylebox("bg", "EditorInspectorCategory"))
- get_child(0).get_child(0).set("theme_override_fonts/font", get_theme_font("bold", "EditorFonts"))
- get_child(0).get_child(0).set("theme_override_font_sizes/font_size",get_theme_font_size("bold_size", "EditorFonts"))
- set("theme_override_styles/panel", get_theme_stylebox("panel", "Panel"))
+ # Copy theme from the editor, but since its a tool script, avoid saving icon resources in tscn
+ if EditorScript.new().get_editor_interface().get_edited_scene_root() != self:
+ set("theme_override_styles/panel", get_theme_stylebox("panel", "Panel"))
+ $VBox/Label.set("theme_override_styles/normal", get_theme_stylebox("bg", "EditorInspectorCategory"))
+ $VBox/Label.set("theme_override_fonts/font", get_theme_font("bold", "EditorFonts"))
+ $VBox/Label.set("theme_override_font_sizes/font_size",get_theme_font_size("bold_size", "EditorFonts"))
+ placement_pin.icon = get_theme_icon("Pin", "EditorIcons")
+ placement_pin.text = ""
+
+ # Setup style for selected assets
+ focus_style = get_theme_stylebox("focus", "Button").duplicate()
+ focus_style.set_border_width_all(2)
+ focus_style.set_border_color(Color(1, 1, 1, .67))
+
+
+func _on_placement_selected(index: int) -> void:
+ emit_signal("placement_changed", index)
+
+
+func _on_slider_changed(value: float) -> void:
+ if list:
+ list.set_entry_size(value)
+
+
+func move_slider(to_side: bool) -> void:
+ if to_side and size_slider.get_parent() != $VBox:
+ size_slider.reparent($VBox)
+ $VBox.move_child(size_slider, 2)
+ size_slider.custom_minimum_size = Vector2(0, 0)
+ elif not to_side and size_slider.get_parent() == $VBox:
+ size_slider.reparent($VBox/PlacementHBox)
+ $VBox/PlacementHBox.move_child(size_slider, 2)
+ size_slider.custom_minimum_size = Vector2(300, 10)
func clear() -> void:
@@ -49,6 +69,7 @@ func clear() -> void:
func add_item(p_resource: Resource = null) -> void:
var entry: ListEntry = ListEntry.new()
+ entry.focus_style = focus_style
var index: int = entries.size()
entry.set_edited_resource(p_resource)
@@ -102,23 +123,38 @@ func notify_resource_changed(p_resource: Resource, p_index: int) -> void:
class ListContainer extends Container:
var height: float = 0
+ var width: float = 83
- func _notification(p_what) -> void:
- if p_what == NOTIFICATION_SORT_CHILDREN:
- height = 0
- var index: int = 0
- var separation: float = 4
- for c in get_children():
- if is_instance_valid(c):
- var width: float = size.x / 3
- c.size = Vector2(width,width) - Vector2(separation, separation)
- c.position = Vector2(index % 3, index / 3) * width + Vector2(separation/3, separation/3)
- height = max(height, c.position.y + width)
- index += 1
-
+
+ func set_entry_size(value: float) -> void:
+ width = clamp(value, 56, 256)
+ redraw()
+
+
+ func redraw() -> void:
+ height = 0
+ var index: int = 0
+ var separation: float = 4
+ var columns: int = 3
+ columns = clamp(size.x / width, 1, 100)
+
+ for c in get_children():
+ if is_instance_valid(c):
+ c.size = Vector2(width, width) - Vector2(separation, separation)
+ c.position = Vector2(index % columns, index / columns) * width + \
+ Vector2(separation / columns, separation / columns)
+ height = max(height, c.position.y + width)
+ index += 1
+
+
func _get_minimum_size() -> Vector2:
return Vector2(0, height)
+
+ func _notification(p_what) -> void:
+ if p_what == NOTIFICATION_SORT_CHILDREN:
+ redraw()
+
##############################################################
## class ListEntry
@@ -143,7 +179,7 @@ class ListEntry extends VBoxContainer:
@onready var clear_icon: Texture2D = get_theme_icon("Close", "EditorIcons")
@onready var edit_icon: Texture2D = get_theme_icon("Edit", "EditorIcons")
@onready var background: StyleBox = get_theme_stylebox("pressed", "Button")
- @onready var focus: StyleBox = get_theme_stylebox("focus", "Button")
+ var focus_style: StyleBox
func _ready() -> void:
@@ -196,11 +232,11 @@ class ListEntry extends VBoxContainer:
texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST_WITH_MIPMAPS
name_label.add_theme_font_size_override("font_size", 4 + rect.size.x/10)
if drop_data:
- draw_style_box(focus, rect)
+ draw_style_box(focus_style, rect)
if is_hovered:
draw_rect(rect, Color(1,1,1,0.2))
if is_selected:
- draw_style_box(focus, rect)
+ draw_style_box(focus_style, rect)
NOTIFICATION_MOUSE_ENTER:
is_hovered = true
name_label.visible = true
diff --git a/addons/terrain_3d/src/asset_dock.tscn b/addons/terrain_3d/src/asset_dock.tscn
new file mode 100644
index 0000000..d9bc025
--- /dev/null
+++ b/addons/terrain_3d/src/asset_dock.tscn
@@ -0,0 +1,69 @@
+[gd_scene load_steps=2 format=3 uid="uid://dkb6hii5e48m2"]
+
+[ext_resource type="Script" path="res://addons/terrain_3d/src/asset_dock.gd" id="1_e23pg"]
+
+[node name="Terrain3D" type="PanelContainer"]
+custom_minimum_size = Vector2(256, 136)
+offset_right = 256.0
+offset_bottom = 128.0
+script = ExtResource("1_e23pg")
+
+[node name="VBox" type="VBoxContainer" parent="."]
+layout_mode = 2
+
+[node name="PlacementHBox" type="HBoxContainer" parent="VBox"]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="VBox/PlacementHBox"]
+layout_mode = 2
+text = "Dock Position: "
+
+[node name="Options" type="OptionButton" parent="VBox/PlacementHBox"]
+layout_mode = 2
+size_flags_horizontal = 3
+item_count = 9
+selected = 5
+popup/item_0/text = "Left_UL"
+popup/item_0/id = 0
+popup/item_1/text = "Left_BL"
+popup/item_1/id = 1
+popup/item_2/text = "Left_UR"
+popup/item_2/id = 2
+popup/item_3/text = "Left_BR"
+popup/item_3/id = 3
+popup/item_4/text = "Right_UL"
+popup/item_4/id = 4
+popup/item_5/text = "Right_BL "
+popup/item_5/id = 5
+popup/item_6/text = "Right_UR"
+popup/item_6/id = 6
+popup/item_7/text = "Right_BR"
+popup/item_7/id = 7
+popup/item_8/text = "Bottom"
+popup/item_8/id = 8
+
+[node name="Pinned" type="Button" parent="VBox/PlacementHBox"]
+layout_mode = 2
+tooltip_text = "Keep panel visible even if Terrain3D is not selected. Useful for keeping dock floating."
+toggle_mode = true
+text = "P"
+flat = true
+
+[node name="Label" type="Label" parent="VBox"]
+layout_mode = 2
+theme_override_font_sizes/font_size = 16
+text = "Textures"
+horizontal_alignment = 1
+vertical_alignment = 1
+
+[node name="SizeSlider" type="HSlider" parent="VBox"]
+custom_minimum_size = Vector2(100, 10)
+layout_mode = 2
+min_value = 56.0
+max_value = 256.0
+value = 83.0
+
+[node name="ScrollContainer" type="ScrollContainer" parent="VBox"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
diff --git a/addons/terrain_3d/editor/components/bake_lod_dialog.gd b/addons/terrain_3d/src/bake_lod_dialog.gd
similarity index 100%
rename from addons/terrain_3d/editor/components/bake_lod_dialog.gd
rename to addons/terrain_3d/src/bake_lod_dialog.gd
diff --git a/addons/terrain_3d/editor/components/bake_lod_dialog.tscn b/addons/terrain_3d/src/bake_lod_dialog.tscn
similarity index 87%
rename from addons/terrain_3d/editor/components/bake_lod_dialog.tscn
rename to addons/terrain_3d/src/bake_lod_dialog.tscn
index 17d5c72..1011c72 100644
--- a/addons/terrain_3d/editor/components/bake_lod_dialog.tscn
+++ b/addons/terrain_3d/src/bake_lod_dialog.tscn
@@ -1,13 +1,13 @@
[gd_scene load_steps=2 format=3 uid="uid://bhvrrmb8bk1bt"]
-[ext_resource type="Script" path="res://addons/terrain_3d/editor/components/bake_lod_dialog.gd" id="1_57670"]
+[ext_resource type="Script" path="res://addons/terrain_3d/src/bake_lod_dialog.gd" id="1_sf76d"]
[node name="bake_lod_dialog" type="ConfirmationDialog"]
title = "Bake Terrain3D Mesh"
position = Vector2i(0, 36)
size = Vector2i(400, 115)
visible = true
-script = ExtResource("1_57670")
+script = ExtResource("1_sf76d")
[node name="VBoxContainer" type="VBoxContainer" parent="."]
anchors_preset = 15
diff --git a/addons/terrain_3d/editor/components/baker.gd b/addons/terrain_3d/src/baker.gd
similarity index 99%
rename from addons/terrain_3d/editor/components/baker.gd
rename to addons/terrain_3d/src/baker.gd
index 37ff153..a4cb99a 100644
--- a/addons/terrain_3d/editor/components/baker.gd
+++ b/addons/terrain_3d/src/baker.gd
@@ -1,6 +1,6 @@
extends Node
-const BakeLodDialog: PackedScene = preload("res://addons/terrain_3d/editor/components/bake_lod_dialog.tscn")
+const BakeLodDialog: PackedScene = preload("res://addons/terrain_3d/src/bake_lod_dialog.tscn")
const BAKE_MESH_DESCRIPTION: String = "This will create a child MeshInstance3D. LOD4+ is recommended. LOD0 is slow and dense with vertices every 1 unit. It is not an optimal mesh."
const BAKE_OCCLUDER_DESCRIPTION: String = "This will create a child OccluderInstance3D. LOD4+ is recommended and will take 5+ seconds per region to generate. LOD0 is unnecessarily dense and slow."
const SET_UP_NAVIGATION_DESCRIPTION: String = "This operation will:
diff --git a/addons/terrain_3d/editor/components/channel_packer.gd b/addons/terrain_3d/src/channel_packer.gd
similarity index 96%
rename from addons/terrain_3d/editor/components/channel_packer.gd
rename to addons/terrain_3d/src/channel_packer.gd
index 30067b1..47c814d 100644
--- a/addons/terrain_3d/editor/components/channel_packer.gd
+++ b/addons/terrain_3d/src/channel_packer.gd
@@ -1,7 +1,7 @@
extends Object
-const WINDOW_SCENE: String = "res://addons/terrain_3d/editor/components/channel_packer.tscn"
-const TEMPLATE_PATH: String = "res://addons/terrain_3d/editor/components/channel_packer_import_template.txt"
+const WINDOW_SCENE: String = "res://addons/terrain_3d/src/channel_packer.tscn"
+const TEMPLATE_PATH: String = "res://addons/terrain_3d/src/channel_packer_import_template.txt"
enum {
IMAGE_ALBEDO,
@@ -198,7 +198,7 @@ func _pack_textures(p_rgb_image: Image, p_a_image: Image, p_dst_path: String, p_
_show_error("Textures must be the same size.")
return
- var output_image: Image = Terrain3D.pack_image(p_rgb_image, p_a_image, p_invert_green)
+ var output_image: Image = Terrain3DUtil.pack_image(p_rgb_image, p_a_image, p_invert_green)
if not output_image:
_show_error("Failed to pack textures.")
diff --git a/addons/terrain_3d/editor/components/channel_packer.tscn b/addons/terrain_3d/src/channel_packer.tscn
similarity index 100%
rename from addons/terrain_3d/editor/components/channel_packer.tscn
rename to addons/terrain_3d/src/channel_packer.tscn
diff --git a/addons/terrain_3d/editor/components/channel_packer_import_template.txt b/addons/terrain_3d/src/channel_packer_import_template.txt
similarity index 100%
rename from addons/terrain_3d/editor/components/channel_packer_import_template.txt
rename to addons/terrain_3d/src/channel_packer_import_template.txt
diff --git a/addons/terrain_3d/editor/components/gradient_operation_builder.gd b/addons/terrain_3d/src/gradient_operation_builder.gd
similarity index 85%
rename from addons/terrain_3d/editor/components/gradient_operation_builder.gd
rename to addons/terrain_3d/src/gradient_operation_builder.gd
index e92875f..f08a734 100644
--- a/addons/terrain_3d/editor/components/gradient_operation_builder.gd
+++ b/addons/terrain_3d/src/gradient_operation_builder.gd
@@ -1,10 +1,10 @@
-extends "res://addons/terrain_3d/editor/components/operation_builder.gd"
+extends "res://addons/terrain_3d/src/operation_builder.gd"
-const PointPicker: Script = preload("res://addons/terrain_3d/editor/components/point_picker.gd")
+const MultiPicker: Script = preload("res://addons/terrain_3d/src/multi_picker.gd")
-func _get_point_picker() -> PointPicker:
+func _get_point_picker() -> MultiPicker:
return tool_settings.settings["gradient_points"]
diff --git a/addons/terrain_3d/editor/components/point_picker.gd b/addons/terrain_3d/src/multi_picker.gd
similarity index 96%
rename from addons/terrain_3d/editor/components/point_picker.gd
rename to addons/terrain_3d/src/multi_picker.gd
index dbc1280..01abd3e 100644
--- a/addons/terrain_3d/editor/components/point_picker.gd
+++ b/addons/terrain_3d/src/multi_picker.gd
@@ -20,10 +20,6 @@ func _init() -> void:
icon_picker = load(ICON_PICKER)
icon_picker_checked = load(ICON_PICKER_CHECKED)
- var label := Label.new()
- label.text = "Points:"
- add_child(label)
-
points.resize(MAX_POINTS)
for i in range(MAX_POINTS):
diff --git a/addons/terrain_3d/editor/components/operation_builder.gd b/addons/terrain_3d/src/operation_builder.gd
similarity index 77%
rename from addons/terrain_3d/editor/components/operation_builder.gd
rename to addons/terrain_3d/src/operation_builder.gd
index ec3d51e..507856d 100644
--- a/addons/terrain_3d/editor/components/operation_builder.gd
+++ b/addons/terrain_3d/src/operation_builder.gd
@@ -1,7 +1,7 @@
extends RefCounted
-const ToolSettings: Script = preload("res://addons/terrain_3d/editor/components/tool_settings.gd")
+const ToolSettings: Script = preload("res://addons/terrain_3d/src/tool_settings.gd")
var tool_settings: ToolSettings
diff --git a/addons/terrain_3d/editor/components/region_gizmo.gd b/addons/terrain_3d/src/region_gizmo.gd
similarity index 100%
rename from addons/terrain_3d/editor/components/region_gizmo.gd
rename to addons/terrain_3d/src/region_gizmo.gd
diff --git a/addons/terrain_3d/editor/components/terrain_tools.gd b/addons/terrain_3d/src/terrain_tools.gd
similarity index 93%
rename from addons/terrain_3d/editor/components/terrain_tools.gd
rename to addons/terrain_3d/src/terrain_tools.gd
index e9fbbcc..f99b0aa 100644
--- a/addons/terrain_3d/editor/components/terrain_tools.gd
+++ b/addons/terrain_3d/src/terrain_tools.gd
@@ -1,8 +1,8 @@
extends HBoxContainer
-const Baker: Script = preload("res://addons/terrain_3d/editor/components/baker.gd")
-const Packer: Script = preload("res://addons/terrain_3d/editor/components/channel_packer.gd")
+const Baker: Script = preload("res://addons/terrain_3d/src/baker.gd")
+const Packer: Script = preload("res://addons/terrain_3d/src/channel_packer.gd")
var plugin: EditorPlugin
var menu_button: MenuButton = MenuButton.new()
diff --git a/addons/terrain_3d/src/tool_settings.gd b/addons/terrain_3d/src/tool_settings.gd
new file mode 100644
index 0000000..00328e1
--- /dev/null
+++ b/addons/terrain_3d/src/tool_settings.gd
@@ -0,0 +1,645 @@
+extends PanelContainer
+
+signal picking(type, callback)
+signal setting_changed
+
+enum Layout {
+ HORIZONTAL,
+ VERTICAL,
+ GRID,
+}
+
+enum SettingType {
+ CHECKBOX,
+ COLOR_SELECT,
+ DOUBLE_SLIDER,
+ OPTION,
+ PICKER,
+ MULTI_PICKER,
+ SLIDER,
+ TYPE_MAX,
+}
+
+const MultiPicker: Script = preload("res://addons/terrain_3d/src/multi_picker.gd")
+const DEFAULT_BRUSH: String = "circle0.exr"
+const BRUSH_PATH: String = "res://addons/terrain_3d/brushes"
+const PICKER_ICON: String = "res://addons/terrain_3d/icons/icon_picker.svg"
+
+# Add settings flags
+const NONE: int = 0x0
+const ALLOW_LARGER: int = 0x1
+const ALLOW_SMALLER: int = 0x2
+const ALLOW_OUT_OF_BOUNDS: int = 0x3 # LARGER|SMALLER
+const NO_LABEL: int = 0x4
+const ADD_SEPARATOR: int = 0x8
+const ADD_SPACER: int = 0x10
+
+var brush_preview_material: ShaderMaterial
+var select_brush_button: Button
+
+var main_list: HBoxContainer
+var advanced_list: VBoxContainer
+var height_list: VBoxContainer
+var scale_list: VBoxContainer
+var rotation_list: VBoxContainer
+var color_list: VBoxContainer
+var settings: Dictionary = {}
+
+
+func _ready() -> void:
+ main_list = HBoxContainer.new()
+ add_child(main_list, true)
+
+ ## Common Settings
+ add_brushes(main_list)
+
+ add_setting({ "name":"size", "type":SettingType.SLIDER, "list":main_list, "default":50, "unit":"m",
+ "range":Vector3(2, 200, 1), "flags":ALLOW_LARGER|ADD_SPACER })
+
+ add_setting({ "name":"strength", "type":SettingType.SLIDER, "list":main_list, "default":10,
+ "unit":"%", "range":Vector3(1, 100, 1), "flags":ALLOW_LARGER })
+
+ add_setting({ "name":"enable", "type":SettingType.CHECKBOX, "list":main_list, "default":true })
+
+ add_setting({ "name":"height", "type":SettingType.SLIDER, "list":main_list, "default":50,
+ "unit":"m", "range":Vector3(-500, 500, 0.1), "flags":ALLOW_OUT_OF_BOUNDS })
+ add_setting({ "name":"height_picker", "type":SettingType.PICKER, "list":main_list,
+ "default":Terrain3DEditor.HEIGHT, "flags":NO_LABEL })
+
+ add_setting({ "name":"color", "type":SettingType.COLOR_SELECT, "list":main_list,
+ "default":Color.WHITE, "flags":ADD_SEPARATOR })
+ add_setting({ "name":"color_picker", "type":SettingType.PICKER, "list":main_list,
+ "default":Terrain3DEditor.COLOR, "flags":NO_LABEL })
+
+ add_setting({ "name":"roughness", "type":SettingType.SLIDER, "list":main_list, "default":0,
+ "unit":"%", "range":Vector3(-100, 100, 1), "flags":ADD_SEPARATOR })
+ add_setting({ "name":"roughness_picker", "type":SettingType.PICKER, "list":main_list,
+ "default":Terrain3DEditor.ROUGHNESS, "flags":NO_LABEL })
+
+ add_setting({ "name":"enable_texture", "label":"Texture", "type":SettingType.CHECKBOX,
+ "list":main_list, "default":true })
+
+ add_setting({ "name":"enable_angle", "label":"Angle", "type":SettingType.CHECKBOX,
+ "list":main_list, "default":true, "flags":ADD_SEPARATOR })
+ add_setting({ "name":"angle", "type":SettingType.SLIDER, "list":main_list, "default":0,
+ "unit":"%", "range":Vector3(0, 337.5, 22.5), "flags":NO_LABEL })
+ add_setting({ "name":"angle_picker", "type":SettingType.PICKER, "list":main_list,
+ "default":Terrain3DEditor.ANGLE, "flags":NO_LABEL })
+ add_setting({ "name":"dynamic_angle", "label":"Dynamic", "type":SettingType.CHECKBOX,
+ "list":main_list, "default":false, "flags":ADD_SPACER })
+
+ add_setting({ "name":"enable_scale", "label":"Scale ±", "type":SettingType.CHECKBOX,
+ "list":main_list, "default":true, "flags":ADD_SEPARATOR })
+ add_setting({ "name":"scale", "label":"±", "type":SettingType.SLIDER, "list":main_list, "default":0,
+ "unit":"%", "range":Vector3(-60, 80, 20), "flags":NO_LABEL })
+ add_setting({ "name":"scale_picker", "type":SettingType.PICKER, "list":main_list,
+ "default":Terrain3DEditor.SCALE, "flags":NO_LABEL })
+
+ ## Slope
+ add_setting({ "name":"slope", "type":SettingType.DOUBLE_SLIDER, "list":main_list,
+ "default":0, "unit":"°", "range":Vector3(0, 180, 1) })
+ add_setting({ "name":"gradient_points", "type":SettingType.MULTI_PICKER, "label":"Points",
+ "list":main_list, "default":Terrain3DEditor.HEIGHT, "flags":ADD_SEPARATOR })
+ add_setting({ "name":"drawable", "type":SettingType.CHECKBOX, "list":main_list, "default":false,
+ "flags":ADD_SEPARATOR })
+ settings["drawable"].toggled.connect(_on_drawable_toggled)
+
+ var spacer: Control = Control.new()
+ spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
+ main_list.add_child(spacer, true)
+
+ ## Advanced Settings Menu
+ advanced_list = create_submenu(main_list, "Advanced", Layout.VERTICAL)
+ add_setting({ "name":"automatic_regions", "type":SettingType.CHECKBOX, "list":advanced_list,
+ "default":true })
+ add_setting({ "name":"align_to_view", "type":SettingType.CHECKBOX, "list":advanced_list,
+ "default":true })
+ add_setting({ "name":"show_cursor_while_painting", "type":SettingType.CHECKBOX, "list":advanced_list,
+ "default":true })
+ advanced_list.add_child(HSeparator.new(), true)
+ add_setting({ "name":"gamma", "type":SettingType.SLIDER, "list":advanced_list, "default":1.0,
+ "unit":"γ", "range":Vector3(0.1, 2.0, 0.01) })
+ add_setting({ "name":"jitter", "type":SettingType.SLIDER, "list":advanced_list, "default":50,
+ "unit":"%", "range":Vector3(0, 100, 1) })
+
+
+func create_submenu(p_parent: Control, p_button_name: String, p_layout: Layout) -> Container:
+ var menu_button: Button = Button.new()
+ menu_button.set_text(p_button_name)
+ menu_button.set_toggle_mode(true)
+ menu_button.set_v_size_flags(SIZE_SHRINK_CENTER)
+ menu_button.toggled.connect(_on_show_submenu.bind(menu_button))
+
+ var submenu: PopupPanel = PopupPanel.new()
+ submenu.popup_hide.connect(menu_button.set_pressed_no_signal.bind(false))
+ var panel_style: StyleBox = get_theme_stylebox("panel", "PopupMenu").duplicate()
+ panel_style.set_content_margin_all(10)
+ submenu.set("theme_override_styles/panel", panel_style)
+
+ # Pop up menu on hover, hide on exit
+ menu_button.mouse_entered.connect(_on_show_submenu.bind(true, menu_button))
+ menu_button.mouse_exited.connect(_on_show_submenu.bind(false, menu_button))
+ submenu.mouse_exited.connect(_on_show_submenu.bind(false, menu_button))
+
+ var sublist: Container
+ match(p_layout):
+ Layout.GRID:
+ sublist = GridContainer.new()
+ Layout.VERTICAL:
+ sublist = VBoxContainer.new()
+ Layout.HORIZONTAL, _:
+ sublist = HBoxContainer.new()
+
+ p_parent.add_child(menu_button, true)
+ menu_button.add_child(submenu, true)
+ submenu.add_child(sublist, true)
+
+ return sublist
+
+
+func _on_show_submenu(p_toggled: bool, p_button: Button) -> void:
+ # Don't show if mouse already down (from painting)
+ if p_toggled and Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT):
+ return
+
+ # Hide menu if mouse is not in button or panel
+ var button_rect: Rect2 = Rect2(p_button.get_screen_transform().origin, p_button.get_global_rect().size)
+ var in_button: bool = button_rect.has_point(DisplayServer.mouse_get_position())
+ var panel: PopupPanel = p_button.get_child(0)
+ var panel_rect: Rect2 = Rect2(panel.position, panel.size)
+ var in_panel: bool = panel_rect.has_point(DisplayServer.mouse_get_position())
+ if not p_toggled and ( in_button or in_panel ):
+ return
+
+ var popup: PopupPanel = p_button.get_child(0)
+ var popup_pos: Vector2 = p_button.get_screen_transform().origin
+ popup.set_visible(p_toggled)
+ popup_pos.y -= popup.get_size().y
+ popup.set_position(popup_pos)
+
+
+func add_brushes(p_parent: Control) -> void:
+ var brush_list: GridContainer = create_submenu(p_parent, "Brush", Layout.GRID)
+ brush_list.name = "BrushList"
+
+ var brush_button_group: ButtonGroup = ButtonGroup.new()
+ brush_button_group.pressed.connect(_on_setting_changed)
+ var default_brush_btn: Button
+
+ var dir: DirAccess = DirAccess.open(BRUSH_PATH)
+ if dir:
+ dir.list_dir_begin()
+ var file_name = dir.get_next()
+ while file_name != "":
+ if !dir.current_is_dir() and file_name.ends_with(".exr"):
+ var img: Image = Image.load_from_file(BRUSH_PATH + "/" + file_name)
+ img = Terrain3DUtil.black_to_alpha(img)
+ var tex: ImageTexture = ImageTexture.create_from_image(img)
+
+ var btn: Button = Button.new()
+ btn.set_custom_minimum_size(Vector2.ONE * 100)
+ btn.set_button_icon(tex)
+ btn.set_meta("image", img)
+ btn.set_expand_icon(true)
+ btn.set_material(_get_brush_preview_material())
+ btn.set_toggle_mode(true)
+ btn.set_button_group(brush_button_group)
+ btn.mouse_entered.connect(_on_brush_hover.bind(true, btn))
+ btn.mouse_exited.connect(_on_brush_hover.bind(false, btn))
+ brush_list.add_child(btn, true)
+ if file_name == DEFAULT_BRUSH:
+ default_brush_btn = btn
+
+ var lbl: Label = Label.new()
+ btn.name = file_name.get_basename().to_pascal_case()
+ btn.add_child(lbl, true)
+ lbl.text = btn.name
+ lbl.visible = false
+ lbl.position.y = 70
+ lbl.add_theme_color_override("font_shadow_color", Color.BLACK)
+ lbl.add_theme_constant_override("shadow_offset_x", 1)
+ lbl.add_theme_constant_override("shadow_offset_y", 1)
+ lbl.add_theme_font_size_override("font_size", 16)
+
+ file_name = dir.get_next()
+
+ brush_list.columns = sqrt(brush_list.get_child_count()) + 2
+
+ if not default_brush_btn:
+ default_brush_btn = brush_button_group.get_buttons()[0]
+ default_brush_btn.set_pressed(true)
+
+ settings["brush"] = brush_button_group
+
+ select_brush_button = brush_list.get_parent().get_parent()
+ # Optionally erase the main brush button text and replace it with the texture
+# select_brush_button.set_button_icon(default_brush_btn.get_button_icon())
+# select_brush_button.set_custom_minimum_size(Vector2.ONE * 36)
+# select_brush_button.set_icon_alignment(HORIZONTAL_ALIGNMENT_CENTER)
+# select_brush_button.set_expand_icon(true)
+
+
+func _on_brush_hover(p_hovering: bool, p_button: Button) -> void:
+ if p_button.get_child_count() > 0:
+ var child = p_button.get_child(0)
+ if child is Label:
+ if p_hovering:
+ child.visible = true
+ else:
+ child.visible = false
+
+
+func _on_pick(p_type: Terrain3DEditor.Tool) -> void:
+ emit_signal("picking", p_type, _on_picked)
+
+
+func _on_picked(p_type: Terrain3DEditor.Tool, p_color: Color, p_global_position: Vector3) -> void:
+ match p_type:
+ Terrain3DEditor.HEIGHT:
+ settings["height"].value = p_color.r if not is_nan(p_color.r) else 0
+ Terrain3DEditor.COLOR:
+ settings["color"].color = p_color if not is_nan(p_color.r) else Color.WHITE
+ Terrain3DEditor.ROUGHNESS:
+ # 200... -.5 converts 0,1 to -100,100
+ settings["roughness"].value = round(200 * (p_color.a - 0.5)) if not is_nan(p_color.r) else 0.499
+ Terrain3DEditor.ANGLE:
+ settings["angle"].value = p_color.r
+ Terrain3DEditor.SCALE:
+ settings["scale"].value = p_color.r
+ _on_setting_changed()
+
+
+func _on_point_pick(p_type: Terrain3DEditor.Tool, p_name: String) -> void:
+ assert(p_type == Terrain3DEditor.HEIGHT)
+ emit_signal("picking", p_type, _on_point_picked.bind(p_name))
+
+
+func _on_point_picked(p_type: Terrain3DEditor.Tool, p_color: Color, p_global_position: Vector3, p_name: String) -> void:
+ assert(p_type == Terrain3DEditor.HEIGHT)
+
+ var point: Vector3 = p_global_position
+ point.y = p_color.r
+ settings[p_name].add_point(point)
+ _on_setting_changed()
+
+
+func add_setting(p_args: Dictionary) -> void:
+ var p_name: StringName = p_args.get("name", "")
+ var p_label: String = p_args.get("label", "") # Optional replacement for name
+ var p_type: SettingType = p_args.get("type", SettingType.TYPE_MAX)
+ var p_list: Control = p_args.get("list")
+ var p_default: Variant = p_args.get("default")
+ var p_suffix: String = p_args.get("unit", "")
+ var p_range: Vector3 = p_args.get("range", Vector3(0, 0, 1))
+ var p_minimum: float = p_range.x
+ var p_maximum: float = p_range.y
+ var p_step: float = p_range.z
+ var p_flags: int = p_args.get("flags", NONE)
+
+ if p_name.is_empty() or p_type == SettingType.TYPE_MAX:
+ return
+
+ var container: HBoxContainer = HBoxContainer.new()
+ container.set_v_size_flags(SIZE_EXPAND_FILL)
+ var control: Control # Houses the setting to be saved
+ var pending_children: Array[Control]
+
+ match p_type:
+ SettingType.CHECKBOX:
+ var checkbox := CheckBox.new()
+ checkbox.set_pressed_no_signal(p_default)
+ checkbox.pressed.connect(_on_setting_changed)
+ pending_children.push_back(checkbox)
+ control = checkbox
+
+ SettingType.COLOR_SELECT:
+ var picker := ColorPickerButton.new()
+ picker.set_custom_minimum_size(Vector2(100, 25))
+ picker.color = Color.WHITE
+ picker.edit_alpha = false
+ picker.get_picker().set_color_mode(ColorPicker.MODE_HSV)
+ picker.color_changed.connect(_on_setting_changed)
+ var popup: PopupPanel = picker.get_popup()
+ popup.mouse_exited.connect(Callable(func(p): p.hide()).bind(popup))
+ pending_children.push_back(picker)
+ control = picker
+
+ SettingType.PICKER:
+ var button := Button.new()
+ button.icon = load(PICKER_ICON)
+ button.tooltip_text = "Pick value from the Terrain"
+ button.pressed.connect(_on_pick.bind(p_default))
+ pending_children.push_back(button)
+ control = button
+
+ SettingType.MULTI_PICKER:
+ var multi_picker: HBoxContainer = MultiPicker.new()
+ multi_picker.pressed.connect(_on_point_pick.bind(p_default, p_name))
+ multi_picker.value_changed.connect(_on_setting_changed)
+ pending_children.push_back(multi_picker)
+ control = multi_picker
+
+ SettingType.OPTION:
+ var option := OptionButton.new()
+ for i in int(p_maximum):
+ option.add_item("a", i)
+ option.selected = p_minimum
+ option.item_selected.connect(_on_setting_changed)
+ pending_children.push_back(option)
+ control = option
+
+ SettingType.SLIDER, SettingType.DOUBLE_SLIDER:
+ var slider: Control
+ if p_type == SettingType.SLIDER:
+ # Create an editable value box
+ var spin_slider := EditorSpinSlider.new()
+ spin_slider.set_flat(false)
+ spin_slider.set_hide_slider(true)
+ spin_slider.value_changed.connect(_on_setting_changed)
+ spin_slider.set_max(p_maximum)
+ spin_slider.set_min(p_minimum)
+ spin_slider.set_step(p_step)
+ spin_slider.set_value(p_default)
+ spin_slider.set_suffix(p_suffix)
+ spin_slider.set_h_size_flags(SIZE_SHRINK_CENTER)
+ spin_slider.set_v_size_flags(SIZE_SHRINK_CENTER)
+ spin_slider.set_custom_minimum_size(Vector2(75, 0))
+
+ # Create horizontal slider linked to the above box
+ slider = HSlider.new()
+ slider.share(spin_slider)
+ if p_flags & ALLOW_LARGER:
+ slider.set_allow_greater(true)
+ if p_flags & ALLOW_SMALLER:
+ slider.set_allow_lesser(true)
+ pending_children.push_back(slider)
+ pending_children.push_back(spin_slider)
+ control = spin_slider
+
+ else: # DOUBLE_SLIDER
+ var label := Label.new()
+ label.set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER)
+ label.set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER)
+ label.set_custom_minimum_size(Vector2(75, 0))
+ slider = DoubleSlider.new()
+ slider.label = label
+ slider.suffix = p_suffix
+ slider.setting_changed.connect(_on_setting_changed)
+ pending_children.push_back(slider)
+ pending_children.push_back(label)
+ control = slider
+
+ slider.set_max(p_maximum)
+ slider.set_min(p_minimum)
+ slider.set_step(p_step)
+ slider.set_value(p_default)
+ slider.set_v_size_flags(SIZE_SHRINK_CENTER)
+ slider.set_h_size_flags(SIZE_SHRINK_END | SIZE_EXPAND)
+ slider.set_custom_minimum_size(Vector2(60, 10))
+
+ control.name = p_name.to_pascal_case()
+ settings[p_name] = control
+
+ # Setup button labels
+ if not (p_flags & NO_LABEL):
+ # Labels are actually buttons styled to look like labels
+ var label := Button.new()
+ label.set("theme_override_styles/normal", get_theme_stylebox("normal", "Label"))
+ label.set("theme_override_styles/hover", get_theme_stylebox("normal", "Label"))
+ label.set("theme_override_styles/pressed", get_theme_stylebox("normal", "Label"))
+ label.set("theme_override_styles/focus", get_theme_stylebox("normal", "Label"))
+ label.pressed.connect(_on_label_pressed.bind(p_name, p_default))
+ if p_label.is_empty():
+ label.set_text(p_name.capitalize() + ": ")
+ else:
+ label.set_text(p_label.capitalize() + ": ")
+ pending_children.push_front(label)
+
+ # Add separators to front
+ if p_flags & ADD_SEPARATOR:
+ pending_children.push_front(VSeparator.new())
+ if p_flags & ADD_SPACER:
+ var spacer := Control.new()
+ spacer.set_custom_minimum_size(Vector2(5, 0))
+ pending_children.push_front(spacer)
+
+ # Add all children to container and list
+ for child in pending_children:
+ container.add_child(child, true)
+ p_list.add_child(container, true)
+
+
+# If label button is pressed, reset value to default or toggle checkbox
+func _on_label_pressed(p_name: String, p_default: Variant) -> void:
+ var control: Control = settings.get(p_name)
+ if not control:
+ return
+ if control is CheckBox:
+ set_setting(p_name, !control.button_pressed)
+ elif p_default != null:
+ set_setting(p_name, p_default)
+
+
+func get_settings() -> Dictionary:
+ var dict: Dictionary
+ for key in settings.keys():
+ dict[key] = get_setting(key)
+ return dict
+
+
+func get_setting(p_setting: String) -> Variant:
+ var object: Object = settings.get(p_setting)
+ var value: Variant
+ if object is Range:
+ value = object.get_value()
+ # Adjust widths of all sliders on update of values
+ var digits: float = count_digits(value)
+ var width: float = clamp( (1 + count_digits(value)) * 19., 50, 80) * clamp(EditorInterface.get_editor_scale(), .9, 2)
+ object.set_custom_minimum_size(Vector2(width, 0))
+ elif object is DoubleSlider:
+ value = Vector2(object.get_min_value(), object.get_max_value())
+ elif object is ButtonGroup:
+ var img: Image = object.get_pressed_button().get_meta("image")
+ var tex: Texture2D = object.get_pressed_button().get_button_icon()
+ value = [ img, tex ]
+ elif object is CheckBox:
+ value = object.is_pressed()
+ elif object is ColorPickerButton:
+ value = object.color
+ elif object is MultiPicker:
+ value = object.get_points()
+ if value == null:
+ value = 0
+ return value
+
+
+func set_setting(p_setting: String, p_value: Variant) -> void:
+ var object: Object = settings.get(p_setting)
+ if object is Range:
+ object.set_value(p_value)
+ elif object is DoubleSlider: # Expects p_value is Vector2
+ object.set_min_value(p_value.x)
+ object.set_max_value(p_value.y)
+ elif object is ButtonGroup: # Expects p_value is Array [ "button name", boolean ]
+ if p_value is Array and p_value.size() == 2:
+ for button in object.get_buttons():
+ if button.name == p_value[0]:
+ button.button_pressed = p_value[1]
+ elif object is CheckBox:
+ object.button_pressed = p_value
+ elif object is ColorPickerButton:
+ object.color = p_value
+ elif object is MultiPicker: # Expects p_value is PackedVector3Array
+ object.points = p_value
+ _on_setting_changed(object)
+
+
+func show_settings(p_settings: PackedStringArray) -> void:
+ for setting in settings.keys():
+ var object: Object = settings[setting]
+ if object is Control:
+ if setting in p_settings:
+ object.get_parent().show()
+ else:
+ object.get_parent().hide()
+ if select_brush_button:
+ if not "brush" in p_settings:
+ select_brush_button.hide()
+ else:
+ select_brush_button.show()
+
+
+func _on_setting_changed(p_data: Variant = null) -> void:
+ # If a button was clicked on a submenu
+ if p_data is Button and p_data.get_parent().get_parent() is PopupPanel:
+ if p_data.get_parent().name == "BrushList":
+ # Optionally Set selected brush texture in main brush button
+# p_data.get_parent().get_parent().get_parent().set_button_icon(p_data.get_button_icon())
+ # Hide popup
+ p_data.get_parent().get_parent().set_visible(false)
+ # Hide label
+ if p_data.get_child_count() > 0:
+ p_data.get_child(0).visible = false
+
+ emit_signal("setting_changed")
+
+
+func _on_drawable_toggled(p_button_pressed: bool) -> void:
+ if not p_button_pressed:
+ settings["gradient_points"].clear()
+
+
+func _get_brush_preview_material() -> ShaderMaterial:
+ if !brush_preview_material:
+ brush_preview_material = ShaderMaterial.new()
+ var shader: Shader = Shader.new()
+
+ var code: String = "shader_type canvas_item;\n"
+ code += "varying vec4 v_vertex_color;\n"
+ code += "void vertex() {\n"
+ code += " v_vertex_color = COLOR;\n"
+ code += "}\n"
+ code += "void fragment(){\n"
+ code += " vec4 tex = texture(TEXTURE, UV);\n"
+ code += " COLOR.a *= pow(tex.r, 0.666);\n"
+ code += " COLOR.rgb = v_vertex_color.rgb;\n"
+ code += "}\n"
+
+ shader.set_code(code)
+ brush_preview_material.set_shader(shader)
+ return brush_preview_material
+
+
+
+# Counts digits of a number including negative sign, decimal points, and up to 3 decimals
+func count_digits(p_value: float) -> int:
+ var count: int = 1
+ for i in range(5, 0, -1):
+ if abs(p_value) >= pow(10, i):
+ count = i+1
+ break
+ if p_value - floor(p_value) >= .1:
+ count += 1 # For the decimal
+ if p_value*10 - floor(p_value*10.) >= .1:
+ count += 1
+ if p_value*100 - floor(p_value*100.) >= .1:
+ count += 1
+ if p_value*1000 - floor(p_value*1000.) >= .1:
+ count += 1
+ # Negative sign
+ if p_value < 0:
+ count += 1
+ return count
+
+
+#### Sub Class DoubleSlider
+
+class DoubleSlider extends Range:
+ signal setting_changed(Vector2)
+ var label: Label
+ var suffix: String
+ var grabbed: bool = false
+ var _max_value: float
+ # TODO Needs to clamp min and max values. Currently allows max slider to go negative.
+
+ func _gui_input(p_event: InputEvent) -> void:
+ if p_event is InputEventMouseButton:
+ if p_event.get_button_index() == MOUSE_BUTTON_LEFT:
+ grabbed = p_event.is_pressed()
+ set_min_max(p_event.get_position().x)
+
+ if p_event is InputEventMouseMotion:
+ if grabbed:
+ set_min_max(p_event.get_position().x)
+
+
+ func _notification(p_what: int) -> void:
+ if p_what == NOTIFICATION_RESIZED:
+ pass
+ if p_what == NOTIFICATION_DRAW:
+ var bg: StyleBox = get_theme_stylebox("slider", "HSlider")
+ var bg_height: float = bg.get_minimum_size().y
+ draw_style_box(bg, Rect2(Vector2(0, (size.y - bg_height) / 2), Vector2(size.x, bg_height)))
+
+ var grabber: Texture2D = get_theme_icon("grabber", "HSlider")
+ var area: StyleBox = get_theme_stylebox("grabber_area", "HSlider")
+ var h: float = size.y / 2 - grabber.get_size().y / 2
+
+ var minpos: Vector2 = Vector2((min_value / _max_value) * size.x - grabber.get_size().x / 2, h)
+ var maxpos: Vector2 = Vector2((max_value / _max_value) * size.x - grabber.get_size().x / 2, h)
+
+ draw_style_box(area, Rect2(Vector2(minpos.x + grabber.get_size().x / 2, (size.y - bg_height) / 2), Vector2(maxpos.x - minpos.x, bg_height)))
+
+ draw_texture(grabber, minpos)
+ draw_texture(grabber, maxpos)
+
+
+ func set_max(p_value: float) -> void:
+ max_value = p_value
+ if _max_value == 0:
+ _max_value = max_value
+ update_label()
+
+
+ func set_min_max(p_xpos: float) -> void:
+ var mid_value_normalized: float = ((max_value + min_value) / 2.0) / _max_value
+ var mid_value: float = size.x * mid_value_normalized
+ var min_active: bool = p_xpos < mid_value
+ var xpos_ranged: float = snappedf((p_xpos / size.x) * _max_value, step)
+
+ if min_active:
+ min_value = xpos_ranged
+ else:
+ max_value = xpos_ranged
+
+ min_value = clamp(min_value, 0, max_value - 10)
+ max_value = clamp(max_value, min_value + 10, _max_value)
+
+ update_label()
+ emit_signal("setting_changed", Vector2(min_value, max_value))
+ queue_redraw()
+
+
+ func update_label() -> void:
+ if label:
+ label.set_text(str(min_value) + suffix + "/" + str(max_value) + suffix)
diff --git a/addons/terrain_3d/editor/components/toolbar.gd b/addons/terrain_3d/src/toolbar.gd
similarity index 98%
rename from addons/terrain_3d/editor/components/toolbar.gd
rename to addons/terrain_3d/src/toolbar.gd
index c8ac76b..c177edb 100644
--- a/addons/terrain_3d/editor/components/toolbar.gd
+++ b/addons/terrain_3d/src/toolbar.gd
@@ -24,7 +24,7 @@ var tool_group: ButtonGroup = ButtonGroup.new()
func _init() -> void:
- set_custom_minimum_size(Vector2(32, 0))
+ set_custom_minimum_size(Vector2(20, 0))
func _ready() -> void:
@@ -59,6 +59,7 @@ func add_tool_button(p_tool: Terrain3DEditor.Tool, p_operation: Terrain3DEditor.
p_tip: String, p_icon: Texture2D, p_group: ButtonGroup) -> void:
var button: Button = Button.new()
+ button.set_name(p_tip.to_pascal_case())
button.set_meta("Tool", p_tool)
button.set_meta("Operation", p_operation)
button.set_tooltip_text(p_tip)
diff --git a/addons/terrain_3d/editor/components/ui.gd b/addons/terrain_3d/src/ui.gd
similarity index 64%
rename from addons/terrain_3d/editor/components/ui.gd
rename to addons/terrain_3d/src/ui.gd
index 41b8a7b..236eb4d 100644
--- a/addons/terrain_3d/editor/components/ui.gd
+++ b/addons/terrain_3d/src/ui.gd
@@ -3,12 +3,11 @@ extends Node
# Includes
-const Toolbar: Script = preload("res://addons/terrain_3d/editor/components/toolbar.gd")
-const ToolSettings: Script = preload("res://addons/terrain_3d/editor/components/tool_settings.gd")
-const TerrainTools: Script = preload("res://addons/terrain_3d/editor/components/terrain_tools.gd")
-const OperationBuilder: Script = preload("res://addons/terrain_3d/editor/components/operation_builder.gd")
-const GradientOperationBuilder: Script = preload("res://addons/terrain_3d/editor/components/gradient_operation_builder.gd")
-const RING1: String = "res://addons/terrain_3d/editor/brushes/ring1.exr"
+const Toolbar: Script = preload("res://addons/terrain_3d/src/toolbar.gd")
+const ToolSettings: Script = preload("res://addons/terrain_3d/src/tool_settings.gd")
+const TerrainTools: Script = preload("res://addons/terrain_3d/src/terrain_tools.gd")
+const OperationBuilder: Script = preload("res://addons/terrain_3d/src/operation_builder.gd")
+const GradientOperationBuilder: Script = preload("res://addons/terrain_3d/src/gradient_operation_builder.gd")
const COLOR_RAISE := Color.WHITE
const COLOR_LOWER := Color.BLACK
const COLOR_SMOOTH := Color(0.5, 0, .1)
@@ -26,6 +25,8 @@ const COLOR_PICK_COLOR := Color.WHITE
const COLOR_PICK_HEIGHT := Color.DARK_RED
const COLOR_PICK_ROUGH := Color.ROYAL_BLUE
+const RING1: String = "res://addons/terrain_3d/brushes/ring1.exr"
+@onready var ring_texture := ImageTexture.create_from_image(Terrain3DUtil.black_to_alpha(Image.load_from_file(RING1)))
var plugin: EditorPlugin # Actually Terrain3DEditorPlugin, but Godot still has CRC errors
var toolbar: Toolbar
@@ -40,14 +41,13 @@ var decal_timer: Timer
var gradient_decals: Array[Decal]
var brush_data: Dictionary
var operation_builder: OperationBuilder
-@onready var picker_texture: ImageTexture = ImageTexture.create_from_image(Image.load_from_file(RING1))
func _enter_tree() -> void:
toolbar = Toolbar.new()
toolbar.hide()
toolbar.connect("tool_changed", _on_tool_changed)
-
+
toolbar_settings = ToolSettings.new()
toolbar_settings.connect("setting_changed", _on_setting_changed)
toolbar_settings.connect("picking", _on_picking)
@@ -61,6 +61,8 @@ func _enter_tree() -> void:
plugin.add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_BOTTOM, toolbar_settings)
plugin.add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_MENU, terrain_tools)
+ _on_tool_changed(Terrain3DEditor.REGION, Terrain3DEditor.ADD)
+
decal = Decal.new()
add_child(decal)
decal_timer = Timer.new()
@@ -87,126 +89,93 @@ func _exit_tree() -> void:
func set_visible(p_visible: bool) -> void:
visible = p_visible
- toolbar.set_visible(p_visible and plugin.terrain)
terrain_tools.set_visible(p_visible)
-
- if p_visible and plugin.terrain:
- p_visible = plugin.editor.get_tool() != Terrain3DEditor.REGION
- toolbar_settings.set_visible(p_visible and plugin.terrain)
+ toolbar.set_visible(p_visible)
+ toolbar_settings.set_visible(p_visible)
update_decal()
func _on_tool_changed(p_tool: Terrain3DEditor.Tool, p_operation: Terrain3DEditor.Operation) -> void:
clear_picking()
- if not visible or not plugin.terrain:
- return
-
- if plugin.editor:
- plugin.editor.set_tool(p_tool)
- plugin.editor.set_operation(p_operation)
-
- if p_tool != Terrain3DEditor.REGION:
- # Select which settings to hide. Options:
- # size, opactiy, height, slope, color, roughness, (height|color|roughness) picker
- var to_hide: PackedStringArray = []
-
- if p_tool == Terrain3DEditor.HEIGHT:
- to_hide.push_back("color")
- to_hide.push_back("color picker")
- to_hide.push_back("roughness")
- to_hide.push_back("roughness picker")
- to_hide.push_back("slope")
- to_hide.push_back("enable")
- if p_operation != Terrain3DEditor.REPLACE:
- to_hide.push_back("height")
- to_hide.push_back("height picker")
- if p_operation != Terrain3DEditor.GRADIENT:
- to_hide.push_back("gradient_points")
- to_hide.push_back("drawable")
-
- elif p_tool == Terrain3DEditor.TEXTURE:
- to_hide.push_back("height")
- to_hide.push_back("height picker")
- to_hide.push_back("gradient_points")
- to_hide.push_back("drawable")
- to_hide.push_back("color")
- to_hide.push_back("color picker")
- to_hide.push_back("roughness")
- to_hide.push_back("roughness picker")
- to_hide.push_back("slope")
- to_hide.push_back("enable")
+ # Select which settings to show. Options in tool_settings.gd:_ready
+ var to_show: PackedStringArray = []
+
+ match p_tool:
+ Terrain3DEditor.HEIGHT:
+ to_show.push_back("brush")
+ to_show.push_back("size")
+ to_show.push_back("strength")
if p_operation == Terrain3DEditor.REPLACE:
- to_hide.push_back("opacity")
+ to_show.push_back("height")
+ to_show.push_back("height_picker")
+ if p_operation == Terrain3DEditor.GRADIENT:
+ to_show.push_back("gradient_points")
+ to_show.push_back("drawable")
+
+ Terrain3DEditor.TEXTURE:
+ to_show.push_back("brush")
+ to_show.push_back("size")
+ to_show.push_back("enable_texture")
+ if p_operation == Terrain3DEditor.ADD:
+ to_show.push_back("strength")
+ to_show.push_back("enable_angle")
+ to_show.push_back("angle")
+ to_show.push_back("angle_picker")
+ to_show.push_back("dynamic_angle")
+ to_show.push_back("enable_scale")
+ to_show.push_back("scale")
+ to_show.push_back("scale_picker")
- elif p_tool == Terrain3DEditor.COLOR:
- to_hide.push_back("height")
- to_hide.push_back("height picker")
- to_hide.push_back("gradient_points")
- to_hide.push_back("drawable")
- to_hide.push_back("roughness")
- to_hide.push_back("roughness picker")
- to_hide.push_back("slope")
- to_hide.push_back("enable")
+ Terrain3DEditor.COLOR:
+ to_show.push_back("brush")
+ to_show.push_back("size")
+ to_show.push_back("strength")
+ to_show.push_back("color")
+ to_show.push_back("color_picker")
- elif p_tool == Terrain3DEditor.ROUGHNESS:
- to_hide.push_back("height")
- to_hide.push_back("height picker")
- to_hide.push_back("gradient_points")
- to_hide.push_back("drawable")
- to_hide.push_back("color")
- to_hide.push_back("color picker")
- to_hide.push_back("slope")
- to_hide.push_back("enable")
+ Terrain3DEditor.ROUGHNESS:
+ to_show.push_back("brush")
+ to_show.push_back("size")
+ to_show.push_back("strength")
+ to_show.push_back("roughness")
+ to_show.push_back("roughness_picker")
- elif p_tool in [ Terrain3DEditor.AUTOSHADER, Terrain3DEditor.HOLES, Terrain3DEditor.NAVIGATION ]:
- to_hide.push_back("height")
- to_hide.push_back("height picker")
- to_hide.push_back("gradient_points")
- to_hide.push_back("drawable")
- to_hide.push_back("color")
- to_hide.push_back("color picker")
- to_hide.push_back("roughness")
- to_hide.push_back("roughness picker")
- to_hide.push_back("slope")
- to_hide.push_back("opacity")
+ Terrain3DEditor.AUTOSHADER, Terrain3DEditor.HOLES, Terrain3DEditor.NAVIGATION:
+ to_show.push_back("brush")
+ to_show.push_back("size")
+ to_show.push_back("enable")
- toolbar_settings.hide_settings(to_hide)
+ _:
+ pass
- toolbar_settings.set_visible(p_tool != Terrain3DEditor.REGION)
+ # Advanced menu settings
+ to_show.push_back("automatic_regions")
+ to_show.push_back("align_to_view")
+ to_show.push_back("show_cursor_while_painting")
+ to_show.push_back("gamma")
+ to_show.push_back("jitter")
+ toolbar_settings.show_settings(to_show)
operation_builder = null
if p_operation == Terrain3DEditor.GRADIENT:
operation_builder = GradientOperationBuilder.new()
operation_builder.tool_settings = toolbar_settings
+ if plugin.editor:
+ plugin.editor.set_tool(p_tool)
+ plugin.editor.set_operation(p_operation)
+
_on_setting_changed()
plugin.update_region_grid()
-
func _on_setting_changed() -> void:
- if not visible or not plugin.terrain:
+ if not plugin.asset_dock:
return
- brush_data = {
- "size": int(toolbar_settings.get_setting("size")),
- "opacity": toolbar_settings.get_setting("opacity") / 100.0,
- "height": toolbar_settings.get_setting("height"),
- "texture_index": plugin.texture_dock.get_selected_index(),
- "color": toolbar_settings.get_setting("color"),
- "roughness": toolbar_settings.get_setting("roughness"),
- "gradient_points": toolbar_settings.get_setting("gradient_points"),
- "enable": toolbar_settings.get_setting("enable"),
- "automatic_regions": toolbar_settings.get_setting("automatic_regions"),
- "align_to_view": toolbar_settings.get_setting("align_to_view"),
- "show_cursor_while_painting": toolbar_settings.get_setting("show_cursor_while_painting"),
- "gamma": toolbar_settings.get_setting("gamma"),
- "jitter": toolbar_settings.get_setting("jitter"),
- }
- var brush_imgs: Array = toolbar_settings.get_setting("brush")
- brush_data["image"] = brush_imgs[0]
- brush_data["texture"] = brush_imgs[1]
-
+ brush_data = toolbar_settings.get_settings()
+ brush_data["strength"] /= 100.0
+ brush_data["texture_index"] = plugin.asset_dock.get_selected_index()
update_decal()
plugin.editor.set_brush_data(brush_data)
@@ -226,7 +195,7 @@ func update_decal() -> void:
else:
# Wait for cursor to recenter after right-click before revealing
# See https://github.com/godotengine/godot/issues/70098
- await get_tree().create_timer(.05).timeout
+ await get_tree().create_timer(.05).timeout
decal.visible = true
decal.size = Vector3.ONE * brush_data["size"]
@@ -239,7 +208,7 @@ func update_decal() -> void:
# Set texture and color
if picking != Terrain3DEditor.TOOL_MAX:
- decal.texture_albedo = picker_texture
+ decal.texture_albedo = ring_texture
decal.size = Vector3.ONE * 10. * plugin.terrain.get_mesh_vertex_spacing()
match picking:
Terrain3DEditor.HEIGHT:
@@ -250,7 +219,7 @@ func update_decal() -> void:
decal.modulate = COLOR_PICK_ROUGH
decal.modulate.a = 1.0
else:
- decal.texture_albedo = brush_data["texture"]
+ decal.texture_albedo = brush_data["brush"][1]
match plugin.editor.get_tool():
Terrain3DEditor.HEIGHT:
match plugin.editor.get_operation():
@@ -270,7 +239,7 @@ func update_decal() -> void:
decal.modulate = COLOR_SLOPE
_:
decal.modulate = Color.WHITE
- decal.modulate.a = max(.3, brush_data["opacity"])
+ decal.modulate.a = max(.3, brush_data["strength"])
Terrain3DEditor.TEXTURE:
match plugin.editor.get_operation():
Terrain3DEditor.REPLACE:
@@ -278,15 +247,15 @@ func update_decal() -> void:
decal.modulate.a = 1.0
Terrain3DEditor.ADD:
decal.modulate = COLOR_SPRAY
- decal.modulate.a = max(.3, brush_data["opacity"])
+ decal.modulate.a = max(.3, brush_data["strength"])
_:
decal.modulate = Color.WHITE
Terrain3DEditor.COLOR:
decal.modulate = brush_data["color"].srgb_to_linear()*.5
- decal.modulate.a = max(.3, brush_data["opacity"])
+ decal.modulate.a = max(.3, brush_data["strength"])
Terrain3DEditor.ROUGHNESS:
decal.modulate = COLOR_ROUGHNESS
- decal.modulate.a = max(.3, brush_data["opacity"])
+ decal.modulate.a = max(.3, brush_data["strength"])
Terrain3DEditor.AUTOSHADER:
decal.modulate = COLOR_AUTOSHADER
decal.modulate.a = 1.0
@@ -298,15 +267,15 @@ func update_decal() -> void:
decal.modulate.a = 1.0
_:
decal.modulate = Color.WHITE
- decal.modulate.a = max(.3, brush_data["opacity"])
+ decal.modulate.a = max(.3, brush_data["strength"])
decal.size.y = max(1000, decal.size.y)
decal.albedo_mix = 1.0
decal.cull_mask = 1 << ( plugin.terrain.get_mouse_layer() - 1 )
decal_timer.start()
-
+
for gradient_decal in gradient_decals:
gradient_decal.visible = false
-
+
if plugin.editor.get_operation() == Terrain3DEditor.GRADIENT:
var index := 0
for point in brush_data["gradient_points"]:
@@ -320,16 +289,16 @@ func update_decal() -> void:
func _get_gradient_decal(index: int) -> Decal:
if gradient_decals.size() > index:
return gradient_decals[index]
-
+
var gradient_decal := Decal.new()
gradient_decal = Decal.new()
- gradient_decal.texture_albedo = picker_texture
+ gradient_decal.texture_albedo = ring_texture
gradient_decal.modulate = COLOR_SLOPE
gradient_decal.size = Vector3.ONE * 10. * plugin.terrain.get_mesh_vertex_spacing()
gradient_decal.size.y = 1000.
gradient_decal.cull_mask = decal.cull_mask
add_child(gradient_decal)
-
+
gradient_decals.push_back(gradient_decal)
return gradient_decal
@@ -351,10 +320,10 @@ func clear_picking() -> void:
func is_picking() -> bool:
if picking != Terrain3DEditor.TOOL_MAX:
return true
-
+
if operation_builder and operation_builder.is_picking():
return true
-
+
return false
@@ -368,12 +337,16 @@ func pick(p_global_position: Vector3) -> void:
color = plugin.terrain.get_storage().get_pixel(Terrain3DStorage.TYPE_COLOR, p_global_position)
Terrain3DEditor.COLOR:
color = plugin.terrain.get_storage().get_color(p_global_position)
+ Terrain3DEditor.ANGLE:
+ color = Color(plugin.terrain.get_storage().get_angle(p_global_position), 0., 0., 1.)
+ Terrain3DEditor.SCALE:
+ color = Color(plugin.terrain.get_storage().get_scale(p_global_position), 0., 0., 1.)
_:
push_error("Unsupported picking type: ", picking)
return
picking_callback.call(picking, color, p_global_position)
picking = Terrain3DEditor.TOOL_MAX
-
+
elif operation_builder and operation_builder.is_picking():
operation_builder.pick(p_global_position, plugin.terrain)
diff --git a/addons/terrain_3d/terrain.gdextension b/addons/terrain_3d/terrain.gdextension
index 7e5d482..2494546 100644
--- a/addons/terrain_3d/terrain.gdextension
+++ b/addons/terrain_3d/terrain.gdextension
@@ -1,7 +1,7 @@
[configuration]
entry_symbol = "terrain_3d_init"
-compatibility_minimum = 4.1
+compatibility_minimum = 4.2
[libraries]
diff --git a/addons/terrain_3d/tools/importer.gd b/addons/terrain_3d/tools/importer.gd
index 219b27c..5eef545 100644
--- a/addons/terrain_3d/tools/importer.gd
+++ b/addons/terrain_3d/tools/importer.gd
@@ -54,14 +54,14 @@ func start_import(p_value: bool) -> void:
var min_max := Vector2(0, 1)
var img: Image
if height_file_name:
- img = Terrain3DStorage.load_image(height_file_name, ResourceLoader.CACHE_MODE_IGNORE, r16_range, r16_size)
- min_max = Terrain3D.get_min_max(img)
+ img = Terrain3DUtil.load_image(height_file_name, ResourceLoader.CACHE_MODE_IGNORE, r16_range, r16_size)
+ min_max = Terrain3DUtil.get_min_max(img)
imported_images[Terrain3DStorage.TYPE_HEIGHT] = img
if control_file_name:
- img = Terrain3DStorage.load_image(control_file_name, ResourceLoader.CACHE_MODE_IGNORE)
+ img = Terrain3DUtil.load_image(control_file_name, ResourceLoader.CACHE_MODE_IGNORE)
imported_images[Terrain3DStorage.TYPE_CONTROL] = img
if color_file_name:
- img = Terrain3DStorage.load_image(color_file_name, ResourceLoader.CACHE_MODE_IGNORE)
+ img = Terrain3DUtil.load_image(color_file_name, ResourceLoader.CACHE_MODE_IGNORE)
imported_images[Terrain3DStorage.TYPE_COLOR] = img
if texture_list.get_texture_count() == 0:
material.show_checkered = false
diff --git a/addons/terrain_3d/tools/importer.tscn b/addons/terrain_3d/tools/importer.tscn
index bf6c4b5..ce4d9e7 100644
--- a/addons/terrain_3d/tools/importer.tscn
+++ b/addons/terrain_3d/tools/importer.tscn
@@ -1,54 +1,16 @@
-[gd_scene load_steps=8 format=3 uid="uid://blaieaqp413k7"]
+[gd_scene load_steps=5 format=3 uid="uid://blaieaqp413k7"]
[ext_resource type="Script" path="res://addons/terrain_3d/tools/importer.gd" id="1_60b8f"]
-[sub_resource type="Terrain3DStorage" id="Terrain3DStorage_5p5ir"]
-version = 0.842
+[sub_resource type="Terrain3DStorage" id="Terrain3DStorage_rmuvl"]
-[sub_resource type="Gradient" id="Gradient_5sc5a"]
-offsets = PackedFloat32Array(0.2, 1)
-colors = PackedColorArray(1, 1, 1, 1, 0, 0, 0, 1)
-
-[sub_resource type="FastNoiseLite" id="FastNoiseLite_lp4p7"]
-noise_type = 2
-frequency = 0.03
-cellular_jitter = 3.0
-cellular_return_type = 0
-domain_warp_enabled = true
-domain_warp_type = 1
-domain_warp_amplitude = 50.0
-domain_warp_fractal_type = 2
-domain_warp_fractal_lacunarity = 1.5
-domain_warp_fractal_gain = 1.0
-
-[sub_resource type="NoiseTexture2D" id="NoiseTexture2D_vyjp8"]
-seamless = true
-color_ramp = SubResource("Gradient_5sc5a")
-noise = SubResource("FastNoiseLite_lp4p7")
-
-[sub_resource type="Terrain3DMaterial" id="Terrain3DMaterial_f0cwi"]
-_shader_parameters = {
-"_mouse_layer": 2147483648,
-"blend_sharpness": null,
-"height_blending": null,
-"macro_variation1": null,
-"macro_variation2": null,
-"noise1_angle": null,
-"noise1_offset": null,
-"noise1_scale": null,
-"noise2_scale": null,
-"noise3_scale": null,
-"noise_texture": SubResource("NoiseTexture2D_vyjp8")
-}
+[sub_resource type="Terrain3DMaterial" id="Terrain3DMaterial_cjpaa"]
show_checkered = true
-[sub_resource type="Terrain3DTextureList" id="Terrain3DTextureList_4yf1r"]
+[sub_resource type="Terrain3DTextureList" id="Terrain3DTextureList_yjkn1"]
[node name="Importer" type="Terrain3D"]
-storage = SubResource("Terrain3DStorage_5p5ir")
-material = SubResource("Terrain3DMaterial_f0cwi")
-texture_list = SubResource("Terrain3DTextureList_4yf1r")
+storage = SubResource("Terrain3DStorage_rmuvl")
+material = SubResource("Terrain3DMaterial_cjpaa")
+texture_list = SubResource("Terrain3DTextureList_yjkn1")
script = ExtResource("1_60b8f")
-import_position = Vector3(-1024, 0, -1024)
-r16_range = Vector2(0, 250)
-r16_size = Vector2i(2048, 2048)
diff --git a/addons/terrain_3d/utils/terrain_3d_objects.gd b/addons/terrain_3d/utils/terrain_3d_objects.gd
new file mode 100644
index 0000000..210742b
--- /dev/null
+++ b/addons/terrain_3d/utils/terrain_3d_objects.gd
@@ -0,0 +1,176 @@
+@tool
+extends Node3D
+class_name Terrain3DObjects
+
+const TransformChangedNotifier: Script = preload("res://addons/terrain_3d/utils/transform_changed_notifier.gd")
+
+const CHILD_HELPER_NAME: StringName = &"TransformChangedSignaller"
+const CHILD_HELPER_PATH: NodePath = ^"TransformChangedSignaller"
+
+var _editor_interface = null
+var _undo_redo = null
+var _terrain_id: int
+var _offsets: Dictionary # Object ID -> Vector3(X, Y offset relative to terrain height, Z)
+var _ignore_transform_change: bool = false
+
+
+func _exit_tree() -> void:
+ if not Engine.is_editor_hint() or not _editor_interface:
+ return
+
+ child_entered_tree.disconnect(_on_child_entered_tree)
+ child_exiting_tree.disconnect(_on_child_exiting_tree)
+
+ for child in get_children():
+ _on_child_exiting_tree(child)
+
+
+func editor_setup(p_plugin) -> void:
+ if _editor_interface:
+ return
+
+ _editor_interface = p_plugin.get_editor_interface()
+ _undo_redo = p_plugin.get_undo_redo()
+
+ for child in get_children():
+ _on_child_entered_tree(child)
+
+ child_entered_tree.connect(_on_child_entered_tree)
+ child_exiting_tree.connect(_on_child_exiting_tree)
+
+
+func get_terrain() -> Terrain3D:
+ var terrain := instance_from_id(_terrain_id) as Terrain3D
+ if not terrain or terrain.is_queued_for_deletion() or not terrain.is_inside_tree():
+ var terrains: Array[Node] = _editor_interface.get_edited_scene_root().find_children("", "Terrain3D")
+ if terrains.size() > 0:
+ terrain = terrains[0]
+ _terrain_id = terrain.get_instance_id() if terrain else 0
+
+ if terrain and terrain.storage and not terrain.storage.maps_edited.is_connected(_on_maps_edited):
+ terrain.storage.maps_edited.connect(_on_maps_edited)
+
+ return terrain
+
+
+func _get_terrain_height(p_global_position: Vector3) -> float:
+ var terrain: Terrain3D = get_terrain()
+ if not terrain or not terrain.storage:
+ return 0.0
+ var height: float = terrain.storage.get_height(p_global_position)
+ if is_nan(height):
+ return 0.0
+ return height
+
+
+func _on_child_entered_tree(p_node: Node) -> void:
+ if not (p_node is Node3D):
+ return
+
+ assert(p_node.get_parent() == self)
+
+ var helper: TransformChangedNotifier = p_node.get_node_or_null(CHILD_HELPER_PATH)
+ if not helper:
+ helper = TransformChangedNotifier.new()
+ helper.name = CHILD_HELPER_NAME
+ p_node.add_child(helper, true, INTERNAL_MODE_BACK)
+ helper.transform_changed.connect(_on_child_transform_changed.bind(p_node))
+ assert(p_node.has_node(CHILD_HELPER_PATH))
+
+ var id: int = p_node.get_instance_id()
+ if not _offsets.has(id):
+ _update_child_offset(p_node)
+ else:
+ _update_child_position(p_node)
+
+
+func _on_child_exiting_tree(p_node: Node) -> void:
+ if not (p_node is Node3D) or not p_node.has_node(CHILD_HELPER_PATH):
+ return
+
+ var helper: TransformChangedNotifier = p_node.get_node_or_null(CHILD_HELPER_PATH)
+ if helper:
+ p_node.remove_child(helper)
+ helper.queue_free()
+
+
+func _is_node_selected(p_node: Node) -> bool:
+ var editor_sel = _editor_interface.get_selection()
+ return editor_sel.get_transformable_selected_nodes().has(p_node)
+
+
+func _on_child_transform_changed(p_node: Node3D) -> void:
+ if _ignore_transform_change:
+ return
+
+ var lmb_down := Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT)
+ if lmb_down and (_is_node_selected(p_node) or _is_node_selected(self)):
+ # The user may be moving the node using gizmos.
+ # We should wait until they're done before updating otherwise gizmos + this node conflict.
+ return
+
+ if not _offsets.has(p_node.get_instance_id()):
+ return
+
+ var old_offset: Vector3 = _offsets[p_node.get_instance_id()]
+ var old_h: float = _get_terrain_height(old_offset)
+ var old_position: Vector3 = old_offset + Vector3(0, old_h, 0)
+ var new_position: Vector3 = p_node.global_position
+ if old_position.is_equal_approx(new_position):
+ return
+ var new_h: float = _get_terrain_height(new_position)
+ var new_offset: Vector3 = new_position - Vector3(0, new_h, 0)
+
+ var translate_without_reposition: bool = Input.is_key_pressed(KEY_SHIFT)
+ var y_changed: bool = not is_equal_approx(old_position.y, p_node.global_position.y)
+ if not y_changed and not translate_without_reposition:
+ new_offset.y = old_offset.y
+ new_position = new_offset + Vector3(0, new_h, 0)
+
+ # Make sure that when the user undo's the translation, the offset change gets undone too!
+ _undo_redo.create_action("Translate", UndoRedo.MERGE_ALL)
+ _undo_redo.add_do_property(self, &"_ignore_transform_change", true)
+ _undo_redo.add_undo_property(self, &"_ignore_transform_change", true)
+ _undo_redo.add_do_property(p_node, &"global_position", new_position)
+ _undo_redo.add_undo_property(p_node, &"global_position", old_position)
+ _undo_redo.add_do_method(self, &"_set_offset", p_node.get_instance_id(), new_offset)
+ _undo_redo.add_undo_method(self, &"_set_offset", p_node.get_instance_id(), old_offset)
+ _undo_redo.add_do_property(self, &"_ignore_transform_change", false)
+ _undo_redo.add_undo_property(self, &"_ignore_transform_change", false)
+ _undo_redo.commit_action()
+
+
+func _set_offset(p_id: int, p_offset: Vector3) -> void:
+ _offsets[p_id] = p_offset
+
+
+# Overwrite current offset stored for node with its current Y position relative to the terrain
+func _update_child_offset(p_node: Node3D) -> void:
+ var position: Vector3 = global_transform * p_node.position
+ var h: float = _get_terrain_height(position)
+ var offset: Vector3 = position - Vector3(0, h, 0)
+ _offsets[p_node.get_instance_id()] = offset
+
+
+# Overwrite node's current position with terrain height + stored offset for this node
+func _update_child_position(p_node: Node3D) -> void:
+ if not _offsets.has(p_node.get_instance_id()):
+ return
+
+ var position: Vector3 = global_transform * p_node.position
+ var h: float = _get_terrain_height(position)
+ var offset: Vector3 = _offsets[p_node.get_instance_id()]
+ var new_position: Vector3 = global_transform.inverse() * (offset + Vector3(0, h, 0))
+ if not p_node.position.is_equal_approx(new_position):
+ p_node.position = new_position
+
+
+func _on_maps_edited(p_edited_aabb: AABB) -> void:
+ var edited_area: AABB = p_edited_aabb.grow(1)
+ edited_area.position.y = -INF
+ edited_area.end.y = INF
+
+ for child in get_children():
+ var node := child as Node3D
+ if node && edited_area.has_point(node.global_position):
+ _update_child_position(node)
diff --git a/addons/terrain_3d/utils/transform_changed_notifier.gd b/addons/terrain_3d/utils/transform_changed_notifier.gd
new file mode 100644
index 0000000..4b8c586
--- /dev/null
+++ b/addons/terrain_3d/utils/transform_changed_notifier.gd
@@ -0,0 +1,14 @@
+@tool
+extends Node3D
+
+signal transform_changed
+
+
+func _ready() -> void:
+ assert(Engine.is_editor_hint())
+ set_notify_transform(true)
+
+
+func _notification(what: int) -> void:
+ if what == NOTIFICATION_TRANSFORM_CHANGED:
+ transform_changed.emit()
diff --git a/assets/fonts/Exo2/Exo2.ttf b/assets/fonts/Exo2/Exo2.ttf
new file mode 100644
index 0000000..b97509d
Binary files /dev/null and b/assets/fonts/Exo2/Exo2.ttf differ
diff --git a/assets/fonts/Exo2/Exo2.ttf.import b/assets/fonts/Exo2/Exo2.ttf.import
new file mode 100644
index 0000000..00f80ac
--- /dev/null
+++ b/assets/fonts/Exo2/Exo2.ttf.import
@@ -0,0 +1,33 @@
+[remap]
+
+importer="font_data_dynamic"
+type="FontFile"
+uid="uid://hbggv2tf174i"
+path="res://.godot/imported/Exo2.ttf-29e177e92d885ac8d1871efda37915d4.fontdata"
+
+[deps]
+
+source_file="res://assets/fonts/Exo2/Exo2.ttf"
+dest_files=["res://.godot/imported/Exo2.ttf-29e177e92d885ac8d1871efda37915d4.fontdata"]
+
+[params]
+
+Rendering=null
+antialiasing=1
+generate_mipmaps=false
+multichannel_signed_distance_field=false
+msdf_pixel_range=8
+msdf_size=48
+allow_system_fallback=true
+force_autohinter=false
+hinting=1
+subpixel_positioning=1
+oversampling=0.0
+Fallbacks=null
+fallbacks=[]
+Compress=null
+compress=true
+preload=[]
+language_support={}
+script_support={}
+opentype_features={}
diff --git a/assets/fonts/Exo2/OFL.txt b/assets/fonts/Exo2/OFL.txt
new file mode 100644
index 0000000..222fbb5
--- /dev/null
+++ b/assets/fonts/Exo2/OFL.txt
@@ -0,0 +1,93 @@
+Copyright 2013 The Exo 2 Project Authors (https://github.com/NDISCOVER/Exo-2.0)
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/assets/fonts/The Most Beautiful Gift One/The Most Beautiful Gift One.otf b/assets/fonts/The Most Beautiful Gift One/The Most Beautiful Gift One.otf
new file mode 100644
index 0000000..c745b7c
Binary files /dev/null and b/assets/fonts/The Most Beautiful Gift One/The Most Beautiful Gift One.otf differ
diff --git a/assets/fonts/The Most Beautiful Gift One/The Most Beautiful Gift One.otf.import b/assets/fonts/The Most Beautiful Gift One/The Most Beautiful Gift One.otf.import
new file mode 100644
index 0000000..e555f1b
--- /dev/null
+++ b/assets/fonts/The Most Beautiful Gift One/The Most Beautiful Gift One.otf.import
@@ -0,0 +1,33 @@
+[remap]
+
+importer="font_data_dynamic"
+type="FontFile"
+uid="uid://0m0krymtpop8"
+path="res://.godot/imported/The Most Beautiful Gift One.otf-1996dbf32044a2718dea817260b9b97c.fontdata"
+
+[deps]
+
+source_file="res://assets/fonts/The Most Beautiful Gift One/The Most Beautiful Gift One.otf"
+dest_files=["res://.godot/imported/The Most Beautiful Gift One.otf-1996dbf32044a2718dea817260b9b97c.fontdata"]
+
+[params]
+
+Rendering=null
+antialiasing=1
+generate_mipmaps=false
+multichannel_signed_distance_field=false
+msdf_pixel_range=8
+msdf_size=48
+allow_system_fallback=true
+force_autohinter=false
+hinting=1
+subpixel_positioning=1
+oversampling=0.0
+Fallbacks=null
+fallbacks=[]
+Compress=null
+compress=true
+preload=[]
+language_support={}
+script_support={}
+opentype_features={}
diff --git a/assets/fonts/The Most Beautiful Gift One/the-most-beautiful-gift-one-eula.txt b/assets/fonts/The Most Beautiful Gift One/the-most-beautiful-gift-one-eula.txt
new file mode 100644
index 0000000..69c8fae
--- /dev/null
+++ b/assets/fonts/The Most Beautiful Gift One/the-most-beautiful-gift-one-eula.txt
@@ -0,0 +1,37 @@
+1001Fonts Free For Commercial Use License (FFC)
+
+Preamble
+In this license, 'The Most Beautiful Gift One' refers to the given .zip file, which may contain one or numerous fonts. These fonts can be of any type (.ttf, .otf, ...) and together they form a 'font family' or in short a 'typeface'.
+
+1. Copyright
+The Most Beautiful Gift One is the intellectual property of its respective author, provided it is original, and is protected by copyright laws in many parts of the world.
+
+2. Usage
+The Most Beautiful Gift One may be downloaded and used free of charge for both personal and commercial use, as long as the usage is not racist or illegal. Personal use refers to all usage that does not generate financial income in a business manner, for instance:
+
+ - personal scrapbooking for yourself
+ - recreational websites and blogs for friends and family
+ - prints such as flyers, posters, t-shirts for churches, charities, and non-profit organizations
+
+Commercial use refers to usage in a business environment, including:
+
+ - business cards, logos, advertising, websites, mobile apps for companies
+ - t-shirts, books, apparel that will be sold for money
+ - flyers, posters for events that charge admission
+ - freelance graphic design work
+ - anything that will generate direct or indirect income
+
+3. Modification
+The Most Beautiful Gift One may not be modified, altered, adapted or built upon without written permission by its respective author. This pertains all files within the downloadable font zip-file.
+
+4. Conversion
+The Most Beautiful Gift One may be converted to other formats such as WOFF, SVG or EOT webfonts, as long as the font is not modified in any other way, such as changing names or altering individual glyphs.
+
+5. Distribution
+While The Most Beautiful Gift One may freely be copied and passed along to other individuals for private use as its original downloadable zip-file, it may not be sold or published without written permission by its respective author.
+
+6. Embedding
+The Most Beautiful Gift One may be embedded into an application such as a web- or mobile app, independant of the number of the application users, as long as the application does not distribute The Most Beautiful Gift One, such as offering it as a download.
+
+7. Disclaimer
+The Most Beautiful Gift One is offered 'as is' without any warranty. 1001fonts.com and the respective author of The Most Beautiful Gift One shall not be liable for any damage derived from using this typeface. By using The Most Beautiful Gift One you agree to the terms of this license.
diff --git a/entities/components/explosive_damage/explosive_damage.tscn b/entities/components/explosive_damage/explosive_damage.tscn
deleted file mode 100644
index f976608..0000000
--- a/entities/components/explosive_damage/explosive_damage.tscn
+++ /dev/null
@@ -1,16 +0,0 @@
-[gd_scene load_steps=3 format=3 uid="uid://qb5sf7awdeui"]
-
-[ext_resource type="Script" path="res://entities/components/explosive_damage/explosive_damage_component.gd" id="1_alx3x"]
-
-[sub_resource type="SphereShape3D" id="SphereShape3D_1htx7"]
-radius = 5.0
-
-[node name="ExplosiveDamage" type="Area3D"]
-collision_mask = 13
-script = ExtResource("1_alx3x")
-
-[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
-shape = SubResource("SphereShape3D_1htx7")
-
-[connection signal="area_shape_entered" from="." to="." method="_on_area_shape_entered"]
-[connection signal="body_entered" from="." to="." method="_on_body_entered"]
diff --git a/entities/components/explosive_damage/explosive_damage_component.gd b/entities/components/explosive_damage/explosive_damage_component.gd
deleted file mode 100644
index af4fd08..0000000
--- a/entities/components/explosive_damage/explosive_damage_component.gd
+++ /dev/null
@@ -1,52 +0,0 @@
-# This file is part of open-fpsz.
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-class_name ExplosiveDamageComponent extends Area3D
-
-## Emitted when the scale tween is finished
-signal finished
-
-@export var damage : float = 89
-@export var impulse_force : int = 1000
-var damage_dealer : MatchParticipant
-
-var _damaged_objects : Dictionary= {
- "bodies": [],
- "areas": []
-}
-
-func _ready() -> void:
- # scale explosive damage aoe
- var tween : Tween = get_tree().create_tween()
- tween.set_trans(Tween.TRANS_SINE)
- tween.tween_property($CollisionShape3D,
- "scale", Vector3.ONE, .3).from(Vector3.ZERO)
- tween.tween_property($CollisionShape3D,
- "scale", Vector3.ZERO, .1).from(Vector3.ONE)
- tween.finished.connect(func() -> void: finished.emit(); queue_free())
-
-func _on_body_entered(body: Node3D) -> void:
- if body is RigidBody3D and body not in _damaged_objects["bodies"]:
- var center_of_mass_global_position : Vector3 = body.center_of_mass + body.global_position
- var direction : Vector3 = ( center_of_mass_global_position - global_position).normalized()
- body.apply_central_impulse(direction * impulse_force)
- _damaged_objects["bodies"].append(body)
-
-func _on_area_shape_entered(_area_rid: RID, area: Area3D, _area_shape_index: int, _local_shape_index: int) -> void:
- if area is HealthComponent and area not in _damaged_objects["areas"]:
- if not is_multiplayer_authority():
- return
- assert(damage_dealer)
- area.damage.rpc(damage, damage_dealer.player_id, damage_dealer.team_id)
- _damaged_objects["areas"].append(area)
diff --git a/entities/components/flag_carry_component.gd b/entities/components/flag_carry_component.gd
index 4d869ec..6f1f851 100644
--- a/entities/components/flag_carry_component.gd
+++ b/entities/components/flag_carry_component.gd
@@ -15,40 +15,35 @@
## This component allows its entity to interact with flags
class_name FlagCarryComponent extends Node3D
-@export var max_throw_speed : float = 10.0
+@export var throw_force : int = 12
@export var mesh : Node3D
-var _carried_flag : Flag
-
-func _process(_delta : float) -> void:
- if _is_carrying():
- _carried_flag.global_position = global_position
- _carried_flag.global_rotation = Vector3(.0, global_rotation.y, .0)
-
-func _is_carrying() -> bool:
- return _carried_flag != null
+var _flag : Flag
func grab(flag : Flag) -> void:
- if not _is_carrying():
- flag.grabbed.emit(owner)
- _carried_flag = flag
+ if flag.state < Flag.FlagState.TAKEN:
show()
+ _flag = flag
+ flag.grabbed.emit(self)
-func drop(dropper : Player) -> void:
- _release(Vector3.ZERO, 0.0, dropper)
+func drop(inherited_velocity : Vector3 = Vector3.ZERO) -> void:
+ _release(inherited_velocity, 0)
-func throw(inherited_velocity : Vector3, dropper : Player) -> void:
- _release(inherited_velocity, max_throw_speed, dropper)
+func throw(inherited_velocity : Vector3 = Vector3.ZERO, _force: int = throw_force) -> void:
+ _release(inherited_velocity, _force)
-func _release(inherited_velocity : Vector3, throw_speed : float, dropper : Player) -> void:
- if not _is_carrying() or !is_inside_tree():
- return
- # update carried flag global rotation based on component global rotation
- _carried_flag.global_rotation = Vector3(.0, global_rotation.y, .0)
- _carried_flag.linear_velocity = inherited_velocity + (global_basis.z * throw_speed)
- # Throw the flag from some distance in front of the player to avoid regrabbing mid-air
- _carried_flag.global_position = owner.global_position + (global_basis.z * 1.7)
- _carried_flag.dropped.emit(dropper)
- _carried_flag = null
+func _release(inherited_velocity : Vector3 = Vector3.ZERO, _force: int = throw_force) -> void:
+ if not _flag or !is_inside_tree(): return
+ _flag.dropped.emit(self)
+ var impulse : Vector3 = _flag.mass * (inherited_velocity + global_basis.z * _force)
+ _flag.apply_central_impulse(impulse) # wake up the flag
+ # @NOTE: throw the flag from some distance in front of the player to avoid regrabbing mid-air
+ # @FIXME: there is a small chance to send the flag under the map
+ _flag.global_position = global_position + (global_basis.z * 1.7)
+ # rotate carried flag based on flag carry global rotation
+ _flag.global_rotation = Vector3(.0, global_rotation.y, .0)
+ # drop reference to flag
+ _flag = null
+ # hide carried flag mesh
hide()
diff --git a/entities/components/health.gd b/entities/components/health.gd
new file mode 100644
index 0000000..9b4f68d
--- /dev/null
+++ b/entities/components/health.gd
@@ -0,0 +1,59 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+## This class defines a Health component for entities.
+class_name Health extends Area3D
+
+## An entity with this component can be either dead or alive.
+enum HealthState { DEAD, ALIVE }
+
+## Emitted when a peer damaged this component too much.
+signal killed(by_peer_id:int)
+## Emitted when value is exhausted.
+signal exhausted()
+## Emitted when the value is updated.
+signal updated(new_value:int)
+
+## Emitted when the health component is damaged.
+signal damaged(source: Node, target: Node, amount: int)
+
+@export var collider:CollisionShape3D
+@export var max_value:int = 255
+@export var value:int = 255:
+ set = set_value
+@export var state : HealthState = HealthState.ALIVE
+
+func _ready() -> void:
+ # only collide with the layer 3 named "Damage", disable monitoring completely
+ collision_layer = 0b00000000_00000000_00000000_00000100
+ collision_mask = 0
+ monitoring = false
+
+func set_value(new_value:int) -> void:
+ value = clampi(new_value, 0, max_value)
+ updated.emit(value)
+
+@rpc("authority", "call_local", "reliable")
+func damage(amount:int, by_peer_id:int) -> void:
+ value -= amount
+ if value == 0 and state != HealthState.DEAD:
+ state = HealthState.DEAD
+ killed.emit(by_peer_id)
+ exhausted.emit(owner)
+
+@rpc("authority", "call_local", "reliable")
+func heal(amount:int = max_value) -> void:
+ # add strictly positive amount to value
+ value += clampi(amount, 1, max_value)
+ state = HealthState.ALIVE
diff --git a/entities/components/health_component.gd b/entities/components/health_component.gd
deleted file mode 100644
index 609f261..0000000
--- a/entities/components/health_component.gd
+++ /dev/null
@@ -1,52 +0,0 @@
-# This file is part of open-fpsz.
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-class_name HealthComponent extends Area3D
-
-@export var match_participant : MatchParticipant
-
-@export var max_health : int = 255
-@export var health : int = 255:
- set(new_health):
- health = new_health
- health_changed.emit(new_health)
-
-signal health_zeroed(killer_id : int)
-signal health_changed(new_health : int)
-
-func _ready() -> void:
- # only collide with the "Damage" layer, disable monitoring completely
- collision_layer = 0b00000000_00000000_00000000_00000100
- monitoring = false
- collision_mask = 0
- call_deferred("heal_full")
-
-@rpc("call_local", "reliable")
-func damage(amount : int, damage_dealer_player_id : int, damage_dealer_team_id : int) -> void:
- if (damage_dealer_team_id == match_participant.team_id) and (damage_dealer_player_id != match_participant.player_id):
- return
-
- health = clampi(health - amount, 0, max_health)
- if health == 0:
- health_zeroed.emit(damage_dealer_player_id)
-
-@rpc("call_local", "reliable")
-func _heal(amount : int) -> void:
- health = clampi(health + amount, 0, max_health)
-
-func heal_full() -> void:
- if not is_multiplayer_authority():
- return
-
- _heal.rpc(max_health)
diff --git a/entities/components/inventory.gd b/entities/components/inventory.gd
index 49c5fff..617bb5f 100644
--- a/entities/components/inventory.gd
+++ b/entities/components/inventory.gd
@@ -1,4 +1,5 @@
# This file is part of open-fpsz.
+# copyright (c) 2024 - anyreso & open-fpsz contributors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -12,63 +13,65 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-## This component allows its entity to interact with pickable nodes
+@tool
+## This component allows its entity to hold [Weapon] nodes.
class_name Inventory extends Node3D
+signal selection_changed(index : int)
+
+signal selected(node: Node)
+signal unselected(node: Node)
+
const _max_items = 3
+
## This is the total capacity of inventory.
@export_range(0, _max_items) var capacity : int = _max_items:
set(value):
capacity = clamp(value, 1, _max_items)
-@export var selected : Node3D
+@export_range(0, _max_items - 1) var cursor : int = 0:
+ set = set_cursor
-signal selection_changed(selected : Node3D, index : int)
+# Function to set the cursor position in the inventory
+func set_cursor(new_cursor: int) -> void:
+ # Ensure the cursor is within the valid range
+ if new_cursor >= 0 and new_cursor < _max_items:
+ # emit unselected signal
+ _emit_child(unselected, cursor)
+ # update cursor position
+ cursor = new_cursor
+ # emit selected signal
+ _emit_child(selected, cursor)
+
+# helper function to emit the selected signal for the current cursor position
+func _emit_child(sig: Signal, idx: int) -> void:
+ var child : Node = get_child(idx)
+ if child:
+ sig.emit(child)
func _ready() -> void:
- # make sure a child is selected if any
- if not selected and get_child_count() > 0:
- selected = get_child(0)
- # make sure this inventory is linked to its owner
- if not owner.inventory:
- owner.inventory = self
+ unselected.connect(_on_unselected)
+ selected.connect(_on_selected)
+
+func _on_unselected(node: Node) -> void:
+ node.hide()
+
+func _on_selected(node: Node) -> void:
+ node.show()
-## This method overrides [method Node.add_child] method to make sure not to exceed the inventory capacity
-func _add_child(node: Node, force_readable_name: bool = false, internal: InternalMode = InternalMode.INTERNAL_MODE_DISABLED) -> void:
+## This method overrides [method Node.add_child] to make sure not to exceed
+## inventory capacity.
+func _add_child(node: Node, force_readable_name: bool = false,
+ internal: InternalMode = InternalMode.INTERNAL_MODE_DISABLED) -> void:
if get_child_count() <= capacity:
super.add_child(node, force_readable_name, internal)
else:
push_warning("inventory is full")
-## This method cycles through the items in the inventory. If an item is already
-## [member selected], it hides the current item, shifts by the specified number
-## of items, and shows the new selected item. If no item is selected, it selects
-## the first item. If the inventory is empty, it displays a warning message.
-func cycle(shift : int = 1) -> void:
- var child_count : int = get_child_count()
- if selected:
- selected.hide()
- var index : int = wrapi(selected.get_index() + shift, 0, child_count)
- selected = get_child(index)
- selection_changed.emit(selected, index)
- selected.show()
- elif child_count > 0:
- selected = get_child(0)
- selection_changed.emit(selected, 0)
- selected.show()
- else:
- push_warning("inventory is empty")
-
-## This method moves the [member selected] cursor to a specific inventory item.
+## This method moves the [member selection] cursor to specified item index.
func select(index : int) -> void:
- if selected:
- selected.hide()
- selected = get_child(index)
- if selected:
- selection_changed.emit(selected, index)
- selected.show()
- else:
- selected = get_child(index)
- if selected:
- selection_changed.emit(selected, index)
- selected.show()
+ cursor = wrapi(index, 0, get_child_count())
+
+## This method cycles through items in the inventory.
+func cycle(shift : int = 1) -> void:
+ select(cursor + shift)
diff --git a/entities/components/match_participant.gd b/entities/components/match_participant.gd
deleted file mode 100644
index 7a6f16a..0000000
--- a/entities/components/match_participant.gd
+++ /dev/null
@@ -1,20 +0,0 @@
-class_name MatchParticipant extends Node
-
-signal player_id_changed(new_player_id : int)
-signal username_changed(new_username : String)
-signal team_id_changed(new_team_id : int)
-
-@export var username : String = "Newblood":
- set(value):
- username = value
- username_changed.emit(username)
-
-@export var player_id : int:
- set(value):
- player_id = value
- player_id_changed.emit(player_id)
-
-@export var team_id : int = 1:
- set(value):
- team_id = value
- team_id_changed.emit(team_id)
diff --git a/entities/flag/flag.gd b/entities/flag/flag.gd
index e1b0013..ea39652 100644
--- a/entities/flag/flag.gd
+++ b/entities/flag/flag.gd
@@ -14,37 +14,45 @@
# along with this program. If not, see .
class_name Flag extends RigidBody3D
-signal grabbed(grabber : Player)
-signal regrabbed(grabber : Player)
-signal dropped(dropper : Player)
+signal grabbed(carry: FlagCarryComponent)
+signal dropped(carry: FlagCarryComponent)
-enum FlagState { ON_STAND, DROPPED, TAKEN }
+enum FlagState {
+ ON_STAND,
+ DROPPED,
+ TAKEN
+}
@export var state : FlagState = FlagState.ON_STAND
@onready var area : Area3D = $GripArea3D
@onready var mesh : Node3D = $Mesh
-var last_carrier : Player = null
+var last_carrier : FlagCarryComponent
func _ready() -> void:
area.body_entered.connect(_on_body_entered)
-
-func _on_grabbed(grabber : Player) -> void:
- if state != FlagState.TAKEN:
- hide()
- state = FlagState.TAKEN
- if (last_carrier == null) or (grabber != last_carrier):
- last_carrier = grabber
- else:
- regrabbed.emit(grabber)
-
-func _on_dropped(_dropper : Player) -> void:
- if state == FlagState.TAKEN:
- show()
- state = FlagState.DROPPED
+ grabbed.connect(_on_grabbed)
+ dropped.connect(_on_dropped)
func _on_body_entered(body: Node) -> void:
if body is Player:
assert(body.flag_carry_component)
- body.flag_carry_component.grab(self)
+ if state < FlagState.TAKEN:
+ body.flag_carry_component.grab(self)
+ hide()
+
+func _on_grabbed(_carry: FlagCarryComponent) -> void:
+ assert(state < FlagState.TAKEN)
+ hide()
+ state = FlagState.TAKEN
+ sleeping = true
+ area.set_deferred("monitoring", false)
+
+func _on_dropped(carry: FlagCarryComponent) -> void:
+ assert(state == FlagState.TAKEN)
+ sleeping = false
+ area.set_deferred("monitoring", true)
+ state = FlagState.DROPPED
+ last_carrier = carry
+ show()
diff --git a/entities/flag/flag.tscn b/entities/flag/flag.tscn
index b014f13..8ddd78a 100644
--- a/entities/flag/flag.tscn
+++ b/entities/flag/flag.tscn
@@ -23,6 +23,9 @@ properties/2/replication_mode = 2
properties/3/path = NodePath(".:state")
properties/3/spawn = true
properties/3/replication_mode = 2
+properties/4/path = NodePath(".:sleeping")
+properties/4/spawn = true
+properties/4/replication_mode = 2
[node name="Flag" type="RigidBody3D"]
collision_layer = 8
@@ -55,6 +58,3 @@ foreground_color = Color(0, 0.75, 0.75, 1)
[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="."]
replication_config = SubResource("SceneReplicationConfig_lpijf")
-
-[connection signal="dropped" from="." to="." method="_on_dropped"]
-[connection signal="grabbed" from="." to="." method="_on_grabbed"]
diff --git a/entities/player/assets/jetpackfx/Particle01.png.import b/entities/player/assets/jetpackfx/Particle01.png.import
index edbb4dd..505568a 100644
--- a/entities/player/assets/jetpackfx/Particle01.png.import
+++ b/entities/player/assets/jetpackfx/Particle01.png.import
@@ -4,15 +4,16 @@ importer="texture"
type="CompressedTexture2D"
uid="uid://dmf12llra7aq5"
path.s3tc="res://.godot/imported/Particle01.png-789728e4e363d58f11747b3cf3c5d5a3.s3tc.ctex"
+path.etc2="res://.godot/imported/Particle01.png-789728e4e363d58f11747b3cf3c5d5a3.etc2.ctex"
metadata={
-"imported_formats": ["s3tc_bptc"],
+"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
[deps]
source_file="res://entities/player/assets/jetpackfx/Particle01.png"
-dest_files=["res://.godot/imported/Particle01.png-789728e4e363d58f11747b3cf3c5d5a3.s3tc.ctex"]
+dest_files=["res://.godot/imported/Particle01.png-789728e4e363d58f11747b3cf3c5d5a3.s3tc.ctex", "res://.godot/imported/Particle01.png-789728e4e363d58f11747b3cf3c5d5a3.etc2.ctex"]
[params]
diff --git a/entities/player/assets/jetpackfx/smoke_01.png.import b/entities/player/assets/jetpackfx/smoke_01.png.import
index f895109..b24e089 100644
--- a/entities/player/assets/jetpackfx/smoke_01.png.import
+++ b/entities/player/assets/jetpackfx/smoke_01.png.import
@@ -4,15 +4,16 @@ importer="texture"
type="CompressedTexture2D"
uid="uid://ct1v5iadtpadm"
path.s3tc="res://.godot/imported/smoke_01.png-5c3d69bc74b2f317eac64d37cd67aed3.s3tc.ctex"
+path.etc2="res://.godot/imported/smoke_01.png-5c3d69bc74b2f317eac64d37cd67aed3.etc2.ctex"
metadata={
-"imported_formats": ["s3tc_bptc"],
+"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
[deps]
source_file="res://entities/player/assets/jetpackfx/smoke_01.png"
-dest_files=["res://.godot/imported/smoke_01.png-5c3d69bc74b2f317eac64d37cd67aed3.s3tc.ctex"]
+dest_files=["res://.godot/imported/smoke_01.png-5c3d69bc74b2f317eac64d37cd67aed3.s3tc.ctex", "res://.godot/imported/smoke_01.png-5c3d69bc74b2f317eac64d37cd67aed3.etc2.ctex"]
[params]
diff --git a/entities/player/assets/jetpackfx/smoke_02.png.import b/entities/player/assets/jetpackfx/smoke_02.png.import
index b9e266f..2140c9c 100644
--- a/entities/player/assets/jetpackfx/smoke_02.png.import
+++ b/entities/player/assets/jetpackfx/smoke_02.png.import
@@ -4,15 +4,16 @@ importer="texture"
type="CompressedTexture2D"
uid="uid://doxo4vfn0bjlp"
path.s3tc="res://.godot/imported/smoke_02.png-a5343a97ff1cefeebcd82e41218739ca.s3tc.ctex"
+path.etc2="res://.godot/imported/smoke_02.png-a5343a97ff1cefeebcd82e41218739ca.etc2.ctex"
metadata={
-"imported_formats": ["s3tc_bptc"],
+"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
[deps]
source_file="res://entities/player/assets/jetpackfx/smoke_02.png"
-dest_files=["res://.godot/imported/smoke_02.png-a5343a97ff1cefeebcd82e41218739ca.s3tc.ctex"]
+dest_files=["res://.godot/imported/smoke_02.png-a5343a97ff1cefeebcd82e41218739ca.s3tc.ctex", "res://.godot/imported/smoke_02.png-a5343a97ff1cefeebcd82e41218739ca.etc2.ctex"]
[params]
diff --git a/entities/player/assets/vanguard_0.png.import b/entities/player/assets/vanguard_0.png.import
index f6e35c1..d12bc74 100644
--- a/entities/player/assets/vanguard_0.png.import
+++ b/entities/player/assets/vanguard_0.png.import
@@ -4,8 +4,9 @@ importer="texture"
type="CompressedTexture2D"
uid="uid://ia3bdpe4rm1m"
path.bptc="res://.godot/imported/vanguard_0.png-601f36bc664114e126d425d5f45085ef.bptc.ctex"
+path.astc="res://.godot/imported/vanguard_0.png-601f36bc664114e126d425d5f45085ef.astc.ctex"
metadata={
-"imported_formats": ["s3tc_bptc"],
+"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={}
@@ -13,7 +14,7 @@ generator_parameters={}
[deps]
source_file="res://entities/player/assets/vanguard_0.png"
-dest_files=["res://.godot/imported/vanguard_0.png-601f36bc664114e126d425d5f45085ef.bptc.ctex"]
+dest_files=["res://.godot/imported/vanguard_0.png-601f36bc664114e126d425d5f45085ef.bptc.ctex", "res://.godot/imported/vanguard_0.png-601f36bc664114e126d425d5f45085ef.astc.ctex"]
[params]
diff --git a/entities/player/assets/vanguard_1.png.import b/entities/player/assets/vanguard_1.png.import
index 7aedb2f..2467a96 100644
--- a/entities/player/assets/vanguard_1.png.import
+++ b/entities/player/assets/vanguard_1.png.import
@@ -4,8 +4,9 @@ importer="texture"
type="CompressedTexture2D"
uid="uid://bvgfmpb2l1juf"
path.bptc="res://.godot/imported/vanguard_1.png-39b5712c4c4119a42b3540a159f8b3f2.bptc.ctex"
+path.astc="res://.godot/imported/vanguard_1.png-39b5712c4c4119a42b3540a159f8b3f2.astc.ctex"
metadata={
-"imported_formats": ["s3tc_bptc"],
+"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={}
@@ -13,7 +14,7 @@ generator_parameters={}
[deps]
source_file="res://entities/player/assets/vanguard_1.png"
-dest_files=["res://.godot/imported/vanguard_1.png-39b5712c4c4119a42b3540a159f8b3f2.bptc.ctex"]
+dest_files=["res://.godot/imported/vanguard_1.png-39b5712c4c4119a42b3540a159f8b3f2.bptc.ctex", "res://.godot/imported/vanguard_1.png-39b5712c4c4119a42b3540a159f8b3f2.astc.ctex"]
[params]
diff --git a/entities/player/assets/vanguard_2.png.import b/entities/player/assets/vanguard_2.png.import
index f04e9f9..4c7cd21 100644
--- a/entities/player/assets/vanguard_2.png.import
+++ b/entities/player/assets/vanguard_2.png.import
@@ -4,8 +4,9 @@ importer="texture"
type="CompressedTexture2D"
uid="uid://vtrbc3eja3df"
path.bptc="res://.godot/imported/vanguard_2.png-986e3665904b0d4758f584d5d3b7b726.bptc.ctex"
+path.astc="res://.godot/imported/vanguard_2.png-986e3665904b0d4758f584d5d3b7b726.astc.ctex"
metadata={
-"imported_formats": ["s3tc_bptc"],
+"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={}
@@ -13,7 +14,7 @@ generator_parameters={}
[deps]
source_file="res://entities/player/assets/vanguard_2.png"
-dest_files=["res://.godot/imported/vanguard_2.png-986e3665904b0d4758f584d5d3b7b726.bptc.ctex"]
+dest_files=["res://.godot/imported/vanguard_2.png-986e3665904b0d4758f584d5d3b7b726.bptc.ctex", "res://.godot/imported/vanguard_2.png-986e3665904b0d4758f584d5d3b7b726.astc.ctex"]
[params]
diff --git a/entities/player/inputs_sync.gd b/entities/player/inputs_sync.gd
index 8837cb7..d348a1e 100644
--- a/entities/player/inputs_sync.gd
+++ b/entities/player/inputs_sync.gd
@@ -12,79 +12,81 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-extends Node
+class_name PlayerInputController extends Node
-@export var jetting : bool = false
-@export var skiing : bool = false
-@export var direction : Vector2 = Vector2.ZERO
-@export var camera_rotation : Vector2
+@export var jetting:bool = false
+@export var skiing:bool = false
+@export var direction:Vector2
+@export var camera_rotation:Vector2
-signal jumped
-signal fired_primary
-signal throwed_flag
+signal jump
+signal primary(pressed: bool)
+signal throw(pressed: bool)
+
+var _mouse_sensitivity:float = .0
func _enter_tree() -> void:
- update_multiplayer_authority(0)
-
-func update_multiplayer_authority(player_id : int) -> void:
- set_multiplayer_authority(player_id)
+ # @NOTE: This is a workaround for player node being too integrated with the
+ # multiplayer layer and blocks player inputs in singleplayer if not set.
+ set_multiplayer_authority(1)
func _unhandled_input(event: InputEvent) -> void:
if is_multiplayer_authority():
- var mouse_mode : Input.MouseMode = Input.get_mouse_mode()
+ var mouse_mode: Input.MouseMode = Input.get_mouse_mode()
+ direction = Input.get_vector("left", "right", "forward", "backward")
+ jetting = Input.is_action_pressed("secondary")
+ skiing = Input.is_action_pressed("ski")
# isolate mouse events
if event is InputEventMouseMotion:
if mouse_mode == Input.MOUSE_MODE_CAPTURED:
# update camera with mouse position relative to last frame
_update_camera(event.relative)
- if event is InputEventMouseButton:
+ elif event is InputEventMouseButton:
# @NOTE: there could be a control setting for direction
if event.button_index == MOUSE_BUTTON_WHEEL_UP and event.pressed:
_cycle.rpc(1)
elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN and event.pressed:
_cycle.rpc(-1)
- if event is InputEventKey:
+ elif event is InputEventKey:
match event.keycode:
KEY_1: _select.rpc(0)
KEY_2: _select.rpc(1)
KEY_3: _select.rpc(2)
- direction = Input.get_vector("left", "right", "forward", "backward")
- if Input.is_action_just_pressed("jump_and_jet"):
+ if event.is_action("primary"):
+ _primary.rpc(event.pressed)
+ if Input.is_action_just_pressed("secondary"):
_jump.rpc()
- if Input.is_action_just_pressed("fire_primary"):
- _fire_primary.rpc()
- if Input.is_action_just_pressed("throw_flag"):
- _throw_flag.rpc()
- jetting = Input.is_action_pressed("jump_and_jet")
- skiing = Input.is_action_pressed("ski")
-
-func _update_camera(relative_motion : Vector2) -> void:
+ if event.is_action("throw") and not event.echo:
+ _throw.rpc(event.pressed)
+
+func _update_camera(relative_motion:Vector2) -> void:
# reverse y axis
if Settings.get_value("controls", "inverted_y_axis"):
relative_motion.y *= -1.0
+ _mouse_sensitivity = Settings.get_value("controls", "mouse_sensitivity")
# clamp vertical rotation (head motion)
- camera_rotation.y -= relative_motion.y * Settings.get_value("controls", "mouse_sensitivity") / 100
- camera_rotation.y = clamp(camera_rotation.y, deg_to_rad(-90.0), deg_to_rad(90.0))
+ camera_rotation.x -= relative_motion.y * _mouse_sensitivity / 100
+ camera_rotation.x = clamp(camera_rotation.x, deg_to_rad(-90.0), deg_to_rad(90.0))
# wrap horizontal rotation (to prevent accumulation)
- camera_rotation.x -= relative_motion.x * Settings.get_value("controls", "mouse_sensitivity") / 100
- camera_rotation.x = wrapf(camera_rotation.x, deg_to_rad(0.0), deg_to_rad(360.0))
+ camera_rotation.y -= relative_motion.x * _mouse_sensitivity / 100
+ camera_rotation.y = wrapf(camera_rotation.y, deg_to_rad(0.0), deg_to_rad(360.0))
-@rpc("call_local", "reliable")
+@rpc("authority", "call_local", "reliable")
func _jump() -> void:
- jumped.emit()
+ jump.emit()
-@rpc("call_local", "reliable")
-func _fire_primary() -> void:
- fired_primary.emit()
+@rpc("authority", "call_local", "reliable")
+func _primary(pressed: bool) -> void:
+ primary.emit(pressed)
-@rpc("call_local", "reliable")
-func _cycle(shift : int) -> void:
+@rpc("authority", "call_local", "reliable")
+func _cycle(shift:int) -> void:
%Inventory.cycle(shift)
-@rpc("call_local", "reliable")
-func _select(index : int) -> void:
+@rpc("authority", "call_local", "reliable")
+func _select(index:int) -> void:
%Inventory.select(index)
-@rpc("call_local", "reliable")
-func _throw_flag() -> void:
- throwed_flag.emit()
+@rpc("authority", "call_local", "reliable")
+func _throw(pressed: bool) -> void:
+ throw.emit(pressed)
diff --git a/entities/player/player.gd b/entities/player/player.gd
index e277f7c..8b1244e 100644
--- a/entities/player/player.gd
+++ b/entities/player/player.gd
@@ -14,140 +14,230 @@
# along with this program. If not, see .
class_name Player extends RigidBody3D
-enum PlayerState { PLAYER_ALIVE, PLAYER_DEAD }
+signal killed(victim: Player, killer:int)
-signal died(player : Player, killer_id : int)
+## Emitted when a source wants to damage this body
+signal damage(source: Node, target: Node, amount: int)
-@export var iff : IFF
-@export var health_component : HealthComponent
-@export var flag_carry_component : FlagCarryComponent
-@export var walkable_surface_sensor : ShapeCast3D
-@export var match_participant : MatchParticipant
+## Emitted when the player respawns, see [member Player.respawn].
+signal respawned(player: Player)
-## The inventory component can store up to [member Inventory.slots] objects. It
-## also has specific slots to hold grenades, packs and a flag.
-@export var inventory : Inventory
-
-@export var tp_mesh : Vanguard
+@export var iff:IFF
+## This is the player [Health] component.
+@export var health:Health
+@export var flag_carry_component:FlagCarryComponent
+@export var walkable_surface_sensor:ShapeCast3D
+@export var hud:HUD
+## The inventory component can store up to [member Inventory.slots] nodes.
+@export var inventory:Inventory
+@export var tp_mesh:Vanguard
+@export var third_person:Node3D
+@export var pivot : Node3D
+@export var camera : Camera3D
@export_category("Parameters")
-@export var ground_speed : float = 48 / 3.6 # m/s
-@export var aerial_control_force : int = 400
-@export var jump_height : float = 2.0
-@export var max_floor_angle : float = 60
+@export var ground_speed:float = 48 / 3.6 # m/s
+@export var aerial_control_force:int = 400
+@export var jump_height:float = 1.0
+@export var max_floor_angle:float = 60
@export_group("Jetpack")
@export var energy: float = 100.0
-@export var energy_charge_rate : float = 20 # energy per second
-@export var energy_drain_rate : float = 25 # energy per second
-@export var energy_max : float = 100.
-@export var jetpack_force_factor : float = 2.
-@export var jetpack_horizontal_force : float = 600
-@export var jetpack_vertical_force : float = 800
+@export var energy_charge_rate:float = 25 # energy per second
+@export var energy_drain_rate:float = 25 # energy per second
+@export var energy_max:float = 100.
+@export var jetpack_force_factor:float = 2.
+@export var jetpack_horizontal_force:float = 600
+@export var jetpack_vertical_force:float = 1200
@export_group("State")
-@export var player_state : PlayerState = PlayerState.PLAYER_ALIVE
-@export var input : Node
+@export var input: PlayerInputController
-@onready var camera : Camera3D = %Pivot/Camera3D
-@onready var hud : CanvasLayer = $HUD
-@onready var animation_player : AnimationPlayer = $AnimationPlayer
-@onready var collision_shape : CollisionShape3D = $CollisionShape3D
-@onready var jetpack_particles : Array = $ThirdPerson/Mesh/JetpackFX.get_children()
+@onready var animation_player:AnimationPlayer = $AnimationPlayer
+@onready var collision_shape:CollisionShape3D = $CollisionShape3D
+@onready var _jetpack_particles:Array = tp_mesh.get_node("JetpackFX").get_children()
-var g : float = ProjectSettings.get_setting("physics/3d/default_gravity") # in m/s²
-var gravity : Vector3 = g * ProjectSettings.get_setting("physics/3d/default_gravity_vector")
-var _jumping : bool = false
+var g:float = ProjectSettings.get_setting("physics/3d/default_gravity") # in m/s²
+var gravity:Vector3 = g * ProjectSettings.get_setting("physics/3d/default_gravity_vector")
+var _jumping:bool = false
-static var pawn_player : Player
+static var pawn_player:Player
+
+@export var username:String = "Newblood":
+ set = set_username
+
+@export var peer_id:int = 1:
+ set = set_peer_id
+
+@export var team_id:int = -1
+
+signal username_changed(new_username:int)
+signal peer_id_changed(new_peer_id:int)
+
+func set_username(new_username:String) -> void:
+ username = new_username
+ username_changed.emit(username)
+
+func set_peer_id(new_peer_id:int) -> void:
+ remove_from_group(str(peer_id))
+ peer_id = new_peer_id
+ add_to_group(str(peer_id))
+ peer_id_changed.emit(peer_id)
+
+# Maximum duration for pumping force when throwing a flag, in seconds.
+@export var throw_duration_max := 1.2
+var throw_timer := Timer.new()
func _ready() -> void:
- match_participant.player_id_changed.connect(_setup_pawn)
- match_participant.player_id_changed.connect(input.update_multiplayer_authority)
- match_participant.username_changed.connect(iff._on_username_changed)
+ input.set_multiplayer_authority(peer_id)
- health_component.health_changed.connect(hud.health_bar.set_value)
- health_component.health_changed.connect(iff.health_bar.set_value)
- health_component.health_changed.emit(health_component.health)
- health_component.health_zeroed.connect(die)
+ username_changed.connect(iff.set_username)
+ username_changed.emit(username) # trigger initial signal
- inventory.selection_changed.connect(_on_inventory_selection_changed)
+ health.updated.connect(hud.health_bar.set_value)
+ health.updated.connect(iff.health_bar.set_value)
+ health.killed.connect(_on_killed)
+
+ input.jump.connect(_jump)
+ input.throw.connect(_on_throw)
+ throw_timer.one_shot = true
+ add_child(throw_timer)
+
+ # bind inventory
+ inventory.selected.connect(func(node: Node) -> void:
+ if node is Weapon:
+ if not input.primary.is_connected(node._on_primary):
+ input.primary.connect(node._on_primary)
+ if not node.ammo_changed.is_connected(hud._on_ammo_changed):
+ node.ammo_changed.connect(hud._on_ammo_changed)
+ node.ammo_changed.emit(node.ammo)
+ )
- input.fired_primary.connect(_trigger)
- input.jumped.connect(_jump)
- input.throwed_flag.connect(_throw_flag)
-
-func _process(_delta : float) -> void:
- %Pivot.global_transform.basis = Basis.from_euler(Vector3(input.camera_rotation.y, input.camera_rotation.x, 0.0))
- if not _is_pawn():
- tp_mesh.global_transform.basis = Basis.from_euler(Vector3(.0, input.camera_rotation.x + PI, 0.0))
- if match_participant and pawn_player:
- if pawn_player.match_participant.team_id == match_participant.team_id:
- iff.fill = Color.GREEN
- else:
- iff.fill = Color.RED
-
-func _physics_process(delta : float) -> void:
- _update_jetpack_energy(delta)
-
-func _setup_pawn(_new_player_id : int) -> void:
+ inventory.unselected.connect(func(node: Node) -> void:
+ if node is Weapon:
+ if input.primary.is_connected(node._on_primary):
+ input.primary.disconnect(node._on_primary)
+ if node is AutomaticWeapon: node._on_primary(false) # release trigger
+ if node.ammo_changed.is_connected(hud._on_ammo_changed):
+ node.ammo_changed.disconnect(hud._on_ammo_changed)
+ hud.ammo_label.text = ""
+ )
+
+ inventory.select(0)
+
+ if Game.type is Singleplayer:
+ damage.connect(
+ func(_source: Node, target: Node, amount: int) -> void:
+ target.health.damage(amount, 0))
+
if _is_pawn():
+ pawn_player = self
camera.current = true
camera.fov = Settings.get_value("video", "fov")
- pawn_player = self
iff.hide()
+ # hide hud on pawn when scoreboard is visible in multiplayer
+ if Game.type is Multiplayer:
+ Game.type.scoreboard.visibility_changed.connect(
+ func() -> void: hud.set_visible(!Game.type.scoreboard.visible))
+ # forward this peer env settings to current viewport world env
+ var world: World3D = get_viewport().find_world_3d()
+ if not world.environment:
+ world.environment = Game.environment
+ world.environment.sdfgi_enabled = Game.environment.sdfgi_enabled
+ world.environment.glow_enabled = Game.environment.glow_enabled
+ world.environment.ssao_enabled = Game.environment.ssao_enabled
+ world.environment.ssr_enabled = Game.environment.ssr_enabled
+ world.environment.ssr_max_steps = Game.environment.ssr_max_steps
+ world.environment.ssil_enabled = Game.environment.ssil_enabled
+ world.environment.volumetric_fog_enabled = Game.environment.volumetric_fog_enabled
else:
- $ThirdPerson.show()
+ third_person.show()
%Inventory.hide()
hud.hide()
+func _process(_delta:float) -> void:
+ if not _is_pawn():
+ if Game.type is Multiplayer and Game.type.mode == Multiplayer.Mode.FREE_FOR_ALL:
+ iff.fill = Color.RED
+ elif is_instance_valid(pawn_player) and team_id:
+ iff.fill = Color.GREEN if team_id == pawn_player.team_id else Color.RED
+ _update_third_person_animations()
+
+ if not is_alive():
+ return
+
+ # compute target rotation from input.camera_rotation
+ var target_euler := Vector3(input.camera_rotation.x, input.camera_rotation.y, 0.0)
+ # smoothly interpolate rotation of pivot node towards target rotation
+ pivot.global_transform.basis = pivot.global_transform.basis.slerp(Basis.from_euler(target_euler), .6)
+
+ if not _is_pawn():
+ # compute target rotation from input.camera_rotation
+ var tp_target_euler := Vector3(.0, input.camera_rotation.y + PI, 0.0)
+ # smoothly interpolate rotation of third person node towards target rotation
+ tp_mesh.global_transform.basis = tp_mesh.global_transform.basis.slerp(Basis.from_euler(tp_target_euler), .6)
+ else:
+ if hud.throw_progress.is_visible_in_tree():
+ var time_elapsed: float = throw_duration_max - throw_timer.time_left
+ hud.throw_progress.set_value(time_elapsed / throw_duration_max * 100)
+
+func _physics_process(delta:float) -> void:
+ _update_jetpack_energy(delta)
+
func _is_pawn() -> bool:
- var peer_is_pawn : bool = multiplayer.get_unique_id() == match_participant.player_id
- return Global.type is Singleplayer or peer_is_pawn
+ if Game.type is Multiplayer:
+ if Game.type.is_peer_connected():
+ return multiplayer.get_unique_id() == peer_id
+ else:
+ queue_free()
+ return true
+
+func _on_throw(pressed: bool) -> void:
+ if pressed:
+ throw_timer.start(throw_duration_max)
+ hud.throw_progress.visible = true
+ else:
+ var time_left: float = throw_timer.time_left
+ throw_timer.stop()
+ var time_elapsed: float = throw_duration_max - time_left
+ var throw_force: float = \
+ time_elapsed / throw_duration_max * flag_carry_component.throw_force
+ flag_carry_component.throw(linear_velocity,
+ clamp(throw_force, 5., flag_carry_component.throw_force))
+ hud.throw_progress.visible = false
+
# @NOTE: this method works only because `tp_mesh` duplicates weapons meshes from the inventory
-func _on_inventory_selection_changed(_selected : Node3D, index : int) -> void:
+func _on_inventory_selection_changed(_selected:Node3D, index:int) -> void:
# hide any visible weapon
for child in tp_mesh.hand_attachment.get_children():
child.hide()
# get corresponding selected weapon mesh for third person
- var tp_weapon : Node3D = tp_mesh.hand_attachment.get_child(index)
+ var tp_weapon:Node3D = tp_mesh.hand_attachment.get_child(index)
if tp_weapon:
tp_weapon.show()
-func _trigger() -> void:
- if not _is_player_dead() and inventory.selected:
- if inventory.selected.has_method("trigger"):
- inventory.selected.trigger()
- else:
- push_warning("cannot trigger weapon")
-
func _jump() -> void:
- if _is_player_dead():
+ if not is_alive():
return
_jumping = true
-func _throw_flag() -> void:
- flag_carry_component.throw(linear_velocity, self)
-
func is_on_floor() -> bool:
return walkable_surface_sensor.is_colliding()
-func _is_skiing() -> bool:
- return input.skiing
-
-func _handle_aerial_control(direction : Vector3) -> void:
+func _handle_aerial_control(direction:Vector3) -> void:
if not input.jetting and not is_on_floor():
apply_force(direction * aerial_control_force)
-func _handle_jetpack(direction : Vector3) -> void:
+func _handle_jetpack(direction:Vector3) -> void:
if input.jetting and energy > 0:
- var up_vector : Vector3 = Vector3.UP * jetpack_vertical_force * jetpack_force_factor
- var side_vector : Vector3 = direction * jetpack_horizontal_force * jetpack_force_factor
+ var up_vector:Vector3 = Vector3.UP * jetpack_vertical_force * jetpack_force_factor
+ var side_vector:Vector3 = direction * jetpack_horizontal_force * jetpack_force_factor
apply_force(up_vector + side_vector)
- display_jetpack_particles()
+ for particle: GPUParticles3D in _jetpack_particles:
+ particle.emitting = true
-func _update_jetpack_energy(delta : float) -> void:
+func _update_jetpack_energy(delta:float) -> void:
if input.jetting and energy > 0:
energy -= energy_drain_rate * delta
else:
@@ -155,35 +245,32 @@ func _update_jetpack_energy(delta : float) -> void:
energy = clamp(energy, 0, energy_max)
hud.energy_bar.value = energy
-func _integrate_forces(_state : PhysicsDirectBodyState3D) -> void:
- # retrieve user's direction vector
- var _input_dir : Vector2 = input.direction
- # compute direction in local space
- var _direction : Vector3 = (transform.basis * Vector3(_input_dir.x, 0, _input_dir.y)).normalized()
-
- _update_third_person_animations()
-
- if _is_player_dead():
+func _integrate_forces(_state:PhysicsDirectBodyState3D) -> void:
+ if not is_alive():
return
+ # retrieve user's direction vector
+ var _input_dir:Vector2 = input.direction
+ # compute direction in local space
+ var _direction:Vector3 = (transform.basis * Vector3(_input_dir.x, 0, _input_dir.y)).normalized()
- # adjust direction based on spring arm rotation
- _direction = _direction.rotated(Vector3.UP, %Pivot.rotation.y)
+ # adjust direction based on pivot rotation
+ _direction = _direction.rotated(Vector3.UP, pivot.rotation.y)
- _handle_aerial_control(_direction)
- _handle_jetpack(_direction)
-
- # handle ski
- if _is_skiing():
- physics_material_override.friction = 0
+ # zero-damping ski
+ if is_on_floor() and input.skiing:
+ linear_damp = lerp(linear_damp, .0, .1)
else:
- physics_material_override.friction = 1
-
+ linear_damp = lerp(
+ linear_damp,
+ ProjectSettings.get_setting("physics/3d/default_linear_damp") + .05,
+ .2)
+
if is_on_floor():
- if not _direction.is_zero_approx() and not _is_skiing():
+ if not _direction.is_zero_approx() and not input.skiing:
# retrieve collision normal
- var normal : Vector3 = walkable_surface_sensor.get_collision_normal(0)
+ var normal:Vector3 = walkable_surface_sensor.get_collision_normal(0)
# calculate the angle between the ground normal and the up vector
- var slope_angle : float = rad_to_deg(acos(normal.dot(Vector3.UP)))
+ var slope_angle:float = rad_to_deg(acos(normal.dot(Vector3.UP)))
# check if the slope angle exceeds the maximum slope angle
if slope_angle <= max_floor_angle:
# adjust direction based on the floor normal to align with the slope
@@ -192,16 +279,19 @@ func _integrate_forces(_state : PhysicsDirectBodyState3D) -> void:
linear_velocity = lerp(linear_velocity, _direction * ground_speed, .1)
if _jumping:
- var v : float = sqrt(2 * g * jump_height)
- apply_central_impulse(Vector3(0, mass * v, 0))
-
+ var v:float = sqrt(2. * g * jump_height)
+ apply_central_impulse(Vector3(0., mass * v, 0.))
+
_jumping = false
+
+ # set ski state
+ physics_material_override.friction = !input.skiing
+
+ _handle_aerial_control(_direction)
+ _handle_jetpack(_direction)
func _update_third_person_animations() -> void:
- if _is_pawn():
- return
-
- if _is_player_dead():
+ if not is_alive():
tp_mesh.set_ground_state(Vanguard.GroundState.GROUND_STATE_DEAD)
return
@@ -209,32 +299,34 @@ func _update_third_person_animations() -> void:
tp_mesh.set_ground_state(Vanguard.GroundState.GROUND_STATE_GROUNDED)
else:
tp_mesh.set_ground_state(Vanguard.GroundState.GROUND_STATE_MID_AIR)
- var local_velocity : Vector3 = (tp_mesh.global_basis.inverse() * linear_velocity)
- const bias : float = 1.2 # Basically match feet speed with ground speed
+
+ var local_velocity:Vector3 = (tp_mesh.global_basis.inverse() * linear_velocity)
+ const bias:float = 1.2 # Basically match feet speed with ground speed
+
tp_mesh.set_locomotion(Vector2(local_velocity.x, local_velocity.z), bias)
-func _is_player_dead() -> bool:
- return player_state != PlayerState.PLAYER_ALIVE
+func is_alive() -> bool:
+ assert(health)
+ return health.state == health.HealthState.ALIVE
-func die(killer_id : int) -> void:
- flag_carry_component.drop(self)
- player_state = PlayerState.PLAYER_DEAD
+func has_flag() -> bool:
+ return flag_carry_component._flag != null
+
+func _on_killed(by_peer_id:int) -> void:
+ flag_carry_component.drop(linear_velocity)
if _is_pawn():
animation_player.play("death")
- died.emit(self, killer_id)
+ killed.emit(self, by_peer_id)
-@rpc("call_local", "reliable")
-func respawn(location : Vector3) -> void:
+@rpc("authority", "call_local", "reliable")
+func respawn(location: Vector3) -> void:
animation_player.stop()
- player_state = PlayerState.PLAYER_ALIVE
- linear_velocity = Vector3()
- health_component.heal_full()
- position = location
+ linear_velocity = Vector3.ZERO
+ health.heal()
+ global_position = location
+ for weapon: Weapon in inventory.get_children():
+ weapon.set_ammo.rpc(weapon.max_ammo)
+ respawned.emit(self)
func _exit_tree() -> void:
- player_state = PlayerState.PLAYER_DEAD
- flag_carry_component.drop(self)
-
-func display_jetpack_particles() -> void:
- for particle: GPUParticles3D in jetpack_particles:
- particle.emitting = true
+ flag_carry_component.drop(linear_velocity)
diff --git a/entities/player/player.tscn b/entities/player/player.tscn
index 9cdbb7a..b850168 100644
--- a/entities/player/player.tscn
+++ b/entities/player/player.tscn
@@ -1,22 +1,20 @@
-[gd_scene load_steps=45 format=3 uid="uid://cbhx1xme0sb7k"]
+[gd_scene load_steps=44 format=3 uid="uid://cbhx1xme0sb7k"]
-[ext_resource type="Script" path="res://entities/player/player.gd" id="1_mk68k"]
+[ext_resource type="Script" path="res://entities/player/player.gd" id="1_y2i7h"]
[ext_resource type="PackedScene" uid="uid://bbeecp3jusppn" path="res://interfaces/hud/iffs/IFF.tscn" id="2_s5wgp"]
[ext_resource type="PackedScene" uid="uid://bcv81ku26xo" path="res://interfaces/hud/hud.tscn" id="3_ccety"]
[ext_resource type="Shape3D" uid="uid://cb8esdlnottdn" path="res://entities/player/resources/collider.tres" id="4_8kvcy"]
-[ext_resource type="Script" path="res://entities/components/inventory.gd" id="8_768qh"]
+[ext_resource type="Script" path="res://entities/components/health.gd" id="4_55muf"]
+[ext_resource type="Script" path="res://entities/components/inventory.gd" id="6_du54c"]
[ext_resource type="PackedScene" uid="uid://drbefw6akui2v" path="res://entities/player/vanguard.tscn" id="8_eiy7q"]
[ext_resource type="Script" path="res://entities/components/flag_carry_component.gd" id="8_pdfbn"]
[ext_resource type="Texture2D" uid="uid://ct1v5iadtpadm" path="res://entities/player/assets/jetpackfx/smoke_01.png" id="9_4pant"]
[ext_resource type="PackedScene" uid="uid://c8co0qa2omjmh" path="res://entities/weapons/space_gun/space_gun.tscn" id="9_achlo"]
[ext_resource type="Texture2D" uid="uid://doxo4vfn0bjlp" path="res://entities/player/assets/jetpackfx/smoke_02.png" id="10_5b1bx"]
[ext_resource type="Texture2D" uid="uid://dmf12llra7aq5" path="res://entities/player/assets/jetpackfx/Particle01.png" id="11_6ndfi"]
-[ext_resource type="Script" path="res://addons/smoothing/smoothing.gd" id="11_k330l"]
-[ext_resource type="Script" path="res://entities/components/health_component.gd" id="14_ctgxn"]
[ext_resource type="PackedScene" uid="uid://b0xql5hi0b52y" path="res://entities/weapons/chaingun/chaingun.tscn" id="15_io0a3"]
[ext_resource type="PackedScene" uid="uid://cstl7yxc75572" path="res://entities/weapons/grenade_launcher/grenade_launcher.tscn" id="16_4xs2j"]
[ext_resource type="PackedScene" uid="uid://d3l7fvbdg6m5g" path="res://entities/flag/assets/flag.glb" id="18_7nkei"]
-[ext_resource type="Script" path="res://entities/components/match_participant.gd" id="18_t2jrs"]
[ext_resource type="Script" path="res://entities/player/inputs_sync.gd" id="18_v4iu1"]
[sub_resource type="PhysicsMaterial" id="PhysicsMaterial_clur0"]
@@ -24,8 +22,35 @@ resource_local_to_scene = true
bounce = 1.0
absorbent = true
-[sub_resource type="SphereShape3D" id="SphereShape3D_hwe6e"]
-radius = 0.2
+[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_h0a20"]
+radius = 0.25
+
+[sub_resource type="Animation" id="Animation_eoxkv"]
+length = 0.001
+tracks/0/type = "value"
+tracks/0/imported = false
+tracks/0/enabled = true
+tracks/0/path = NodePath("Pivot:position")
+tracks/0/interp = 1
+tracks/0/loop_wrap = true
+tracks/0/keys = {
+"times": PackedFloat32Array(0),
+"transitions": PackedFloat32Array(1),
+"update": 0,
+"values": [Vector3(0, 0.625, 0)]
+}
+tracks/1/type = "value"
+tracks/1/imported = false
+tracks/1/enabled = true
+tracks/1/path = NodePath("Pivot:rotation")
+tracks/1/interp = 1
+tracks/1/loop_wrap = true
+tracks/1/keys = {
+"times": PackedFloat32Array(0),
+"transitions": PackedFloat32Array(1),
+"update": 0,
+"values": [Vector3(0, 0, 0)]
+}
[sub_resource type="Animation" id="Animation_yqgrk"]
resource_name = "death"
@@ -51,11 +76,12 @@ tracks/1/keys = {
"times": PackedFloat32Array(0, 0.5),
"transitions": PackedFloat32Array(1, 1),
"update": 0,
-"values": [Vector3(0, 0.5, 0), Vector3(0, -0.114794, 0)]
+"values": [Vector3(0, 0.625, 0), Vector3(0, 0.625, 0)]
}
[sub_resource type="AnimationLibrary" id="AnimationLibrary_hg307"]
_data = {
+"RESET": SubResource("Animation_eoxkv"),
"death": SubResource("Animation_yqgrk")
}
@@ -169,83 +195,88 @@ material = SubResource("StandardMaterial3D_2jwv2")
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_rqdp6"]
properties/0/path = NodePath(".:linear_velocity")
-properties/0/spawn = false
+properties/0/spawn = true
properties/0/replication_mode = 1
properties/1/path = NodePath(".:position")
-properties/1/spawn = false
+properties/1/spawn = true
properties/1/replication_mode = 1
-properties/2/path = NodePath("HealthComponent:health")
-properties/2/spawn = false
+properties/2/path = NodePath(".:peer_id")
+properties/2/spawn = true
properties/2/replication_mode = 2
-properties/3/path = NodePath(".:player_state")
-properties/3/spawn = false
+properties/3/path = NodePath(".:username")
+properties/3/spawn = true
properties/3/replication_mode = 2
+properties/4/path = NodePath(".:team_id")
+properties/4/spawn = true
+properties/4/replication_mode = 2
+properties/5/path = NodePath("Health:value")
+properties/5/spawn = true
+properties/5/replication_mode = 2
+properties/6/path = NodePath("Pivot/FlagCarryComponent:visible")
+properties/6/spawn = true
+properties/6/replication_mode = 2
+properties/7/path = NodePath("Health:state")
+properties/7/spawn = true
+properties/7/replication_mode = 1
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_5j4ew"]
properties/0/path = NodePath(".:direction")
properties/0/spawn = true
-properties/0/replication_mode = 1
+properties/0/replication_mode = 2
properties/1/path = NodePath(".:jetting")
properties/1/spawn = true
properties/1/replication_mode = 2
properties/2/path = NodePath(".:camera_rotation")
properties/2/spawn = true
-properties/2/replication_mode = 1
+properties/2/replication_mode = 2
properties/3/path = NodePath(".:skiing")
properties/3/spawn = true
properties/3/replication_mode = 2
-[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_7na0c"]
-properties/0/path = NodePath(".:username")
-properties/0/spawn = true
-properties/0/replication_mode = 1
-properties/1/path = NodePath(".:player_id")
-properties/1/spawn = true
-properties/1/replication_mode = 1
-properties/2/path = NodePath(".:team_id")
-properties/2/spawn = true
-properties/2/replication_mode = 1
+[sub_resource type="SphereShape3D" id="SphereShape3D_hwe6e"]
+radius = 0.2
-[node name="Player" type="RigidBody3D" node_paths=PackedStringArray("iff", "health_component", "flag_carry_component", "walkable_surface_sensor", "match_participant", "inventory", "tp_mesh", "input") groups=["Player"]]
+[node name="Player" type="RigidBody3D" node_paths=PackedStringArray("iff", "health", "flag_carry_component", "walkable_surface_sensor", "hud", "inventory", "tp_mesh", "third_person", "pivot", "camera", "input")]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0)
collision_mask = 2147483649
collision_priority = 9.0
axis_lock_angular_x = true
axis_lock_angular_y = true
axis_lock_angular_z = true
-mass = 75.0
+mass = 80.0
physics_material_override = SubResource("PhysicsMaterial_clur0")
+gravity_scale = 1.2
can_sleep = false
continuous_cd = true
-script = ExtResource("1_mk68k")
+linear_damp_mode = 1
+linear_damp = 0.15
+script = ExtResource("1_y2i7h")
iff = NodePath("IFF")
-health_component = NodePath("HealthComponent")
+health = NodePath("Health")
flag_carry_component = NodePath("Pivot/FlagCarryComponent")
walkable_surface_sensor = NodePath("WalkableSurfaceSensor")
-match_participant = NodePath("MatchParticipant")
+hud = NodePath("HUD")
inventory = NodePath("Pivot/Inventory")
tp_mesh = NodePath("ThirdPerson/Mesh")
-jump_height = 1.5
+third_person = NodePath("ThirdPerson")
+pivot = NodePath("Pivot")
+camera = NodePath("Pivot/Camera3D")
+max_floor_angle = 80.0
input = NodePath("Inputs")
+[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
+shape = ExtResource("4_8kvcy")
+
[node name="IFF" parent="." instance=ExtResource("2_s5wgp")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.1, 0)
fill = Color(0, 1, 0, 1)
-[node name="HealthComponent" type="Area3D" parent="." node_paths=PackedStringArray("match_participant")]
-script = ExtResource("14_ctgxn")
-match_participant = NodePath("../MatchParticipant")
+[node name="Health" type="Area3D" parent="." node_paths=PackedStringArray("collider")]
+script = ExtResource("4_55muf")
+collider = NodePath("CollisionShape3D")
-[node name="CollisionShape3D" type="CollisionShape3D" parent="HealthComponent"]
-shape = ExtResource("4_8kvcy")
-
-[node name="WalkableSurfaceSensor" type="ShapeCast3D" parent="."]
-transform = Transform3D(1.05, 0, 0, 0, 1.05, 0, 0, 0, 1.05, 0, -0.85, 0)
-shape = SubResource("SphereShape3D_hwe6e")
-target_position = Vector3(0, 0, 0)
-collision_mask = 2147483648
-
-[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
-shape = ExtResource("4_8kvcy")
+[node name="CollisionShape3D" type="CollisionShape3D" parent="Health"]
+shape = SubResource("CapsuleShape3D_h0a20")
[node name="HUD" parent="." instance=ExtResource("3_ccety")]
@@ -253,21 +284,19 @@ shape = ExtResource("4_8kvcy")
libraries = {
"": SubResource("AnimationLibrary_hg307")
}
-autoplay = "shoot"
+autoplay = "RESET"
playback_default_blend_time = 0.05
[node name="Pivot" type="Node3D" parent="."]
-unique_name_in_owner = true
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 0)
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.625, 0)
[node name="Camera3D" type="Camera3D" parent="Pivot"]
fov = 90.0
-near = 0.1
[node name="Inventory" type="Node3D" parent="Pivot"]
unique_name_in_owner = true
transform = Transform3D(-1, 0, 8.74228e-08, 0, 1, 0, -8.74228e-08, 0, -1, 0.15, -0.2, -0.45)
-script = ExtResource("8_768qh")
+script = ExtResource("6_du54c")
[node name="SpaceGun" parent="Pivot/Inventory" instance=ExtResource("9_achlo")]
@@ -297,84 +326,55 @@ transform = Transform3D(-1, 0, -8.74228e-08, 0, 1, 0, 8.74228e-08, 0, -1, 0, 0,
spine_ik_target_attachment = NodePath("../../Pivot/SpineIKTarget")
[node name="Skeleton3D" parent="ThirdPerson/Mesh/Node" index="0"]
-bones/0/position = Vector3(-0.0048219, 0.946668, 0.00678214)
-bones/0/rotation = Quaternion(-0.0341192, -0.409249, -0.0209221, 0.911545)
-bones/2/rotation = Quaternion(-0.00595832, -0.0014545, 0.0101407, 0.99993)
-bones/4/rotation = Quaternion(0.0691268, 0.00227406, 0.0147948, 0.997496)
-bones/6/rotation = Quaternion(0.160951, 0.00623474, 0.0096637, 0.986895)
-bones/8/rotation = Quaternion(0.675761, -0.399581, 0.409695, 0.464577)
-bones/10/rotation = Quaternion(0.212344, -0.0870591, -0.287653, 0.929831)
-bones/12/rotation = Quaternion(0.0811501, 0.206806, -0.754068, 0.618084)
-bones/14/rotation = Quaternion(0.00943715, 0.0971687, -0.145046, 0.984597)
-bones/20/rotation = Quaternion(-0.123455, 0.0248346, 0.23344, 0.964183)
-bones/24/rotation = Quaternion(0.0450683, -0.000817796, 0.0508488, 0.997689)
-bones/26/rotation = Quaternion(0.100545, -1.16532e-07, 0.00792588, 0.994901)
-bones/28/rotation = Quaternion(0.288532, -1.83796e-07, 0.0227447, 0.9572)
-bones/32/rotation = Quaternion(0.00674081, -0.0316083, 0.105474, 0.993897)
-bones/34/rotation = Quaternion(0.780684, 4.23384e-08, 0.0615407, 0.621889)
-bones/36/rotation = Quaternion(0.580978, -1.56224e-07, 0.0457976, 0.81263)
-bones/40/rotation = Quaternion(0.0913739, 0.113691, 0.100265, 0.984211)
-bones/42/rotation = Quaternion(0.836841, 4.40546e-07, 0.0659669, 0.543457)
-bones/44/rotation = Quaternion(0.633142, 6.48257e-09, 0.04991, 0.772425)
-bones/50/rotation = Quaternion(0.729888, -4.88266e-08, 0.0575362, 0.681141)
-bones/52/rotation = Quaternion(0.624011, -9.63141e-08, 0.04919, 0.779865)
-bones/56/rotation = Quaternion(-0.0254885, 0.0439755, -0.00910637, 0.998666)
-bones/58/rotation = Quaternion(-0.0119802, 0.289956, -0.0527053, 0.955513)
-bones/62/rotation = Quaternion(0.689943, 0.375565, -0.410267, 0.463261)
-bones/64/rotation = Quaternion(0.337746, -0.290519, 0.159479, 0.880961)
-bones/66/rotation = Quaternion(0.540371, -0.580679, 0.406779, 0.453147)
bones/68/position = Vector3(-1.77636e-17, 0.240718, 0)
-bones/68/rotation = Quaternion(0.0150529, -0.289001, 0.0720108, 0.954498)
-bones/70/rotation = Quaternion(0.155965, 0.0109114, -0.00107202, 0.987702)
-bones/72/rotation = Quaternion(0.563923, 4.19095e-08, -0.0577906, 0.823803)
-bones/74/rotation = Quaternion(0.285209, 0.0197164, -0.0936782, 0.953673)
-bones/78/rotation = Quaternion(0.057484, -0.0698946, -0.0100642, 0.995846)
-bones/80/rotation = Quaternion(0.433127, 5.44824e-08, -0.0443866, 0.900239)
-bones/82/rotation = Quaternion(0.274138, -1.97906e-08, -0.0280937, 0.96128)
-bones/86/rotation = Quaternion(0.244152, 0.0521336, 0.176446, 0.952123)
-bones/88/rotation = Quaternion(0.0146391, -1.48637e-07, -0.121951, 0.992428)
-bones/90/rotation = Quaternion(-0.147655, -0.0737763, 0.197847, 0.966236)
-bones/94/rotation = Quaternion(0.180682, 0.0832945, -0.00387314, 0.980001)
-bones/96/rotation = Quaternion(0.245651, -6.35919e-08, -0.0251742, 0.969032)
-bones/98/rotation = Quaternion(0.246432, 7.6252e-08, -0.0252543, 0.968831)
-bones/102/rotation = Quaternion(0.179829, 0.0890365, -0.000307644, 0.97966)
-bones/104/rotation = Quaternion(0.388149, 1.28057e-07, -0.0397774, 0.920738)
-bones/106/rotation = Quaternion(0.372324, -1.37021e-07, -0.0381557, 0.927318)
-bones/110/rotation = Quaternion(-0.167577, 0.223934, 0.958827, 0.0492099)
-bones/112/rotation = Quaternion(-0.466474, -0.0088339, -0.0232928, 0.884184)
-bones/114/rotation = Quaternion(0.575696, 0.0793941, -0.0250592, 0.813414)
-bones/116/rotation = Quaternion(0.355062, 0.0493655, 0.0246355, 0.933213)
-bones/120/rotation = Quaternion(0.115252, 0.282473, 0.945749, -0.111737)
-bones/122/rotation = Quaternion(-0.494906, -0.0647935, 0.0183973, 0.866332)
-bones/124/rotation = Quaternion(0.417677, -0.0431149, 0.00625689, 0.90755)
-bones/126/rotation = Quaternion(0.397818, -0.0427722, -0.00601182, 0.916447)
[node name="HandAttachment" parent="ThirdPerson/Mesh/Node/Skeleton3D" index="0"]
-transform = Transform3D(-0.152214, 0.0548832, 0.986823, 0.933991, 0.334546, 0.125459, -0.323252, 0.94078, -0.102183, -0.261612, 1.14328, 0.0896011)
+transform = Transform3D(-0.152213, 0.0548833, 0.986823, 0.933991, 0.334546, 0.125458, -0.323252, 0.94078, -0.102183, -0.261612, 1.14328, 0.0896012)
[node name="grip" parent="ThirdPerson/Mesh/Node/Skeleton3D/HandAttachment/SpaceGun/Mesh/Armature/Skeleton3D" index="0"]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2.84217e-14, 1.19209e-07, 2.38419e-07)
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2.6068e-14, -1.19209e-07, 9.53674e-07)
+
+[node name="grip" parent="ThirdPerson/Mesh/Node/Skeleton3D/HandAttachment/SpaceGun/Mesh/Armature/Skeleton3D/grip" index="0"]
+layers = 2
[node name="main" parent="ThirdPerson/Mesh/Node/Skeleton3D/HandAttachment/SpaceGun/Mesh/Armature/Skeleton3D" index="1"]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2.84217e-14, 1.19209e-07, 2.38419e-07)
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2.6068e-14, -1.19209e-07, 9.53674e-07)
+
+[node name="main" parent="ThirdPerson/Mesh/Node/Skeleton3D/HandAttachment/SpaceGun/Mesh/Armature/Skeleton3D/main" index="0"]
+layers = 2
[node name="sides" parent="ThirdPerson/Mesh/Node/Skeleton3D/HandAttachment/SpaceGun/Mesh/Armature/Skeleton3D" index="2"]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2.84217e-14, 1.19209e-07, 2.38419e-07)
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2.6068e-14, -1.19209e-07, 9.53674e-07)
+
+[node name="sides" parent="ThirdPerson/Mesh/Node/Skeleton3D/HandAttachment/SpaceGun/Mesh/Armature/Skeleton3D/sides" index="0"]
+layers = 2
[node name="BarrelsInner" parent="ThirdPerson/Mesh/Node/Skeleton3D/HandAttachment/ChainGun/Armature/Skeleton3D" index="0"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.692504, 1.30946)
+[node name="BarrelsInner" parent="ThirdPerson/Mesh/Node/Skeleton3D/HandAttachment/ChainGun/Armature/Skeleton3D/BarrelsInner" index="0"]
+layers = 2
+
[node name="BarrelsOuter" parent="ThirdPerson/Mesh/Node/Skeleton3D/HandAttachment/ChainGun/Armature/Skeleton3D" index="1"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.692504, 1.30946)
+[node name="BarrelsOuter" parent="ThirdPerson/Mesh/Node/Skeleton3D/HandAttachment/ChainGun/Armature/Skeleton3D/BarrelsOuter" index="0"]
+layers = 2
+
[node name="Base" parent="ThirdPerson/Mesh/Node/Skeleton3D/HandAttachment/ChainGun/Armature/Skeleton3D" index="2"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.692504, 1.30946)
+[node name="Base" parent="ThirdPerson/Mesh/Node/Skeleton3D/HandAttachment/ChainGun/Armature/Skeleton3D/Base" index="0"]
+layers = 2
+
[node name="Grip" parent="ThirdPerson/Mesh/Node/Skeleton3D/HandAttachment/ChainGun/Armature/Skeleton3D" index="3"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.692504, 1.30946)
-[node name="Barrels" parent="ThirdPerson/Mesh/Node/Skeleton3D/HandAttachment/ChainGun" index="3"]
-transform = Transform3D(-1, 0, 8.9407e-08, 8.47504e-08, 0, 1, 0, 1, -3.72529e-09, -5.96046e-08, 0, -0.55315)
+[node name="Grip" parent="ThirdPerson/Mesh/Node/Skeleton3D/HandAttachment/ChainGun/Armature/Skeleton3D/Grip" index="0"]
+layers = 2
+
+[node name="Barrels" parent="ThirdPerson/Mesh/Node/Skeleton3D/HandAttachment/ChainGun" index="2"]
+transform = Transform3D(-1, 0, 8.74227e-08, 8.74227e-08, 0, 1, 0, 1, 0, -2.32831e-10, 0.692504, 0.756307)
[node name="Skeleton3D" parent="ThirdPerson/Mesh/Node/Skeleton3D/HandAttachment/GrenadeLauncher/Armature" index="0"]
bones/0/rotation = Quaternion(0, 0.707107, 0.707107, 0)
@@ -385,17 +385,33 @@ bones/3/rotation = Quaternion(0, 0.707107, 0.707107, 0)
[node name="barrel" parent="ThirdPerson/Mesh/Node/Skeleton3D/HandAttachment/GrenadeLauncher/Armature/Skeleton3D" index="0"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.19209e-07, 0)
+[node name="barrel" parent="ThirdPerson/Mesh/Node/Skeleton3D/HandAttachment/GrenadeLauncher/Armature/Skeleton3D/barrel" index="0"]
+layers = 2
+
[node name="grip" parent="ThirdPerson/Mesh/Node/Skeleton3D/HandAttachment/GrenadeLauncher/Armature/Skeleton3D" index="1"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.082471, -0.0653242)
+[node name="grip" parent="ThirdPerson/Mesh/Node/Skeleton3D/HandAttachment/GrenadeLauncher/Armature/Skeleton3D/grip" index="0"]
+layers = 2
+
[node name="main" parent="ThirdPerson/Mesh/Node/Skeleton3D/HandAttachment/GrenadeLauncher/Armature/Skeleton3D" index="2"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.19209e-07, 0)
+[node name="main" parent="ThirdPerson/Mesh/Node/Skeleton3D/HandAttachment/GrenadeLauncher/Armature/Skeleton3D/main" index="0"]
+layers = 2
+
+[node name="vanguard_Mesh" parent="ThirdPerson/Mesh/Node/Skeleton3D" index="1"]
+layers = 2
+
+[node name="vanguard_visor" parent="ThirdPerson/Mesh/Node/Skeleton3D" index="2"]
+layers = 2
+
[node name="JetpackFX" type="Node3D" parent="ThirdPerson/Mesh"]
transform = Transform3D(0.467164, -0.312366, -0.827155, -0.12476, 0.902867, -0.41142, 0.875325, 0.295397, 0.382816, 0.143745, 0.353192, -0.140279)
[node name="Smoke1" type="GPUParticles3D" parent="ThirdPerson/Mesh/JetpackFX"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.150648, 0)
+layers = 2
emitting = false
lifetime = 0.5
one_shot = true
@@ -405,6 +421,7 @@ draw_pass_1 = SubResource("QuadMesh_hegkl")
[node name="Smoke2" type="GPUParticles3D" parent="ThirdPerson/Mesh/JetpackFX"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.108846, 0)
+layers = 2
emitting = false
lifetime = 0.5
one_shot = true
@@ -413,6 +430,7 @@ process_material = SubResource("ParticleProcessMaterial_l8e6j")
draw_pass_1 = SubResource("QuadMesh_aeure")
[node name="Fire1" type="GPUParticles3D" parent="ThirdPerson/Mesh/JetpackFX"]
+layers = 2
emitting = false
amount = 16
lifetime = 0.3
@@ -421,12 +439,7 @@ fixed_fps = 60
process_material = SubResource("ParticleProcessMaterial_q1vdw")
draw_pass_1 = SubResource("QuadMesh_uc7ts")
-[node name="Smoothing" type="Node3D" parent="."]
-script = ExtResource("11_k330l")
-target = NodePath("..")
-flags = 3
-
-[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="."]
+[node name="MultiplayerSync" type="MultiplayerSynchronizer" parent="."]
replication_config = SubResource("SceneReplicationConfig_rqdp6")
[node name="Inputs" type="Node" parent="."]
@@ -435,11 +448,14 @@ script = ExtResource("18_v4iu1")
[node name="InputsSync" type="MultiplayerSynchronizer" parent="Inputs"]
replication_config = SubResource("SceneReplicationConfig_5j4ew")
-[node name="MatchParticipant" type="Node" parent="."]
-script = ExtResource("18_t2jrs")
+[node name="WalkableSurfaceSensor" type="ShapeCast3D" parent="."]
+transform = Transform3D(1.05, 0, 0, 0, 1.05, 0, 0, 0, 1.05, 0, -0.85, 0)
+shape = SubResource("SphereShape3D_hwe6e")
+target_position = Vector3(0, 0, 0)
+max_results = 16
+collision_mask = 2147483648
-[node name="MatchParticipantSync" type="MultiplayerSynchronizer" parent="MatchParticipant"]
-replication_config = SubResource("SceneReplicationConfig_7na0c")
+[connection signal="child_entered_tree" from="Pivot/Inventory" to="Pivot/Inventory" method="_on_child_entered_tree"]
[editable path="ThirdPerson/Mesh"]
[editable path="ThirdPerson/Mesh/Node/Skeleton3D/HandAttachment/SpaceGun"]
diff --git a/entities/player/resources/collider.tres b/entities/player/resources/collider.tres
index 0195e9e..16ef470 100644
--- a/entities/player/resources/collider.tres
+++ b/entities/player/resources/collider.tres
@@ -1,4 +1,5 @@
[gd_resource type="CapsuleShape3D" format=3 uid="uid://cb8esdlnottdn"]
[resource]
+resource_local_to_scene = true
radius = 0.25
diff --git a/entities/player/vanguard.gd b/entities/player/vanguard.gd
index 14d3179..a27b62c 100644
--- a/entities/player/vanguard.gd
+++ b/entities/player/vanguard.gd
@@ -1,3 +1,17 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
class_name Vanguard extends Node
@export var spine_ik_target_attachment : Marker3D
diff --git a/entities/projectiles/arc_projectile.gd b/entities/projectiles/arc_projectile.gd
new file mode 100644
index 0000000..6ffd40e
--- /dev/null
+++ b/entities/projectiles/arc_projectile.gd
@@ -0,0 +1,31 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+class_name ArcProjectile extends ExplosiveProjectile
+
+var _g: float = ProjectSettings.get_setting("physics/3d/default_gravity") # in m/s²
+var _gravity: Vector3 = _g * ProjectSettings.get_setting("physics/3d/default_gravity_vector")
+
+func _physics_process(delta : float) -> void:
+ # compute motion
+ var previous_global_position : Vector3 = global_position
+ # apply gravity
+ velocity += _gravity * delta
+ # compute new position
+ global_position += velocity * delta
+ # handle collision
+ if shape_cast:
+ shape_cast.target_position = to_local(previous_global_position)
+ if shape_cast.is_colliding():
+ destroy(shape_cast.collision_result[0].point)
diff --git a/entities/projectiles/damages/explosive_damage.gd b/entities/projectiles/damages/explosive_damage.gd
new file mode 100644
index 0000000..708248c
--- /dev/null
+++ b/entities/projectiles/damages/explosive_damage.gd
@@ -0,0 +1,63 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+## This class defines an explosive damage area.
+##
+## It detects nearby bodies within a radius defined by a child [CollisionShape3D]
+## or [CollisionPolygon3D] and also initiates a blast that applies impulse and
+## damages to detected bodies based on their distance from the explosion.
+class_name ExplosiveDamage extends Area3D
+
+## The base amount of damage.
+@export var damage: int = 1
+## The magnitude of blast force, in Newtons.
+@export var blast_force: int = 1500
+## A factor that determines how damage decreases with distance from the explosion center.
+@export var falloff: Curve
+## The entity responsible for dealing damage.
+var source: Node = null
+# The number of physics frames to process for blast detection.
+var _blast_frames: int = 0
+
+func _init() -> void:
+ if not body_shape_entered.is_connected(_on_body_shape_entered):
+ body_shape_entered.connect(_on_body_shape_entered)
+
+# queue free area after processing few frames (enough for body detection)
+func _physics_process(_delta: float) -> void:
+ _blast_frames += 1
+ if _blast_frames >= 2:
+ queue_free()
+
+# overlapping bodies signal handler
+func _on_body_shape_entered(_body_rid: RID, body: Node, _body_shape_idx: int, local_shape_idx: int) -> void:
+ # retrieve area shape
+ var shape : SphereShape3D = shape_owner_get_shape(
+ shape_find_owner(local_shape_idx), local_shape_idx)
+ # retrieve vector from current node origin to body global center of mass
+ var direction: Vector3 = (body.global_transform.origin + body.center_of_mass) - global_transform.origin
+ # sample curve texture if any to get falloff value at current distance
+ var weight := 1.0
+ if falloff:
+ weight = falloff.sample(direction.length() / shape.radius)
+ # handle blast
+ if body is RigidBody3D:
+ var impulse: Vector3 = direction.normalized() * (1000 * weight)
+ # apply body impulse based on distance from explosion origin
+ body.apply_impulse(impulse, global_transform.origin)
+ # handle blast damages
+ if body is Player:
+ # deal damage based on distance from explosion origin
+ body.damage.emit(source, body, damage * weight)
+
diff --git a/entities/components/projectile_spawner.gd b/entities/projectiles/explosive_projectile.gd
similarity index 62%
rename from entities/components/projectile_spawner.gd
rename to entities/projectiles/explosive_projectile.gd
index 373e369..0d98ab2 100644
--- a/entities/components/projectile_spawner.gd
+++ b/entities/projectiles/explosive_projectile.gd
@@ -12,20 +12,13 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-class_name ProjectileSpawner extends Node3D
+class_name ExplosiveProjectile extends Projectile
-@export_category("Projectile")
-@export var PROJECTILE : PackedScene
-@export_range(0., 1., .01) var inheritance : float = .5 # ratio
+@export var EXPLOSION:PackedScene
-func new_projectile(
- projectile_type : Object,
- owner_velocity : Vector3,
- shooter : MatchParticipant
-) -> Node3D:
- return projectile_type.new_projectile(
- self,
- owner_velocity * inheritance,
- shooter,
- PROJECTILE
- )
+func destroy(location:Vector3 = global_position) -> void:
+ var explosion : Node3D = EXPLOSION.instantiate()
+ add_sibling(explosion)
+ explosion.global_position = location
+ explosion.source = source
+ queue_free()
diff --git a/entities/projectiles/projectile.gd b/entities/projectiles/projectile.gd
new file mode 100644
index 0000000..2be60af
--- /dev/null
+++ b/entities/projectiles/projectile.gd
@@ -0,0 +1,63 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+## This class defines a projectile.
+class_name Projectile extends Node3D
+
+# @NOTE: while it's the correct fundamental constant, we're really looking
+# for « terminal velocity » here instead because earth is not a vacuum
+const _speed_of_light := 299792458 # in m/s
+
+## The initial speed when launched, measured in meters per second (m/s or mps).
+@export_range(0, _speed_of_light) var speed : float = 42
+## The time before it automatically self-destructs (never by default), measured in seconds (s).
+@export var lifespan : float = 0.
+## A knockback impulse applied to bodies it collides with, in Newtons.
+@export var knockback: int
+## The base amount of damage.
+@export var damage: int
+## The [param source] that launched this projectile.
+var source: Node
+
+var deflectable:bool = false
+var inheritance:Vector3 = Vector3.ZERO
+var shape_cast : ShapeCast3D = null
+var velocity:Vector3 = Vector3.ZERO
+
+func _init() -> void:
+ # do not inherit transform from parent
+ top_level = true
+ # ensure projectile has shape
+ ready.connect(func() -> void:
+ for child in get_children():
+ if child is ShapeCast3D:
+ shape_cast = child
+ assert(shape_cast)
+ if lifespan > 0:
+ get_tree().create_timer(lifespan).timeout.connect(destroy)
+ )
+
+func destroy(_location: Vector3 = global_position) -> void:
+ queue_free()
+
+func _physics_process(delta : float) -> void:
+ # compute motion
+ var previous_global_position : Vector3 = global_position
+ # compute new position
+ global_position += velocity * delta
+ # handle collision
+ if shape_cast:
+ shape_cast.target_position = to_local(previous_global_position)
+ if shape_cast.is_colliding():
+ destroy(shape_cast.collision_result[0].point)
diff --git a/entities/weapons/automatic_weapon.gd b/entities/weapons/automatic_weapon.gd
new file mode 100644
index 0000000..47a153e
--- /dev/null
+++ b/entities/weapons/automatic_weapon.gd
@@ -0,0 +1,29 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+class_name AutomaticWeapon extends Weapon
+
+var _auto_trigger := Timer.new()
+
+func _init() -> void:
+ super._init()
+ _auto_trigger.timeout.connect(trigger)
+ add_child(_auto_trigger)
+
+func _on_primary(pressed: bool) -> void:
+ if pressed:
+ trigger()
+ _auto_trigger.start(cooldown)
+ else:
+ _auto_trigger.stop()
diff --git a/entities/weapons/chaingun/chaingun.gd b/entities/weapons/chaingun/chaingun.gd
index 80cd8b9..a2e0e6a 100644
--- a/entities/weapons/chaingun/chaingun.gd
+++ b/entities/weapons/chaingun/chaingun.gd
@@ -12,33 +12,44 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-class_name ChainGun extends Node3D
+class_name ChainGun extends AutomaticWeapon
-@export_range(0, 250) var ammo : int = 250
-@export var fire_rate : float = .1 # seconds
-@export var reload_time : float = 0. # seconds
+@export var _PROJECTILE : PackedScene
+## The factor of velocity from the owner of this [Weapon] inherited by this [Projectile].
+@export_range(0., 1., .01) var inheritance : float = 1.
-@export_category("Animations")
@export var anim_player : AnimationPlayer
+## The angular deviation from ideal line of fire in degrees
+@export var min_spread: float
+## The maximum angle the spread will increase to during heatup.
+@export var max_spread: float
+## The amount of time it takes for the chaingun to spin up or spin down.
+@export var spin_period: float
+## The amount of time it takes for the chaingun to overheat.
+@export var heat_period: float
+#@export var heat_time: float
+## The fraction of the [member heat_period] at which the gun can start firing again
+@export var cooldown_threshold: float
+## Multiplier for the speed when calculating cooldown/heatup
+@export var speed_cooldown_factor: float
+## The [Shader] responsible for rendering heat while firing.
+@export var heat_shader: Shader
+# heat flag
+var overheated: bool
-# Called when the node enters the scene tree for the first time.
-func _ready() -> void:
- pass # Replace with function body.
-
-func trigger() -> void:
+func _on_triggered() -> void:
# play the fire animation
anim_player.play("fire")
# init projectile
- var projectile : Node3D = $ProjectileSpawner.new_projectile(
- ChainGunProjectile,
- owner.linear_velocity,
- owner.match_participant
- )
- # add to owner of player for now
- Global.type.add_child(projectile)
+ var projectile : ChainGunProjectile = _PROJECTILE.instantiate()
+ projectile.velocity = projectile.speed * $Nozzle.global_basis.z.normalized() + owner.linear_velocity * inheritance
+ projectile.source = owner
+ owner.add_child(projectile)
+ projectile.global_transform = $Nozzle.global_transform
projectile.shape_cast.add_exception(owner)
func _on_visibility_changed() -> void:
if self.visible:
- anim_player.play("equip")
+ #anim_player.play("equip")
#self.sounds.play("equip")
+ pass
diff --git a/entities/weapons/chaingun/chaingun.tscn b/entities/weapons/chaingun/chaingun.tscn
index b2855f8..c72a167 100644
--- a/entities/weapons/chaingun/chaingun.tscn
+++ b/entities/weapons/chaingun/chaingun.tscn
@@ -1,19 +1,23 @@
-[gd_scene load_steps=5 format=3 uid="uid://b0xql5hi0b52y"]
+[gd_scene load_steps=4 format=3 uid="uid://b0xql5hi0b52y"]
[ext_resource type="PackedScene" uid="uid://cumv8wij5uufk" path="res://entities/weapons/chaingun/assets/chaingun.glb" id="1_d8tdv"]
[ext_resource type="Script" path="res://entities/weapons/chaingun/chaingun.gd" id="2_qsqeh"]
-[ext_resource type="Script" path="res://entities/components/projectile_spawner.gd" id="3_knyrc"]
[ext_resource type="PackedScene" uid="uid://b0sfwgilfbpx5" path="res://entities/weapons/chaingun/projectile.tscn" id="4_p63ts"]
[node name="ChainGun" node_paths=PackedStringArray("anim_player") instance=ExtResource("1_d8tdv")]
script = ExtResource("2_qsqeh")
+_PROJECTILE = ExtResource("4_p63ts")
anim_player = NodePath("AnimationPlayer")
-
-[node name="ProjectileSpawner" type="Marker3D" parent="." index="0"]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.124544, 0.476417)
-script = ExtResource("3_knyrc")
-PROJECTILE = ExtResource("4_p63ts")
-inheritance = 1.0
+min_spread = 1.0
+max_spread = 4.0
+spin_period = 0.5
+heat_period = 3.0
+cooldown_threshold = 1.7
+speed_cooldown_factor = 0.001
+ammo = 150
+max_ammo = 150
+ammo_usage = 1
+cooldown = 0.1
[node name="Skeleton3D" parent="Armature" index="0"]
bones/1/rotation = Quaternion(3.09086e-08, 0.707107, 0.707107, 3.09086e-08)
@@ -32,14 +36,18 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.692504, 1.30946)
[node name="Grip" parent="Armature/Skeleton3D" index="3"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.692504, 1.30946)
-[node name="AnimationPlayer" parent="." index="2"]
+[node name="AnimationPlayer" parent="." index="1"]
autoplay = "idle"
-[node name="Barrels" type="BoneAttachment3D" parent="." index="3"]
+[node name="Barrels" type="BoneAttachment3D" parent="." index="2"]
transform = Transform3D(-1, 0, 8.74227e-08, 8.74227e-08, 0, 1, 0, 1, 0, -2.32831e-10, 0.692504, 0.756307)
bone_name = "barrels"
bone_idx = 1
use_external_skeleton = true
external_skeleton = NodePath("../Armature/Skeleton3D")
+[node name="Nozzle" type="Marker3D" parent="." index="3"]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.98023e-08, 0.123522, 0.477262)
+
+[connection signal="triggered" from="." to="." method="_on_triggered"]
[connection signal="visibility_changed" from="." to="." method="_on_visibility_changed"]
diff --git a/entities/weapons/chaingun/projectile.gd b/entities/weapons/chaingun/projectile.gd
index 6782c67..c162a21 100644
--- a/entities/weapons/chaingun/projectile.gd
+++ b/entities/weapons/chaingun/projectile.gd
@@ -12,44 +12,19 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-class_name ChainGunProjectile extends Node3D
+class_name ChainGunProjectile extends Projectile
-@export var speed : float = 180. # m/s
-@export var lifespan : float = .5 # in seconds
-@export var build_up_time : float = .6 # seconds
-
-var shooter : MatchParticipant
-var velocity : Vector3 = Vector3.ZERO
-
-@onready var shape_cast : ShapeCast3D = $ShapeCast3D
-
-# Called when the node enters the scene tree for the first time.
-func _ready() -> void:
- $Timer.wait_time = lifespan
-
-func _on_timer_timeout() -> void:
- queue_free()
-
-func _physics_process(delta : float) -> void:
+func _physics_process(delta: float) -> void:
+ var previous_global_position: Vector3 = global_position
global_position += velocity * delta
- if shape_cast.is_colliding():
- for collision_id in shape_cast.get_collision_count():
- var collider : Object = shape_cast.get_collider(collision_id)
+ # handle collision
+ if shape_cast:
+ var local_global_position: Vector3 = to_local(previous_global_position)
+ shape_cast.target_position = Vector3(
+ local_global_position.x, -local_global_position.z, local_global_position.y)
+ shape_cast.target_position = shape_cast.target_position
+ if shape_cast.is_colliding():
+ var collider: Node = shape_cast.get_collider(0)
if collider is Player:
- if is_multiplayer_authority():
- assert(shooter)
- collider.health_component.damage.rpc(25, shooter.player_id, shooter.team_id)
- queue_free()
-
-## This method is a parameterized constructor for the [ChainGunProjectile] scene of the [ChainGun]
-static func new_projectile(
- origin : Marker3D,
- inherited_velocity : Vector3,
- _shooter : MatchParticipant,
- scene : PackedScene
-) -> ChainGunProjectile:
- var projectile : ChainGunProjectile = scene.instantiate()
- projectile.shooter = _shooter
- projectile.transform = origin.global_transform
- projectile.velocity = origin.global_basis.z.normalized() * projectile.speed + inherited_velocity
- return projectile
+ collider.damage.emit(source, collider, damage)
+ destroy(shape_cast.collision_result[0].point)
diff --git a/entities/weapons/chaingun/projectile.tscn b/entities/weapons/chaingun/projectile.tscn
index 7c891e4..10aa8a7 100644
--- a/entities/weapons/chaingun/projectile.tscn
+++ b/entities/weapons/chaingun/projectile.tscn
@@ -9,17 +9,15 @@ height = 2.5
[node name="ChainGunProjectile" instance=ExtResource("1_p7mmn")]
script = ExtResource("2_jjfwk")
+speed = 128.0
+lifespan = 3.0
+damage = 15
[node name="ShapeCast3D" type="ShapeCast3D" parent="." index="0"]
-transform = Transform3D(1, 0, 0, 0, -4.37114e-08, -1, 0, 1, -4.37114e-08, 0, 0, 4.29628)
+transform = Transform3D(1, 0, 0, 0, -4.37114e-08, -1, 0, 1, -4.37114e-08, 0, 0, 1.25)
shape = SubResource("CapsuleShape3D_dbodg")
-target_position = Vector3(0, -3, 0)
+target_position = Vector3(0, 0, 0)
collision_mask = 2147483653
[node name="tracerinner" parent="." index="1"]
-transform = Transform3D(0.0505494, 0, 0, 0, 0.0505494, 0, 0, 0, 0.0505494, 0, 0, 2.49628)
-
-[node name="Timer" type="Timer" parent="." index="2"]
-autostart = true
-
-[connection signal="timeout" from="Timer" to="." method="_on_timer_timeout"]
+transform = Transform3D(0.0505494, 0, 0, 0, 0.0505494, 0, 0, 0, 0.0505494, 0, 0, 2.5)
diff --git a/entities/weapons/grenade_launcher/assets/resources/explosion_material.tres b/entities/weapons/grenade_launcher/assets/resources/explosion_material.tres
new file mode 100644
index 0000000..45bcc5e
--- /dev/null
+++ b/entities/weapons/grenade_launcher/assets/resources/explosion_material.tres
@@ -0,0 +1,19 @@
+[gd_resource type="ShaderMaterial" load_steps=4 format=3 uid="uid://c80t026c2gpxx"]
+
+[ext_resource type="Shader" uid="uid://d2aeepgs6xnsg" path="res://entities/weapons/grenade_launcher/assets/resources/explosion_shader.tres" id="1_dmawi"]
+[ext_resource type="Texture2D" uid="uid://dt6c44uknktrg" path="res://entities/weapons/grenade_launcher/assets/textures/explosion_texture_alb.png" id="2_4up7w"]
+
+[sub_resource type="CompressedTexture2D" id="CompressedTexture2D_itmuc"]
+load_path = "res://.godot/imported/explosion_texture_flowmap.png-3137b307308713f172b3be796ef584b9.s3tc.ctex"
+
+[resource]
+render_priority = 0
+shader = ExtResource("1_dmawi")
+shader_parameter/albedo = Color(0.941176, 0.941176, 0.941176, 1)
+shader_parameter/particles_anim_h_frames = 5
+shader_parameter/particles_anim_v_frames = 5
+shader_parameter/particles_anim_loop = false
+shader_parameter/smoothing = true
+shader_parameter/flow_strength = 0.015
+shader_parameter/texture_albedo = ExtResource("2_4up7w")
+shader_parameter/texture_flow = SubResource("CompressedTexture2D_itmuc")
diff --git a/entities/weapons/grenade_launcher/assets/resources/explosion_shader.tres b/entities/weapons/grenade_launcher/assets/resources/explosion_shader.tres
new file mode 100644
index 0000000..572490d
--- /dev/null
+++ b/entities/weapons/grenade_launcher/assets/resources/explosion_shader.tres
@@ -0,0 +1,58 @@
+[gd_resource type="Shader" format=3 uid="uid://d2aeepgs6xnsg"]
+
+[resource]
+code = "shader_type spatial;
+render_mode unshaded;
+
+// @TODO: add support for `Keep scale` billboard option so that we can spawn
+// different scale using a curve in the particles process material
+
+uniform vec4 albedo : source_color;
+uniform sampler2D texture_albedo : source_color,filter_linear_mipmap,repeat_disable;
+uniform sampler2D texture_flow : hint_normal,filter_linear_mipmap,repeat_disable;
+uniform int particles_anim_h_frames;
+uniform int particles_anim_v_frames;
+uniform bool particles_anim_loop;
+uniform bool smoothing = false;
+uniform float flow_strength = 0.015;
+
+varying vec2 next_UV;
+varying float timer;
+
+void vertex() {
+ mat4 mat_world = mat4(normalize(INV_VIEW_MATRIX[0]), normalize(INV_VIEW_MATRIX[1]) ,normalize(INV_VIEW_MATRIX[2]), MODEL_MATRIX[3]);
+ mat_world = mat_world * mat4(vec4(cos(INSTANCE_CUSTOM.x), -sin(INSTANCE_CUSTOM.x), 0.0, 0.0), vec4(sin(INSTANCE_CUSTOM.x), cos(INSTANCE_CUSTOM.x), 0.0, 0.0), vec4(0.0, 0.0, 1.0, 0.0), vec4(0.0, 0.0, 0.0, 1.0));
+ MODELVIEW_MATRIX = VIEW_MATRIX * mat_world;
+ MODELVIEW_NORMAL_MATRIX = mat3(MODELVIEW_MATRIX);
+ float h_frames = float(particles_anim_h_frames);
+ float v_frames = float(particles_anim_v_frames);
+ float particle_total_frames = float(particles_anim_h_frames * particles_anim_v_frames);
+ float particle_frame = floor(INSTANCE_CUSTOM.z * float(particle_total_frames));
+ if (!particles_anim_loop) {
+ particle_frame = clamp(particle_frame, 0.0, particle_total_frames - 1.0);
+ } else {
+ particle_frame = mod(particle_frame, particle_total_frames);
+ }
+ UV /= vec2(h_frames, v_frames);
+ next_UV = UV;
+ UV += vec2(mod(particle_frame, h_frames) / h_frames, floor((particle_frame + 0.5) / h_frames) / v_frames);
+ next_UV += vec2(mod(particle_frame + 1.0, h_frames) / h_frames, floor((particle_frame + 1.0 + 0.5) / h_frames) / v_frames);
+ timer = fract(INSTANCE_CUSTOM.y * h_frames * v_frames);
+}
+
+void fragment() {
+ vec4 albedo_tex;
+ if (smoothing) {
+ vec2 flow_tex = texture(texture_flow, UV).rg;
+ flow_tex -= 0.5;
+ flow_tex *= 2.0;
+ vec2 flow_uv = UV + flow_tex * timer * -flow_strength;
+ vec2 reverse_flow_uv = next_UV + flow_tex * (1.0 - timer) * flow_strength;
+ albedo_tex = mix(texture(texture_albedo, flow_uv), texture(texture_albedo, reverse_flow_uv), timer);
+ } else {
+ albedo_tex = texture(texture_albedo, UV);
+ }
+ albedo_tex *= COLOR;
+ ALBEDO = albedo.rgb * albedo_tex.rgb;
+ ALPHA *= albedo.a * albedo_tex.a;
+}"
diff --git a/entities/weapons/grenade_launcher/assets/resources/explosion_texture_albedo.tres b/entities/weapons/grenade_launcher/assets/resources/explosion_texture_albedo.tres
new file mode 100644
index 0000000..72cf18f
--- /dev/null
+++ b/entities/weapons/grenade_launcher/assets/resources/explosion_texture_albedo.tres
@@ -0,0 +1,4 @@
+[gd_resource type="CompressedTexture2D" format=3 uid="uid://b0003clv8jv3n"]
+
+[resource]
+load_path = "res://.godot/imported/explosion_texture_alb.png-1ff7964ed65c51040c0956ec9ef6a893.s3tc.ctex"
diff --git a/entities/weapons/grenade_launcher/assets/resources/explosion_texture_flowmap.tres b/entities/weapons/grenade_launcher/assets/resources/explosion_texture_flowmap.tres
new file mode 100644
index 0000000..eb41676
--- /dev/null
+++ b/entities/weapons/grenade_launcher/assets/resources/explosion_texture_flowmap.tres
@@ -0,0 +1,4 @@
+[gd_resource type="CompressedTexture2D" format=3 uid="uid://ddlahr6k2hg5g"]
+
+[resource]
+load_path = "res://.godot/imported/explosion_texture_flowmap.png-3137b307308713f172b3be796ef584b9.s3tc.ctex"
diff --git a/entities/weapons/grenade_launcher/assets/resources/material_flare.tres b/entities/weapons/grenade_launcher/assets/resources/material_flare.tres
index 9af4613..68b4d2d 100644
--- a/entities/weapons/grenade_launcher/assets/resources/material_flare.tres
+++ b/entities/weapons/grenade_launcher/assets/resources/material_flare.tres
@@ -10,4 +10,3 @@ shading_mode = 0
vertex_color_use_as_albedo = true
albedo_color = Color(0.860369, 0.860369, 0.860369, 1)
albedo_texture = SubResource("CompressedTexture2D_uiuwk")
-billboard_keep_scale = true
diff --git a/entities/weapons/grenade_launcher/assets/resources/particle_process_material_fire.tres b/entities/weapons/grenade_launcher/assets/resources/particle_process_material_fire.tres
deleted file mode 100644
index 2a1ee2e..0000000
--- a/entities/weapons/grenade_launcher/assets/resources/particle_process_material_fire.tres
+++ /dev/null
@@ -1,27 +0,0 @@
-[gd_resource type="ParticleProcessMaterial" load_steps=5 format=3 uid="uid://c78xg30ynapmt"]
-
-[sub_resource type="Gradient" id="Gradient_q75yo"]
-offsets = PackedFloat32Array(0.00806452, 1)
-colors = PackedColorArray(1, 1, 1, 0, 1, 1, 1, 0)
-
-[sub_resource type="GradientTexture1D" id="GradientTexture1D_gt2l8"]
-gradient = SubResource("Gradient_q75yo")
-
-[sub_resource type="Curve" id="Curve_jt11q"]
-_data = [Vector2(0, 1), 0.0, 0.0, 0, 0, Vector2(0.980263, 0), 0.0, 0.0, 0, 0]
-point_count = 2
-
-[sub_resource type="CurveTexture" id="CurveTexture_nb67t"]
-curve = SubResource("Curve_jt11q")
-
-[resource]
-lifetime_randomness = 1.0
-emission_shape = 1
-emission_sphere_radius = 0.3
-spread = 180.0
-gravity = Vector3(0, -2, 0)
-scale_min = 0.75
-scale_max = 1.5
-scale_curve = SubResource("CurveTexture_nb67t")
-color = Color(5, 2, 1, 1)
-color_ramp = SubResource("GradientTexture1D_gt2l8")
diff --git a/entities/weapons/grenade_launcher/assets/resources/particle_process_material_sparks.tres b/entities/weapons/grenade_launcher/assets/resources/particle_process_material_sparks.tres
index 4c74b5d..ce79718 100644
--- a/entities/weapons/grenade_launcher/assets/resources/particle_process_material_sparks.tres
+++ b/entities/weapons/grenade_launcher/assets/resources/particle_process_material_sparks.tres
@@ -9,23 +9,28 @@ max_value = 5.0
_data = [Vector2(0, 5), 0.0, 0.0, 0, 0, Vector2(1, 0), 0.0, 0.0, 0, 0]
point_count = 2
-[sub_resource type="Curve" id="Curve_1s1qr"]
+[sub_resource type="Curve" id="Curve_26exw"]
_data = [Vector2(0, 1), 0.0, 0.0, 0, 0, Vector2(1, 1), 0.0, 0.0, 0, 0]
point_count = 2
[sub_resource type="CurveXYZTexture" id="CurveXYZTexture_eoauk"]
curve_x = SubResource("Curve_ei0ls")
curve_y = SubResource("Curve_q68ud")
-curve_z = SubResource("Curve_1s1qr")
+curve_z = SubResource("Curve_26exw")
[resource]
particle_flag_align_y = true
emission_shape = 1
emission_sphere_radius = 0.2
-spread = 180.0
-initial_velocity_min = 20.0
+direction = Vector3(0, 1, 0)
+spread = 90.0
+initial_velocity_min = 15.0
initial_velocity_max = 25.0
-gravity = Vector3(0, -19.6, 0)
+radial_velocity_min = 10.0
+radial_velocity_max = 10.0
+gravity = Vector3(0, 0, 0)
scale_min = 0.2
scale_curve = SubResource("CurveXYZTexture_eoauk")
color = Color(5, 2, 1, 1)
+turbulence_influence_min = 0.01
+turbulence_influence_max = 0.01
diff --git a/entities/weapons/grenade_launcher/assets/resources/projectile_shape.tres b/entities/weapons/grenade_launcher/assets/resources/projectile_shape.tres
new file mode 100644
index 0000000..6d3800f
--- /dev/null
+++ b/entities/weapons/grenade_launcher/assets/resources/projectile_shape.tres
@@ -0,0 +1,4 @@
+[gd_resource type="SphereShape3D" format=3 uid="uid://b8d4jwso2dcdu"]
+
+[resource]
+radius = 0.062
diff --git a/entities/weapons/grenade_launcher/assets/textures/Flare00.png.import b/entities/weapons/grenade_launcher/assets/textures/Flare00.png.import
index 05c5441..4a19dd8 100644
--- a/entities/weapons/grenade_launcher/assets/textures/Flare00.png.import
+++ b/entities/weapons/grenade_launcher/assets/textures/Flare00.png.import
@@ -4,15 +4,16 @@ importer="texture"
type="CompressedTexture2D"
uid="uid://chchhrpwmho2c"
path.s3tc="res://.godot/imported/Flare00.png-75515c1e8df86aba86a9e55bb8ca7901.s3tc.ctex"
+path.etc2="res://.godot/imported/Flare00.png-75515c1e8df86aba86a9e55bb8ca7901.etc2.ctex"
metadata={
-"imported_formats": ["s3tc_bptc"],
+"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
[deps]
source_file="res://entities/weapons/grenade_launcher/assets/textures/Flare00.png"
-dest_files=["res://.godot/imported/Flare00.png-75515c1e8df86aba86a9e55bb8ca7901.s3tc.ctex"]
+dest_files=["res://.godot/imported/Flare00.png-75515c1e8df86aba86a9e55bb8ca7901.s3tc.ctex", "res://.godot/imported/Flare00.png-75515c1e8df86aba86a9e55bb8ca7901.etc2.ctex"]
[params]
diff --git a/entities/weapons/grenade_launcher/assets/textures/explosion_texture_alb.png b/entities/weapons/grenade_launcher/assets/textures/explosion_texture_alb.png
new file mode 100644
index 0000000..bb88353
Binary files /dev/null and b/entities/weapons/grenade_launcher/assets/textures/explosion_texture_alb.png differ
diff --git a/entities/weapons/grenade_launcher/assets/textures/explosion_texture_alb.png.import b/entities/weapons/grenade_launcher/assets/textures/explosion_texture_alb.png.import
new file mode 100644
index 0000000..e7d03b5
--- /dev/null
+++ b/entities/weapons/grenade_launcher/assets/textures/explosion_texture_alb.png.import
@@ -0,0 +1,36 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dt6c44uknktrg"
+path.s3tc="res://.godot/imported/explosion_texture_alb.png-1ff7964ed65c51040c0956ec9ef6a893.s3tc.ctex"
+path.etc2="res://.godot/imported/explosion_texture_alb.png-1ff7964ed65c51040c0956ec9ef6a893.etc2.ctex"
+metadata={
+"imported_formats": ["s3tc_bptc", "etc2_astc"],
+"vram_texture": true
+}
+
+[deps]
+
+source_file="res://entities/weapons/grenade_launcher/assets/textures/explosion_texture_alb.png"
+dest_files=["res://.godot/imported/explosion_texture_alb.png-1ff7964ed65c51040c0956ec9ef6a893.s3tc.ctex", "res://.godot/imported/explosion_texture_alb.png-1ff7964ed65c51040c0956ec9ef6a893.etc2.ctex"]
+
+[params]
+
+compress/mode=2
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=true
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=true
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=0
diff --git a/entities/weapons/grenade_launcher/assets/textures/explosion_texture_flowmap.png b/entities/weapons/grenade_launcher/assets/textures/explosion_texture_flowmap.png
new file mode 100644
index 0000000..37e319a
Binary files /dev/null and b/entities/weapons/grenade_launcher/assets/textures/explosion_texture_flowmap.png differ
diff --git a/entities/weapons/grenade_launcher/assets/textures/explosion_texture_flowmap.png.import b/entities/weapons/grenade_launcher/assets/textures/explosion_texture_flowmap.png.import
new file mode 100644
index 0000000..f0c2e22
--- /dev/null
+++ b/entities/weapons/grenade_launcher/assets/textures/explosion_texture_flowmap.png.import
@@ -0,0 +1,36 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://b6pjaen40x34u"
+path.s3tc="res://.godot/imported/explosion_texture_flowmap.png-3137b307308713f172b3be796ef584b9.s3tc.ctex"
+path.etc2="res://.godot/imported/explosion_texture_flowmap.png-3137b307308713f172b3be796ef584b9.etc2.ctex"
+metadata={
+"imported_formats": ["s3tc_bptc", "etc2_astc"],
+"vram_texture": true
+}
+
+[deps]
+
+source_file="res://entities/weapons/grenade_launcher/assets/textures/explosion_texture_flowmap.png"
+dest_files=["res://.godot/imported/explosion_texture_flowmap.png-3137b307308713f172b3be796ef584b9.s3tc.ctex", "res://.godot/imported/explosion_texture_flowmap.png-3137b307308713f172b3be796ef584b9.etc2.ctex"]
+
+[params]
+
+compress/mode=2
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=1
+compress/channel_pack=0
+mipmaps/generate=true
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=0
diff --git a/entities/weapons/grenade_launcher/explosion.gd b/entities/weapons/grenade_launcher/explosion.gd
index 08a7233..e631c7c 100644
--- a/entities/weapons/grenade_launcher/explosion.gd
+++ b/entities/weapons/grenade_launcher/explosion.gd
@@ -1,13 +1,29 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
class_name GrenadeLauncherProjectileExplosion extends Node3D
## The component responsible for explosion damages
-@export var explosive_damage : ExplosiveDamageComponent
+@export var explosive_damage : ExplosiveDamage
-## The component responsible for owner identification
-var shooter : MatchParticipant:
- set(value):
- shooter = value
- explosive_damage.damage_dealer = value
+## The source of explosion.
+var source: Node:
+ set = set_source
+
+func set_source(new_source: Node) -> void:
+ source = new_source
+ explosive_damage.source = new_source
## This is increment by 1 when a particle emitter is finished
var _finished_count : int = 0
@@ -25,14 +41,3 @@ func _on_finished() -> void:
if _finished_count == $Particles.get_child_count():
queue_free()
-## This method is a static factory constructor for the
-## [GrenadeLauncherProjectileExplosion] scene of the [GrenadeLauncherProjectile].
-static func new_explosion(
- origin : Vector3,
- _shooter : MatchParticipant,
- scene : PackedScene
-) -> GrenadeLauncherProjectileExplosion:
- var explosion : GrenadeLauncherProjectileExplosion = scene.instantiate()
- explosion.position = origin
- explosion.shooter = _shooter
- return explosion
diff --git a/entities/weapons/grenade_launcher/explosion.tscn b/entities/weapons/grenade_launcher/explosion.tscn
index 74542b6..4ebb880 100644
--- a/entities/weapons/grenade_launcher/explosion.tscn
+++ b/entities/weapons/grenade_launcher/explosion.tscn
@@ -1,11 +1,37 @@
-[gd_scene load_steps=9 format=3 uid="uid://bb43ae1f5j8lw"]
+[gd_scene load_steps=13 format=3 uid="uid://bb43ae1f5j8lw"]
[ext_resource type="Script" path="res://entities/weapons/grenade_launcher/explosion.gd" id="1_xb4jo"]
-[ext_resource type="PackedScene" uid="uid://qb5sf7awdeui" path="res://entities/components/explosive_damage/explosive_damage.tscn" id="2_rq8du"]
+[ext_resource type="Script" path="res://entities/projectiles/damages/explosive_damage.gd" id="2_r5wb7"]
[ext_resource type="Material" uid="uid://v7s5w3uw4hmb" path="res://entities/weapons/grenade_launcher/assets/resources/particle_process_material_sparks.tres" id="3_4u6ue"]
+[ext_resource type="Material" uid="uid://c80t026c2gpxx" path="res://entities/weapons/grenade_launcher/assets/resources/explosion_material.tres" id="3_tsixm"]
[ext_resource type="Material" uid="uid://bbqjhgs44rnss" path="res://entities/weapons/grenade_launcher/assets/resources/material_flare.tres" id="4_634rq"]
[ext_resource type="Material" uid="uid://cykwajkj5aaqx" path="res://entities/weapons/grenade_launcher/assets/resources/particle_process_material_flash.tres" id="5_mntrp"]
-[ext_resource type="Material" uid="uid://c78xg30ynapmt" path="res://entities/weapons/grenade_launcher/assets/resources/particle_process_material_fire.tres" id="6_s25g3"]
+
+[sub_resource type="Curve" id="Curve_m5722"]
+_data = [Vector2(0.247387, 1), 0.0, 0.0, 0, 0, Vector2(1, 0), 0.0, 0.0, 0, 0]
+point_count = 2
+
+[sub_resource type="SphereShape3D" id="SphereShape3D_1htx7"]
+radius = 10.6
+
+[sub_resource type="ParticleProcessMaterial" id="ParticleProcessMaterial_dlm7y"]
+emission_shape_offset = Vector3(0, 0.25, 0)
+angle_min = -20.0
+angle_max = 20.0
+direction = Vector3(0, 1, 0)
+initial_velocity_min = 2.0
+initial_velocity_max = 2.0
+gravity = Vector3(0, 0, 0)
+damping_min = 1.0
+damping_max = 1.0
+color = Color(1.5, 1.5, 1.5, 1)
+hue_variation_max = 0.05
+anim_speed_min = 1.0
+anim_speed_max = 1.0
+
+[sub_resource type="QuadMesh" id="QuadMesh_35tvq"]
+material = ExtResource("3_tsixm")
+size = Vector2(5, 5)
[sub_resource type="QuadMesh" id="QuadMesh_wt5ts"]
material = ExtResource("4_634rq")
@@ -17,18 +43,39 @@ material = ExtResource("4_634rq")
script = ExtResource("1_xb4jo")
explosive_damage = NodePath("ExplosiveDamage")
-[node name="ExplosiveDamage" parent="." instance=ExtResource("2_rq8du")]
+[node name="ExplosiveDamage" type="Area3D" parent="."]
+collision_mask = 13
+script = ExtResource("2_r5wb7")
+damage = 128
+blast_force = 2000
+falloff = SubResource("Curve_m5722")
+
+[node name="CollisionShape3D" type="CollisionShape3D" parent="ExplosiveDamage"]
+shape = SubResource("SphereShape3D_1htx7")
[node name="Particles" type="Node3D" parent="."]
+[node name="Explosion" type="GPUParticles3D" parent="Particles"]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0)
+emitting = false
+amount = 1
+lifetime = 2.0
+one_shot = true
+speed_scale = 3.0
+fixed_fps = 60
+draw_order = 3
+process_material = SubResource("ParticleProcessMaterial_dlm7y")
+draw_pass_1 = SubResource("QuadMesh_35tvq")
+
[node name="Sparks" type="GPUParticles3D" parent="Particles"]
+process_priority = 1
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.00311279, 0.0152655, -0.000196457)
emitting = false
-amount = 20
+amount = 12
lifetime = 0.3
one_shot = true
explosiveness = 1.0
-fixed_fps = 60
+randomness = 0.3
process_material = ExtResource("3_4u6ue")
draw_pass_1 = SubResource("QuadMesh_wt5ts")
@@ -41,16 +88,3 @@ explosiveness = 1.0
fixed_fps = 60
process_material = ExtResource("5_mntrp")
draw_pass_1 = SubResource("QuadMesh_mv2qw")
-
-[node name="Fire" type="GPUParticles3D" parent="Particles"]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.00311279, 0.0152655, -0.000196457)
-emitting = false
-amount = 13
-lifetime = 0.55
-one_shot = true
-explosiveness = 1.0
-fixed_fps = 60
-process_material = ExtResource("6_s25g3")
-draw_pass_1 = SubResource("QuadMesh_wt5ts")
-
-[editable path="ExplosiveDamage"]
diff --git a/entities/weapons/grenade_launcher/grenade_launcher.gd b/entities/weapons/grenade_launcher/grenade_launcher.gd
index d1559da..186bcaa 100644
--- a/entities/weapons/grenade_launcher/grenade_launcher.gd
+++ b/entities/weapons/grenade_launcher/grenade_launcher.gd
@@ -1,29 +1,39 @@
-class_name GrenadeLauncher extends Node3D
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+class_name GrenadeLauncher extends Weapon
-@export_range(0, 24) var ammo : int = 24
-@export var fire_rate : float = 1. # seconds
-@export var reload_time : float = 1. # seconds
-
-@export_category("Animations")
+@export var _PROJECTILE : PackedScene
+## The factor of velocity from the owner of this [Weapon] inherited by this [Projectile].
+@export_range(0., 1., .01) var inheritance : float = 1.
@export var anim_player : AnimationPlayer
-# Called when the node enters the scene tree for the first time.
-func _ready() -> void:
- pass # Replace with function body.
-
-func trigger() -> void:
+func _on_triggered() -> void:
# play the fire animation
anim_player.play("fire")
# init projectile
- var projectile : Node3D = $ProjectileSpawner.new_projectile(
- GrenadeLauncherProjectile,
- owner.linear_velocity,
- owner.match_participant
- )
- # add to owner of player for now
- Global.type.add_child(projectile)
+ var projectile : GrenadeLauncherProjectile = _PROJECTILE.instantiate()
+ var direction: Vector3 = $Nozzle.global_basis.z.normalized()
+ projectile.velocity = direction * projectile.speed + owner.linear_velocity * inheritance
+ projectile.source = owner
+ owner.add_child(projectile)
+ projectile.global_position = $Nozzle.global_position
+ projectile.global_basis = $Nozzle.global_basis
+ projectile.shape_cast.add_exception(owner)
func _on_visibility_changed() -> void:
if self.visible:
- anim_player.play("equip")
+ #anim_player.play("equip")
#self.sounds.play("equip")
+ pass
diff --git a/entities/weapons/grenade_launcher/grenade_launcher.tscn b/entities/weapons/grenade_launcher/grenade_launcher.tscn
index 3623136..24e333b 100644
--- a/entities/weapons/grenade_launcher/grenade_launcher.tscn
+++ b/entities/weapons/grenade_launcher/grenade_launcher.tscn
@@ -1,19 +1,17 @@
-[gd_scene load_steps=5 format=3 uid="uid://cstl7yxc75572"]
+[gd_scene load_steps=4 format=3 uid="uid://cstl7yxc75572"]
[ext_resource type="PackedScene" uid="uid://bbp260t2qivxk" path="res://entities/weapons/grenade_launcher/assets/models/grenade_launcher.glb" id="1_keuur"]
[ext_resource type="Script" path="res://entities/weapons/grenade_launcher/grenade_launcher.gd" id="2_38xn3"]
[ext_resource type="PackedScene" uid="uid://dak767xehqa6x" path="res://entities/weapons/grenade_launcher/projectile.tscn" id="3_rg5nk"]
-[ext_resource type="Script" path="res://entities/components/projectile_spawner.gd" id="4_5h5sw"]
[node name="GrenadeLauncher" node_paths=PackedStringArray("anim_player") instance=ExtResource("1_keuur")]
script = ExtResource("2_38xn3")
+_PROJECTILE = ExtResource("3_rg5nk")
+inheritance = 0.8
anim_player = NodePath("AnimationPlayer")
-
-[node name="ProjectileSpawner" type="Node3D" parent="." index="0"]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0.266945)
-script = ExtResource("4_5h5sw")
-PROJECTILE = ExtResource("3_rg5nk")
-inheritance = 0.75
+ammo = 12
+max_ammo = 12
+ammo_usage = 1
[node name="barrel" parent="Armature/Skeleton3D" index="0"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.19209e-07, 0)
@@ -24,4 +22,8 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.082471, -0.0653242)
[node name="main" parent="Armature/Skeleton3D" index="2"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.19209e-07, 0)
+[node name="Nozzle" type="Marker3D" parent="." index="2"]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0.259886)
+
+[connection signal="triggered" from="." to="." method="_on_triggered"]
[connection signal="visibility_changed" from="." to="." method="_on_visibility_changed"]
diff --git a/entities/weapons/grenade_launcher/projectile.gd b/entities/weapons/grenade_launcher/projectile.gd
index f1699c9..3f9b528 100644
--- a/entities/weapons/grenade_launcher/projectile.gd
+++ b/entities/weapons/grenade_launcher/projectile.gd
@@ -12,46 +12,48 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-class_name GrenadeLauncherProjectile extends RigidBody3D
+class_name GrenadeLauncherProjectile extends ArcProjectile
-@export_category("Parameters")
-@export var EXPLOSION : PackedScene
-@export var collider : CollisionShape3D
-@export var speed : float = 44. # m/s
-@export var lifespan : float = 1.6 # in seconds
+## The time before
+@export var fuse_time : float = 1.
+@export_range(0., 1.) var bounce_velocity_modifier: float = .24
-var shooter : MatchParticipant
-var pending_explosion : bool = false
+var fuse_timer := Timer.new()
-## Called when the node enters the scene tree for the first time.
func _ready() -> void:
- $Timer.wait_time = lifespan
- $Timer.start()
+ fuse_timer.wait_time = fuse_time
+ fuse_timer.one_shot = true
+ fuse_timer.autostart = true
+ add_child(fuse_timer)
+ # remove exceptions after a short time so that it can collide again with owner
+ get_tree().create_timer(.3).timeout.connect(func() -> void:
+ shape_cast.clear_exceptions())
-## This method enable collision signals to be emitted when the lifespan timer
-## is finished.
-func _on_timer_timeout() -> void:
- max_contacts_reported = 1
- set_contact_monitor(true)
+func _physics_process(delta : float) -> void:
+ if not shape_cast: set_physics_process(false)
+ velocity += _gravity * delta
+ shape_cast.target_position = to_local(global_position + velocity * delta)
+ shape_cast.force_shapecast_update()
+ if shape_cast.is_colliding():
+ var collider: Node = shape_cast.get_collider(0)
+ var hit_point: Vector3 = shape_cast.get_collision_point(0)
+ # explode on player hit or when fuse timer is exhausted
+ if collider is Player or fuse_timer.is_stopped():
+ destroy(hit_point)
+ return
+ # bounce projectile
+ var hit_normal : Vector3 = shape_cast.get_collision_normal(0)
+ velocity = calc_bounce_velocity(velocity, hit_normal.normalized())
+ global_position = hit_point + hit_normal.normalized() * shape_cast.shape.radius
+ global_position += velocity * delta
+ else:
+ # move projectile
+ global_position += velocity * delta
-## This method is responsible for spawning the projectile explosion and freeing
-## it when a collision is detected once the lifespan timer is finished.
-func _on_body_entered(_body : Node) -> void:
- var explosion : GrenadeLauncherProjectileExplosion = \
- GrenadeLauncherProjectileExplosion.new_explosion(position, shooter, EXPLOSION)
- add_sibling(explosion)
- queue_free()
+## This method computes projectile bounce velocity.
+func calc_bounce_velocity(_velocity: Vector3, hit_normal: Vector3) -> Vector3:
+ return mirror_vector_by_normal(_velocity, hit_normal.normalized()) * bounce_velocity_modifier
-## This method is a static factory constructor for the [GrenadeLauncherProjectile]
-## scene of the [GrenadeLauncher].
-static func new_projectile(
- origin : Node3D,
- inherited_velocity : Vector3,
- _shooter : MatchParticipant,
- scene : PackedScene
-) -> GrenadeLauncherProjectile:
- var projectile : GrenadeLauncherProjectile = scene.instantiate()
- projectile.shooter = _shooter
- projectile.transform = origin.global_transform
- projectile.linear_velocity = origin.global_basis.z.normalized() * projectile.speed + inherited_velocity
- return projectile
+## This method computes reflected vector using the formula: R = V - 2 * (V dot N) * N
+func mirror_vector_by_normal(_velocity: Vector3, hit_normal_unit_vector: Vector3) -> Vector3:
+ return _velocity - 2 * _velocity.dot(hit_normal_unit_vector) * hit_normal_unit_vector
diff --git a/entities/weapons/grenade_launcher/projectile.tscn b/entities/weapons/grenade_launcher/projectile.tscn
index d0d6b2f..4f087bf 100644
--- a/entities/weapons/grenade_launcher/projectile.tscn
+++ b/entities/weapons/grenade_launcher/projectile.tscn
@@ -1,46 +1,30 @@
-[gd_scene load_steps=7 format=3 uid="uid://dak767xehqa6x"]
+[gd_scene load_steps=5 format=3 uid="uid://dak767xehqa6x"]
[ext_resource type="Script" path="res://entities/weapons/grenade_launcher/projectile.gd" id="1_i8v2u"]
[ext_resource type="PackedScene" uid="uid://bb43ae1f5j8lw" path="res://entities/weapons/grenade_launcher/explosion.tscn" id="2_cxty7"]
-[ext_resource type="Script" path="res://addons/smoothing/smoothing.gd" id="3_wyge0"]
-
-[sub_resource type="PhysicsMaterial" id="PhysicsMaterial_r263x"]
-rough = true
-bounce = 0.5
-
-[sub_resource type="SphereShape3D" id="SphereShape3D_kipwv"]
-radius = 0.062
+[ext_resource type="Shape3D" uid="uid://b8d4jwso2dcdu" path="res://entities/weapons/grenade_launcher/assets/resources/projectile_shape.tres" id="3_fxdq6"]
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_miu4v"]
albedo_color = Color(0.2, 0.2, 0.2, 1)
-[node name="GrenadeLauncherProjectile" type="RigidBody3D" node_paths=PackedStringArray("collider")]
-collision_layer = 2147483648
-collision_mask = 2147483648
-mass = 5.0
-physics_material_override = SubResource("PhysicsMaterial_r263x")
-continuous_cd = true
+[node name="GrenadeLauncherProjectile" type="Node3D"]
script = ExtResource("1_i8v2u")
EXPLOSION = ExtResource("2_cxty7")
-collider = NodePath("CollisionShape3D")
+speed = 54.0
-[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
-shape = SubResource("SphereShape3D_kipwv")
+[node name="ShapeCast3D" type="ShapeCast3D" parent="."]
+shape = ExtResource("3_fxdq6")
+target_position = Vector3(0, 0, 0)
+collision_mask = 2147483649
-[node name="Timer" type="Timer" parent="."]
-one_shot = true
-
-[node name="Smoothing" type="Node3D" parent="."]
+[node name="Head" type="CSGSphere3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.000792712, 0, -0.000432983)
-script = ExtResource("3_wyge0")
-flags = 3
-
-[node name="Mesh" type="CSGSphere3D" parent="Smoothing"]
radius = 0.062
radial_segments = 24
rings = 12
material = SubResource("StandardMaterial3D_miu4v")
-[connection signal="body_entered" from="." to="." method="_on_body_entered"]
-[connection signal="body_shape_entered" from="." to="." method="_on_body_shape_entered"]
-[connection signal="timeout" from="Timer" to="." method="_on_timer_timeout"]
+[node name="Body" type="CSGCylinder3D" parent="."]
+transform = Transform3D(0.125, 0, -2.64698e-23, 2.64698e-23, -2.71011e-09, 0.125, 0, -0.062, -5.46392e-09, 0, 0, -0.062)
+sides = 12
+material = SubResource("StandardMaterial3D_miu4v")
diff --git a/entities/weapons/space_gun/explosion.gd b/entities/weapons/space_gun/explosion.gd
new file mode 100644
index 0000000..08616f0
--- /dev/null
+++ b/entities/weapons/space_gun/explosion.gd
@@ -0,0 +1,33 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+class_name SpaceGunProjectileExplosion extends Node3D
+
+@export var explosive_damage : ExplosiveDamage
+
+var source: Node:
+ set = set_source
+
+func set_source(new_source: Node) -> void:
+ source = new_source
+ explosive_damage.source = new_source
+
+@onready var particles : GPUParticles3D = $Fire
+
+func _ready() -> void:
+ top_level = true
+ particles.emitting = true
+ particles.finished.connect(func() -> void: queue_free())
+ if source:
+ explosive_damage.source = source
diff --git a/entities/weapons/space_gun/explosion.tscn b/entities/weapons/space_gun/explosion.tscn
new file mode 100644
index 0000000..8e1e5f5
--- /dev/null
+++ b/entities/weapons/space_gun/explosion.tscn
@@ -0,0 +1,68 @@
+[gd_scene load_steps=12 format=3 uid="uid://00uv1dfxlv47"]
+
+[ext_resource type="Script" path="res://entities/weapons/space_gun/explosion.gd" id="1_3xpsk"]
+[ext_resource type="Script" path="res://entities/projectiles/damages/explosive_damage.gd" id="2_beh8d"]
+
+[sub_resource type="Curve" id="Curve_l54ao"]
+_data = [Vector2(0, 1), 0.0, 0.0, 0, 0, Vector2(1, 0), 0.0, 0.0, 0, 0]
+point_count = 2
+
+[sub_resource type="CurveTexture" id="CurveTexture_08dbu"]
+curve = SubResource("Curve_l54ao")
+
+[sub_resource type="Curve" id="Curve_21akj"]
+_data = [Vector2(0, 0), 0.0, 0.0, 0, 0, Vector2(1, 1), 0.0, 0.0, 0, 0]
+point_count = 2
+
+[sub_resource type="CurveTexture" id="CurveTexture_b4xy8"]
+curve = SubResource("Curve_21akj")
+
+[sub_resource type="ParticleProcessMaterial" id="ParticleProcessMaterial_i271t"]
+direction = Vector3(0, 1, 0)
+spread = 180.0
+initial_velocity_min = 24.0
+initial_velocity_max = 24.0
+scale_curve = SubResource("CurveTexture_08dbu")
+turbulence_enabled = true
+turbulence_influence_over_life = SubResource("CurveTexture_b4xy8")
+
+[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_wpu51"]
+albedo_color = Color(0.819608, 0, 0, 1)
+emission_enabled = true
+emission = Color(1, 0.254902, 0.109804, 1)
+emission_energy_multiplier = 4.0
+
+[sub_resource type="SphereMesh" id="SphereMesh_k7x87"]
+material = SubResource("StandardMaterial3D_wpu51")
+
+[sub_resource type="Curve" id="Curve_ck840"]
+_data = [Vector2(0.254355, 1), 0.0, 0.0, 0, 0, Vector2(1, 0), 0.0, 0.0, 0, 0]
+point_count = 2
+
+[sub_resource type="SphereShape3D" id="SphereShape3D_s611t"]
+radius = 7.2
+
+[node name="SpaceGunProjectileExplosion" type="Node3D" node_paths=PackedStringArray("explosive_damage")]
+top_level = true
+script = ExtResource("1_3xpsk")
+explosive_damage = NodePath("ExplosiveDamage")
+
+[node name="Fire" type="GPUParticles3D" parent="."]
+emitting = false
+amount = 24
+lifetime = 0.5
+one_shot = true
+explosiveness = 1.0
+fixed_fps = 60
+process_material = SubResource("ParticleProcessMaterial_i271t")
+draw_pass_1 = SubResource("SphereMesh_k7x87")
+
+[node name="ExplosiveDamage" type="Area3D" parent="."]
+collision_layer = 0
+collision_mask = 13
+script = ExtResource("2_beh8d")
+damage = 102
+falloff = SubResource("Curve_ck840")
+
+[node name="CollisionShape3D" type="CollisionShape3D" parent="ExplosiveDamage"]
+shape = SubResource("SphereShape3D_s611t")
diff --git a/entities/weapons/space_gun/projectile.gd b/entities/weapons/space_gun/projectile.gd
index 10854b7..d7e6575 100644
--- a/entities/weapons/space_gun/projectile.gd
+++ b/entities/weapons/space_gun/projectile.gd
@@ -12,63 +12,5 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-class_name SpaceGunProjectile extends Node3D
-
-@export_category("Parameters")
-@export var EXPLOSION : PackedScene
-@export var speed : float = 78.4 # m/s
-@export var lifespan : float = 5.0 # in seconds
-@export var _time_to_trail_enable : float = 0.05 # in seconds
-
-@onready var shape_cast : ShapeCast3D = $ShapeCast3D
-@onready var game : Node3D = get_tree().get_current_scene()
-@onready var projectile_trail : Trail3D = $Smoothing/ProjectileTrail
-
-var velocity : Vector3 = Vector3.ZERO
-var shooter : MatchParticipant
-
-func _ready() -> void:
- var lifespan_timer : Timer = Timer.new()
- lifespan_timer.wait_time = lifespan
- lifespan_timer.one_shot = true
- lifespan_timer.autostart = true
- lifespan_timer.timeout.connect(self_destruct)
- add_child(lifespan_timer)
- projectile_trail._trail_enabled = false
- var trail_enable_timer : Timer = Timer.new()
- trail_enable_timer.wait_time = _time_to_trail_enable
- trail_enable_timer.one_shot = true
- trail_enable_timer.autostart = true
- trail_enable_timer.timeout.connect(_enable_trail)
- add_child(trail_enable_timer)
-
-func self_destruct() -> void:
- explode(position)
-
-func explode(spawn_position : Vector3) -> void:
- game.add_child(SpaceGunProjectileExplosion.new_explosion(spawn_position, shooter, EXPLOSION))
- queue_free()
-
-func _physics_process(delta : float) -> void:
- var previous_position : Vector3 = global_position
- global_position += velocity * delta
- shape_cast.target_position = to_local(previous_position)
- if shape_cast.is_colliding():
- var contact_point : Vector3 = shape_cast.collision_result[0].point
- explode(contact_point)
-
-## This method is a parameterized constructor for the [SpaceGunProjectile] scene of the [SpaceGun]
-static func new_projectile(
- origin : Marker3D,
- inherited_velocity : Vector3,
- _shooter : MatchParticipant,
- scene : PackedScene
-) -> SpaceGunProjectile:
- var projectile : SpaceGunProjectile = scene.instantiate()
- projectile.shooter = _shooter
- projectile.transform = origin.global_transform
- projectile.velocity = origin.global_basis.z.normalized() * projectile.speed + inherited_velocity
- return projectile
-
-func _enable_trail() -> void:
- projectile_trail._trail_enabled = true
+## This class defines the projectile of a [SpaceGun].
+class_name SpaceGunProjectile extends ExplosiveProjectile
diff --git a/entities/weapons/space_gun/projectile.tscn b/entities/weapons/space_gun/projectile.tscn
index 4f93ffd..f2c32a5 100644
--- a/entities/weapons/space_gun/projectile.tscn
+++ b/entities/weapons/space_gun/projectile.tscn
@@ -1,53 +1,47 @@
-[gd_scene load_steps=8 format=3 uid="uid://dn1tcakam5egs"]
+[gd_scene load_steps=7 format=3 uid="uid://61mogxsei3rq"]
-[ext_resource type="Script" path="res://entities/weapons/space_gun/projectile.gd" id="1_4j1dp"]
-[ext_resource type="PackedScene" path="res://entities/weapons/space_gun/projectile_explosion.tscn" id="2_llml6"]
-[ext_resource type="Script" path="res://addons/smoothing/smoothing.gd" id="3_dmi64"]
-[ext_resource type="Script" path="res://entities/weapons/space_gun/projectile_trail.gd" id="3_ygqbh"]
-
-[sub_resource type="SphereShape3D" id="SphereShape3D_umtte"]
-radius = 0.15
+[ext_resource type="Script" path="res://entities/weapons/space_gun/projectile.gd" id="2_n6e2j"]
+[ext_resource type="PackedScene" uid="uid://00uv1dfxlv47" path="res://entities/weapons/space_gun/explosion.tscn" id="2_s8tpq"]
+[ext_resource type="Shape3D" uid="uid://dkhoeg2bwfbga" path="res://entities/weapons/space_gun/projectile_shape.tres" id="3_j3hyk"]
+[ext_resource type="Script" path="res://entities/weapons/space_gun/projectile_trail.gd" id="4_ojig2"]
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_o6j55"]
albedo_color = Color(0, 0.498039, 0.854902, 1)
emission_enabled = true
emission = Color(0.482353, 0.65098, 1, 1)
-[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_4a265"]
+[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_wqkw2"]
transparency = 1
blend_mode = 1
cull_mode = 2
shading_mode = 0
vertex_color_use_as_albedo = true
-[node name="Projectile" type="Node3D"]
-script = ExtResource("1_4j1dp")
-EXPLOSION = ExtResource("2_llml6")
+[node name="SpaceGunProjectile" type="Node3D"]
+top_level = true
+script = ExtResource("2_n6e2j")
+EXPLOSION = ExtResource("2_s8tpq")
+speed = 78.4
+lifespan = 6.0
[node name="ShapeCast3D" type="ShapeCast3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 0.2, 0, 0, 0, 1, 0, 0, 0)
-shape = SubResource("SphereShape3D_umtte")
+shape = ExtResource("3_j3hyk")
target_position = Vector3(0, 0, 0)
-margin = 0.1
-max_results = 1
collision_mask = 2147483649
+collide_with_areas = true
debug_shape_custom_color = Color(1, 0, 0, 1)
-[node name="Smoothing" type="Node3D" parent="."]
-script = ExtResource("3_dmi64")
-target = NodePath("..")
-
-[node name="Mesh" type="CSGSphere3D" parent="Smoothing"]
+[node name="Mesh" type="CSGSphere3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 0.2, 0, 0, 0, 1, 0, 0, 0)
radius = 0.15
radial_segments = 24
rings = 8
material = SubResource("StandardMaterial3D_o6j55")
-[node name="ProjectileTrail" type="MeshInstance3D" parent="Smoothing"]
-material_override = SubResource("StandardMaterial3D_4a265")
-skeleton = NodePath("../..")
-script = ExtResource("3_ygqbh")
+[node name="ProjectileTrail" type="MeshInstance3D" parent="."]
+material_override = SubResource("StandardMaterial3D_wqkw2")
+script = ExtResource("4_ojig2")
_start_width = 0.15
_lifespan = 0.25
_start_color = Color(0, 0.498039, 0.854902, 1)
diff --git a/entities/weapons/space_gun/projectile_explosion.gd b/entities/weapons/space_gun/projectile_explosion.gd
deleted file mode 100644
index ae76b68..0000000
--- a/entities/weapons/space_gun/projectile_explosion.gd
+++ /dev/null
@@ -1,38 +0,0 @@
-# This file is part of open-fpsz.
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-class_name SpaceGunProjectileExplosion extends Node3D
-
-@export var explosive_damage : ExplosiveDamageComponent
-
-@onready var fire : GPUParticles3D = $Fire
-
-var explosion_effect_pending : bool = false
-
-var shooter : MatchParticipant:
- set(value):
- shooter = value
- assert(explosive_damage)
- explosive_damage.damage_dealer = value
-
-func _ready() -> void:
- fire.emitting = true
- fire.finished.connect(func() -> void: queue_free())
-
-## This method is a parameterized constructor for the [SpaceGunProjectileExplosion] scene of the [SpaceGunProjectile]
-static func new_explosion(spawn_position : Vector3, _shooter : MatchParticipant, scene : PackedScene) -> SpaceGunProjectileExplosion:
- var explosion : SpaceGunProjectileExplosion = scene.instantiate()
- explosion.shooter = _shooter
- explosion.position = spawn_position
- return explosion
diff --git a/entities/weapons/space_gun/projectile_explosion.tscn b/entities/weapons/space_gun/projectile_explosion.tscn
deleted file mode 100644
index 60e481e..0000000
--- a/entities/weapons/space_gun/projectile_explosion.tscn
+++ /dev/null
@@ -1,44 +0,0 @@
-[gd_scene load_steps=8 format=3 uid="uid://8atq41j7wd55"]
-
-[ext_resource type="Script" path="res://entities/weapons/space_gun/projectile_explosion.gd" id="1_fp5td"]
-[ext_resource type="PackedScene" uid="uid://qb5sf7awdeui" path="res://entities/components/explosive_damage/explosive_damage.tscn" id="2_js0ht"]
-
-[sub_resource type="Curve" id="Curve_rg204"]
-_data = [Vector2(0, 1), 0.0, 0.0, 0, 0, Vector2(1, 0), 0.0, 0.0, 0, 0]
-point_count = 2
-
-[sub_resource type="CurveTexture" id="CurveTexture_08dbu"]
-curve = SubResource("Curve_rg204")
-
-[sub_resource type="ParticleProcessMaterial" id="ParticleProcessMaterial_3mf41"]
-spread = 180.0
-initial_velocity_min = 12.0
-initial_velocity_max = 12.0
-scale_curve = SubResource("CurveTexture_08dbu")
-
-[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_wpu51"]
-albedo_color = Color(0.819608, 0, 0, 1)
-emission_enabled = true
-emission = Color(1, 0.254902, 0.109804, 1)
-emission_energy_multiplier = 4.0
-
-[sub_resource type="SphereMesh" id="SphereMesh_k3pnh"]
-material = SubResource("StandardMaterial3D_wpu51")
-
-[node name="ProjectileExplosion" type="Node3D" node_paths=PackedStringArray("explosive_damage")]
-script = ExtResource("1_fp5td")
-explosive_damage = NodePath("ExplosiveDamage")
-
-[node name="Fire" type="GPUParticles3D" parent="."]
-emitting = false
-amount = 24
-lifetime = 0.5
-one_shot = true
-explosiveness = 1.0
-fixed_fps = 60
-process_material = SubResource("ParticleProcessMaterial_3mf41")
-draw_pass_1 = SubResource("SphereMesh_k3pnh")
-
-[node name="ExplosiveDamage" parent="." instance=ExtResource("2_js0ht")]
-
-[editable path="ExplosiveDamage"]
diff --git a/entities/weapons/space_gun/projectile_shape.tres b/entities/weapons/space_gun/projectile_shape.tres
new file mode 100644
index 0000000..ed604ea
--- /dev/null
+++ b/entities/weapons/space_gun/projectile_shape.tres
@@ -0,0 +1,4 @@
+[gd_resource type="SphereShape3D" format=3 uid="uid://dkhoeg2bwfbga"]
+
+[resource]
+radius = 0.15
diff --git a/entities/weapons/space_gun/space_gun.gd b/entities/weapons/space_gun/space_gun.gd
index 24a5a55..003ba95 100644
--- a/entities/weapons/space_gun/space_gun.gd
+++ b/entities/weapons/space_gun/space_gun.gd
@@ -12,40 +12,26 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-class_name SpaceGun extends Node3D
+## This class defines a SpaceGun.
+class_name SpaceGun extends Weapon
-@export var fire_rate : float = 1. # seconds
-@export var reload_time : float = 1. # seconds
+@export var _PROJECTILE : PackedScene
+## The factor of velocity from the owner of this [Weapon] inherited by this [Projectile].
+@export_range(0., 1., .01) var inheritance : float = 1.
@export var anim_player : AnimationPlayer
-enum WeaponState { WEAPON_READY, WEAPON_RELOADING }
-var state : WeaponState = WeaponState.WEAPON_READY
-
-func can_fire() -> bool:
- return state == WeaponState.WEAPON_READY
-
-func trigger() -> void:
- # check permission
- if not can_fire():
- return
+func _on_triggered() -> void:
# play the fire animation
anim_player.play("fire")
- var projectile : Node3D = $ProjectileSpawner.new_projectile(
- SpaceGunProjectile,
- owner.linear_velocity,
- owner.match_participant
- )
- # add to owner of player for now
- Global.type.add_child(projectile)
- # ensure projectile does not collide with owner
- var collider : ShapeCast3D = projectile.shape_cast
- collider.add_exception(owner)
- # update states
- state = WeaponState.WEAPON_RELOADING
- await get_tree().create_timer(reload_time).timeout
- state = WeaponState.WEAPON_READY
+ var projectile : SpaceGunProjectile = _PROJECTILE.instantiate()
+ projectile.velocity = projectile.speed * $Nozzle.global_basis.z.normalized() + owner.linear_velocity * inheritance
+ projectile.source = owner
+ owner.add_child(projectile)
+ projectile.global_transform = $Nozzle.global_transform
+ projectile.shape_cast.add_exception(owner)
func _on_visibility_changed() -> void:
if self.visible:
- anim_player.play("equip")
- #self.sounds.play("equip")
+ #anim_player.play("equip")
+ #sounds.play("equip")
+ pass
diff --git a/entities/weapons/space_gun/space_gun.tscn b/entities/weapons/space_gun/space_gun.tscn
index b25d081..5c24aae 100644
--- a/entities/weapons/space_gun/space_gun.tscn
+++ b/entities/weapons/space_gun/space_gun.tscn
@@ -1,22 +1,65 @@
-[gd_scene load_steps=5 format=3 uid="uid://c8co0qa2omjmh"]
+[gd_scene load_steps=7 format=3 uid="uid://c8co0qa2omjmh"]
[ext_resource type="Script" path="res://entities/weapons/space_gun/space_gun.gd" id="1_6sm4s"]
-[ext_resource type="PackedScene" uid="uid://dn1tcakam5egs" path="res://entities/weapons/space_gun/projectile.tscn" id="2_wvneg"]
+[ext_resource type="PackedScene" uid="uid://61mogxsei3rq" path="res://entities/weapons/space_gun/projectile.tscn" id="2_knhfa"]
[ext_resource type="PackedScene" uid="uid://bjcn37ops3bro" path="res://entities/weapons/space_gun/assets/disclauncher.glb" id="3_5k2xm"]
-[ext_resource type="Script" path="res://entities/components/projectile_spawner.gd" id="3_ihk6g"]
+
+[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_4b7xv"]
+resource_name = "mainmat"
+cull_mode = 2
+albedo_color = Color(0.321236, 0.321236, 0.321236, 1)
+roughness = 0.5
+
+[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_mfe6r"]
+resource_name = "secmat"
+cull_mode = 2
+albedo_color = Color(0.569947, 0.569947, 0.569947, 1)
+roughness = 0.5
+
+[sub_resource type="ArrayMesh" id="ArrayMesh_jl80g"]
+resource_name = "disclauncher_Cube_001"
+_surfaces = [{
+"aabb": AABB(-0.288198, -0.287635, -1.43225, 0.576397, 0.691169, 2.39171),
+"attribute_data": PackedByteArray(0, 0, 192, 62, 0, 0, 0, 0, 0, 0, 0, 62, 0, 0, 128, 62, 0, 0, 192, 62, 0, 0, 128, 63, 0, 0, 32, 63, 0, 0, 0, 0, 0, 0, 96, 63, 0, 0, 128, 62, 0, 0, 32, 63, 0, 0, 128, 63, 0, 0, 192, 62, 0, 0, 64, 63, 0, 0, 0, 62, 0, 0, 0, 63, 0, 0, 192, 62, 0, 0, 64, 63, 0, 0, 32, 63, 0, 0, 64, 63, 0, 0, 96, 63, 0, 0, 0, 63, 0, 0, 32, 63, 0, 0, 64, 63, 0, 0, 192, 62, 0, 0, 128, 62, 0, 0, 192, 62, 0, 0, 128, 62, 0, 0, 192, 62, 0, 0, 128, 62, 0, 0, 32, 63, 0, 0, 128, 62, 0, 0, 32, 63, 0, 0, 128, 62, 0, 0, 32, 63, 0, 0, 128, 62, 0, 0, 192, 62, 0, 0, 0, 63, 0, 0, 192, 62, 0, 0, 0, 63, 0, 0, 192, 62, 0, 0, 0, 63, 0, 0, 32, 63, 0, 0, 0, 63, 0, 0, 32, 63, 0, 0, 0, 63, 0, 0, 32, 63, 0, 0, 0, 63, 0, 0, 0, 62, 60, 188, 199, 62, 0, 0, 0, 62, 60, 188, 199, 62, 0, 0, 192, 62, 227, 33, 92, 63, 0, 0, 192, 62, 227, 33, 92, 63, 0, 0, 96, 63, 58, 188, 199, 62, 0, 0, 96, 63, 58, 188, 199, 62, 0, 0, 32, 63, 227, 33, 92, 63, 0, 0, 32, 63, 227, 33, 92, 63, 0, 0, 192, 62, 60, 188, 199, 62, 0, 0, 192, 62, 60, 188, 199, 62, 0, 0, 192, 62, 60, 188, 199, 62, 0, 0, 192, 62, 60, 188, 199, 62, 0, 0, 32, 63, 60, 188, 199, 62, 0, 0, 32, 63, 60, 188, 199, 62, 0, 0, 32, 63, 60, 188, 199, 62, 0, 0, 32, 63, 60, 188, 199, 62, 170, 170, 10, 63, 0, 0, 64, 63, 170, 170, 10, 63, 0, 0, 64, 63, 170, 170, 10, 63, 0, 0, 64, 63, 170, 170, 10, 63, 0, 0, 64, 63, 170, 170, 234, 62, 0, 0, 64, 63, 170, 170, 234, 62, 0, 0, 64, 63, 170, 170, 234, 62, 0, 0, 64, 63, 170, 170, 234, 62, 0, 0, 64, 63, 170, 170, 10, 63, 0, 0, 0, 63, 170, 170, 10, 63, 0, 0, 0, 63, 170, 170, 10, 63, 0, 0, 0, 63, 170, 170, 10, 63, 0, 0, 0, 63, 170, 170, 234, 62, 0, 0, 0, 63, 170, 170, 234, 62, 0, 0, 0, 63, 170, 170, 234, 62, 0, 0, 0, 63, 170, 170, 234, 62, 0, 0, 0, 63, 170, 170, 10, 63, 60, 188, 199, 62, 170, 170, 10, 63, 60, 188, 199, 62, 170, 170, 10, 63, 60, 188, 199, 62, 170, 170, 10, 63, 60, 188, 199, 62, 170, 170, 234, 62, 60, 188, 199, 62, 170, 170, 234, 62, 60, 188, 199, 62, 170, 170, 234, 62, 60, 188, 199, 62, 170, 170, 234, 62, 60, 188, 199, 62, 170, 170, 10, 63, 227, 33, 92, 63, 170, 170, 10, 63, 227, 33, 92, 63, 170, 170, 10, 63, 227, 33, 92, 63, 170, 170, 10, 63, 227, 33, 92, 63, 170, 170, 234, 62, 227, 33, 92, 63, 170, 170, 234, 62, 227, 33, 92, 63, 170, 170, 234, 62, 227, 33, 92, 63, 170, 170, 234, 62, 227, 33, 92, 63, 0, 0, 96, 63, 28, 222, 227, 62, 0, 0, 96, 63, 28, 222, 227, 62, 0, 0, 32, 63, 241, 16, 78, 63, 0, 0, 32, 63, 241, 16, 78, 63, 0, 0, 192, 62, 30, 222, 227, 62, 0, 0, 192, 62, 30, 222, 227, 62, 0, 0, 192, 62, 30, 222, 227, 62, 0, 0, 0, 62, 30, 222, 227, 62, 0, 0, 192, 62, 242, 16, 78, 63, 0, 0, 192, 62, 242, 16, 78, 63, 0, 0, 32, 63, 30, 222, 227, 62, 0, 0, 32, 63, 30, 222, 227, 62, 0, 0, 32, 63, 30, 222, 227, 62, 0, 0, 32, 63, 30, 222, 227, 62, 170, 170, 234, 62, 30, 222, 227, 62, 170, 170, 234, 62, 30, 222, 227, 62, 170, 170, 234, 62, 30, 222, 227, 62, 170, 170, 234, 62, 30, 222, 227, 62, 170, 170, 10, 63, 30, 222, 227, 62, 170, 170, 10, 63, 30, 222, 227, 62, 170, 170, 10, 63, 30, 222, 227, 62, 170, 170, 10, 63, 30, 222, 227, 62, 170, 170, 234, 62, 242, 16, 78, 63, 170, 170, 234, 62, 242, 16, 78, 63, 170, 170, 234, 62, 242, 16, 78, 63, 170, 170, 234, 62, 242, 16, 78, 63, 170, 170, 10, 63, 242, 16, 78, 63, 170, 170, 10, 63, 242, 16, 78, 63, 170, 170, 10, 63, 242, 16, 78, 63, 170, 170, 10, 63, 242, 16, 78, 63, 0, 0, 0, 62, 44, 205, 213, 62, 0, 0, 0, 62, 44, 205, 213, 62, 0, 0, 192, 62, 106, 25, 85, 63, 0, 0, 192, 62, 106, 25, 85, 63, 171, 170, 234, 62, 42, 205, 213, 62, 171, 170, 234, 62, 42, 205, 213, 62, 171, 170, 234, 62, 42, 205, 213, 62, 171, 170, 234, 62, 42, 205, 213, 62, 170, 170, 10, 63, 44, 205, 213, 62, 170, 170, 10, 63, 44, 205, 213, 62, 0, 0, 192, 62, 44, 205, 213, 62, 0, 0, 192, 62, 44, 205, 213, 62, 0, 0, 192, 62, 44, 205, 213, 62, 0, 0, 192, 62, 44, 205, 213, 62, 171, 170, 234, 62, 106, 25, 85, 63, 171, 170, 234, 62, 106, 25, 85, 63, 171, 170, 234, 62, 106, 25, 85, 63, 171, 170, 234, 62, 106, 25, 85, 63, 170, 170, 10, 63, 106, 25, 85, 63, 170, 170, 10, 63, 106, 25, 85, 63, 0, 0, 96, 63, 42, 194, 225, 62, 0, 0, 32, 63, 234, 30, 79, 63, 0, 0, 192, 62, 44, 194, 225, 62, 0, 0, 192, 62, 44, 194, 225, 62, 0, 0, 192, 62, 44, 194, 225, 62, 172, 170, 234, 62, 234, 30, 79, 63, 172, 170, 234, 62, 234, 30, 79, 63, 172, 170, 234, 62, 234, 30, 79, 63, 172, 170, 234, 62, 234, 30, 79, 63, 170, 170, 10, 63, 234, 30, 79, 63, 170, 170, 10, 63, 234, 30, 79, 63, 170, 170, 10, 63, 234, 30, 79, 63, 0, 0, 0, 62, 44, 194, 225, 62, 0, 0, 192, 62, 234, 30, 79, 63, 0, 0, 192, 62, 234, 30, 79, 63, 1, 0, 32, 63, 44, 194, 225, 62, 1, 0, 32, 63, 44, 194, 225, 62, 172, 170, 234, 62, 42, 194, 225, 62, 172, 170, 234, 62, 42, 194, 225, 62, 172, 170, 234, 62, 42, 194, 225, 62, 172, 170, 234, 62, 42, 194, 225, 62, 170, 170, 10, 63, 44, 194, 225, 62, 170, 170, 10, 63, 44, 194, 225, 62, 170, 170, 10, 63, 44, 194, 225, 62, 0, 0, 0, 62, 56, 247, 204, 62, 0, 0, 0, 62, 56, 247, 204, 62, 0, 0, 192, 62, 100, 132, 89, 63, 0, 0, 192, 62, 100, 132, 89, 63, 0, 0, 32, 63, 56, 247, 204, 62, 0, 0, 32, 63, 56, 247, 204, 62, 171, 170, 234, 62, 56, 247, 204, 62, 171, 170, 234, 62, 56, 247, 204, 62, 171, 170, 234, 62, 56, 247, 204, 62, 171, 170, 234, 62, 56, 247, 204, 62, 170, 170, 10, 63, 56, 247, 204, 62, 170, 170, 10, 63, 56, 247, 204, 62, 170, 170, 10, 63, 56, 247, 204, 62, 1, 0, 96, 63, 54, 247, 204, 62, 0, 0, 32, 63, 101, 132, 89, 63, 0, 0, 192, 62, 56, 247, 204, 62, 0, 0, 192, 62, 56, 247, 204, 62, 0, 0, 192, 62, 56, 247, 204, 62, 0, 0, 192, 62, 56, 247, 204, 62, 170, 170, 234, 62, 100, 132, 89, 63, 170, 170, 234, 62, 100, 132, 89, 63, 170, 170, 234, 62, 100, 132, 89, 63, 170, 170, 234, 62, 100, 132, 89, 63, 170, 170, 10, 63, 101, 132, 89, 63, 170, 170, 10, 63, 101, 132, 89, 63, 170, 170, 10, 63, 101, 132, 89, 63, 170, 170, 234, 62, 0, 0, 0, 0, 170, 170, 234, 62, 0, 0, 0, 0, 170, 170, 234, 62, 0, 0, 128, 63, 170, 170, 10, 63, 0, 0, 0, 0, 170, 170, 10, 63, 0, 0, 0, 0, 170, 170, 10, 63, 0, 0, 128, 63, 170, 170, 10, 63, 0, 0, 128, 62, 170, 170, 10, 63, 0, 0, 128, 62, 170, 170, 10, 63, 0, 0, 128, 62, 170, 170, 234, 62, 0, 0, 128, 62, 170, 170, 234, 62, 0, 0, 128, 62, 170, 170, 234, 62, 0, 0, 128, 62, 0, 0, 192, 62, 176, 170, 170, 61, 85, 85, 85, 62, 0, 0, 128, 62, 0, 0, 192, 62, 176, 170, 170, 61, 0, 0, 192, 62, 172, 170, 42, 62, 85, 85, 149, 62, 0, 0, 128, 62, 0, 0, 192, 62, 172, 170, 42, 62, 170, 170, 234, 62, 172, 170, 42, 62, 170, 170, 234, 62, 172, 170, 42, 62, 170, 170, 234, 62, 172, 170, 42, 62, 170, 170, 234, 62, 176, 170, 170, 61, 170, 170, 234, 62, 176, 170, 170, 61, 170, 170, 234, 62, 176, 170, 170, 61, 170, 170, 234, 62, 76, 11, 52, 63, 170, 170, 234, 62, 181, 244, 11, 63, 0, 0, 32, 63, 172, 170, 42, 62, 85, 85, 53, 63, 0, 0, 128, 62, 0, 0, 32, 63, 172, 170, 42, 62, 0, 0, 32, 63, 176, 170, 170, 61, 170, 170, 74, 63, 0, 0, 128, 62, 0, 0, 32, 63, 176, 170, 170, 61, 170, 170, 10, 63, 168, 170, 42, 62, 170, 170, 10, 63, 168, 170, 42, 62, 170, 170, 10, 63, 168, 170, 42, 62, 170, 170, 10, 63, 176, 170, 170, 61, 170, 170, 10, 63, 176, 170, 170, 61, 170, 170, 10, 63, 176, 170, 170, 61, 170, 170, 10, 63, 76, 11, 52, 63, 170, 170, 10, 63, 181, 244, 11, 63, 0, 0, 192, 62, 64, 142, 227, 61, 28, 199, 113, 62, 0, 0, 128, 62, 0, 0, 192, 62, 64, 142, 227, 61, 0, 0, 192, 62, 232, 56, 14, 62, 114, 28, 135, 62, 0, 0, 128, 62, 0, 0, 192, 62, 232, 56, 14, 62, 170, 170, 234, 62, 0, 72, 36, 62, 170, 170, 234, 62, 0, 72, 36, 62, 170, 170, 234, 62, 16, 112, 183, 61, 170, 170, 234, 62, 16, 112, 183, 61, 0, 0, 32, 63, 232, 56, 14, 62, 198, 113, 60, 63, 0, 0, 128, 62, 0, 0, 32, 63, 232, 56, 14, 62, 0, 0, 32, 63, 64, 142, 227, 61, 56, 142, 67, 63, 0, 0, 128, 62, 0, 0, 32, 63, 64, 142, 227, 61, 170, 170, 10, 63, 72, 142, 32, 62, 170, 170, 10, 63, 72, 142, 32, 62, 170, 170, 10, 63, 112, 227, 190, 61, 170, 170, 10, 63, 112, 227, 190, 61, 0, 0, 0, 62, 12, 172, 162, 62, 0, 0, 0, 62, 12, 172, 162, 62, 0, 0, 192, 62, 251, 169, 110, 63, 0, 0, 192, 62, 251, 169, 110, 63, 0, 0, 0, 62, 164, 194, 138, 62, 0, 0, 0, 62, 164, 194, 138, 62, 0, 0, 192, 62, 175, 158, 122, 63, 0, 0, 192, 62, 175, 158, 122, 63, 1, 0, 192, 62, 162, 194, 138, 62, 1, 0, 192, 62, 162, 194, 138, 62, 1, 0, 192, 62, 162, 194, 138, 62, 1, 0, 192, 62, 162, 194, 138, 62, 0, 0, 192, 62, 12, 172, 162, 62, 0, 0, 192, 62, 12, 172, 162, 62, 0, 0, 192, 62, 12, 172, 162, 62, 0, 0, 192, 62, 12, 172, 162, 62, 170, 170, 234, 62, 88, 20, 4, 62, 170, 170, 234, 62, 175, 158, 122, 63, 170, 170, 234, 62, 175, 158, 122, 63, 170, 170, 234, 62, 112, 203, 212, 62, 170, 170, 234, 62, 250, 169, 110, 63, 170, 170, 234, 62, 250, 169, 110, 63, 170, 170, 234, 62, 12, 172, 162, 62, 170, 170, 234, 62, 12, 172, 162, 62, 170, 170, 234, 62, 12, 172, 162, 62, 170, 170, 234, 62, 164, 194, 138, 62, 170, 170, 234, 62, 164, 194, 138, 62, 170, 170, 234, 62, 164, 194, 138, 62, 170, 170, 234, 62, 88, 143, 52, 62, 169, 170, 234, 62, 80, 22, 196, 62, 170, 170, 234, 62, 80, 10, 101, 62, 168, 170, 234, 62, 46, 97, 179, 62, 0, 0, 0, 62, 128, 123, 161, 62, 255, 255, 191, 62, 65, 66, 111, 63, 0, 0, 0, 62, 220, 217, 139, 62, 0, 0, 192, 62, 19, 19, 122, 63, 0, 0, 192, 62, 220, 217, 139, 62, 0, 0, 192, 62, 220, 217, 139, 62, 0, 0, 192, 62, 128, 123, 161, 62, 0, 0, 192, 62, 128, 123, 161, 62, 170, 170, 234, 62, 180, 119, 17, 62, 170, 170, 234, 62, 19, 19, 122, 63, 170, 170, 234, 62, 82, 126, 205, 62, 170, 170, 234, 62, 64, 66, 111, 63, 170, 170, 234, 62, 128, 123, 161, 62, 170, 170, 234, 62, 128, 123, 161, 62, 170, 170, 234, 62, 220, 217, 139, 62, 170, 170, 234, 62, 220, 217, 139, 62, 170, 170, 234, 62, 104, 54, 62, 62, 169, 170, 234, 62, 184, 210, 190, 62, 170, 170, 234, 62, 16, 245, 106, 62, 168, 170, 234, 62, 28, 39, 176, 62, 0, 0, 192, 62, 0, 0, 0, 63, 0, 0, 96, 63, 58, 188, 199, 62, 0, 0, 192, 62, 60, 188, 199, 62, 0, 0, 192, 62, 60, 188, 199, 62, 0, 0, 32, 63, 60, 188, 199, 62, 0, 0, 32, 63, 60, 188, 199, 62, 170, 170, 234, 62, 0, 0, 64, 63, 170, 170, 234, 62, 0, 0, 0, 63, 170, 170, 234, 62, 0, 0, 0, 63, 170, 170, 10, 63, 60, 188, 199, 62, 170, 170, 234, 62, 60, 188, 199, 62, 170, 170, 234, 62, 227, 33, 92, 63, 0, 0, 96, 63, 28, 222, 227, 62, 0, 0, 32, 63, 241, 16, 78, 63, 0, 0, 192, 62, 30, 222, 227, 62, 0, 0, 32, 63, 30, 222, 227, 62, 170, 170, 234, 62, 30, 222, 227, 62, 170, 170, 10, 63, 30, 222, 227, 62, 170, 170, 10, 63, 30, 222, 227, 62, 170, 170, 234, 62, 242, 16, 78, 63, 170, 170, 10, 63, 242, 16, 78, 63, 170, 170, 10, 63, 234, 30, 79, 63, 1, 0, 32, 63, 44, 194, 225, 62, 170, 170, 10, 63, 44, 194, 225, 62, 0, 0, 0, 62, 56, 247, 204, 62, 0, 0, 0, 62, 56, 247, 204, 62, 0, 0, 192, 62, 100, 132, 89, 63, 0, 0, 32, 63, 56, 247, 204, 62, 170, 170, 10, 63, 56, 247, 204, 62, 170, 170, 10, 63, 101, 132, 89, 63, 170, 170, 234, 62, 172, 170, 42, 62, 0, 0, 192, 62, 232, 56, 14, 62, 170, 170, 234, 62, 0, 72, 36, 62, 170, 170, 10, 63, 112, 227, 190, 61, 1, 0, 192, 62, 162, 194, 138, 62, 0, 0, 0, 62, 128, 123, 161, 62, 0, 0, 192, 62, 220, 217, 139, 62, 0, 0, 192, 62, 128, 123, 161, 62),
+"format": 34359742487,
+"index_count": 468,
+"index_data": PackedByteArray(99, 0, 11, 0, 75, 0, 99, 0, 42, 0, 11, 0, 41, 0, 21, 0, 9, 0, 41, 0, 49, 0, 21, 0, 58, 0, 17, 0, 39, 0, 58, 0, 180, 0, 17, 0, 207, 0, 3, 0, 201, 0, 207, 0, 175, 0, 3, 0, 1, 0, 241, 0, 237, 0, 241, 0, 188, 0, 13, 0, 241, 0, 216, 0, 188, 0, 185, 0, 241, 0, 1, 0, 241, 0, 213, 0, 216, 0, 241, 0, 185, 0, 213, 0, 16, 0, 29, 0, 37, 0, 29, 0, 202, 0, 4, 0, 29, 0, 226, 0, 202, 0, 199, 0, 29, 0, 16, 0, 29, 0, 223, 0, 226, 0, 29, 0, 199, 0, 223, 0, 150, 0, 28, 0, 159, 0, 150, 0, 36, 0, 28, 0, 146, 0, 32, 0, 161, 0, 146, 0, 24, 0, 32, 0, 156, 0, 38, 0, 151, 0, 156, 0, 57, 0, 38, 0, 177, 0, 31, 0, 5, 0, 177, 0, 66, 0, 31, 0, 235, 0, 70, 0, 253, 0, 235, 0, 27, 0, 70, 0, 164, 0, 60, 0, 153, 0, 164, 0, 34, 0, 60, 0, 155, 0, 59, 0, 158, 0, 155, 0, 62, 0, 59, 0, 184, 0, 172, 0, 193, 0, 184, 0, 0, 0, 172, 0, 243, 0, 182, 0, 2, 1, 243, 0, 14, 0, 182, 0, 6, 0, 53, 0, 45, 0, 6, 0, 18, 0, 53, 0, 44, 0, 48, 0, 40, 0, 44, 0, 52, 0, 48, 0, 81, 0, 46, 0, 95, 0, 81, 0, 8, 0, 46, 0, 96, 0, 43, 0, 100, 0, 96, 0, 47, 0, 43, 0, 129, 0, 101, 0, 132, 0, 129, 0, 97, 0, 101, 0, 135, 0, 94, 0, 127, 0, 135, 0, 80, 0, 94, 0, 55, 0, 92, 0, 51, 0, 55, 0, 88, 0, 92, 0, 20, 0, 87, 0, 54, 0, 20, 0, 78, 0, 87, 0, 50, 0, 85, 0, 23, 0, 50, 0, 91, 0, 85, 0, 7, 0, 76, 0, 19, 0, 7, 0, 79, 0, 76, 0, 22, 0, 73, 0, 10, 0, 22, 0, 83, 0, 73, 0, 131, 0, 74, 0, 123, 0, 131, 0, 98, 0, 74, 0, 148, 0, 117, 0, 165, 0, 148, 0, 105, 0, 117, 0, 167, 0, 120, 0, 170, 0, 167, 0, 118, 0, 120, 0, 142, 0, 111, 0, 145, 0, 142, 0, 109, 0, 111, 0, 126, 0, 106, 0, 140, 0, 126, 0, 114, 0, 106, 0, 134, 0, 112, 0, 124, 0, 134, 0, 102, 0, 112, 0, 82, 0, 122, 0, 72, 0, 82, 0, 137, 0, 122, 0, 79, 0, 124, 0, 76, 0, 79, 0, 134, 0, 124, 0, 90, 0, 138, 0, 84, 0, 90, 0, 143, 0, 138, 0, 77, 0, 139, 0, 86, 0, 77, 0, 125, 0, 139, 0, 89, 0, 144, 0, 93, 0, 89, 0, 141, 0, 144, 0, 104, 0, 128, 0, 116, 0, 104, 0, 136, 0, 128, 0, 119, 0, 133, 0, 121, 0, 119, 0, 130, 0, 133, 0, 71, 0, 171, 0, 67, 0, 71, 0, 168, 0, 171, 0, 26, 0, 166, 0, 69, 0, 26, 0, 149, 0, 166, 0, 65, 0, 160, 0, 30, 0, 65, 0, 169, 0, 160, 0, 108, 0, 157, 0, 110, 0, 108, 0, 154, 0, 157, 0, 115, 0, 152, 0, 107, 0, 115, 0, 163, 0, 152, 0, 103, 0, 162, 0, 113, 0, 103, 0, 147, 0, 162, 0, 7, 1, 63, 0, 0, 1, 7, 1, 197, 0, 63, 0, 205, 0, 56, 0, 211, 0, 205, 0, 179, 0, 56, 0, 251, 0, 196, 0, 5, 1, 251, 0, 68, 0, 196, 0, 12, 0, 190, 0, 181, 0, 12, 0, 187, 0, 190, 0, 214, 0, 195, 0, 221, 0, 214, 0, 186, 0, 195, 0, 176, 0, 210, 0, 64, 0, 176, 0, 208, 0, 210, 0, 178, 0, 198, 0, 15, 0, 178, 0, 204, 0, 198, 0, 231, 0, 203, 0, 227, 0, 231, 0, 209, 0, 203, 0, 189, 0, 219, 0, 192, 0, 189, 0, 217, 0, 219, 0, 215, 0, 220, 0, 218, 0, 215, 0, 212, 0, 220, 0, 206, 0, 224, 0, 200, 0, 206, 0, 229, 0, 224, 0, 228, 0, 225, 0, 222, 0, 228, 0, 230, 0, 225, 0, 173, 0, 4, 1, 194, 0, 173, 0, 248, 0, 4, 1, 18, 1, 5, 1, 25, 1, 18, 1, 251, 0, 5, 1, 191, 0, 3, 1, 183, 0, 191, 0, 6, 1, 3, 1, 27, 1, 0, 1, 21, 1, 27, 1, 7, 1, 0, 1, 35, 0, 255, 0, 61, 0, 35, 0, 247, 0, 255, 0, 13, 1, 1, 1, 22, 1, 13, 1, 242, 0, 1, 1, 2, 0, 250, 0, 174, 0, 2, 0, 239, 0, 250, 0, 9, 1, 252, 0, 19, 1, 9, 1, 234, 0, 252, 0, 25, 0, 245, 0, 33, 0, 25, 0, 233, 0, 245, 0, 10, 1, 240, 0, 12, 1, 10, 1, 236, 0, 240, 0, 232, 0, 14, 1, 244, 0, 232, 0, 8, 1, 14, 1, 238, 0, 17, 1, 249, 0, 238, 0, 11, 1, 17, 1, 246, 0, 20, 1, 254, 0, 246, 0, 15, 1, 20, 1, 6, 1, 23, 1, 3, 1, 6, 1, 26, 1, 23, 1, 26, 1, 21, 1, 23, 1, 26, 1, 27, 1, 21, 1, 248, 0, 24, 1, 4, 1, 248, 0, 16, 1, 24, 1, 16, 1, 25, 1, 24, 1, 16, 1, 18, 1, 25, 1),
+"lods": [0.171633, PackedByteArray(48, 1, 11, 0, 41, 1, 48, 1, 42, 0, 11, 0, 131, 0, 98, 0, 41, 1, 131, 0, 41, 1, 123, 0, 47, 1, 42, 0, 100, 0, 47, 1, 48, 1, 49, 1, 47, 1, 34, 1, 42, 0, 54, 1, 34, 1, 47, 1, 54, 1, 8, 0, 34, 1, 39, 1, 47, 1, 49, 1, 54, 1, 47, 1, 39, 1, 39, 1, 49, 1, 120, 0, 39, 1, 120, 0, 57, 1, 54, 1, 39, 1, 252, 0, 9, 1, 54, 1, 252, 0, 9, 1, 252, 0, 19, 1, 39, 1, 57, 1, 65, 0, 65, 0, 57, 1, 160, 0, 65, 0, 160, 0, 30, 0, 177, 0, 65, 0, 30, 0, 177, 0, 30, 0, 5, 0, 40, 0, 21, 0, 9, 0, 40, 0, 48, 0, 21, 0, 44, 0, 48, 0, 40, 0, 44, 0, 35, 1, 48, 0, 6, 0, 35, 1, 44, 0, 6, 0, 18, 0, 35, 1, 37, 1, 17, 0, 39, 0, 37, 1, 180, 0, 17, 0, 56, 1, 37, 1, 33, 1, 56, 1, 33, 1, 55, 1, 38, 1, 37, 1, 56, 1, 38, 1, 56, 1, 110, 0, 55, 1, 29, 1, 159, 0, 55, 1, 32, 1, 29, 1, 16, 0, 29, 1, 32, 1, 226, 0, 29, 1, 16, 0, 29, 1, 226, 0, 4, 0, 44, 1, 38, 1, 110, 0, 44, 1, 110, 0, 51, 1, 44, 1, 51, 1, 46, 1, 52, 1, 30, 1, 38, 1, 52, 1, 63, 1, 65, 1, 52, 1, 65, 1, 31, 1, 42, 1, 52, 1, 38, 1, 42, 1, 38, 1, 44, 1, 20, 0, 42, 1, 44, 1, 20, 0, 44, 1, 36, 1, 36, 1, 44, 1, 92, 0, 36, 1, 92, 0, 50, 0, 50, 0, 85, 0, 23, 0, 50, 0, 45, 1, 85, 0, 7, 0, 53, 1, 42, 1, 7, 0, 42, 1, 28, 1, 45, 1, 143, 0, 50, 1, 45, 1, 50, 1, 82, 0, 82, 0, 50, 1, 122, 0, 82, 0, 122, 0, 72, 0, 207, 0, 3, 0, 225, 0, 207, 0, 175, 0, 3, 0, 61, 1, 207, 0, 225, 0, 228, 0, 61, 1, 225, 0, 204, 0, 228, 0, 225, 0, 178, 0, 204, 0, 225, 0, 178, 0, 225, 0, 15, 0, 1, 0, 241, 0, 237, 0, 216, 0, 241, 0, 1, 0, 241, 0, 216, 0, 13, 0, 215, 0, 172, 0, 193, 0, 215, 0, 0, 0, 172, 0, 215, 0, 193, 0, 220, 0, 215, 0, 220, 0, 60, 1, 59, 1, 60, 1, 58, 1, 12, 0, 215, 0, 58, 1, 12, 0, 58, 1, 181, 0, 243, 0, 182, 0, 1, 1, 243, 0, 14, 0, 182, 0, 64, 1, 62, 1, 1, 1, 64, 1, 1, 1, 22, 1, 22, 0, 40, 1, 10, 0, 22, 0, 43, 1, 40, 1, 7, 1, 63, 0, 0, 1, 7, 1, 197, 0, 63, 0, 27, 1, 7, 1, 0, 1, 27, 1, 0, 1, 21, 1, 26, 1, 27, 1, 21, 1, 26, 1, 21, 1, 23, 1, 6, 1, 26, 1, 23, 1, 6, 1, 23, 1, 3, 1, 191, 0, 6, 1, 3, 1, 191, 0, 3, 1, 183, 0, 205, 0, 56, 0, 211, 0, 205, 0, 179, 0, 56, 0, 251, 0, 196, 0, 5, 1, 251, 0, 68, 0, 196, 0, 18, 1, 251, 0, 5, 1, 18, 1, 5, 1, 25, 1, 16, 1, 18, 1, 25, 1, 16, 1, 25, 1, 24, 1, 248, 0, 16, 1, 24, 1, 248, 0, 24, 1, 4, 1, 173, 0, 248, 0, 4, 1, 173, 0, 4, 1, 194, 0, 176, 0, 210, 0, 64, 0, 176, 0, 208, 0, 210, 0, 35, 0, 254, 0, 38, 1, 35, 0, 20, 1, 254, 0, 35, 0, 15, 1, 20, 1, 2, 0, 249, 0, 174, 0, 2, 0, 238, 0, 249, 0, 238, 0, 17, 1, 249, 0, 238, 0, 11, 1, 17, 1, 10, 1, 240, 0, 12, 1, 10, 1, 236, 0, 240, 0)],
+"material": SubResource("StandardMaterial3D_4b7xv"),
+"name": "mainmat",
+"primitive": 3,
+"uv_scale": Vector4(0, 0, 0, 0),
+"vertex_count": 322,
+"vertex_data": PackedByteArray(175, 65, 11, 190, 232, 71, 143, 190, 213, 158, 117, 63, 175, 65, 11, 190, 232, 71, 143, 190, 213, 158, 117, 63, 175, 65, 11, 190, 232, 71, 143, 190, 213, 158, 117, 63, 83, 115, 219, 189, 16, 156, 206, 62, 213, 158, 117, 63, 83, 115, 219, 189, 16, 156, 206, 62, 213, 158, 117, 63, 83, 115, 219, 189, 16, 156, 206, 62, 213, 158, 117, 63, 184, 179, 173, 189, 172, 126, 43, 190, 173, 42, 173, 191, 184, 179, 173, 189, 172, 126, 43, 190, 173, 42, 173, 191, 184, 179, 173, 189, 172, 126, 43, 190, 173, 42, 173, 191, 108, 51, 209, 189, 128, 119, 184, 62, 53, 90, 164, 191, 108, 51, 209, 189, 128, 119, 184, 62, 53, 90, 164, 191, 108, 51, 209, 189, 128, 119, 184, 62, 53, 90, 164, 191, 175, 65, 11, 62, 232, 71, 143, 190, 213, 158, 117, 63, 175, 65, 11, 62, 232, 71, 143, 190, 213, 158, 117, 63, 175, 65, 11, 62, 232, 71, 143, 190, 213, 158, 117, 63, 83, 115, 219, 61, 16, 156, 206, 62, 213, 158, 117, 63, 83, 115, 219, 61, 16, 156, 206, 62, 213, 158, 117, 63, 83, 115, 219, 61, 16, 156, 206, 62, 213, 158, 117, 63, 184, 179, 173, 61, 172, 126, 43, 190, 173, 42, 173, 191, 184, 179, 173, 61, 172, 126, 43, 190, 173, 42, 173, 191, 184, 179, 173, 61, 172, 126, 43, 190, 173, 42, 173, 191, 108, 51, 209, 61, 128, 119, 184, 62, 53, 90, 164, 191, 108, 51, 209, 61, 128, 119, 184, 62, 53, 90, 164, 191, 108, 51, 209, 61, 128, 119, 184, 62, 53, 90, 164, 191, 71, 59, 50, 190, 38, 17, 137, 190, 93, 9, 195, 190, 71, 59, 50, 190, 38, 17, 137, 190, 93, 9, 195, 190, 71, 59, 50, 190, 38, 17, 137, 190, 93, 9, 195, 190, 71, 59, 50, 190, 38, 17, 137, 190, 93, 9, 195, 190, 83, 115, 219, 189, 16, 156, 206, 62, 92, 9, 195, 190, 83, 115, 219, 189, 16, 156, 206, 62, 92, 9, 195, 190, 83, 115, 219, 189, 16, 156, 206, 62, 92, 9, 195, 190, 83, 115, 219, 189, 16, 156, 206, 62, 92, 9, 195, 190, 71, 59, 50, 62, 38, 17, 137, 190, 93, 9, 195, 190, 71, 59, 50, 62, 38, 17, 137, 190, 93, 9, 195, 190, 71, 59, 50, 62, 38, 17, 137, 190, 93, 9, 195, 190, 71, 59, 50, 62, 38, 17, 137, 190, 93, 9, 195, 190, 83, 115, 219, 61, 16, 156, 206, 62, 92, 9, 195, 190, 83, 115, 219, 61, 16, 156, 206, 62, 92, 9, 195, 190, 83, 115, 219, 61, 16, 156, 206, 62, 92, 9, 195, 190, 83, 115, 219, 61, 16, 156, 206, 62, 92, 9, 195, 190, 102, 84, 85, 190, 176, 199, 150, 61, 221, 83, 183, 191, 102, 84, 85, 190, 176, 199, 150, 61, 221, 83, 183, 191, 102, 84, 85, 190, 176, 199, 150, 61, 221, 83, 183, 191, 102, 84, 85, 190, 176, 199, 150, 61, 221, 83, 183, 191, 110, 213, 73, 190, 176, 148, 106, 189, 221, 83, 183, 191, 110, 213, 73, 190, 176, 148, 106, 189, 221, 83, 183, 191, 110, 213, 73, 190, 176, 148, 106, 189, 221, 83, 183, 191, 110, 213, 73, 190, 176, 148, 106, 189, 221, 83, 183, 191, 102, 84, 85, 62, 176, 199, 150, 61, 221, 83, 183, 191, 102, 84, 85, 62, 176, 199, 150, 61, 221, 83, 183, 191, 102, 84, 85, 62, 176, 199, 150, 61, 221, 83, 183, 191, 102, 84, 85, 62, 176, 199, 150, 61, 221, 83, 183, 191, 110, 213, 73, 62, 176, 148, 106, 189, 221, 83, 183, 191, 110, 213, 73, 62, 176, 148, 106, 189, 221, 83, 183, 191, 110, 213, 73, 62, 176, 148, 106, 189, 221, 83, 183, 191, 110, 213, 73, 62, 176, 148, 106, 189, 221, 83, 183, 191, 116, 66, 99, 62, 230, 146, 23, 62, 92, 9, 195, 190, 116, 66, 99, 62, 230, 146, 23, 62, 92, 9, 195, 190, 116, 66, 99, 62, 230, 146, 23, 62, 92, 9, 195, 190, 116, 66, 99, 62, 230, 146, 23, 62, 92, 9, 195, 190, 173, 252, 106, 62, 104, 202, 210, 188, 93, 9, 195, 190, 173, 252, 106, 62, 104, 202, 210, 188, 93, 9, 195, 190, 173, 252, 106, 62, 104, 202, 210, 188, 93, 9, 195, 190, 173, 252, 106, 62, 104, 202, 210, 188, 93, 9, 195, 190, 116, 66, 99, 190, 230, 146, 23, 62, 92, 9, 195, 190, 116, 66, 99, 190, 230, 146, 23, 62, 92, 9, 195, 190, 116, 66, 99, 190, 230, 146, 23, 62, 92, 9, 195, 190, 116, 66, 99, 190, 230, 146, 23, 62, 92, 9, 195, 190, 173, 252, 106, 190, 104, 202, 210, 188, 93, 9, 195, 190, 173, 252, 106, 190, 104, 202, 210, 188, 93, 9, 195, 190, 173, 252, 106, 190, 104, 202, 210, 188, 93, 9, 195, 190, 173, 252, 106, 190, 104, 202, 210, 188, 93, 9, 195, 190, 123, 225, 217, 189, 62, 38, 179, 62, 235, 175, 101, 191, 123, 225, 217, 189, 62, 38, 179, 62, 235, 175, 101, 191, 123, 225, 217, 189, 62, 38, 179, 62, 235, 175, 101, 191, 123, 225, 217, 189, 62, 38, 179, 62, 235, 175, 101, 191, 196, 235, 105, 61, 199, 249, 78, 190, 94, 152, 90, 191, 196, 235, 105, 61, 199, 249, 78, 190, 94, 152, 90, 191, 196, 235, 105, 61, 199, 249, 78, 190, 94, 152, 90, 191, 196, 235, 105, 189, 199, 249, 78, 190, 94, 152, 90, 191, 196, 235, 105, 189, 199, 249, 78, 190, 94, 152, 90, 191, 196, 235, 105, 189, 199, 249, 78, 190, 94, 152, 90, 191, 123, 225, 217, 61, 62, 38, 179, 62, 235, 175, 101, 191, 123, 225, 217, 61, 62, 38, 179, 62, 235, 175, 101, 191, 123, 225, 217, 61, 62, 38, 179, 62, 235, 175, 101, 191, 123, 225, 217, 61, 62, 38, 179, 62, 235, 175, 101, 191, 122, 13, 131, 62, 168, 82, 78, 189, 53, 22, 104, 191, 122, 13, 131, 62, 168, 82, 78, 189, 53, 22, 104, 191, 122, 13, 131, 62, 168, 82, 78, 189, 53, 22, 104, 191, 122, 13, 131, 62, 168, 82, 78, 189, 53, 22, 104, 191, 56, 237, 133, 62, 192, 246, 226, 61, 53, 22, 104, 191, 56, 237, 133, 62, 192, 246, 226, 61, 53, 22, 104, 191, 56, 237, 133, 62, 192, 246, 226, 61, 53, 22, 104, 191, 56, 237, 133, 62, 192, 246, 226, 61, 53, 22, 104, 191, 122, 13, 131, 190, 168, 82, 78, 189, 53, 22, 104, 191, 122, 13, 131, 190, 168, 82, 78, 189, 53, 22, 104, 191, 122, 13, 131, 190, 168, 82, 78, 189, 53, 22, 104, 191, 122, 13, 131, 190, 168, 82, 78, 189, 53, 22, 104, 191, 56, 237, 133, 190, 192, 246, 226, 61, 53, 22, 104, 191, 56, 237, 133, 190, 192, 246, 226, 61, 53, 22, 104, 191, 56, 237, 133, 190, 192, 246, 226, 61, 53, 22, 104, 191, 56, 237, 133, 190, 192, 246, 226, 61, 53, 22, 104, 191, 62, 200, 142, 189, 20, 84, 95, 190, 218, 184, 31, 191, 62, 200, 142, 189, 20, 84, 95, 190, 218, 184, 31, 191, 62, 200, 142, 189, 20, 84, 95, 190, 218, 184, 31, 191, 62, 200, 142, 189, 20, 84, 95, 190, 218, 184, 31, 191, 220, 30, 146, 62, 160, 49, 64, 189, 114, 205, 36, 191, 220, 30, 146, 62, 160, 49, 64, 189, 114, 205, 36, 191, 220, 30, 146, 62, 160, 49, 64, 189, 114, 205, 36, 191, 220, 30, 146, 62, 160, 49, 64, 189, 114, 205, 36, 191, 186, 142, 147, 62, 36, 135, 4, 62, 114, 205, 36, 191, 186, 142, 147, 62, 36, 135, 4, 62, 114, 205, 36, 191, 62, 200, 142, 61, 20, 84, 95, 190, 218, 184, 31, 191, 62, 200, 142, 61, 20, 84, 95, 190, 218, 184, 31, 191, 62, 200, 142, 61, 20, 84, 95, 190, 218, 184, 31, 191, 62, 200, 142, 61, 20, 84, 95, 190, 218, 184, 31, 191, 220, 30, 146, 190, 160, 49, 64, 189, 114, 205, 36, 191, 220, 30, 146, 190, 160, 49, 64, 189, 114, 205, 36, 191, 220, 30, 146, 190, 160, 49, 64, 189, 114, 205, 36, 191, 220, 30, 146, 190, 160, 49, 64, 189, 114, 205, 36, 191, 186, 142, 147, 190, 36, 135, 4, 62, 114, 205, 36, 191, 186, 142, 147, 190, 36, 135, 4, 62, 114, 205, 36, 191, 24, 55, 140, 189, 76, 200, 145, 62, 55, 199, 91, 191, 24, 55, 140, 189, 76, 200, 145, 62, 55, 199, 91, 191, 180, 3, 116, 61, 85, 183, 79, 190, 238, 237, 87, 191, 180, 3, 116, 61, 85, 183, 79, 190, 238, 237, 87, 191, 180, 3, 116, 61, 85, 183, 79, 190, 238, 237, 87, 191, 223, 79, 133, 190, 80, 52, 76, 189, 115, 255, 93, 191, 223, 79, 133, 190, 80, 52, 76, 189, 115, 255, 93, 191, 223, 79, 133, 190, 80, 52, 76, 189, 115, 255, 93, 191, 223, 79, 133, 190, 80, 52, 76, 189, 115, 255, 93, 191, 115, 248, 135, 190, 240, 172, 232, 61, 115, 255, 93, 191, 115, 248, 135, 190, 240, 172, 232, 61, 115, 255, 93, 191, 115, 248, 135, 190, 240, 172, 232, 61, 115, 255, 93, 191, 180, 3, 116, 189, 85, 183, 79, 190, 238, 237, 87, 191, 180, 3, 116, 189, 85, 183, 79, 190, 238, 237, 87, 191, 180, 3, 116, 189, 85, 183, 79, 190, 238, 237, 87, 191, 24, 55, 140, 61, 76, 200, 145, 62, 55, 199, 91, 191, 24, 55, 140, 61, 76, 200, 145, 62, 55, 199, 91, 191, 223, 79, 133, 62, 80, 52, 76, 189, 115, 255, 93, 191, 223, 79, 133, 62, 80, 52, 76, 189, 115, 255, 93, 191, 223, 79, 133, 62, 80, 52, 76, 189, 115, 255, 93, 191, 223, 79, 133, 62, 80, 52, 76, 189, 115, 255, 93, 191, 115, 248, 135, 62, 240, 172, 232, 61, 115, 255, 93, 191, 115, 248, 135, 62, 240, 172, 232, 61, 115, 255, 93, 191, 115, 248, 135, 62, 240, 172, 232, 61, 115, 255, 93, 191, 195, 53, 139, 189, 139, 46, 87, 190, 241, 75, 241, 190, 195, 53, 139, 189, 139, 46, 87, 190, 241, 75, 241, 190, 195, 53, 139, 189, 139, 46, 87, 190, 241, 75, 241, 190, 195, 53, 139, 189, 139, 46, 87, 190, 241, 75, 241, 190, 154, 128, 140, 61, 72, 216, 148, 62, 193, 46, 244, 190, 154, 128, 140, 61, 72, 216, 148, 62, 193, 46, 244, 190, 196, 184, 97, 62, 152, 81, 55, 189, 45, 19, 245, 190, 196, 184, 97, 62, 152, 81, 55, 189, 45, 19, 245, 190, 196, 184, 97, 62, 152, 81, 55, 189, 45, 19, 245, 190, 196, 184, 97, 62, 152, 81, 55, 189, 45, 19, 245, 190, 88, 202, 98, 62, 226, 125, 16, 62, 45, 19, 245, 190, 88, 202, 98, 62, 226, 125, 16, 62, 45, 19, 245, 190, 88, 202, 98, 62, 226, 125, 16, 62, 45, 19, 245, 190, 154, 128, 140, 189, 72, 216, 148, 62, 193, 46, 244, 190, 154, 128, 140, 189, 72, 216, 148, 62, 193, 46, 244, 190, 195, 53, 139, 61, 139, 46, 87, 190, 241, 75, 241, 190, 195, 53, 139, 61, 139, 46, 87, 190, 241, 75, 241, 190, 195, 53, 139, 61, 139, 46, 87, 190, 241, 75, 241, 190, 195, 53, 139, 61, 139, 46, 87, 190, 241, 75, 241, 190, 196, 184, 97, 190, 152, 81, 55, 189, 45, 19, 245, 190, 196, 184, 97, 190, 152, 81, 55, 189, 45, 19, 245, 190, 196, 184, 97, 190, 152, 81, 55, 189, 45, 19, 245, 190, 196, 184, 97, 190, 152, 81, 55, 189, 45, 19, 245, 190, 88, 202, 98, 190, 226, 125, 16, 62, 45, 19, 245, 190, 88, 202, 98, 190, 226, 125, 16, 62, 45, 19, 245, 190, 88, 202, 98, 190, 226, 125, 16, 62, 45, 19, 245, 190, 173, 252, 106, 190, 104, 202, 210, 188, 213, 158, 117, 63, 173, 252, 106, 190, 104, 202, 210, 188, 213, 158, 117, 63, 173, 252, 106, 190, 104, 202, 210, 188, 213, 158, 117, 63, 116, 66, 99, 190, 230, 146, 23, 62, 213, 158, 117, 63, 116, 66, 99, 190, 230, 146, 23, 62, 213, 158, 117, 63, 116, 66, 99, 190, 230, 146, 23, 62, 213, 158, 117, 63, 116, 66, 99, 62, 230, 146, 23, 62, 213, 158, 117, 63, 116, 66, 99, 62, 230, 146, 23, 62, 213, 158, 117, 63, 116, 66, 99, 62, 230, 146, 23, 62, 213, 158, 117, 63, 173, 252, 106, 62, 104, 202, 210, 188, 213, 158, 117, 63, 173, 252, 106, 62, 104, 202, 210, 188, 213, 158, 117, 63, 173, 252, 106, 62, 104, 202, 210, 188, 213, 158, 117, 63, 230, 121, 42, 189, 125, 235, 146, 190, 213, 158, 117, 63, 230, 121, 42, 189, 125, 235, 146, 190, 213, 158, 117, 63, 230, 121, 42, 189, 125, 235, 146, 190, 213, 158, 117, 63, 230, 121, 42, 61, 125, 235, 146, 190, 213, 158, 117, 63, 230, 121, 42, 61, 125, 235, 146, 190, 213, 158, 117, 63, 230, 121, 42, 61, 125, 235, 146, 190, 213, 158, 117, 63, 3, 181, 171, 61, 76, 82, 22, 189, 213, 158, 117, 63, 3, 181, 171, 61, 76, 82, 22, 189, 213, 158, 117, 63, 3, 181, 171, 61, 76, 82, 22, 189, 213, 158, 117, 63, 3, 181, 171, 189, 76, 82, 22, 189, 213, 158, 117, 63, 3, 181, 171, 189, 76, 82, 22, 189, 213, 158, 117, 63, 3, 181, 171, 189, 76, 82, 22, 189, 213, 158, 117, 63, 3, 181, 171, 189, 76, 82, 22, 189, 93, 9, 195, 190, 3, 181, 171, 61, 76, 82, 22, 189, 93, 9, 195, 190, 226, 76, 18, 61, 16, 156, 206, 62, 213, 158, 117, 63, 226, 76, 18, 61, 16, 156, 206, 62, 213, 158, 117, 63, 226, 76, 18, 61, 16, 156, 206, 62, 213, 158, 117, 63, 226, 76, 18, 189, 16, 156, 206, 62, 213, 158, 117, 63, 226, 76, 18, 189, 16, 156, 206, 62, 213, 158, 117, 63, 226, 76, 18, 189, 16, 156, 206, 62, 213, 158, 117, 63, 141, 195, 133, 61, 230, 146, 23, 62, 213, 158, 117, 63, 141, 195, 133, 61, 230, 146, 23, 62, 213, 158, 117, 63, 141, 195, 133, 61, 230, 146, 23, 62, 213, 158, 117, 63, 141, 195, 133, 189, 230, 146, 23, 62, 213, 158, 117, 63, 141, 195, 133, 189, 230, 146, 23, 62, 213, 158, 117, 63, 141, 195, 133, 189, 230, 146, 23, 62, 213, 158, 117, 63, 141, 195, 133, 189, 230, 146, 23, 62, 92, 9, 195, 190, 141, 195, 133, 61, 230, 146, 23, 62, 92, 9, 195, 190, 134, 101, 5, 189, 217, 68, 147, 190, 53, 24, 115, 63, 134, 101, 5, 189, 217, 68, 147, 190, 53, 24, 115, 63, 134, 101, 5, 189, 217, 68, 147, 190, 53, 24, 115, 63, 134, 101, 5, 61, 217, 68, 147, 190, 53, 24, 115, 63, 134, 101, 5, 61, 217, 68, 147, 190, 53, 24, 115, 63, 134, 101, 5, 61, 217, 68, 147, 190, 53, 24, 115, 63, 144, 157, 145, 61, 22, 52, 67, 189, 53, 24, 115, 63, 144, 157, 145, 61, 22, 52, 67, 189, 53, 24, 115, 63, 144, 157, 145, 189, 22, 52, 67, 189, 53, 24, 115, 63, 144, 157, 145, 189, 22, 52, 67, 189, 53, 24, 115, 63, 47, 17, 67, 60, 16, 156, 206, 62, 137, 243, 113, 63, 47, 17, 67, 60, 16, 156, 206, 62, 137, 243, 113, 63, 47, 17, 67, 60, 16, 156, 206, 62, 137, 243, 113, 63, 47, 17, 67, 188, 16, 156, 206, 62, 137, 243, 113, 63, 47, 17, 67, 188, 16, 156, 206, 62, 137, 243, 113, 63, 47, 17, 67, 188, 16, 156, 206, 62, 137, 243, 113, 63, 149, 33, 76, 61, 176, 216, 30, 62, 137, 243, 113, 63, 149, 33, 76, 61, 176, 216, 30, 62, 137, 243, 113, 63, 149, 33, 76, 189, 176, 216, 30, 62, 137, 243, 113, 63, 149, 33, 76, 189, 176, 216, 30, 62, 137, 243, 113, 63, 95, 250, 28, 190, 92, 227, 136, 190, 71, 138, 159, 62, 95, 250, 28, 190, 92, 227, 136, 190, 71, 138, 159, 62, 95, 250, 28, 190, 92, 227, 136, 190, 71, 138, 159, 62, 95, 250, 28, 190, 92, 227, 136, 190, 71, 138, 159, 62, 76, 109, 17, 190, 52, 85, 143, 190, 77, 38, 66, 63, 76, 109, 17, 190, 52, 85, 143, 190, 77, 38, 66, 63, 76, 109, 17, 190, 52, 85, 143, 190, 77, 38, 66, 63, 76, 109, 17, 190, 52, 85, 143, 190, 77, 38, 66, 63, 76, 109, 17, 62, 52, 85, 143, 190, 77, 38, 66, 63, 76, 109, 17, 62, 52, 85, 143, 190, 77, 38, 66, 63, 76, 109, 17, 62, 52, 85, 143, 190, 77, 38, 66, 63, 76, 109, 17, 62, 52, 85, 143, 190, 77, 38, 66, 63, 95, 250, 28, 62, 92, 227, 136, 190, 71, 138, 159, 62, 95, 250, 28, 62, 92, 227, 136, 190, 71, 138, 159, 62, 95, 250, 28, 62, 92, 227, 136, 190, 71, 138, 159, 62, 95, 250, 28, 62, 92, 227, 136, 190, 71, 138, 159, 62, 173, 252, 106, 190, 104, 202, 210, 188, 77, 38, 66, 63, 173, 252, 106, 190, 104, 202, 210, 188, 77, 38, 66, 63, 173, 252, 106, 190, 104, 202, 210, 188, 77, 38, 66, 63, 173, 252, 106, 190, 104, 202, 210, 188, 67, 138, 159, 62, 173, 252, 106, 190, 104, 202, 210, 188, 67, 138, 159, 62, 173, 252, 106, 190, 104, 202, 210, 188, 67, 138, 159, 62, 173, 252, 106, 62, 104, 202, 210, 188, 67, 138, 159, 62, 173, 252, 106, 62, 104, 202, 210, 188, 67, 138, 159, 62, 173, 252, 106, 62, 104, 202, 210, 188, 67, 138, 159, 62, 173, 252, 106, 62, 104, 202, 210, 188, 77, 38, 66, 63, 173, 252, 106, 62, 104, 202, 210, 188, 77, 38, 66, 63, 173, 252, 106, 62, 104, 202, 210, 188, 77, 38, 66, 63, 3, 181, 171, 189, 76, 82, 22, 189, 77, 38, 66, 63, 3, 181, 171, 189, 76, 82, 22, 189, 67, 138, 159, 62, 3, 181, 171, 61, 76, 82, 22, 189, 77, 38, 66, 63, 3, 181, 171, 61, 76, 82, 22, 189, 67, 138, 159, 62, 204, 17, 24, 190, 166, 26, 120, 190, 211, 235, 170, 62, 204, 17, 24, 190, 166, 26, 120, 190, 211, 235, 170, 62, 75, 170, 11, 190, 53, 229, 119, 190, 173, 238, 60, 63, 75, 170, 11, 190, 53, 229, 119, 190, 173, 238, 60, 63, 75, 170, 11, 62, 53, 229, 119, 190, 173, 238, 60, 63, 75, 170, 11, 62, 53, 229, 119, 190, 173, 238, 60, 63, 204, 17, 24, 62, 166, 26, 120, 190, 211, 235, 170, 62, 204, 17, 24, 62, 166, 26, 120, 190, 211, 235, 170, 62, 173, 252, 106, 190, 104, 202, 210, 188, 173, 238, 60, 63, 173, 252, 106, 190, 104, 202, 210, 188, 173, 238, 60, 63, 173, 252, 106, 190, 104, 202, 210, 188, 207, 235, 170, 62, 173, 252, 106, 190, 104, 202, 210, 188, 207, 235, 170, 62, 173, 252, 106, 62, 104, 202, 210, 188, 207, 235, 170, 62, 173, 252, 106, 62, 104, 202, 210, 188, 207, 235, 170, 62, 173, 252, 106, 62, 104, 202, 210, 188, 173, 238, 60, 63, 173, 252, 106, 62, 104, 202, 210, 188, 173, 238, 60, 63, 3, 181, 171, 189, 76, 82, 22, 189, 173, 238, 60, 63, 3, 181, 171, 189, 76, 82, 22, 189, 207, 235, 170, 62, 3, 181, 171, 61, 76, 82, 22, 189, 173, 238, 60, 63, 3, 181, 171, 61, 76, 82, 22, 189, 207, 235, 170, 62, 184, 179, 173, 61, 172, 126, 43, 190, 173, 42, 173, 191, 83, 115, 219, 189, 16, 156, 206, 62, 92, 9, 195, 190, 71, 59, 50, 62, 38, 17, 137, 190, 93, 9, 195, 190, 71, 59, 50, 62, 38, 17, 137, 190, 93, 9, 195, 190, 83, 115, 219, 61, 16, 156, 206, 62, 92, 9, 195, 190, 83, 115, 219, 61, 16, 156, 206, 62, 92, 9, 195, 190, 110, 213, 73, 190, 176, 148, 106, 189, 221, 83, 183, 191, 110, 213, 73, 62, 176, 148, 106, 189, 221, 83, 183, 191, 110, 213, 73, 62, 176, 148, 106, 189, 221, 83, 183, 191, 116, 66, 99, 62, 230, 146, 23, 62, 92, 9, 195, 190, 173, 252, 106, 62, 104, 202, 210, 188, 93, 9, 195, 190, 173, 252, 106, 190, 104, 202, 210, 188, 93, 9, 195, 190, 123, 225, 217, 189, 62, 38, 179, 62, 235, 175, 101, 191, 123, 225, 217, 189, 62, 38, 179, 62, 235, 175, 101, 191, 196, 235, 105, 61, 199, 249, 78, 190, 94, 152, 90, 191, 123, 225, 217, 61, 62, 38, 179, 62, 235, 175, 101, 191, 122, 13, 131, 62, 168, 82, 78, 189, 53, 22, 104, 191, 56, 237, 133, 62, 192, 246, 226, 61, 53, 22, 104, 191, 56, 237, 133, 62, 192, 246, 226, 61, 53, 22, 104, 191, 122, 13, 131, 190, 168, 82, 78, 189, 53, 22, 104, 191, 56, 237, 133, 190, 192, 246, 226, 61, 53, 22, 104, 191, 115, 248, 135, 190, 240, 172, 232, 61, 115, 255, 93, 191, 24, 55, 140, 61, 76, 200, 145, 62, 55, 199, 91, 191, 115, 248, 135, 62, 240, 172, 232, 61, 115, 255, 93, 191, 195, 53, 139, 189, 139, 46, 87, 190, 241, 75, 241, 190, 195, 53, 139, 189, 139, 46, 87, 190, 241, 75, 241, 190, 195, 53, 139, 189, 139, 46, 87, 190, 241, 75, 241, 190, 154, 128, 140, 61, 72, 216, 148, 62, 193, 46, 244, 190, 88, 202, 98, 62, 226, 125, 16, 62, 45, 19, 245, 190, 88, 202, 98, 190, 226, 125, 16, 62, 45, 19, 245, 190, 3, 181, 171, 61, 76, 82, 22, 189, 213, 158, 117, 63, 134, 101, 5, 61, 217, 68, 147, 190, 53, 24, 115, 63, 144, 157, 145, 61, 22, 52, 67, 189, 53, 24, 115, 63, 149, 33, 76, 189, 176, 216, 30, 62, 137, 243, 113, 63, 76, 109, 17, 62, 52, 85, 143, 190, 77, 38, 66, 63, 204, 17, 24, 190, 166, 26, 120, 190, 211, 235, 170, 62, 75, 170, 11, 62, 53, 229, 119, 190, 173, 238, 60, 63, 204, 17, 24, 62, 166, 26, 120, 190, 211, 235, 170, 62, 255, 127, 255, 127, 146, 93, 200, 238, 233, 130, 0, 0, 232, 255, 254, 191, 163, 34, 196, 94, 102, 94, 49, 239, 255, 127, 255, 127, 5, 150, 251, 244, 255, 127, 255, 255, 0, 0, 255, 191, 173, 39, 172, 167, 172, 167, 40, 236, 102, 202, 0, 0, 210, 54, 39, 239, 79, 136, 0, 0, 255, 255, 255, 191, 97, 68, 15, 56, 85, 43, 165, 230, 94, 212, 255, 255, 29, 153, 231, 225, 255, 127, 167, 252, 0, 0, 255, 191, 173, 41, 241, 175, 183, 161, 79, 226, 255, 127, 255, 127, 84, 151, 84, 244, 233, 130, 0, 0, 253, 255, 28, 192, 91, 221, 196, 94, 156, 161, 50, 239, 255, 127, 255, 127, 82, 88, 40, 236, 255, 127, 255, 255, 0, 0, 255, 191, 81, 216, 172, 167, 82, 88, 40, 236, 102, 202, 0, 0, 68, 209, 48, 232, 79, 136, 0, 0, 255, 255, 255, 191, 157, 187, 15, 56, 248, 210, 124, 229, 94, 212, 255, 255, 138, 123, 182, 232, 255, 127, 167, 252, 0, 0, 255, 191, 81, 214, 241, 175, 210, 97, 149, 223, 209, 177, 0, 0, 255, 255, 255, 191, 255, 127, 16, 0, 255, 255, 255, 191, 222, 29, 18, 57, 127, 83, 180, 231, 143, 28, 238, 100, 160, 100, 71, 242, 39, 197, 255, 255, 0, 0, 255, 191, 255, 127, 255, 255, 19, 0, 255, 191, 83, 37, 185, 199, 130, 174, 200, 229, 173, 39, 172, 167, 172, 167, 40, 236, 209, 177, 0, 0, 255, 255, 255, 191, 255, 127, 16, 0, 255, 255, 255, 191, 32, 226, 18, 57, 237, 173, 86, 238, 111, 227, 238, 100, 96, 155, 74, 242, 39, 197, 255, 255, 0, 0, 255, 191, 255, 127, 255, 255, 0, 0, 255, 191, 171, 218, 185, 199, 222, 73, 134, 230, 81, 216, 172, 167, 82, 88, 40, 236, 255, 255, 255, 255, 227, 117, 240, 250, 94, 212, 255, 255, 174, 119, 113, 231, 173, 41, 241, 175, 44, 158, 149, 223, 88, 6, 43, 110, 4, 121, 99, 252, 255, 255, 255, 255, 129, 121, 192, 252, 102, 202, 0, 0, 186, 46, 48, 232, 97, 68, 15, 56, 213, 45, 238, 228, 88, 6, 43, 110, 2, 121, 84, 252, 255, 255, 255, 255, 14, 134, 247, 252, 94, 212, 255, 255, 225, 102, 231, 225, 81, 214, 241, 175, 196, 92, 121, 227, 166, 249, 43, 110, 252, 134, 84, 252, 255, 255, 255, 255, 27, 138, 240, 250, 102, 202, 0, 0, 131, 73, 59, 255, 157, 187, 15, 56, 169, 212, 165, 230, 166, 249, 43, 110, 250, 134, 100, 252, 255, 127, 0, 0, 255, 255, 255, 191, 171, 218, 185, 199, 124, 81, 200, 229, 81, 216, 172, 167, 82, 88, 40, 236, 211, 253, 72, 136, 185, 125, 212, 254, 32, 226, 18, 57, 36, 172, 65, 233, 111, 227, 238, 100, 94, 155, 71, 242, 211, 253, 72, 136, 141, 125, 218, 254, 8, 119, 8, 247, 222, 253, 28, 14, 255, 127, 0, 0, 255, 255, 255, 191, 83, 37, 185, 199, 204, 181, 132, 230, 173, 39, 172, 167, 172, 167, 40, 236, 43, 2, 72, 136, 113, 130, 218, 254, 246, 136, 8, 247, 255, 255, 255, 191, 222, 29, 18, 57, 17, 82, 86, 238, 143, 28, 238, 100, 158, 100, 74, 242, 43, 2, 72, 136, 69, 130, 212, 254, 255, 127, 177, 175, 0, 0, 255, 191, 255, 127, 167, 252, 0, 0, 255, 191, 195, 83, 226, 163, 136, 186, 178, 222, 173, 41, 241, 175, 219, 170, 87, 233, 79, 136, 0, 0, 255, 255, 255, 191, 195, 186, 97, 51, 186, 204, 25, 222, 157, 187, 15, 56, 79, 205, 156, 225, 79, 136, 0, 0, 255, 255, 255, 191, 59, 69, 97, 51, 112, 48, 143, 224, 97, 68, 15, 56, 45, 49, 164, 226, 255, 127, 177, 175, 0, 0, 255, 191, 255, 127, 167, 252, 0, 0, 255, 191, 59, 172, 226, 163, 205, 70, 221, 228, 81, 214, 241, 175, 231, 89, 173, 229, 195, 186, 97, 51, 142, 207, 143, 224, 157, 187, 15, 56, 226, 206, 176, 226, 166, 249, 43, 110, 53, 135, 136, 252, 64, 252, 108, 112, 34, 132, 239, 253, 59, 172, 226, 163, 158, 69, 77, 223, 81, 214, 241, 175, 35, 85, 88, 233, 166, 249, 43, 110, 31, 135, 134, 252, 64, 252, 108, 112, 31, 132, 236, 253, 59, 69, 97, 51, 124, 51, 232, 221, 97, 68, 15, 56, 175, 50, 156, 225, 88, 6, 43, 110, 222, 120, 134, 252, 190, 3, 108, 112, 223, 123, 236, 253, 195, 83, 226, 163, 49, 185, 221, 228, 173, 41, 241, 175, 249, 164, 209, 228, 88, 6, 43, 110, 201, 120, 136, 252, 190, 3, 108, 112, 220, 123, 239, 253, 79, 136, 0, 0, 255, 255, 255, 191, 255, 127, 88, 6, 255, 255, 255, 191, 229, 68, 252, 52, 223, 52, 9, 222, 200, 72, 177, 68, 114, 57, 70, 224, 25, 187, 252, 52, 32, 203, 10, 222, 54, 183, 177, 68, 141, 198, 70, 224, 200, 218, 7, 127, 88, 129, 82, 255, 83, 253, 91, 113, 7, 131, 135, 254, 200, 218, 7, 127, 170, 129, 70, 255, 83, 253, 91, 113, 244, 130, 134, 254, 79, 136, 0, 0, 255, 255, 255, 191, 255, 127, 88, 6, 255, 255, 255, 191, 25, 187, 252, 52, 199, 202, 195, 221, 54, 183, 177, 68, 169, 196, 165, 223, 229, 68, 252, 52, 55, 53, 195, 221, 200, 72, 177, 68, 87, 59, 165, 223, 54, 37, 7, 127, 84, 126, 70, 255, 171, 2, 91, 113, 10, 125, 134, 254, 54, 37, 7, 127, 166, 126, 82, 255, 171, 2, 91, 113, 247, 124, 135, 254, 255, 127, 177, 175, 0, 0, 255, 191, 195, 83, 226, 163, 170, 186, 50, 222, 79, 136, 0, 0, 255, 255, 255, 191, 195, 186, 97, 51, 106, 204, 211, 221, 25, 187, 252, 52, 14, 203, 251, 221, 59, 69, 97, 51, 148, 51, 211, 221, 229, 68, 252, 52, 241, 52, 251, 221, 190, 3, 108, 112, 223, 123, 237, 253, 171, 2, 91, 113, 11, 125, 119, 254, 195, 83, 226, 163, 73, 185, 211, 227, 190, 3, 108, 112, 219, 123, 239, 253, 171, 2, 91, 113, 13, 125, 131, 254, 79, 136, 0, 0, 255, 255, 255, 191, 59, 69, 97, 51, 185, 48, 80, 224, 229, 68, 252, 52, 148, 52, 69, 222, 255, 127, 177, 175, 0, 0, 255, 191, 59, 172, 226, 163, 196, 70, 128, 228, 195, 186, 97, 51, 122, 207, 125, 224, 25, 187, 252, 52, 106, 203, 69, 222, 64, 252, 108, 112, 35, 132, 239, 253, 83, 253, 91, 113, 241, 130, 131, 254, 59, 172, 226, 163, 84, 69, 50, 222, 64, 252, 108, 112, 31, 132, 237, 253, 83, 253, 91, 113, 243, 130, 119, 254, 209, 177, 0, 0, 255, 255, 255, 191, 255, 127, 88, 6, 255, 255, 255, 191, 200, 72, 177, 68, 139, 58, 232, 223, 222, 29, 18, 57, 238, 82, 56, 229, 39, 197, 255, 255, 0, 0, 255, 191, 171, 218, 185, 199, 65, 70, 149, 230, 54, 183, 177, 68, 141, 197, 241, 223, 32, 226, 18, 57, 16, 173, 56, 229, 200, 218, 7, 127, 66, 129, 65, 255, 211, 253, 72, 136, 169, 125, 219, 254, 171, 218, 185, 199, 170, 78, 54, 230, 200, 218, 7, 127, 90, 129, 82, 255, 211, 253, 72, 136, 186, 125, 201, 254, 39, 197, 255, 255, 0, 0, 255, 191, 83, 37, 185, 199, 113, 176, 19, 230, 209, 177, 0, 0, 255, 255, 255, 191, 255, 127, 88, 6, 255, 255, 255, 191, 54, 183, 177, 68, 85, 195, 52, 223, 32, 226, 18, 57, 53, 173, 191, 236, 200, 72, 177, 68, 169, 60, 52, 223, 222, 29, 18, 57, 37, 83, 243, 235, 54, 37, 7, 127, 164, 126, 82, 255, 43, 2, 72, 136, 68, 130, 201, 254, 83, 37, 185, 199, 189, 185, 149, 230, 54, 37, 7, 127, 188, 126, 65, 255, 43, 2, 72, 136, 86, 130, 219, 254, 255, 127, 255, 127, 107, 105, 181, 244, 246, 136, 8, 247, 255, 255, 255, 191, 163, 34, 196, 94, 98, 94, 50, 239, 255, 127, 255, 127, 172, 167, 40, 236, 255, 127, 0, 0, 255, 255, 255, 191, 173, 39, 172, 167, 172, 167, 40, 236, 255, 127, 255, 127, 54, 109, 154, 246, 255, 127, 0, 0, 255, 255, 255, 191, 81, 216, 172, 167, 82, 88, 40, 236, 255, 127, 255, 127, 108, 162, 200, 238, 91, 221, 196, 94, 152, 161, 49, 239, 8, 119, 8, 247, 255, 255, 255, 191, 255, 127, 255, 127, 171, 98, 84, 241, 233, 130, 0, 0, 233, 255, 255, 191, 238, 186, 180, 137, 189, 109, 194, 246, 255, 127, 255, 127, 121, 146, 193, 246, 233, 130, 0, 0, 250, 214, 135, 191, 16, 69, 180, 137, 20, 146, 171, 248, 255, 127, 255, 127, 94, 159, 79, 240, 8, 119, 8, 247, 255, 255, 255, 191, 16, 69, 180, 137, 65, 146, 194, 246, 255, 127, 255, 127, 133, 109, 193, 246, 246, 136, 8, 247, 255, 255, 255, 191, 238, 186, 180, 137, 244, 146, 182, 255, 246, 136, 8, 247, 239, 45, 22, 195, 8, 119, 8, 247, 4, 254, 37, 13, 255, 127, 255, 127, 157, 96, 78, 240, 255, 127, 255, 255, 0, 0, 88, 202, 53, 76, 14, 121, 37, 114, 148, 248, 255, 127, 255, 127, 73, 141, 90, 249, 255, 127, 255, 255, 202, 23, 255, 191, 201, 179, 14, 121, 70, 138, 253, 248, 255, 127, 255, 127, 181, 114, 90, 249, 255, 127, 0, 0, 255, 255, 255, 191, 53, 76, 14, 121, 101, 124, 193, 249, 255, 127, 255, 127, 238, 163, 7, 238, 255, 127, 0, 0, 255, 255, 255, 191, 201, 179, 14, 121, 217, 141, 148, 248, 255, 127, 0, 0, 255, 255, 255, 191, 255, 127, 0, 0, 255, 255, 255, 191, 255, 127, 255, 127, 21, 134, 244, 252, 233, 130, 0, 0, 179, 254, 112, 220, 238, 186, 180, 137, 229, 109, 61, 247, 255, 127, 255, 127, 96, 131, 78, 254, 233, 130, 0, 0, 20, 218, 144, 191, 16, 69, 180, 137, 20, 146, 188, 248, 255, 127, 255, 127, 138, 125, 196, 254, 16, 69, 180, 137, 49, 146, 214, 246, 255, 127, 255, 127, 16, 127, 135, 255, 238, 186, 180, 137, 253, 148, 31, 255, 255, 127, 255, 127, 84, 117, 169, 250, 255, 127, 255, 255, 0, 0, 34, 208, 53, 76, 14, 121, 187, 114, 165, 248, 255, 127, 255, 127, 167, 120, 83, 252, 255, 127, 255, 255, 81, 30, 255, 191, 201, 179, 14, 121, 23, 138, 2, 249, 255, 127, 255, 127, 224, 131, 14, 254, 53, 76, 14, 121, 113, 125, 223, 249, 255, 127, 255, 127, 227, 132, 141, 253, 201, 179, 14, 121, 165, 141, 154, 248, 255, 127, 215, 67, 255, 255, 255, 191, 255, 127, 16, 0, 255, 255, 255, 191, 37, 50, 76, 101, 160, 95, 122, 239, 143, 28, 238, 100, 150, 100, 78, 242, 52, 211, 0, 0, 254, 255, 255, 191, 233, 130, 0, 0, 255, 255, 255, 191, 37, 26, 39, 68, 105, 92, 154, 237, 163, 34, 196, 94, 102, 94, 48, 239, 52, 211, 0, 0, 254, 255, 255, 191, 233, 130, 0, 0, 142, 252, 245, 191, 217, 229, 39, 68, 247, 163, 170, 238, 91, 221, 196, 94, 153, 161, 50, 239, 255, 127, 215, 67, 255, 255, 255, 191, 255, 127, 16, 0, 255, 255, 255, 191, 217, 205, 76, 101, 64, 161, 169, 239, 111, 227, 238, 100, 110, 155, 79, 242, 246, 136, 8, 247, 255, 255, 255, 191, 37, 26, 39, 68, 7, 92, 170, 238, 163, 34, 196, 94, 101, 94, 50, 239, 246, 136, 8, 247, 217, 41, 61, 195, 37, 50, 76, 101, 178, 94, 169, 239, 143, 28, 238, 100, 144, 100, 79, 242, 217, 205, 76, 101, 94, 160, 122, 239, 111, 227, 238, 100, 104, 155, 78, 242, 8, 119, 8, 247, 197, 252, 106, 21, 217, 229, 39, 68, 151, 163, 141, 237, 91, 221, 196, 94, 152, 161, 48, 239, 8, 119, 8, 247, 255, 255, 255, 191, 246, 136, 8, 247, 255, 255, 255, 191, 246, 136, 8, 247, 31, 44, 39, 195, 8, 119, 8, 247, 255, 255, 255, 191, 8, 119, 8, 247, 237, 251, 255, 26, 255, 127, 215, 67, 255, 255, 255, 191, 37, 50, 76, 101, 153, 95, 126, 239, 52, 211, 0, 0, 254, 255, 255, 191, 37, 26, 39, 68, 99, 92, 114, 237, 52, 211, 0, 0, 255, 255, 255, 191, 217, 229, 39, 68, 238, 163, 163, 238, 255, 127, 215, 67, 255, 255, 255, 191, 217, 205, 76, 101, 99, 161, 169, 239, 246, 136, 8, 247, 97, 35, 124, 195, 37, 26, 39, 68, 24, 92, 156, 238, 246, 136, 8, 247, 1, 37, 108, 195, 37, 50, 76, 101, 155, 94, 169, 239, 217, 205, 76, 101, 105, 160, 128, 239, 8, 119, 8, 247, 192, 251, 42, 28, 217, 229, 39, 68, 155, 163, 114, 237, 8, 119, 8, 247, 255, 255, 255, 191, 246, 136, 8, 247, 62, 202, 203, 66, 246, 136, 8, 247, 147, 39, 83, 195, 8, 119, 8, 247, 140, 250, 44, 36, 8, 119, 8, 247, 198, 250, 168, 34, 79, 136, 0, 0, 68, 209, 48, 232, 128, 161, 255, 255, 0, 0, 255, 191, 211, 209, 202, 46, 255, 255, 255, 191, 255, 127, 16, 0, 255, 255, 255, 191, 116, 168, 255, 255, 0, 0, 255, 191, 171, 218, 185, 199, 0, 0, 255, 191, 207, 50, 84, 71, 213, 45, 238, 228, 136, 216, 0, 0, 27, 138, 240, 250, 191, 225, 81, 89, 169, 212, 165, 230, 120, 224, 126, 171, 124, 81, 200, 229, 78, 218, 56, 91, 36, 172, 65, 233, 33, 25, 113, 110, 17, 82, 86, 238, 255, 127, 167, 252, 0, 0, 255, 191, 183, 71, 173, 166, 136, 186, 178, 222, 37, 164, 215, 31, 255, 255, 255, 191, 255, 127, 167, 252, 0, 0, 255, 191, 235, 219, 111, 85, 142, 207, 143, 224, 71, 184, 173, 166, 158, 69, 77, 223, 64, 252, 108, 112, 158, 69, 77, 223, 128, 29, 163, 91, 124, 51, 232, 221, 48, 29, 45, 165, 49, 185, 221, 228, 254, 4, 45, 125, 73, 185, 211, 227, 161, 159, 63, 167, 0, 0, 255, 191, 201, 252, 227, 112, 84, 69, 50, 222, 32, 154, 240, 20, 255, 255, 255, 191, 32, 93, 103, 28, 255, 255, 255, 191, 23, 52, 222, 70, 139, 58, 232, 223, 55, 205, 233, 234, 0, 0, 255, 191, 28, 231, 179, 170, 170, 78, 54, 230, 9, 19, 10, 155, 189, 185, 149, 230, 140, 102, 48, 132, 94, 159, 79, 240, 16, 69, 180, 137, 96, 131, 78, 254, 115, 92, 218, 133, 138, 125, 196, 254, 221, 158, 220, 123, 227, 132, 141, 253, 217, 229, 39, 68, 254, 255, 255, 191, 255, 127, 16, 0, 255, 255, 255, 191, 217, 229, 39, 68, 255, 255, 255, 191, 255, 127, 16, 0, 255, 255, 255, 191)
+}, {
+"aabb": AABB(-0.288198, -0.267709, -0.867179, 0.576397, 0.671243, 1.82663),
+"attribute_data": PackedByteArray(0, 0, 0, 62, 60, 188, 199, 62, 0, 0, 96, 63, 58, 188, 199, 62, 0, 0, 192, 62, 60, 188, 199, 62, 0, 0, 32, 63, 60, 188, 199, 62, 170, 170, 10, 63, 60, 188, 199, 62, 170, 170, 234, 62, 60, 188, 199, 62, 170, 170, 10, 63, 227, 33, 92, 63, 170, 170, 234, 62, 227, 33, 92, 63, 1, 0, 32, 63, 42, 205, 213, 62, 1, 0, 32, 63, 42, 205, 213, 62, 1, 0, 32, 63, 42, 205, 213, 62, 1, 0, 32, 63, 42, 205, 213, 62, 170, 170, 10, 63, 44, 205, 213, 62, 170, 170, 10, 63, 44, 205, 213, 62, 0, 0, 96, 63, 44, 205, 213, 62, 0, 0, 96, 63, 44, 205, 213, 62, 0, 0, 32, 63, 106, 25, 85, 63, 0, 0, 32, 63, 106, 25, 85, 63, 170, 170, 10, 63, 106, 25, 85, 63, 170, 170, 10, 63, 106, 25, 85, 63, 0, 0, 96, 63, 42, 194, 225, 62, 0, 0, 32, 63, 234, 30, 79, 63, 170, 170, 10, 63, 234, 30, 79, 63, 1, 0, 32, 63, 44, 194, 225, 62, 1, 0, 32, 63, 44, 194, 225, 62, 170, 170, 10, 63, 44, 194, 225, 62, 0, 0, 32, 63, 56, 247, 204, 62, 0, 0, 32, 63, 56, 247, 204, 62, 170, 170, 10, 63, 56, 247, 204, 62, 1, 0, 96, 63, 54, 247, 204, 62, 0, 0, 32, 63, 101, 132, 89, 63, 170, 170, 10, 63, 101, 132, 89, 63, 170, 170, 234, 62, 172, 170, 42, 62, 170, 170, 234, 62, 176, 170, 170, 61, 170, 170, 234, 62, 76, 11, 52, 63, 170, 170, 234, 62, 181, 244, 11, 63, 170, 170, 10, 63, 168, 170, 42, 62, 170, 170, 10, 63, 176, 170, 170, 61, 170, 170, 10, 63, 76, 11, 52, 63, 170, 170, 10, 63, 181, 244, 11, 63, 170, 170, 234, 62, 0, 72, 36, 62, 170, 170, 234, 62, 0, 72, 36, 62, 170, 170, 234, 62, 16, 112, 183, 61, 170, 170, 234, 62, 16, 112, 183, 61, 170, 170, 234, 62, 89, 11, 49, 63, 170, 170, 234, 62, 89, 11, 49, 63, 170, 170, 234, 62, 168, 244, 14, 63, 170, 170, 234, 62, 168, 244, 14, 63, 170, 170, 10, 63, 72, 142, 32, 62, 170, 170, 10, 63, 72, 142, 32, 62, 170, 170, 10, 63, 112, 227, 190, 61, 170, 170, 10, 63, 112, 227, 190, 61, 170, 170, 10, 63, 83, 75, 47, 63, 170, 170, 10, 63, 83, 75, 47, 63, 170, 170, 10, 63, 174, 180, 16, 63, 170, 170, 10, 63, 174, 180, 16, 63, 170, 170, 234, 62, 88, 143, 52, 62, 169, 170, 234, 62, 80, 22, 196, 62, 170, 170, 234, 62, 80, 10, 101, 62, 168, 170, 234, 62, 46, 97, 179, 62, 170, 170, 234, 62, 8, 48, 56, 62, 170, 170, 234, 62, 8, 48, 56, 62, 170, 170, 234, 62, 64, 214, 194, 62, 170, 170, 234, 62, 64, 214, 194, 62, 170, 170, 234, 62, 152, 105, 97, 62, 170, 170, 234, 62, 152, 105, 97, 62, 170, 170, 234, 62, 60, 161, 180, 62, 170, 170, 234, 62, 60, 161, 180, 62, 0, 0, 0, 62, 128, 123, 161, 62, 255, 255, 191, 62, 65, 66, 111, 63, 0, 0, 0, 62, 220, 217, 139, 62, 0, 0, 192, 62, 19, 19, 122, 63, 0, 0, 192, 62, 220, 217, 139, 62, 0, 0, 192, 62, 220, 217, 139, 62, 0, 0, 192, 62, 128, 123, 161, 62, 0, 0, 192, 62, 128, 123, 161, 62, 170, 170, 234, 62, 19, 19, 122, 63, 170, 170, 234, 62, 64, 66, 111, 63, 170, 170, 234, 62, 128, 123, 161, 62, 170, 170, 234, 62, 220, 217, 139, 62, 170, 170, 234, 62, 104, 54, 62, 62, 169, 170, 234, 62, 184, 210, 190, 62, 170, 170, 234, 62, 16, 245, 106, 62, 168, 170, 234, 62, 28, 39, 176, 62, 170, 170, 234, 62, 136, 143, 65, 62, 170, 170, 234, 62, 136, 143, 65, 62, 169, 170, 234, 62, 174, 185, 189, 62, 169, 170, 234, 62, 174, 185, 189, 62, 168, 170, 234, 62, 236, 155, 103, 62, 168, 170, 234, 62, 236, 155, 103, 62, 170, 170, 234, 62, 36, 64, 177, 62, 170, 170, 234, 62, 36, 64, 177, 62, 0, 0, 32, 63, 56, 247, 204, 62, 170, 170, 234, 62, 0, 72, 36, 62, 170, 170, 234, 62, 16, 112, 183, 61, 170, 170, 234, 62, 89, 11, 49, 63, 170, 170, 234, 62, 168, 244, 14, 63),
+"format": 34359742487,
+"index_count": 180,
+"index_data": PackedByteArray(31, 0, 17, 0, 30, 0, 31, 0, 19, 0, 17, 0, 25, 0, 10, 0, 24, 0, 25, 0, 12, 0, 10, 0, 23, 0, 15, 0, 20, 0, 23, 0, 9, 0, 15, 0, 18, 0, 21, 0, 16, 0, 18, 0, 22, 0, 21, 0, 13, 0, 27, 0, 11, 0, 13, 0, 28, 0, 27, 0, 8, 0, 29, 0, 14, 0, 8, 0, 26, 0, 29, 0, 3, 0, 0, 0, 1, 0, 3, 0, 2, 0, 0, 0, 3, 0, 5, 0, 2, 0, 3, 0, 4, 0, 5, 0, 0, 0, 6, 0, 1, 0, 0, 0, 7, 0, 6, 0, 67, 0, 35, 0, 59, 0, 67, 0, 47, 0, 35, 0, 49, 0, 39, 0, 55, 0, 49, 0, 36, 0, 39, 0, 57, 0, 45, 0, 63, 0, 57, 0, 34, 0, 45, 0, 62, 0, 46, 0, 66, 0, 62, 0, 44, 0, 46, 0, 37, 0, 53, 0, 38, 0, 37, 0, 51, 0, 53, 0, 50, 0, 54, 0, 52, 0, 50, 0, 48, 0, 54, 0, 42, 0, 64, 0, 40, 0, 42, 0, 60, 0, 64, 0, 86, 0, 66, 0, 90, 0, 86, 0, 62, 0, 66, 0, 33, 0, 61, 0, 43, 0, 33, 0, 56, 0, 61, 0, 81, 0, 63, 0, 87, 0, 81, 0, 57, 0, 63, 0, 41, 0, 58, 0, 32, 0, 41, 0, 65, 0, 58, 0, 91, 0, 59, 0, 83, 0, 91, 0, 67, 0, 59, 0, 68, 0, 72, 0, 74, 0, 68, 0, 70, 0, 72, 0, 71, 0, 77, 0, 76, 0, 71, 0, 69, 0, 77, 0, 75, 0, 79, 0, 78, 0, 75, 0, 73, 0, 79, 0, 65, 0, 82, 0, 58, 0, 65, 0, 89, 0, 82, 0, 89, 0, 83, 0, 82, 0, 89, 0, 91, 0, 83, 0, 56, 0, 85, 0, 61, 0, 56, 0, 80, 0, 85, 0, 80, 0, 87, 0, 85, 0, 80, 0, 81, 0, 87, 0, 60, 0, 88, 0, 64, 0, 60, 0, 84, 0, 88, 0, 84, 0, 90, 0, 88, 0, 84, 0, 86, 0, 90, 0),
+"lods": [0.0081632, PackedByteArray(31, 0, 16, 0, 30, 0, 31, 0, 18, 0, 16, 0, 18, 0, 21, 0, 16, 0, 18, 0, 22, 0, 21, 0, 25, 0, 92, 0, 23, 0, 25, 0, 12, 0, 92, 0, 12, 0, 28, 0, 92, 0, 23, 0, 92, 0, 14, 0, 92, 0, 29, 0, 14, 0, 23, 0, 14, 0, 20, 0, 3, 0, 0, 0, 1, 0, 0, 0, 6, 0, 1, 0, 0, 0, 7, 0, 6, 0, 3, 0, 2, 0, 0, 0, 3, 0, 5, 0, 2, 0, 3, 0, 4, 0, 5, 0, 96, 0, 35, 0, 59, 0, 93, 0, 96, 0, 59, 0, 93, 0, 59, 0, 83, 0, 93, 0, 83, 0, 82, 0, 93, 0, 82, 0, 58, 0, 93, 0, 58, 0, 32, 0, 95, 0, 96, 0, 93, 0, 94, 0, 95, 0, 93, 0, 33, 0, 56, 0, 94, 0, 56, 0, 95, 0, 94, 0, 56, 0, 80, 0, 95, 0, 80, 0, 81, 0, 95, 0, 81, 0, 57, 0, 95, 0, 57, 0, 34, 0, 95, 0, 48, 0, 39, 0, 54, 0, 48, 0, 36, 0, 39, 0, 50, 0, 48, 0, 54, 0, 50, 0, 54, 0, 52, 0, 37, 0, 50, 0, 52, 0, 37, 0, 52, 0, 38, 0, 68, 0, 72, 0, 74, 0, 68, 0, 70, 0, 72, 0, 71, 0, 77, 0, 76, 0, 71, 0, 69, 0, 77, 0, 75, 0, 79, 0, 78, 0, 75, 0, 73, 0, 79, 0)],
+"material": SubResource("StandardMaterial3D_mfe6r"),
+"name": "secmat",
+"primitive": 3,
+"uv_scale": Vector4(0, 0, 0, 0),
+"vertex_count": 97,
+"vertex_data": PackedByteArray(71, 59, 50, 190, 38, 17, 137, 190, 93, 9, 195, 190, 83, 115, 219, 189, 16, 156, 206, 62, 92, 9, 195, 190, 71, 59, 50, 62, 38, 17, 137, 190, 93, 9, 195, 190, 83, 115, 219, 61, 16, 156, 206, 62, 92, 9, 195, 190, 116, 66, 99, 62, 230, 146, 23, 62, 92, 9, 195, 190, 173, 252, 106, 62, 104, 202, 210, 188, 93, 9, 195, 190, 116, 66, 99, 190, 230, 146, 23, 62, 92, 9, 195, 190, 173, 252, 106, 190, 104, 202, 210, 188, 93, 9, 195, 190, 248, 227, 137, 61, 18, 240, 143, 62, 77, 154, 35, 191, 248, 227, 137, 61, 18, 240, 143, 62, 77, 154, 35, 191, 248, 227, 137, 61, 18, 240, 143, 62, 77, 154, 35, 191, 248, 227, 137, 61, 18, 240, 143, 62, 77, 154, 35, 191, 186, 142, 147, 62, 36, 135, 4, 62, 114, 205, 36, 191, 186, 142, 147, 62, 36, 135, 4, 62, 114, 205, 36, 191, 248, 227, 137, 189, 18, 240, 143, 62, 77, 154, 35, 191, 248, 227, 137, 189, 18, 240, 143, 62, 77, 154, 35, 191, 248, 227, 137, 189, 18, 240, 143, 62, 77, 154, 35, 191, 248, 227, 137, 189, 18, 240, 143, 62, 77, 154, 35, 191, 186, 142, 147, 190, 36, 135, 4, 62, 114, 205, 36, 191, 186, 142, 147, 190, 36, 135, 4, 62, 114, 205, 36, 191, 24, 55, 140, 189, 76, 200, 145, 62, 55, 199, 91, 191, 24, 55, 140, 189, 76, 200, 145, 62, 55, 199, 91, 191, 115, 248, 135, 190, 240, 172, 232, 61, 115, 255, 93, 191, 24, 55, 140, 61, 76, 200, 145, 62, 55, 199, 91, 191, 24, 55, 140, 61, 76, 200, 145, 62, 55, 199, 91, 191, 115, 248, 135, 62, 240, 172, 232, 61, 115, 255, 93, 191, 154, 128, 140, 61, 72, 216, 148, 62, 193, 46, 244, 190, 154, 128, 140, 61, 72, 216, 148, 62, 193, 46, 244, 190, 88, 202, 98, 62, 226, 125, 16, 62, 45, 19, 245, 190, 154, 128, 140, 189, 72, 216, 148, 62, 193, 46, 244, 190, 154, 128, 140, 189, 72, 216, 148, 62, 193, 46, 244, 190, 88, 202, 98, 190, 226, 125, 16, 62, 45, 19, 245, 190, 3, 181, 171, 61, 76, 82, 22, 189, 213, 158, 117, 63, 3, 181, 171, 189, 76, 82, 22, 189, 213, 158, 117, 63, 3, 181, 171, 189, 76, 82, 22, 189, 93, 9, 195, 190, 3, 181, 171, 61, 76, 82, 22, 189, 93, 9, 195, 190, 141, 195, 133, 61, 230, 146, 23, 62, 213, 158, 117, 63, 141, 195, 133, 189, 230, 146, 23, 62, 213, 158, 117, 63, 141, 195, 133, 189, 230, 146, 23, 62, 92, 9, 195, 190, 141, 195, 133, 61, 230, 146, 23, 62, 92, 9, 195, 190, 144, 157, 145, 61, 22, 52, 67, 189, 53, 24, 115, 63, 144, 157, 145, 61, 22, 52, 67, 189, 53, 24, 115, 63, 144, 157, 145, 189, 22, 52, 67, 189, 53, 24, 115, 63, 144, 157, 145, 189, 22, 52, 67, 189, 53, 24, 115, 63, 144, 157, 145, 189, 22, 52, 67, 189, 93, 9, 195, 190, 144, 157, 145, 189, 22, 52, 67, 189, 93, 9, 195, 190, 144, 157, 145, 61, 22, 52, 67, 189, 93, 9, 195, 190, 144, 157, 145, 61, 22, 52, 67, 189, 93, 9, 195, 190, 149, 33, 76, 61, 176, 216, 30, 62, 137, 243, 113, 63, 149, 33, 76, 61, 176, 216, 30, 62, 137, 243, 113, 63, 149, 33, 76, 189, 176, 216, 30, 62, 137, 243, 113, 63, 149, 33, 76, 189, 176, 216, 30, 62, 137, 243, 113, 63, 149, 33, 76, 189, 176, 216, 30, 62, 92, 9, 195, 190, 149, 33, 76, 189, 176, 216, 30, 62, 92, 9, 195, 190, 149, 33, 76, 61, 176, 216, 30, 62, 92, 9, 195, 190, 149, 33, 76, 61, 176, 216, 30, 62, 92, 9, 195, 190, 3, 181, 171, 189, 76, 82, 22, 189, 77, 38, 66, 63, 3, 181, 171, 189, 76, 82, 22, 189, 67, 138, 159, 62, 3, 181, 171, 61, 76, 82, 22, 189, 77, 38, 66, 63, 3, 181, 171, 61, 76, 82, 22, 189, 67, 138, 159, 62, 144, 157, 145, 189, 22, 52, 67, 189, 173, 0, 64, 63, 144, 157, 145, 189, 22, 52, 67, 189, 173, 0, 64, 63, 144, 157, 145, 189, 22, 52, 67, 189, 23, 238, 156, 62, 144, 157, 145, 189, 22, 52, 67, 189, 23, 238, 156, 62, 144, 157, 145, 61, 22, 52, 67, 189, 173, 0, 64, 63, 144, 157, 145, 61, 22, 52, 67, 189, 173, 0, 64, 63, 144, 157, 145, 61, 22, 52, 67, 189, 23, 238, 156, 62, 144, 157, 145, 61, 22, 52, 67, 189, 23, 238, 156, 62, 204, 17, 24, 190, 166, 26, 120, 190, 211, 235, 170, 62, 204, 17, 24, 190, 166, 26, 120, 190, 211, 235, 170, 62, 75, 170, 11, 190, 53, 229, 119, 190, 173, 238, 60, 63, 75, 170, 11, 190, 53, 229, 119, 190, 173, 238, 60, 63, 75, 170, 11, 62, 53, 229, 119, 190, 173, 238, 60, 63, 75, 170, 11, 62, 53, 229, 119, 190, 173, 238, 60, 63, 204, 17, 24, 62, 166, 26, 120, 190, 211, 235, 170, 62, 204, 17, 24, 62, 166, 26, 120, 190, 211, 235, 170, 62, 173, 252, 106, 190, 104, 202, 210, 188, 173, 238, 60, 63, 173, 252, 106, 190, 104, 202, 210, 188, 207, 235, 170, 62, 173, 252, 106, 62, 104, 202, 210, 188, 207, 235, 170, 62, 173, 252, 106, 62, 104, 202, 210, 188, 173, 238, 60, 63, 3, 181, 171, 189, 76, 82, 22, 189, 173, 238, 60, 63, 3, 181, 171, 189, 76, 82, 22, 189, 207, 235, 170, 62, 3, 181, 171, 61, 76, 82, 22, 189, 173, 238, 60, 63, 3, 181, 171, 61, 76, 82, 22, 189, 207, 235, 170, 62, 144, 157, 145, 189, 22, 52, 67, 189, 225, 210, 58, 63, 144, 157, 145, 189, 22, 52, 67, 189, 225, 210, 58, 63, 144, 157, 145, 189, 22, 52, 67, 189, 51, 58, 168, 62, 144, 157, 145, 189, 22, 52, 67, 189, 51, 58, 168, 62, 144, 157, 145, 61, 22, 52, 67, 189, 225, 210, 58, 63, 144, 157, 145, 61, 22, 52, 67, 189, 225, 210, 58, 63, 144, 157, 145, 61, 22, 52, 67, 189, 51, 58, 168, 62, 144, 157, 145, 61, 22, 52, 67, 189, 51, 58, 168, 62, 154, 128, 140, 61, 72, 216, 148, 62, 193, 46, 244, 190, 144, 157, 145, 61, 22, 52, 67, 189, 53, 24, 115, 63, 144, 157, 145, 189, 22, 52, 67, 189, 53, 24, 115, 63, 144, 157, 145, 189, 22, 52, 67, 189, 93, 9, 195, 190, 144, 157, 145, 61, 22, 52, 67, 189, 93, 9, 195, 190, 255, 127, 255, 127, 135, 137, 196, 4, 255, 127, 255, 127, 153, 139, 204, 5, 255, 127, 255, 127, 255, 255, 255, 63, 255, 127, 255, 127, 201, 165, 25, 237, 255, 127, 255, 127, 255, 255, 255, 63, 255, 127, 255, 127, 255, 255, 255, 63, 255, 127, 255, 127, 65, 139, 161, 5, 255, 127, 255, 127, 83, 133, 169, 2, 36, 135, 255, 255, 0, 0, 255, 191, 255, 127, 238, 253, 0, 0, 255, 191, 63, 186, 70, 202, 29, 57, 95, 219, 76, 182, 53, 195, 61, 58, 19, 220, 63, 186, 70, 202, 125, 56, 148, 219, 76, 182, 53, 195, 175, 57, 90, 220, 36, 135, 255, 255, 0, 0, 255, 191, 255, 127, 238, 253, 0, 0, 255, 191, 191, 69, 70, 202, 129, 199, 148, 219, 178, 73, 53, 195, 84, 198, 92, 220, 191, 69, 70, 202, 222, 198, 94, 219, 178, 73, 53, 195, 193, 197, 19, 220, 255, 127, 238, 253, 0, 0, 255, 191, 191, 69, 70, 202, 255, 198, 104, 219, 191, 69, 70, 202, 93, 198, 51, 219, 255, 127, 238, 253, 0, 0, 255, 191, 63, 186, 70, 202, 161, 57, 51, 219, 63, 186, 70, 202, 253, 56, 105, 219, 36, 135, 255, 255, 0, 0, 255, 191, 76, 182, 53, 195, 227, 57, 64, 220, 76, 182, 53, 195, 62, 57, 146, 220, 36, 135, 255, 255, 0, 0, 255, 191, 178, 73, 53, 195, 192, 198, 146, 220, 178, 73, 53, 195, 24, 198, 62, 220, 208, 68, 207, 196, 255, 255, 255, 191, 46, 187, 207, 196, 255, 255, 255, 191, 46, 187, 207, 196, 100, 227, 94, 111, 208, 68, 207, 196, 18, 68, 58, 166, 188, 87, 67, 40, 255, 255, 255, 191, 66, 168, 67, 40, 255, 255, 255, 191, 66, 168, 67, 40, 255, 255, 255, 191, 188, 87, 67, 40, 255, 255, 255, 191, 255, 127, 255, 255, 255, 255, 255, 191, 208, 68, 207, 196, 255, 255, 255, 191, 255, 127, 255, 255, 255, 255, 255, 191, 46, 187, 207, 196, 255, 255, 255, 191, 255, 127, 255, 255, 255, 255, 255, 191, 46, 187, 207, 196, 47, 227, 63, 111, 255, 127, 255, 255, 255, 255, 255, 191, 208, 68, 207, 196, 255, 255, 255, 191, 255, 127, 0, 0, 255, 255, 255, 191, 188, 87, 67, 40, 255, 255, 255, 191, 255, 127, 0, 0, 255, 255, 255, 191, 66, 168, 67, 40, 255, 255, 255, 191, 255, 127, 0, 0, 255, 255, 255, 191, 66, 168, 67, 40, 255, 255, 255, 191, 255, 127, 0, 0, 255, 255, 255, 191, 188, 87, 67, 40, 255, 255, 255, 191, 46, 187, 207, 196, 255, 255, 255, 191, 46, 187, 207, 196, 251, 214, 39, 104, 208, 68, 207, 196, 255, 255, 255, 191, 208, 68, 207, 196, 11, 67, 200, 165, 255, 127, 255, 255, 0, 0, 109, 215, 46, 187, 207, 196, 255, 255, 255, 191, 255, 127, 255, 255, 255, 255, 183, 44, 46, 187, 207, 196, 214, 214, 18, 104, 255, 127, 255, 255, 194, 193, 255, 63, 208, 68, 207, 196, 10, 166, 163, 47, 255, 127, 255, 255, 255, 255, 18, 41, 208, 68, 207, 196, 169, 67, 13, 166, 255, 127, 16, 0, 255, 255, 255, 191, 173, 37, 174, 91, 77, 91, 161, 237, 255, 127, 16, 0, 255, 255, 255, 191, 173, 37, 174, 91, 72, 91, 165, 237, 255, 127, 16, 0, 255, 255, 255, 191, 81, 218, 174, 91, 186, 164, 165, 237, 255, 127, 16, 0, 255, 255, 255, 191, 81, 218, 174, 91, 179, 164, 163, 237, 173, 37, 174, 91, 68, 91, 165, 237, 173, 37, 174, 91, 75, 91, 163, 237, 81, 218, 174, 91, 177, 164, 161, 237, 81, 218, 174, 91, 182, 164, 165, 237, 46, 187, 207, 196, 116, 79, 223, 212, 46, 187, 207, 196, 116, 79, 223, 212, 208, 68, 207, 196, 198, 165, 193, 47, 208, 68, 207, 196, 180, 64, 199, 164, 255, 127, 255, 255, 255, 255, 107, 46, 46, 187, 207, 196, 59, 199, 255, 94, 255, 127, 255, 255, 255, 255, 90, 45, 46, 187, 207, 196, 116, 79, 223, 212, 255, 127, 255, 255, 0, 0, 82, 221, 208, 68, 207, 196, 78, 64, 155, 164, 255, 127, 255, 255, 255, 255, 32, 43, 208, 68, 207, 196, 88, 65, 13, 165, 85, 166, 46, 217, 0, 0, 255, 191, 228, 81, 227, 209, 255, 255, 255, 191, 176, 171, 77, 212, 255, 255, 255, 191, 26, 174, 227, 209, 255, 255, 255, 191, 78, 84, 77, 212, 255, 255, 255, 191)
+}]
+blend_shape_mode = 0
[node name="SpaceGun" type="Node3D" node_paths=PackedStringArray("anim_player")]
script = ExtResource("1_6sm4s")
+_PROJECTILE = ExtResource("2_knhfa")
+inheritance = 0.5
anim_player = NodePath("Mesh/AnimationPlayer")
-
-[node name="ProjectileSpawner" type="Marker3D" parent="."]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0.27)
-script = ExtResource("3_ihk6g")
-PROJECTILE = ExtResource("2_wvneg")
+ammo = 24
+max_ammo = 24
+ammo_usage = 1
[node name="Mesh" parent="." instance=ExtResource("3_5k2xm")]
[node name="Skeleton3D" parent="Mesh/Armature" index="0"]
+bones/0/position = Vector3(-2.3537e-15, -0.454934, -1.09865)
bones/0/rotation = Quaternion(0.707107, -5.33851e-08, -5.33851e-08, 0.707107)
bones/0/scale = Vector3(1, 1, 1)
bones/1/rotation = Quaternion(-0.707107, 5.33851e-08, 5.33851e-08, 0.707107)
@@ -27,17 +70,24 @@ bones/3/rotation = Quaternion(-0.707107, 5.33851e-08, 5.33851e-08, 0.707107)
bones/3/scale = Vector3(1, 1, 1)
[node name="grip" parent="Mesh/Armature/Skeleton3D" index="0"]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2.84217e-14, 1.19209e-07, 2.38419e-07)
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2.6068e-14, -1.19209e-07, 9.53674e-07)
[node name="main" parent="Mesh/Armature/Skeleton3D" index="1"]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2.84217e-14, 1.19209e-07, 2.38419e-07)
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2.6068e-14, -1.19209e-07, 9.53674e-07)
+
+[node name="main" parent="Mesh/Armature/Skeleton3D/main" index="0"]
+mesh = SubResource("ArrayMesh_jl80g")
[node name="sides" parent="Mesh/Armature/Skeleton3D" index="2"]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2.84217e-14, 1.19209e-07, 2.38419e-07)
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2.6068e-14, -1.19209e-07, 9.53674e-07)
[node name="AnimationPlayer" parent="Mesh" index="1"]
autoplay = "idle"
+[node name="Nozzle" type="Marker3D" parent="."]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.0135887, -0.0108357)
+
+[connection signal="triggered" from="." to="." method="_on_triggered"]
[connection signal="visibility_changed" from="." to="." method="_on_visibility_changed"]
[editable path="Mesh"]
diff --git a/entities/weapons/weapon.gd b/entities/weapons/weapon.gd
new file mode 100644
index 0000000..df22d6f
--- /dev/null
+++ b/entities/weapons/weapon.gd
@@ -0,0 +1,64 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+## This class defines a [Weapon].
+class_name Weapon extends Node3D
+
+## Emitted after [member Weapon.trigger] is called.
+signal triggered
+
+## Emitted when [member ammo] is changed (useful for [HUD] display).
+signal ammo_changed(new_ammo: int)
+
+# enum WeaponState { EQUIPPED, UNEQUIPPED }
+
+## The ammunition count.
+@export_range(0, 9223372036854775295) var ammo: int:
+ set = set_ammo
+
+@rpc("authority", "call_local", "reliable")
+func set_ammo(new_ammo: int) -> void:
+ ammo = new_ammo
+ ammo_changed.emit(new_ammo)
+
+## The maxmimum ammunition count.
+@export_range(0, 9223372036854775295) var max_ammo: int
+## The number of ammo used when [member triggered] is emitted.
+@export_range(0, 9223372036854775295) var ammo_usage: int
+## The cooldown until this [Weapon] can be triggered again.
+@export var cooldown: float = 1.
+
+## The internal timer to handle trigger cooldown
+var _cooldown_timer: Timer
+
+func _init() -> void:
+ _cooldown_timer = Timer.new()
+ _cooldown_timer.one_shot = true
+ _cooldown_timer.wait_time = cooldown
+ add_child(_cooldown_timer)
+
+## This methods triggers the [Weapon].
+func trigger() -> void:
+ if not _cooldown_timer.is_stopped() or ammo < ammo_usage:
+ return
+ # deduct ammo_usage from ammo count
+ ammo -= ammo_usage
+ # start cooldown timer
+ _cooldown_timer.start(cooldown)
+ # emit triggered signal
+ triggered.emit()
+
+# primary action handler
+func _on_primary(pressed: bool) -> void:
+ if pressed: trigger()
diff --git a/environments/skyboxes/kloppenheim_06_puresky_2k.exr b/environments/skyboxes/kloppenheim_06_puresky_2k.exr
deleted file mode 100644
index e139b22..0000000
Binary files a/environments/skyboxes/kloppenheim_06_puresky_2k.exr and /dev/null differ
diff --git a/environments/skyboxes/kloppenheim_06_puresky_2k.exr.import b/environments/skyboxes/kloppenheim_06_puresky_2k.exr.import
deleted file mode 100644
index 9c1dd8d..0000000
--- a/environments/skyboxes/kloppenheim_06_puresky_2k.exr.import
+++ /dev/null
@@ -1,35 +0,0 @@
-[remap]
-
-importer="texture"
-type="CompressedTexture2D"
-uid="uid://btdbu0qbe1646"
-path.bptc="res://.godot/imported/kloppenheim_06_puresky_2k.exr-ec828ffc9b05b2c03fb35e84cd230d2d.bptc.ctex"
-metadata={
-"imported_formats": ["s3tc_bptc"],
-"vram_texture": true
-}
-
-[deps]
-
-source_file="res://environments/skyboxes/kloppenheim_06_puresky_2k.exr"
-dest_files=["res://.godot/imported/kloppenheim_06_puresky_2k.exr-ec828ffc9b05b2c03fb35e84cd230d2d.bptc.ctex"]
-
-[params]
-
-compress/mode=2
-compress/high_quality=true
-compress/lossy_quality=0.7
-compress/hdr_compression=1
-compress/normal_map=0
-compress/channel_pack=0
-mipmaps/generate=false
-mipmaps/limit=-1
-roughness/mode=0
-roughness/src_normal=""
-process/fix_alpha_border=true
-process/premult_alpha=false
-process/normal_map_invert_y=false
-process/hdr_as_srgb=false
-process/hdr_clamp_exposure=false
-process/size_limit=0
-detect_3d/compress_to=0
diff --git a/interfaces/global_theme.tres b/interfaces/global_theme.tres
new file mode 100644
index 0000000..d551688
--- /dev/null
+++ b/interfaces/global_theme.tres
@@ -0,0 +1,6 @@
+[gd_resource type="Theme" load_steps=2 format=3 uid="uid://bec7g0fax3c8o"]
+
+[ext_resource type="FontFile" uid="uid://hbggv2tf174i" path="res://assets/fonts/Exo2/Exo2.ttf" id="1_u2ykv"]
+
+[resource]
+default_font = ExtResource("1_u2ykv")
diff --git a/interfaces/hud/hud.gd b/interfaces/hud/hud.gd
new file mode 100644
index 0000000..f802aad
--- /dev/null
+++ b/interfaces/hud/hud.gd
@@ -0,0 +1,32 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+class_name HUD extends CanvasLayer
+
+@export var health_bar:ProgressBar
+@export var energy_bar:ProgressBar
+@export var timer_label:Label
+@export var objective_label:Label
+@export var debug_label:Label
+@export var ammo_label:Label
+@export var throw_progress:TextureProgressBar
+
+@onready var player:Player = owner
+
+func _ready() -> void:
+ health_bar.set_value(player.health.max_value)
+ energy_bar.set_value(player.energy_max)
+
+func _on_ammo_changed(new_ammo: int) -> void:
+ ammo_label.text = str(new_ammo)
diff --git a/interfaces/hud/hud.tscn b/interfaces/hud/hud.tscn
index 0f1fa06..0ea1afe 100644
--- a/interfaces/hud/hud.tscn
+++ b/interfaces/hud/hud.tscn
@@ -1,52 +1,11 @@
-[gd_scene load_steps=8 format=3 uid="uid://bcv81ku26xo"]
+[gd_scene load_steps=13 format=3 uid="uid://bcv81ku26xo"]
+[ext_resource type="Script" path="res://interfaces/hud/timer_label.gd" id="1_6qrx6"]
[ext_resource type="StyleBox" uid="uid://dcn1ll2ra4lwn" path="res://interfaces/hud/vitals/background.tres" id="1_gmv44"]
+[ext_resource type="Script" path="res://interfaces/hud/hud.gd" id="1_yev76"]
[ext_resource type="StyleBox" uid="uid://bq7rjpm38pao7" path="res://interfaces/hud/vitals/health_foreground.tres" id="2_6ejsl"]
-
-[sub_resource type="GDScript" id="GDScript_2vxif"]
-script/source = "# This file is part of open-fpsz.
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-extends CanvasLayer
-
-@export var health_bar : ProgressBar
-@export var energy_bar : ProgressBar
-@export var debug_label : Label
-"
-
-[sub_resource type="GDScript" id="GDScript_w8l21"]
-script/source = "extends Label
-
-@onready var player : Player = get_parent().owner
-
-# Called when the node enters the scene tree for the first time.
-func _ready() -> void:
- if not OS.is_debug_build():
- queue_free()
-
-# Called every frame. 'delta' is the elapsed time since the previous frame.
-func _process(_delta : float) -> void:
- text = \"\"
- text += \"fps: %d (%.2f mspf)\\n\" % [Engine.get_frames_per_second(), 1000.0 / Engine.get_frames_per_second()]
- text += \"position: %d, %d, %d\\n\" % [player.position.x, player.position.y, player.position.z]
- text += \"speed: %d km/h\\n\" % (player.linear_velocity.length() * 3.6)
- var velocity_xz := Vector3(player.linear_velocity.x, 0., player.linear_velocity.z)
- text += \"speed_xz: %d km/h\\n\" % (velocity_xz.length() * 3.6)
- var viewport_render_size : Vector2i = get_viewport().size * get_viewport().scaling_3d_scale
- text += \"3D viewport resolution: %d × %d (%d%%)\\n\" \\
- % [viewport_render_size.x, viewport_render_size.y, round(get_viewport().scaling_3d_scale * 100)]
-"
+[ext_resource type="Theme" uid="uid://bec7g0fax3c8o" path="res://interfaces/global_theme.tres" id="2_gbqw5"]
+[ext_resource type="Texture2D" uid="uid://1mwcg0cd0msw" path="res://interfaces/hud/throw_force_texture.svg" id="6_rrus8"]
[sub_resource type="Shader" id="Shader_gaah5"]
code = "shader_type canvas_item;
@@ -67,6 +26,33 @@ void fragment() {
shader = SubResource("Shader_gaah5")
shader_parameter/color = Color(1, 1, 1, 1)
+[sub_resource type="SystemFont" id="SystemFont_oo638"]
+subpixel_positioning = 0
+
+[sub_resource type="GDScript" id="GDScript_w8l21"]
+script/source = "extends Label
+
+@onready var player : Player = owner.get_parent()
+
+# Called when the node enters the scene tree for the first time.
+func _ready() -> void:
+ if not OS.is_debug_build():
+ queue_free()
+
+# Called every frame. 'delta' is the elapsed time since the previous frame.
+func _process(_delta : float) -> void:
+ text = \"\"
+ text += \"fps: %d (%.2f mspf)\\n\" % [Engine.get_frames_per_second(), 1000.0 / Engine.get_frames_per_second()]
+ text += \"position: %d, %d, %d\\n\" % [player.position.x, player.position.y, player.position.z]
+ text += \"speed: %d km/h\\n\" % (player.linear_velocity.length() * 3.6)
+ var velocity_xz := Vector3(player.linear_velocity.x, 0., player.linear_velocity.z)
+ text += \"speed_xz: %d km/h\\n\" % (velocity_xz.length() * 3.6)
+ var viewport_render_size : Vector2i = get_viewport().size * get_viewport().scaling_3d_scale
+ text += \"3D viewport resolution: %d × %d (%d%%)\\n\" \\
+ % [viewport_render_size.x, viewport_render_size.y, round(get_viewport().scaling_3d_scale * 100)]
+ text += \"health: %d\" % player.health.value
+"
+
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_f23s3"]
bg_color = Color(0.0901961, 0.87451, 0.760784, 0.65098)
corner_radius_top_left = 3
@@ -75,17 +61,23 @@ corner_radius_bottom_right = 3
corner_radius_bottom_left = 3
anti_aliasing = false
-[node name="HUD" type="CanvasLayer" node_paths=PackedStringArray("health_bar", "energy_bar")]
-script = SubResource("GDScript_2vxif")
+[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_2pxd4"]
+properties/0/path = NodePath("MarginContainer/Multiplayer/TimerLabel:text")
+properties/0/spawn = true
+properties/0/replication_mode = 2
+properties/1/path = NodePath("MarginContainer/Multiplayer/ObjectiveLabel:visible")
+properties/1/spawn = true
+properties/1/replication_mode = 2
+
+[node name="HUD" type="CanvasLayer" node_paths=PackedStringArray("health_bar", "energy_bar", "timer_label", "objective_label", "debug_label", "ammo_label", "throw_progress")]
+script = ExtResource("1_yev76")
health_bar = NodePath("MarginContainer/HBoxContainer/VBoxContainer/HealthBar")
energy_bar = NodePath("MarginContainer/HBoxContainer/VBoxContainer/EnergyBar")
-
-[node name="DebugLabel" type="Label" parent="."]
-offset_left = 20.0
-offset_top = 20.0
-offset_right = 21.0
-offset_bottom = 43.0
-script = SubResource("GDScript_w8l21")
+timer_label = NodePath("MarginContainer/Multiplayer/TimerLabel")
+objective_label = NodePath("MarginContainer/Multiplayer/ObjectiveLabel")
+debug_label = NodePath("MarginContainer/DebugLabel")
+ammo_label = NodePath("MarginContainer/HBoxContainer/HBoxContainer/AmmoLabel")
+throw_progress = NodePath("MarginContainer/CenterContainer/TextureProgressBar")
[node name="Reticle" type="ColorRect" parent="."]
material = SubResource("ShaderMaterial_7blp5")
@@ -103,24 +95,53 @@ grow_vertical = 2
mouse_filter = 2
[node name="MarginContainer" type="MarginContainer" parent="."]
-anchors_preset = 12
-anchor_top = 1.0
+anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
-offset_top = -42.0
grow_horizontal = 2
-grow_vertical = 0
-theme_override_constants/margin_left = 20
-theme_override_constants/margin_right = 20
-theme_override_constants/margin_bottom = 20
+grow_vertical = 2
+theme = ExtResource("2_gbqw5")
+theme_override_constants/margin_left = 24
+theme_override_constants/margin_top = 24
+theme_override_constants/margin_right = 24
+theme_override_constants/margin_bottom = 24
+
+[node name="Multiplayer" type="VBoxContainer" parent="MarginContainer"]
+layout_mode = 2
+
+[node name="TimerLabel" type="Label" parent="MarginContainer/Multiplayer"]
+layout_mode = 2
+theme_override_colors/font_color = Color(1, 1, 1, 1)
+theme_override_font_sizes/font_size = 22
+text = "Warmup"
+horizontal_alignment = 1
+script = ExtResource("1_6qrx6")
+
+[node name="ObjectiveLabel" type="Label" parent="MarginContainer/Multiplayer"]
+visible = false
+layout_mode = 2
+theme_override_colors/font_color = Color(1, 0.75, 0, 1)
+theme_override_colors/font_shadow_color = Color(0, 0, 0, 0.5)
+theme_override_font_sizes/font_size = 24
+text = "YOU ARE THE RABBIT"
+horizontal_alignment = 1
+uppercase = true
+
+[node name="DebugLabel" type="Label" parent="MarginContainer"]
+layout_mode = 2
+size_flags_vertical = 0
+theme_override_fonts/font = SubResource("SystemFont_oo638")
+script = SubResource("GDScript_w8l21")
[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer"]
layout_mode = 2
size_flags_horizontal = 3
+size_flags_vertical = 10
[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
+size_flags_vertical = 8
size_flags_stretch_ratio = 0.2
theme_override_constants/separation = 6
@@ -132,7 +153,7 @@ theme_override_styles/background = ExtResource("1_gmv44")
theme_override_styles/fill = ExtResource("2_6ejsl")
max_value = 255.0
step = 1.0
-value = 1.0
+value = 255.0
show_percentage = false
[node name="EnergyBar" type="ProgressBar" parent="MarginContainer/HBoxContainer/VBoxContainer"]
@@ -141,10 +162,42 @@ layout_mode = 2
mouse_filter = 2
theme_override_styles/background = ExtResource("1_gmv44")
theme_override_styles/fill = SubResource("StyleBoxFlat_f23s3")
-value = 60.0
+value = 100.0
show_percentage = false
[node name="Spacer" type="Control" parent="MarginContainer/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
-size_flags_stretch_ratio = 0.8
+size_flags_stretch_ratio = 0.6
+
+[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_stretch_ratio = 0.2
+
+[node name="AmmoLabel" type="Label" parent="MarginContainer/HBoxContainer/HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_font_sizes/font_size = 20
+text = "24
+"
+horizontal_alignment = 2
+vertical_alignment = 1
+
+[node name="CenterContainer" type="CenterContainer" parent="MarginContainer"]
+layout_mode = 2
+
+[node name="TextureProgressBar" type="TextureProgressBar" parent="MarginContainer/CenterContainer"]
+visible = false
+layout_mode = 2
+value = 100.0
+fill_mode = 4
+texture_progress = ExtResource("6_rrus8")
+radial_initial_angle = 293.0
+radial_fill_degrees = 135.0
+
+[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="."]
+replication_interval = 0.1
+replication_config = SubResource("SceneReplicationConfig_2pxd4")
+
+[node name="CenterContainer" type="CenterContainer" parent="."]
diff --git a/interfaces/hud/iffs/iff.gd b/interfaces/hud/iffs/iff.gd
index 65ebec4..6588488 100644
--- a/interfaces/hud/iffs/iff.gd
+++ b/interfaces/hud/iffs/iff.gd
@@ -51,8 +51,10 @@ signal border_changed(new_border : float)
## The username to display on top of this indicator.
@export var username : String = "Username":
- set(value):
- username = value
+ set = set_username
+
+func set_username(new_name : String) -> void:
+ username = new_name
username_changed.emit(username)
## The foreground color to use for this indicator.
diff --git a/interfaces/hud/throw_force_texture.svg b/interfaces/hud/throw_force_texture.svg
new file mode 100644
index 0000000..a6ef532
--- /dev/null
+++ b/interfaces/hud/throw_force_texture.svg
@@ -0,0 +1,46 @@
+
+
+
+
diff --git a/interfaces/hud/throw_force_texture.svg.import b/interfaces/hud/throw_force_texture.svg.import
new file mode 100644
index 0000000..55e1f14
--- /dev/null
+++ b/interfaces/hud/throw_force_texture.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://1mwcg0cd0msw"
+path="res://.godot/imported/throw_force_texture.svg-c8f74c490a74ad039fa1bdb24add5fd6.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://interfaces/hud/throw_force_texture.svg"
+dest_files=["res://.godot/imported/throw_force_texture.svg-c8f74c490a74ad039fa1bdb24add5fd6.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/interfaces/hud/timer_label.gd b/interfaces/hud/timer_label.gd
new file mode 100644
index 0000000..45a875d
--- /dev/null
+++ b/interfaces/hud/timer_label.gd
@@ -0,0 +1,32 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+extends Label
+
+var timer: Timer = null
+
+func _ready() -> void:
+ if Game.type is Multiplayer:
+ timer = Game.type.timer
+ else:
+ visible = false
+ set_process(false)
+
+func _process(_delta: float) -> void:
+ if timer and not timer.is_stopped() \
+ and !(multiplayer.multiplayer_peer is OfflineMultiplayerPeer) \
+ and multiplayer.is_server():
+ var minutes: int = int(timer.time_left / 60)
+ var seconds : int = int(fmod(timer.time_left, 60))
+ text = "%d:%02d" % [minutes, seconds]
diff --git a/interfaces/menus/boot/boot.gd b/interfaces/menus/boot/boot.gd
index 1ec638b..d28a79e 100644
--- a/interfaces/menus/boot/boot.gd
+++ b/interfaces/menus/boot/boot.gd
@@ -16,30 +16,37 @@ class_name BootMenu extends CanvasLayer
signal start_demo
+@export var multiplayer_panel : MultiplayerPanelContainer
+@export var settings_panel : SettingsPanelContainer
+
+func _ready() -> void:
+ multiplayer_panel.menu_pressed.connect(_on_main_menu_pressed)
+ settings_panel.closed.connect(_on_main_menu_pressed)
+
func _on_demo_pressed() -> void:
start_demo.emit()
func _on_multiplayer_pressed() -> void:
- $MultiplayerPanelContainer.hide()
- $MultiplayerPanelContainer.tab_container.current_tab = 0
- $SettingsPanelContainer.hide()
+ multiplayer_panel.hide()
+ multiplayer_panel.tab_container.current_tab = 0
+ settings_panel.hide()
$MainPanelContainer.hide()
- $MultiplayerPanelContainer.show()
+ multiplayer_panel.show()
show()
func _on_settings_pressed() -> void:
- $MultiplayerPanelContainer.hide()
- $MultiplayerPanelContainer.tab_container.current_tab = 0
- $SettingsPanelContainer.show()
+ multiplayer_panel.hide()
+ multiplayer_panel.tab_container.current_tab = 0
+ settings_panel.show()
$MainPanelContainer.hide()
show()
func _on_quit_pressed() -> void:
- get_tree().quit()
+ Game.quit()
func _on_main_menu_pressed() -> void:
- $MultiplayerPanelContainer.hide()
- $MultiplayerPanelContainer.tab_container.current_tab = 0
- $SettingsPanelContainer.hide()
+ multiplayer_panel.hide()
+ settings_panel.hide()
+ multiplayer_panel.tab_container.current_tab = 0
$MainPanelContainer.show()
show()
diff --git a/interfaces/menus/boot/boot.tscn b/interfaces/menus/boot/boot.tscn
index 9e9e020..c609bd1 100644
--- a/interfaces/menus/boot/boot.tscn
+++ b/interfaces/menus/boot/boot.tscn
@@ -1,1001 +1,10 @@
-[gd_scene load_steps=28 format=3 uid="uid://bjctlqvs33nqy"]
+[gd_scene load_steps=8 format=3 uid="uid://bjctlqvs33nqy"]
[ext_resource type="Script" path="res://interfaces/menus/boot/boot.gd" id="1_6acql"]
[ext_resource type="Texture2D" uid="uid://c1tjamjm8qjog" path="res://interfaces/menus/boot/background.webp" id="1_ph586"]
-
-[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_krqeq"]
-bg_color = Color(0.501961, 0.501961, 0.501961, 0.25098)
-
-[sub_resource type="GDScript" id="GDScript_tc1bm"]
-script/source = "# This file is part of open-fpsz.
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-extends PanelContainer
-
-const DEFAULT_HOST : String = \"localhost\"
-const DEFAULT_PORT : int = 9000
-const CONFIG_FILE_PATH : String = \"user://profile.cfg\"
-
-var _join_address : RegEx = RegEx.new()
-var _registered_ports : RegEx = RegEx.new()
-var _config_file : ConfigFile = ConfigFile.new()
-
-signal start_server(port : int)
-signal join_server(host : String, port : int)
-
-@onready var modal : Control = $Modal
-@onready var menu : CanvasLayer = get_parent()
-@onready var map_selector : OptionButton = %MapSelector
-
-@export var tab_container : TabContainer
-
-func _ready() -> void:
- # see https://datatracker.ietf.org/doc/html/rfc1700
- _registered_ports.compile(r'^(?:102[4-9]|10[3-9]\\d|1[1-9]\\d{2}|[2-9]\\d{3}|[1-5]\\d{4}|6[0-4]\\d{3}|65[0-4]\\d{2}|655[0-2]\\d|6553[0-5])$')
- _join_address.compile(r'^(?[a-zA-Z0-9.-]+)(:(?:102[4-9]|10[3-9]\\d|1[1-9]\\d{2}|[2-9]\\d{3}|[1-5]\\d{4}|6[0-4]\\d{3}|65[0-4]\\d{2}|655[0-2]\\d|6553[0-5]))?$')
- _load_config()
- call_deferred(\"_populate_map_selector\")
-
-func _populate_map_selector() -> void:
- for map : PackedScene in MapsManager.maps:
- var map_name : String = map._bundled.names[0]
- map_selector.add_item(map_name)
-
-func _load_config() -> void:
- var error : Error = _config_file.load(CONFIG_FILE_PATH)
- if error != OK:
- return
-
- var profile_name : String = _config_file.get_value(\"profile\", \"name\", \"Newblood\")
- %ProfileName.text = profile_name
-
-func _on_save_pressed() -> void:
- _config_file.set_value(\"profile\", \"name\", %ProfileName.text)
- _config_file.save(CONFIG_FILE_PATH)
-
-func _on_menu_pressed() -> void:
- hide()
- owner.get_node(\"Main\").show()
-
-func _on_quit_pressed() -> void:
- get_tree().quit()
-
-func _on_host_pressed() -> void:
- var port : int = DEFAULT_PORT
- # check for registered ports number matches
- if %ServerPort.text:
- var result : RegExMatch = _registered_ports.search(%ServerPort.text)
- if result: # port is valid
- port = int(result.get_string())
- else: # port is not valid
- push_warning(\"A valid port number in the range 1024-65535 is required.\")
- return
-
- start_server.emit(port, %ProfileName.text)
-
-func _on_join_pressed() -> void:
- var addr : Array = [DEFAULT_HOST, DEFAULT_PORT]
- # validate join address input
- var result : RegExMatch = _join_address.search(%JoinAddress.text)
- if result: # address is valid
- addr[0] = result.get_string(\"host\")
- var rport : String = result.get_string(\"port\")
- if rport: addr[1] = int(rport)
-
- $Modal.show()
- join_server.emit(addr[0], addr[1], %ProfileName.text)
-
-func _on_connected_to_server() -> void:
- $Modal.hide()
- menu.hide()
-
-func _on_connection_failed() -> void:
- $Modal.hide()
-"
-
-[sub_resource type="GDScript" id="GDScript_gbnwv"]
-resource_name = "Settings"
-script/source = "# This file is part of open-fpsz.
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-extends PanelContainer
-
-# Quality presets.
-
-func _on_very_low_preset_pressed() -> void:
- %TAAOptionButton.selected = 0
- %MSAAOptionButton.selected = 0
- %FXAAOptionButton.selected = 0
- %ShadowSizeOptionButton.selected = 0
- %ShadowFilterOptionButton.selected = 0
- %MeshLODOptionButton.selected = 0
- %SDFGIOptionButton.selected = 0
- %GlowOptionButton.selected = 0
- %SSAOOptionButton.selected = 0
- %SSReflectionsOptionButton.selected = 0
- %SSILOptionButton.selected = 0
- %VolumetricFogOptionButton.selected = 0
- update_preset()
-
-func _on_low_preset_pressed() -> void:
- %TAAOptionButton.selected = 0
- %MSAAOptionButton.selected = 0
- %FXAAOptionButton.selected = 1
- %ShadowSizeOptionButton.selected = 1
- %ShadowFilterOptionButton.selected = 1
- %MeshLODOptionButton.selected = 1
- %SDFGIOptionButton.selected = 0
- %GlowOptionButton.selected = 0
- %SSAOOptionButton.selected = 0
- %SSReflectionsOptionButton.selected = 0
- %SSILOptionButton.selected = 0
- %VolumetricFogOptionButton.selected = 0
- update_preset()
-
-
-func _on_medium_preset_pressed() -> void:
- %TAAOptionButton.selected = 1
- %MSAAOptionButton.selected = 0
- %FXAAOptionButton.selected = 0
- %ShadowSizeOptionButton.selected = 2
- %ShadowFilterOptionButton.selected = 2
- %MeshLODOptionButton.selected = 1
- %SDFGIOptionButton.selected = 1
- %GlowOptionButton.selected = 1
- %SSAOOptionButton.selected = 1
- %SSReflectionsOptionButton.selected = 1
- %SSILOptionButton.selected = 0
- %VolumetricFogOptionButton.selected = 1
- update_preset()
-
-
-func _on_high_preset_pressed() -> void:
- %TAAOptionButton.selected = 1
- %MSAAOptionButton.selected = 0
- %FXAAOptionButton.selected = 0
- %ShadowSizeOptionButton.selected = 3
- %ShadowFilterOptionButton.selected = 3
- %MeshLODOptionButton.selected = 2
- %SDFGIOptionButton.selected = 1
- %GlowOptionButton.selected = 2
- %SSAOOptionButton.selected = 2
- %SSReflectionsOptionButton.selected = 2
- %SSILOptionButton.selected = 2
- %VolumetricFogOptionButton.selected = 2
- update_preset()
-
-
-func _on_ultra_preset_pressed() -> void:
- %TAAOptionButton.selected = 1
- %MSAAOptionButton.selected = 1
- %FXAAOptionButton.selected = 0
- %ShadowSizeOptionButton.selected = 4
- %ShadowFilterOptionButton.selected = 4
- %MeshLODOptionButton.selected = 3
- %SDFGIOptionButton.selected = 2
- %GlowOptionButton.selected = 2
- %SSAOOptionButton.selected = 3
- %SSReflectionsOptionButton.selected = 3
- %SSILOptionButton.selected = 3
- %VolumetricFogOptionButton.selected = 2
- update_preset()
-
-
-func update_preset() -> void:
- # Simulate options being manually selected to run their respective update code.
- %TAAOptionButton.item_selected.emit(%TAAOptionButton.selected)
- %MSAAOptionButton.item_selected.emit(%MSAAOptionButton.selected)
- %FXAAOptionButton.item_selected.emit(%FXAAOptionButton.selected)
- %ShadowSizeOptionButton.item_selected.emit(%ShadowSizeOptionButton.selected)
- %ShadowFilterOptionButton.item_selected.emit(%ShadowFilterOptionButton.selected)
- %MeshLODOptionButton.item_selected.emit(%MeshLODOptionButton.selected)
- %SDFGIOptionButton.item_selected.emit(%SDFGIOptionButton.selected)
- %GlowOptionButton.item_selected.emit(%GlowOptionButton.selected)
- %SSAOOptionButton.item_selected.emit(%SSAOOptionButton.selected)
- %SSReflectionsOptionButton.item_selected.emit(%SSReflectionsOptionButton.selected)
- %SSILOptionButton.item_selected.emit(%SSILOptionButton.selected)
- %VolumetricFogOptionButton.item_selected.emit(%VolumetricFogOptionButton.selected)
-
-func _on_apply_pressed() -> void:
- Settings.apply()
-
-func _on_reset_pressed() -> void:
- Settings.reset()
-
-func _on_close_pressed() -> void:
- self.hide()
- %MainPanelContainer.show()
-
-# Adjustment settings.
-
-#func _on_brightness_slider_value_changed(value: float) -> void:
- ## This is a setting that is attached to the environment.
- ## If your game requires you to change the environment,
- ## then be sure to run this function again to make the setting effective.
- ## The slider value is clamped between 0.5 and 4.
- #world_environment.environment.set_adjustment_brightness(value)
-
-#func _on_contrast_slider_value_changed(value: float) -> void:
- ## This is a setting that is attached to the environment.
- ## If your game requires you to change the environment,
- ## then be sure to run this function again to make the setting effective.
- ## The slider value is clamped between 0.5 and 4.
- #world_environment.environment.set_adjustment_contrast(value)
-
-#func _on_saturation_slider_value_changed(value: float) -> void:
- ## This is a setting that is attached to the environment.
- ## If your game requires you to change the environment,
- ## then be sure to run this function again to make the setting effective.
- ## The slider value is clamped between 0.5 and 10.
- #world_environment.environment.set_adjustment_saturation(value)
-"
-
-[sub_resource type="GDScript" id="GDScript_7votw"]
-script/source = "# This file is part of open-fpsz.
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-extends OptionButton
-
-func _ready() -> void:
- self.selected = Settings.get_value(\"ui\", \"scale\")
- self.item_selected.emit(self.selected)
-
-func _on_item_selected(index: int) -> void:
- Settings.set_value(\"ui\", \"scale\", clamp(index, 0, 2))
- # When the screen changes size, we need to update the 3D
- # viewport quality setting. If we don't do this, the viewport will take
- # the size from the main viewport.
- var new_size := Vector2(
- ProjectSettings.get_setting(&\"display/window/size/viewport_width\"),
- ProjectSettings.get_setting(&\"display/window/size/viewport_height\")
- )
- # compute new size
- if index == 0: # Smaller (66%)
- new_size *= 1.5
- elif index == 1: # Small (80%)
- new_size *= 1.25
- elif index == 2: # Medium (100%) (default)
- new_size *= 1.
- elif index == 3: # Large (133%)
- new_size *= .75
- elif index == 4: # Larger (200%)
- new_size *= .5
- # update scale
- get_tree().root.set_content_scale_size(new_size)
-"
-
-[sub_resource type="GDScript" id="GDScript_ux54k"]
-script/source = "# This file is part of open-fpsz.
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-extends HBoxContainer
-
-@export var min_value : float = .01
-@export var max_value : float = 1.
-@export var step : float = .01
-
-func _ready() -> void:
- var value : float = Settings.get_value(\"controls\", \"mouse_sensitivity\")
- for control : Range in [$SpinBox, $Slider]:
- control.min_value = min_value
- control.max_value = max_value
- control.step = step
- control.value = value
- _on_value_changed(value)
-
-func _on_value_changed(new_value : float) -> void:
- Settings.set_value(\"controls\", \"mouse_sensitivity\", new_value)
-
-func _on_spin_box_value_changed(new_value : float) -> void:
- _on_value_changed(new_value)
- $Slider.value = new_value
-
-func _on_slider_value_changed(new_value : float) -> void:
- _on_value_changed(new_value)
- $SpinBox.value = new_value
-"
-
-[sub_resource type="GDScript" id="GDScript_v5ux6"]
-script/source = "# This file is part of open-fpsz.
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-extends HBoxContainer
-
-@export var min_value : int = 70
-@export var max_value : int = 150
-@export var step : int = 1
-
-func _ready() -> void:
- var value : int = Settings.get_value(\"video\", \"fov\")
- for control : Range in [$SpinBox, $Slider]:
- control.min_value = min_value
- control.max_value = max_value
- control.step = step
- control.value = value
- _on_value_changed(value)
-
-func _on_value_changed(new_value : int) -> void:
- Settings.set_value(\"video\", \"fov\", new_value)
-
-func _on_spin_box_value_changed(new_value : int) -> void:
- _on_value_changed(new_value)
- $Slider.value = new_value
-
-func _on_slider_value_changed(new_value : int) -> void:
- _on_value_changed(new_value)
- $SpinBox.value = new_value
-"
-
-[sub_resource type="GDScript" id="GDScript_viqb1"]
-script/source = "# This file is part of open-fpsz.
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-extends HSlider
-
-func _ready() -> void:
- self.value = Settings.get_value(\"video\", \"quality\")
- self.value_changed.emit(self.value)
-
-func _on_value_changed(new_value : float) -> void:
- Settings.set_value(\"video\", \"quality\", new_value)
- get_viewport().scaling_3d_scale = new_value
-"
-
-[sub_resource type="GDScript" id="GDScript_mnbe0"]
-script/source = "# This file is part of open-fpsz.
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-extends OptionButton
-
-func _ready() -> void:
- self.selected = Settings.get_value(\"video\", \"filter\")
- self.item_selected.emit(self.selected)
-
-func _on_item_selected(index: int) -> void:
- Settings.set_value(\"video\", \"filter\", index)
- # Viewport scale mode setting.
- if index == 0: # Bilinear (Fastest)
- get_viewport().scaling_3d_mode = Viewport.SCALING_3D_MODE_BILINEAR
- # FSR Sharpness is only effective when the scaling mode is FSR 1.0.
- %FSRSharpnessLabel.visible = false
- %FSRSharpnessSlider.visible = false
- elif index == 1: # FSR 1.0 (Fast)
- get_viewport().scaling_3d_mode = Viewport.SCALING_3D_MODE_FSR
- # FSR Sharpness is only effective when the scaling mode is FSR 1.0.
- %FSRSharpnessLabel.visible = true
- %FSRSharpnessSlider.visible = true
-"
-
-[sub_resource type="GDScript" id="GDScript_c3ngv"]
-script/source = "# This file is part of open-fpsz.
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-extends HSlider
-
-func _ready() -> void:
- self.value = Settings.get_value(\"video\", \"fsr_sharpness\")
- self.value_changed.emit(self.value)
-
-func _on_value_changed(new_value : float) -> void:
- Settings.set_value(\"video\", \"fsr_sharpness\", new_value)
- # Lower FSR sharpness values result in a sharper image.
- # Invert the slider so that higher values result in a sharper image,
- # which is generally expected from users.
- get_viewport().fsr_sharpness = 2.0 - new_value
-"
-
-[sub_resource type="GDScript" id="GDScript_xa5kp"]
-script/source = "# This file is part of open-fpsz.
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-extends OptionButton
-
-func _ready() -> void:
- self.selected = Settings.get_value(\"video\", \"window_mode\")
- self.item_selected.emit(self.selected)
-
-func _on_item_selected(index : int) -> void:
- Settings.set_value(\"video\", \"window_mode\", clamp(index, 0, 2))
- if index == 0: # Windowed (default)
- get_tree().root.set_mode(Window.MODE_WINDOWED)
- elif index == 1: # Fullscreen
- get_tree().root.set_mode(Window.MODE_FULLSCREEN)
- elif index == 2: # Exclusive Fullscreen
- get_tree().root.set_mode(Window.MODE_EXCLUSIVE_FULLSCREEN)
-"
-
-[sub_resource type="GDScript" id="GDScript_jl1uk"]
-script/source = "# This file is part of open-fpsz.
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-extends OptionButton
-
-func _ready() -> void:
- self.selected = Settings.get_value(\"video\", \"vsync\")
- self.item_selected.emit(self.selected)
-
-func _on_item_selected(index : int) -> void:
- Settings.set_value(\"video\", \"vsync\", index)
- # Vsync is enabled by default.
- # Vertical synchronization locks framerate and makes screen tearing not visible at the cost of
- # higher input latency and stuttering when the framerate target is not met.
- # Adaptive V-Sync automatically disables V-Sync when the framerate target is not met, and enables
- # V-Sync otherwise. This prevents suttering and reduces input latency when the framerate target
- # is not met, at the cost of visible tearing.
- if index == 0: # Disabled (default)
- DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_DISABLED)
- elif index == 1: # Adaptive
- DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_ADAPTIVE)
- elif index == 2: # Enabled
- DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_ENABLED)
-"
-
-[sub_resource type="GDScript" id="GDScript_6qqbt"]
-script/source = "# This file is part of open-fpsz.
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-extends OptionButton
-
-func _ready() -> void:
- self.selected = Settings.get_value(\"video\", \"taa\")
- self.item_selected.emit(self.selected)
-
-func _on_item_selected(index: int) -> void:
- Settings.set_value(\"video\", \"taa\", index)
- # Temporal antialiasing. Smooths out everything including specular aliasing, but can introduce
- # ghosting artifacts and blurring in motion. Moderate performance cost.
- # @NOTE: https://github.com/TokisanGames/Terrain3D/issues/302
- # get_viewport().use_taa = index == 1
-"
-
-[sub_resource type="GDScript" id="GDScript_ulkhx"]
-script/source = "# This file is part of open-fpsz.
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-extends OptionButton
-
-func _ready() -> void:
- self.selected = Settings.get_value(\"video\", \"msaa\")
- self.item_selected.emit(self.selected)
-
-func _on_item_selected(index: int) -> void:
- Settings.set_value(\"video\", \"msaa\", index)
- # Multi-sample anti-aliasing. High quality, but slow. It also does not smooth out the edges of
- # transparent (alpha scissor) textures.
- if index == 0: # Disabled (default)
- get_viewport().msaa_3d = Viewport.MSAA_DISABLED
- elif index == 1: # 2×
- get_viewport().msaa_3d = Viewport.MSAA_2X
- elif index == 2: # 4×
- get_viewport().msaa_3d = Viewport.MSAA_4X
- elif index == 3: # 8×
- get_viewport().msaa_3d = Viewport.MSAA_8X
-"
-
-[sub_resource type="GDScript" id="GDScript_k6fl5"]
-script/source = "# This file is part of open-fpsz.
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-extends OptionButton
-
-func _ready() -> void:
- self.selected = Settings.get_value(\"video\", \"fxaa\")
- self.item_selected.emit(self.selected)
-
-func _on_item_selected(index: int) -> void:
- Settings.set_value(\"video\", \"fxaa\", index)
- # Fast approximate anti-aliasing. Much faster than MSAA (and works on alpha scissor edges),
- # but blurs the whole scene rendering slightly.
- get_viewport().screen_space_aa = int(index == 1) as Viewport.ScreenSpaceAA
-"
-
-[sub_resource type="GDScript" id="GDScript_cph1u"]
-script/source = "# This file is part of open-fpsz.
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-extends OptionButton
-
-func _ready() -> void:
- self.selected = Settings.get_value(\"quality\", \"shadow_size\")
- self.item_selected.emit(self.selected)
-
-func _on_item_selected(index : int) -> void:
- Settings.set_value(\"quality\", \"shadow_size\", index)
- if index == 0: # Minimum
- RenderingServer.directional_shadow_atlas_set_size(512, true)
- # Adjust shadow bias according to shadow resolution.
- # Higher resultions can use a lower bias without suffering from shadow acne.
- #Settings.set_value(\"quality\", \"shadow_bias\", 0.06)
- #directional_light.shadow_bias = Settings.get_value(\"quality\", \"shadow_bias\")
-
- # Disable positional (omni/spot) light shadows entirely to further improve performance.
- # These often don't contribute as much to a scene compared to directional light shadows.
- get_viewport().positional_shadow_atlas_size = 0
- if index == 1: # Very Low
- RenderingServer.directional_shadow_atlas_set_size(1024, true)
- #Settings.set_value(\"quality\", \"shadow_bias\", 0.04)
- #directional_light.shadow_bias = Settings.get_value(\"quality\", \"shadow_bias\")
- get_viewport().positional_shadow_atlas_size = 1024
- if index == 2: # Low
- RenderingServer.directional_shadow_atlas_set_size(2048, true)
- #Settings.set_value(\"quality\", \"shadow_bias\", 0.03)
- #directional_light.shadow_bias = Settings.get_value(\"quality\", \"shadow_bias\")
- get_viewport().positional_shadow_atlas_size = 2048
- if index == 3: # Medium (default)
- RenderingServer.directional_shadow_atlas_set_size(4096, true)
- #Settings.set_value(\"quality\", \"shadow_bias\", 0.02)
- #directional_light.shadow_bias = Settings.get_value(\"quality\", \"shadow_bias\")
- get_viewport().positional_shadow_atlas_size = 4096
- if index == 4: # High
- RenderingServer.directional_shadow_atlas_set_size(8192, true)
- #Settings.set_value(\"quality\", \"shadow_bias\", 0.01)
- #directional_light.shadow_bias = Settings.get_value(\"quality\", \"shadow_bias\")
- get_viewport().positional_shadow_atlas_size = 8192
- if index == 5: # Ultra
- RenderingServer.directional_shadow_atlas_set_size(16384, true)
- #Settings.set_value(\"quality\", \"shadow_bias\", 0.005)
- #directional_light.shadow_bias = Settings.get_value(\"quality\", \"shadow_bias\")
- get_viewport().positional_shadow_atlas_size = 16384
-"
-
-[sub_resource type="GDScript" id="GDScript_uhm5l"]
-script/source = "# This file is part of open-fpsz.
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-extends OptionButton
-
-func _ready() -> void:
- self.selected = Settings.get_value(\"quality\", \"shadow_filter\")
- self.item_selected.emit(self.selected)
-
-func _on_item_selected(index : int) -> void:
- Settings.set_value(\"quality\", \"shadow_filter\", index)
- if index == 0: # Very Low
- RenderingServer.directional_soft_shadow_filter_set_quality(RenderingServer.SHADOW_QUALITY_HARD)
- RenderingServer.positional_soft_shadow_filter_set_quality(RenderingServer.SHADOW_QUALITY_HARD)
- if index == 1: # Low
- RenderingServer.directional_soft_shadow_filter_set_quality(RenderingServer.SHADOW_QUALITY_SOFT_VERY_LOW)
- RenderingServer.positional_soft_shadow_filter_set_quality(RenderingServer.SHADOW_QUALITY_SOFT_VERY_LOW)
- if index == 2: # Medium (default)
- RenderingServer.directional_soft_shadow_filter_set_quality(RenderingServer.SHADOW_QUALITY_SOFT_LOW)
- RenderingServer.positional_soft_shadow_filter_set_quality(RenderingServer.SHADOW_QUALITY_SOFT_LOW)
- if index == 3: # High
- RenderingServer.directional_soft_shadow_filter_set_quality(RenderingServer.SHADOW_QUALITY_SOFT_MEDIUM)
- RenderingServer.positional_soft_shadow_filter_set_quality(RenderingServer.SHADOW_QUALITY_SOFT_MEDIUM)
- if index == 4: # Very High
- RenderingServer.directional_soft_shadow_filter_set_quality(RenderingServer.SHADOW_QUALITY_SOFT_HIGH)
- RenderingServer.positional_soft_shadow_filter_set_quality(RenderingServer.SHADOW_QUALITY_SOFT_HIGH)
- if index == 5: # Ultra
- RenderingServer.directional_soft_shadow_filter_set_quality(RenderingServer.SHADOW_QUALITY_SOFT_ULTRA)
- RenderingServer.positional_soft_shadow_filter_set_quality(RenderingServer.SHADOW_QUALITY_SOFT_ULTRA)
-"
-
-[sub_resource type="GDScript" id="GDScript_4cgn8"]
-script/source = "# This file is part of open-fpsz.
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-extends OptionButton
-
-func _ready() -> void:
- self.selected = Settings.get_value(\"quality\", \"mesh_lod\")
- self.item_selected.emit(self.selected)
-
-func _on_item_selected(index : int) -> void:
- Settings.set_value(\"quality\", \"mesh_lod\", index)
- if index == 0: # Very Low
- get_viewport().mesh_lod_threshold = 8.0
- if index == 0: # Low
- get_viewport().mesh_lod_threshold = 4.0
- if index == 1: # Medium
- get_viewport().mesh_lod_threshold = 2.0
- if index == 2: # High (default)
- get_viewport().mesh_lod_threshold = 1.0
- if index == 3: # Ultra
- # Always use highest LODs to avoid any form of pop-in.
- get_viewport().mesh_lod_threshold = 0.0
-"
-
-[sub_resource type="GDScript" id="GDScript_xfygm"]
-script/source = "# This file is part of open-fpsz.
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-extends OptionButton
-
-func _ready() -> void:
- self.selected = Settings.get_value(\"environment\", \"sdfgi\")
- self.item_selected.emit(self.selected)
-
-func _on_item_selected(index: int) -> void:
- Settings.set_value(\"environment\", \"sdfgi\", index)
- # This is a setting that is attached to the environment.
- # If your game requires you to change the environment,
- # then be sure to run this function again to make the setting effective.
- if index == 0: # Disabled (default)
- MapsManager.environment.sdfgi_enabled = false
- elif index == 1: # Low
- MapsManager.environment.sdfgi_enabled = true
- RenderingServer.gi_set_use_half_resolution(true)
- elif index == 2: # High
- MapsManager.environment.sdfgi_enabled = true
- RenderingServer.gi_set_use_half_resolution(false)
-"
-
-[sub_resource type="GDScript" id="GDScript_k3pso"]
-script/source = "# This file is part of open-fpsz.
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-extends OptionButton
-
-func _ready() -> void:
- self.selected = Settings.get_value(\"environment\", \"glow\")
- self.item_selected.emit(self.selected)
-
-func _on_item_selected(index: int) -> void:
- Settings.set_value(\"environment\", \"glow\", index)
- ## This is a setting that is attached to the environment.
- ## If your game requires you to change the environment,
- ## then be sure to run this function again to make the setting effective.
- if index == 0: # Disabled (default)
- MapsManager.environment.glow_enabled = false
- elif index == 1: # Low
- MapsManager.environment.glow_enabled = true
- elif index == 2: # High
- MapsManager.environment.glow_enabled = true
-"
-
-[sub_resource type="GDScript" id="GDScript_0sqe1"]
-script/source = "# This file is part of open-fpsz.
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-extends OptionButton
-
-func _ready() -> void:
- self.selected = Settings.get_value(\"environment\", \"ssao\")
- self.item_selected.emit(self.selected)
-
-func _on_item_selected(index: int) -> void:
- Settings.set_value(\"environment\", \"ssao\", index)
- # This is a setting that is attached to the environment.
- # If your game requires you to change the environment,
- # then be sure to run this function again to make the setting effective.
- if index == 0: # Disabled (default)
- MapsManager.environment.ssao_enabled = false
- elif index == 1: # Very Low
- MapsManager.environment.ssao_enabled = true
- RenderingServer.environment_set_ssao_quality(RenderingServer.ENV_SSAO_QUALITY_VERY_LOW, true, 0.5, 2, 50, 300)
- elif index == 2: # Low
- MapsManager.environment.ssao_enabled = true
- RenderingServer.environment_set_ssao_quality(RenderingServer.ENV_SSAO_QUALITY_VERY_LOW, true, 0.5, 2, 50, 300)
- elif index == 3: # Medium
- MapsManager.environment.ssao_enabled = true
- RenderingServer.environment_set_ssao_quality(RenderingServer.ENV_SSAO_QUALITY_MEDIUM, true, 0.5, 2, 50, 300)
- elif index == 4: # High
- MapsManager.environment.ssao_enabled = true
- RenderingServer.environment_set_ssao_quality(RenderingServer.ENV_SSAO_QUALITY_HIGH, true, 0.5, 2, 50, 300)
-"
-
-[sub_resource type="GDScript" id="GDScript_cb82q"]
-script/source = "# This file is part of open-fpsz.
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-extends OptionButton
-
-func _ready() -> void:
- self.selected = Settings.get_value(\"environment\", \"ssr\")
- self.item_selected.emit(self.selected)
-
-func _on_item_selected(index: int) -> void:
- Settings.set_value(\"environment\", \"ssr\", index)
- # This is a setting that is attached to the environment.
- # If your game requires you to change the environment,
- # then be sure to run this function again to make the setting effective.
- if index == 0: # Disabled (default)
- MapsManager.environment.ssr_enabled = false
- elif index == 1: # Low
- MapsManager.environment.ssr_enabled = true
- MapsManager.environment.ssr_max_steps = 8
- elif index == 2: # Medium
- MapsManager.environment.ssr_enabled = true
- MapsManager.environment.ssr_max_steps = 32
- elif index == 3: # High
- MapsManager.environment.ssr_enabled = true
- MapsManager.environment.ssr_max_steps = 56
-"
-
-[sub_resource type="GDScript" id="GDScript_qrf3h"]
-script/source = "# This file is part of open-fpsz.
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-extends OptionButton
-
-func _ready() -> void:
- self.selected = Settings.get_value(\"environment\", \"ssil\")
- self.item_selected.emit(self.selected)
-
-func _on_item_selected(index: int) -> void:
- Settings.set_value(\"environment\", \"ssil\", index)
- # This is a setting that is attached to the environment.
- # If your game requires you to change the environment,
- # then be sure to run this function again to make the setting effective.
- if index == 0: # Disabled (default)
- MapsManager.environment.ssil_enabled = false
- if index == 1: # Very Low
- MapsManager.environment.ssil_enabled = true
- RenderingServer.environment_set_ssil_quality(RenderingServer.ENV_SSIL_QUALITY_VERY_LOW, true, 0.5, 4, 50, 300)
- if index == 2: # Low
- MapsManager.environment.ssil_enabled = true
- RenderingServer.environment_set_ssil_quality(RenderingServer.ENV_SSIL_QUALITY_LOW, true, 0.5, 4, 50, 300)
- if index == 3: # Medium
- MapsManager.environment.ssil_enabled = true
- RenderingServer.environment_set_ssil_quality(RenderingServer.ENV_SSIL_QUALITY_MEDIUM, true, 0.5, 4, 50, 300)
- if index == 4: # High
- MapsManager.environment.ssil_enabled = true
- RenderingServer.environment_set_ssil_quality(RenderingServer.ENV_SSIL_QUALITY_HIGH, true, 0.5, 4, 50, 300)
-"
-
-[sub_resource type="GDScript" id="GDScript_80uen"]
-script/source = "# This file is part of open-fpsz.
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-extends OptionButton
-
-func _ready() -> void:
- self.selected = Settings.get_value(\"environment\", \"volumetric_fog\")
- self.item_selected.emit(self.selected)
-
-func _on_item_selected(index: int) -> void:
- Settings.set_value(\"environment\", \"volumetric_fog\", index)
- if index == 0: # Disabled (default)
- MapsManager.environment.volumetric_fog_enabled = false
- if index == 1: # Low
- MapsManager.environment.volumetric_fog_enabled = true
- RenderingServer.environment_set_volumetric_fog_filter_active(false)
- if index == 2: # High
- MapsManager.environment.volumetric_fog_enabled = true
- RenderingServer.environment_set_volumetric_fog_filter_active(true)
-"
+[ext_resource type="PackedScene" uid="uid://7chfb47enqwm" path="res://interfaces/menus/boot/multiplayer/multiplayer_panel_container.tscn" id="3_d2ufv"]
+[ext_resource type="PackedScene" uid="uid://cvu66n4bm10w3" path="res://interfaces/menus/boot/settings/settings_panel_container.tscn" id="4_yk7cf"]
+[ext_resource type="Theme" uid="uid://bec7g0fax3c8o" path="res://interfaces/global_theme.tres" id="5_unqvv"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_c4ymk"]
bg_color = Color(0, 0, 0, 0)
@@ -1003,8 +12,10 @@ bg_color = Color(0, 0, 0, 0)
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_dq4me"]
bg_color = Color(0, 0.5, 0.5, 0.2)
-[node name="BootMenu" type="CanvasLayer"]
+[node name="BootMenu" type="CanvasLayer" node_paths=PackedStringArray("multiplayer_panel", "settings_panel")]
script = ExtResource("1_6acql")
+multiplayer_panel = NodePath("MultiplayerPanelContainer")
+settings_panel = NodePath("SettingsPanelContainer")
[node name="TextureRect" type="TextureRect" parent="."]
anchors_preset = 15
@@ -1016,936 +27,23 @@ mouse_filter = 2
texture = ExtResource("1_ph586")
stretch_mode = 6
-[node name="MultiplayerPanelContainer" type="PanelContainer" parent="." node_paths=PackedStringArray("tab_container")]
+[node name="MultiplayerPanelContainer" parent="." instance=ExtResource("3_d2ufv")]
unique_name_in_owner = true
visible = false
-anchors_preset = 15
-anchor_right = 1.0
-anchor_bottom = 1.0
-grow_horizontal = 2
-grow_vertical = 2
-theme_override_styles/panel = SubResource("StyleBoxFlat_krqeq")
-script = SubResource("GDScript_tc1bm")
-tab_container = NodePath("MarginContainer/VBoxContainer/TabContainer")
+theme = ExtResource("5_unqvv")
-[node name="MarginContainer" type="MarginContainer" parent="MultiplayerPanelContainer"]
-layout_mode = 2
-theme_override_constants/margin_left = 20
-theme_override_constants/margin_top = 20
-theme_override_constants/margin_right = 20
-theme_override_constants/margin_bottom = 20
-
-[node name="VBoxContainer" type="VBoxContainer" parent="MultiplayerPanelContainer/MarginContainer"]
-layout_mode = 2
-
-[node name="TabContainer" type="TabContainer" parent="MultiplayerPanelContainer/MarginContainer/VBoxContainer"]
-layout_mode = 2
-size_flags_vertical = 3
-theme_override_constants/side_margin = 0
-
-[node name="Profile" type="TabBar" parent="MultiplayerPanelContainer/MarginContainer/VBoxContainer/TabContainer"]
-layout_mode = 2
-theme_override_constants/h_separation = 0
-
-[node name="MarginContainer" type="MarginContainer" parent="MultiplayerPanelContainer/MarginContainer/VBoxContainer/TabContainer/Profile"]
-layout_mode = 0
-offset_right = 1116.0
-offset_bottom = 577.0
-theme_override_constants/margin_left = 20
-theme_override_constants/margin_top = 20
-theme_override_constants/margin_right = 20
-theme_override_constants/margin_bottom = 20
-
-[node name="HBoxContainer" type="HBoxContainer" parent="MultiplayerPanelContainer/MarginContainer/VBoxContainer/TabContainer/Profile/MarginContainer"]
-layout_mode = 2
-theme_override_constants/separation = 20
-
-[node name="LeftBox" type="Control" parent="MultiplayerPanelContainer/MarginContainer/VBoxContainer/TabContainer/Profile/MarginContainer/HBoxContainer"]
-layout_mode = 2
-size_flags_horizontal = 3
-size_flags_stretch_ratio = 0.25
-
-[node name="Top" type="VBoxContainer" parent="MultiplayerPanelContainer/MarginContainer/VBoxContainer/TabContainer/Profile/MarginContainer/HBoxContainer/LeftBox"]
-layout_mode = 1
-anchors_preset = 10
-anchor_right = 1.0
-offset_bottom = 101.0
-grow_horizontal = 2
-size_flags_horizontal = 3
-size_flags_stretch_ratio = 0.25
-
-[node name="Create" type="Button" parent="MultiplayerPanelContainer/MarginContainer/VBoxContainer/TabContainer/Profile/MarginContainer/HBoxContainer/LeftBox/Top"]
-layout_mode = 2
-disabled = true
-text = "Create"
-
-[node name="Delete" type="Button" parent="MultiplayerPanelContainer/MarginContainer/VBoxContainer/TabContainer/Profile/MarginContainer/HBoxContainer/LeftBox/Top"]
-layout_mode = 2
-disabled = true
-text = "Delete"
-
-[node name="Save" type="Button" parent="MultiplayerPanelContainer/MarginContainer/VBoxContainer/TabContainer/Profile/MarginContainer/HBoxContainer/LeftBox/Top"]
-layout_mode = 2
-text = "Save"
-
-[node name="Bottom" type="VBoxContainer" parent="MultiplayerPanelContainer/MarginContainer/VBoxContainer/TabContainer/Profile/MarginContainer/HBoxContainer/LeftBox"]
-layout_mode = 1
-anchors_preset = 12
-anchor_top = 1.0
-anchor_right = 1.0
-anchor_bottom = 1.0
-offset_top = -31.0
-grow_horizontal = 2
-grow_vertical = 0
-size_flags_horizontal = 0
-
-[node name="MainMenu" type="Button" parent="MultiplayerPanelContainer/MarginContainer/VBoxContainer/TabContainer/Profile/MarginContainer/HBoxContainer/LeftBox/Bottom"]
-layout_mode = 2
-size_flags_horizontal = 3
-text = "Main Menu
-"
-
-[node name="Quit" type="Button" parent="MultiplayerPanelContainer/MarginContainer/VBoxContainer/TabContainer/Profile/MarginContainer/HBoxContainer/LeftBox/Bottom"]
-layout_mode = 2
-size_flags_horizontal = 3
-text = "Quit"
-
-[node name="RightBox" type="VBoxContainer" parent="MultiplayerPanelContainer/MarginContainer/VBoxContainer/TabContainer/Profile/MarginContainer/HBoxContainer"]
-layout_mode = 2
-size_flags_horizontal = 3
-
-[node name="Label" type="Label" parent="MultiplayerPanelContainer/MarginContainer/VBoxContainer/TabContainer/Profile/MarginContainer/HBoxContainer/RightBox"]
-layout_mode = 2
-text = "Current Profile:"
-
-[node name="ProfileName" type="LineEdit" parent="MultiplayerPanelContainer/MarginContainer/VBoxContainer/TabContainer/Profile/MarginContainer/HBoxContainer/RightBox"]
-unique_name_in_owner = true
-layout_mode = 2
-size_flags_horizontal = 3
-text = "Newblood"
-
-[node name="Join" type="TabBar" parent="MultiplayerPanelContainer/MarginContainer/VBoxContainer/TabContainer"]
+[node name="SettingsPanelContainer" parent="." instance=ExtResource("4_yk7cf")]
visible = false
-layout_mode = 2
-select_with_rmb = true
-
-[node name="MarginContainer" type="MarginContainer" parent="MultiplayerPanelContainer/MarginContainer/VBoxContainer/TabContainer/Join"]
-layout_mode = 1
-anchors_preset = 15
-anchor_right = 1.0
-anchor_bottom = 1.0
-grow_horizontal = 2
-grow_vertical = 2
-theme_override_constants/margin_left = 20
-theme_override_constants/margin_top = 20
-theme_override_constants/margin_right = 20
-theme_override_constants/margin_bottom = 20
-
-[node name="HBoxContainer" type="HBoxContainer" parent="MultiplayerPanelContainer/MarginContainer/VBoxContainer/TabContainer/Join/MarginContainer"]
-layout_mode = 2
-theme_override_constants/separation = 10
-
-[node name="LeftBox" type="Control" parent="MultiplayerPanelContainer/MarginContainer/VBoxContainer/TabContainer/Join/MarginContainer/HBoxContainer"]
-layout_mode = 2
-size_flags_horizontal = 3
-size_flags_stretch_ratio = 0.25
-
-[node name="Top" type="VBoxContainer" parent="MultiplayerPanelContainer/MarginContainer/VBoxContainer/TabContainer/Join/MarginContainer/HBoxContainer/LeftBox"]
-layout_mode = 1
-anchors_preset = 10
-anchor_right = 1.0
-offset_bottom = 101.0
-grow_horizontal = 2
-size_flags_horizontal = 3
-size_flags_stretch_ratio = 0.25
-
-[node name="Join" type="Button" parent="MultiplayerPanelContainer/MarginContainer/VBoxContainer/TabContainer/Join/MarginContainer/HBoxContainer/LeftBox/Top"]
-layout_mode = 2
-text = "Join"
-
-[node name="Bottom" type="VBoxContainer" parent="MultiplayerPanelContainer/MarginContainer/VBoxContainer/TabContainer/Join/MarginContainer/HBoxContainer/LeftBox"]
-layout_mode = 1
-anchors_preset = 12
-anchor_top = 1.0
-anchor_right = 1.0
-anchor_bottom = 1.0
-offset_top = -31.0
-grow_horizontal = 2
-grow_vertical = 0
-size_flags_horizontal = 0
-
-[node name="MainMenu" type="Button" parent="MultiplayerPanelContainer/MarginContainer/VBoxContainer/TabContainer/Join/MarginContainer/HBoxContainer/LeftBox/Bottom"]
-layout_mode = 2
-size_flags_horizontal = 3
-text = "Main Menu
-"
-
-[node name="Quit" type="Button" parent="MultiplayerPanelContainer/MarginContainer/VBoxContainer/TabContainer/Join/MarginContainer/HBoxContainer/LeftBox/Bottom"]
-layout_mode = 2
-size_flags_horizontal = 3
-text = "Quit"
-
-[node name="RightBox" type="VBoxContainer" parent="MultiplayerPanelContainer/MarginContainer/VBoxContainer/TabContainer/Join/MarginContainer/HBoxContainer"]
-layout_mode = 2
-size_flags_horizontal = 3
-
-[node name="JoinAddress" type="LineEdit" parent="MultiplayerPanelContainer/MarginContainer/VBoxContainer/TabContainer/Join/MarginContainer/HBoxContainer/RightBox"]
-unique_name_in_owner = true
-layout_mode = 2
-placeholder_text = "localhost"
-
-[node name="Host" type="TabBar" parent="MultiplayerPanelContainer/MarginContainer/VBoxContainer/TabContainer"]
-visible = false
-layout_mode = 2
-select_with_rmb = true
-
-[node name="MarginContainer" type="MarginContainer" parent="MultiplayerPanelContainer/MarginContainer/VBoxContainer/TabContainer/Host"]
-layout_mode = 1
-anchors_preset = 15
-anchor_right = 1.0
-anchor_bottom = 1.0
-grow_horizontal = 2
-grow_vertical = 2
-theme_override_constants/margin_left = 20
-theme_override_constants/margin_top = 20
-theme_override_constants/margin_right = 20
-theme_override_constants/margin_bottom = 20
-
-[node name="HBoxContainer" type="HBoxContainer" parent="MultiplayerPanelContainer/MarginContainer/VBoxContainer/TabContainer/Host/MarginContainer"]
-layout_mode = 2
-theme_override_constants/separation = 10
-
-[node name="LeftBox" type="Control" parent="MultiplayerPanelContainer/MarginContainer/VBoxContainer/TabContainer/Host/MarginContainer/HBoxContainer"]
-layout_mode = 2
-size_flags_horizontal = 3
-size_flags_stretch_ratio = 0.25
-
-[node name="Top" type="VBoxContainer" parent="MultiplayerPanelContainer/MarginContainer/VBoxContainer/TabContainer/Host/MarginContainer/HBoxContainer/LeftBox"]
-layout_mode = 1
-anchors_preset = 10
-anchor_right = 1.0
-offset_bottom = 101.0
-grow_horizontal = 2
-size_flags_horizontal = 3
-size_flags_stretch_ratio = 0.25
-
-[node name="Host" type="Button" parent="MultiplayerPanelContainer/MarginContainer/VBoxContainer/TabContainer/Host/MarginContainer/HBoxContainer/LeftBox/Top"]
-layout_mode = 2
-text = "Host"
-
-[node name="Bottom" type="VBoxContainer" parent="MultiplayerPanelContainer/MarginContainer/VBoxContainer/TabContainer/Host/MarginContainer/HBoxContainer/LeftBox"]
-layout_mode = 1
-anchors_preset = 12
-anchor_top = 1.0
-anchor_right = 1.0
-anchor_bottom = 1.0
-offset_top = -66.0
-grow_horizontal = 2
-grow_vertical = 0
-size_flags_horizontal = 0
-
-[node name="MainMenu" type="Button" parent="MultiplayerPanelContainer/MarginContainer/VBoxContainer/TabContainer/Host/MarginContainer/HBoxContainer/LeftBox/Bottom"]
-layout_mode = 2
-size_flags_horizontal = 3
-text = "Main Menu
-"
-
-[node name="Quit" type="Button" parent="MultiplayerPanelContainer/MarginContainer/VBoxContainer/TabContainer/Host/MarginContainer/HBoxContainer/LeftBox/Bottom"]
-layout_mode = 2
-size_flags_horizontal = 3
-text = "Quit"
-
-[node name="RightBox" type="VBoxContainer" parent="MultiplayerPanelContainer/MarginContainer/VBoxContainer/TabContainer/Host/MarginContainer/HBoxContainer"]
-layout_mode = 2
-size_flags_horizontal = 3
-
-[node name="ServerPort" type="LineEdit" parent="MultiplayerPanelContainer/MarginContainer/VBoxContainer/TabContainer/Host/MarginContainer/HBoxContainer/RightBox"]
-unique_name_in_owner = true
-layout_mode = 2
-placeholder_text = "9000"
-
-[node name="MapSelector" type="OptionButton" parent="MultiplayerPanelContainer/MarginContainer/VBoxContainer/TabContainer/Host/MarginContainer/HBoxContainer/RightBox"]
-unique_name_in_owner = true
-layout_mode = 2
-clip_text = true
-allow_reselect = true
-
-[node name="Modal" type="Panel" parent="MultiplayerPanelContainer"]
-visible = false
-layout_mode = 2
-
-[node name="Label" type="Label" parent="MultiplayerPanelContainer/Modal"]
-layout_mode = 1
-anchors_preset = 8
-anchor_left = 0.5
-anchor_top = 0.5
-anchor_right = 0.5
-anchor_bottom = 0.5
-offset_left = -59.5
-offset_top = -11.5
-offset_right = 59.5
-offset_bottom = 11.5
-grow_horizontal = 2
-grow_vertical = 2
-text = "CONNECTING..."
-
-[node name="SettingsPanelContainer" type="PanelContainer" parent="."]
-visible = false
-anchors_preset = 15
-anchor_right = 1.0
-anchor_bottom = 1.0
-grow_horizontal = 2
-grow_vertical = 2
-size_flags_horizontal = 3
-script = SubResource("GDScript_gbnwv")
-
-[node name="VBoxContainer" type="VBoxContainer" parent="SettingsPanelContainer"]
-layout_mode = 2
-
-[node name="ScrollContainer" type="ScrollContainer" parent="SettingsPanelContainer/VBoxContainer"]
-custom_minimum_size = Vector2(480, 0)
-layout_mode = 2
-size_flags_vertical = 3
-scroll_vertical = 100
-
-[node name="MarginContainer" type="MarginContainer" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer"]
-layout_mode = 2
-size_flags_horizontal = 3
-theme_override_constants/margin_left = 15
-theme_override_constants/margin_top = 20
-theme_override_constants/margin_right = 15
-theme_override_constants/margin_bottom = 20
-
-[node name="VBoxContainer" type="VBoxContainer" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer"]
-layout_mode = 2
-size_flags_horizontal = 3
-size_flags_vertical = 3
-theme_override_constants/separation = 10
-
-[node name="PresetSection" type="Label" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer"]
-layout_mode = 2
-theme_override_colors/font_color = Color(0.682353, 0.917647, 1, 1)
-text = "Presets"
-horizontal_alignment = 1
-
-[node name="Presets" type="HBoxContainer" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer"]
-layout_mode = 2
-
-[node name="VeryLowPreset" type="Button" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/Presets"]
-layout_mode = 2
-size_flags_horizontal = 3
-text = "Very Low"
-
-[node name="LowPreset" type="Button" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/Presets"]
-layout_mode = 2
-size_flags_horizontal = 3
-text = "Low"
-
-[node name="MediumPreset" type="Button" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/Presets"]
-layout_mode = 2
-size_flags_horizontal = 3
-text = "Medium"
-
-[node name="HighPreset" type="Button" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/Presets"]
-layout_mode = 2
-size_flags_horizontal = 3
-text = "High"
-
-[node name="UltraPreset" type="Button" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/Presets"]
-layout_mode = 2
-size_flags_horizontal = 3
-text = "Ultra"
-
-[node name="HSeparator" type="HSeparator" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer"]
-layout_mode = 2
-
-[node name="UISection" type="Label" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer"]
-layout_mode = 2
-theme_override_colors/font_color = Color(0.683425, 0.916893, 1, 1)
-text = "User Interface Settings"
-horizontal_alignment = 1
-
-[node name="UIGridContainer" type="GridContainer" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer"]
-layout_mode = 2
-size_flags_horizontal = 3
-size_flags_vertical = 3
-columns = 2
-
-[node name="UIScaleLabel" type="Label" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/UIGridContainer"]
-layout_mode = 2
-size_flags_horizontal = 3
-theme_override_font_sizes/font_size = 16
-text = "UI Scale:"
-
-[node name="UIScaleOptionButton" type="OptionButton" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/UIGridContainer"]
-layout_mode = 2
-size_flags_horizontal = 3
-theme_override_font_sizes/font_size = 16
-item_count = 5
-selected = 2
-popup/item_0/text = "Smaller (66%)"
-popup/item_0/id = 0
-popup/item_1/text = "Small (80%)"
-popup/item_1/id = 1
-popup/item_2/text = "Medium (100%)"
-popup/item_2/id = 2
-popup/item_3/text = "Large (133%)"
-popup/item_3/id = 3
-popup/item_4/text = "Larger (200%)"
-popup/item_4/id = 4
-script = SubResource("GDScript_7votw")
-
-[node name="ControlsSection" type="Label" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer"]
-layout_mode = 2
-theme_override_colors/font_color = Color(0.683425, 0.916893, 1, 1)
-text = "Controls Settings"
-horizontal_alignment = 1
-
-[node name="ControlsGridContainer" type="GridContainer" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer"]
-layout_mode = 2
-columns = 2
-
-[node name="SensitivityLabel" type="Label" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ControlsGridContainer"]
-layout_mode = 2
-size_flags_horizontal = 3
-text = "Mouse sensitivity"
-
-[node name="SensitivityControls" type="HBoxContainer" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ControlsGridContainer"]
-layout_mode = 2
-size_flags_horizontal = 3
-script = SubResource("GDScript_ux54k")
-
-[node name="SpinBox" type="SpinBox" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ControlsGridContainer/SensitivityControls"]
-layout_mode = 2
-
-[node name="Slider" type="HSlider" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ControlsGridContainer/SensitivityControls"]
-layout_mode = 2
-size_flags_horizontal = 3
-size_flags_vertical = 4
-
-[node name="InvertedYLabel" type="Label" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ControlsGridContainer"]
-layout_mode = 2
-size_flags_horizontal = 3
-text = "Inverted Y"
-
-[node name="InvertedYCheckbox" type="CheckBox" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ControlsGridContainer"]
-unique_name_in_owner = true
-layout_mode = 2
-size_flags_horizontal = 0
-
-[node name="ViewportSection" type="Label" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer"]
-layout_mode = 2
-theme_override_colors/font_color = Color(0.683425, 0.916893, 1, 1)
-text = "Video Settings"
-horizontal_alignment = 1
-
-[node name="ViewportGridContainer" type="GridContainer" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer"]
-layout_mode = 2
-columns = 2
-
-[node name="FOVLabel" type="Label" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer"]
-layout_mode = 2
-size_flags_horizontal = 3
-theme_override_font_sizes/font_size = 16
-text = "Field of View:"
-
-[node name="FOVControls" type="HBoxContainer" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer"]
-unique_name_in_owner = true
-layout_mode = 2
-script = SubResource("GDScript_v5ux6")
-
-[node name="SpinBox" type="SpinBox" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer/FOVControls"]
-layout_mode = 2
-min_value = 70.0
-max_value = 150.0
-value = 90.0
-
-[node name="Slider" type="HSlider" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer/FOVControls"]
-layout_mode = 2
-size_flags_horizontal = 3
-size_flags_vertical = 4
-min_value = 70.0
-max_value = 150.0
-value = 90.0
-
-[node name="QualityLabel" type="Label" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer"]
-layout_mode = 2
-size_flags_horizontal = 3
-theme_override_font_sizes/font_size = 16
-text = "Resolution Scale:"
-
-[node name="QualitySlider" type="HSlider" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer"]
-layout_mode = 2
-size_flags_horizontal = 3
-size_flags_vertical = 1
-min_value = 0.25
-max_value = 2.0
-step = 0.05
-value = 1.0
-script = SubResource("GDScript_viqb1")
-
-[node name="FilterLabel" type="Label" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer"]
-layout_mode = 2
-size_flags_horizontal = 3
-theme_override_font_sizes/font_size = 16
-text = "Display Filter:"
-
-[node name="FilterOptionButton" type="OptionButton" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer"]
-layout_mode = 2
-size_flags_horizontal = 3
-theme_override_font_sizes/font_size = 16
-item_count = 2
-selected = 0
-popup/item_0/text = "Bilinear (Fastest)"
-popup/item_0/id = 0
-popup/item_1/text = "FSR 1.0 (Fast)"
-popup/item_1/id = 1
-script = SubResource("GDScript_mnbe0")
-
-[node name="FSRSharpnessLabel" type="Label" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer"]
-unique_name_in_owner = true
-layout_mode = 2
-size_flags_horizontal = 3
-theme_override_font_sizes/font_size = 16
-text = "FSR Sharpness:"
-
-[node name="FSRSharpnessSlider" type="HSlider" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer"]
-unique_name_in_owner = true
-layout_mode = 2
-size_flags_horizontal = 3
-size_flags_vertical = 1
-max_value = 2.0
-step = 0.2
-script = SubResource("GDScript_c3ngv")
-
-[node name="WindowModeLabel" type="Label" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer"]
-layout_mode = 2
-size_flags_horizontal = 3
-theme_override_font_sizes/font_size = 16
-text = "Window Mode:"
-
-[node name="WindowModeOptionButton" type="OptionButton" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer"]
-layout_mode = 2
-size_flags_horizontal = 3
-theme_override_font_sizes/font_size = 16
-item_count = 3
-selected = 2
-popup/item_0/text = "Windowed"
-popup/item_0/id = 0
-popup/item_1/text = "Fullscreen"
-popup/item_1/id = 1
-popup/item_2/text = "Exclusive Fullscreen"
-popup/item_2/id = 2
-script = SubResource("GDScript_xa5kp")
-
-[node name="VsyncLabel" type="Label" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer"]
-layout_mode = 2
-size_flags_horizontal = 3
-theme_override_font_sizes/font_size = 16
-text = "V-Sync:"
-
-[node name="VsyncOptionButton" type="OptionButton" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer"]
-layout_mode = 2
-size_flags_horizontal = 3
-theme_override_font_sizes/font_size = 16
-item_count = 3
-selected = 0
-popup/item_0/text = "Disabled"
-popup/item_0/id = 0
-popup/item_1/text = "Adaptive"
-popup/item_1/id = 1
-popup/item_2/text = "Enabled"
-popup/item_2/id = 2
-script = SubResource("GDScript_jl1uk")
-
-[node name="TAALabel" type="Label" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer"]
-visible = false
-layout_mode = 2
-size_flags_horizontal = 3
-theme_override_font_sizes/font_size = 16
-text = "Anti-Aliasing (TAA):"
-
-[node name="TAAOptionButton" type="OptionButton" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer"]
-unique_name_in_owner = true
-visible = false
-layout_mode = 2
-size_flags_horizontal = 3
-theme_override_font_sizes/font_size = 16
-item_count = 2
-selected = 0
-popup/item_0/text = "Disabled (Fastest)"
-popup/item_0/id = 0
-popup/item_1/text = "Enabled (Average)"
-popup/item_1/id = 1
-script = SubResource("GDScript_6qqbt")
-
-[node name="MSAALabel" type="Label" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer"]
-layout_mode = 2
-size_flags_horizontal = 3
-theme_override_font_sizes/font_size = 16
-text = "Anti-Aliasing (MSAA):"
-
-[node name="MSAAOptionButton" type="OptionButton" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer"]
-unique_name_in_owner = true
-layout_mode = 2
-size_flags_horizontal = 3
-theme_override_font_sizes/font_size = 16
-item_count = 4
-selected = 0
-popup/item_0/text = "Disabled (Fastest)"
-popup/item_0/id = 0
-popup/item_1/text = "2× (Average)"
-popup/item_1/id = 1
-popup/item_2/text = "4× (Slow)"
-popup/item_2/id = 2
-popup/item_3/text = "8× (Slower)"
-popup/item_3/id = 3
-script = SubResource("GDScript_ulkhx")
-
-[node name="FXAALabel" type="Label" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer"]
-layout_mode = 2
-size_flags_horizontal = 3
-theme_override_font_sizes/font_size = 16
-text = "Anti-Aliasing (FXAA):"
-
-[node name="FXAAOptionButton" type="OptionButton" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer"]
-unique_name_in_owner = true
-layout_mode = 2
-size_flags_horizontal = 3
-theme_override_font_sizes/font_size = 16
-item_count = 2
-selected = 0
-popup/item_0/text = "Disabled (Fastest)"
-popup/item_0/id = 0
-popup/item_1/text = "Enabled (Fast)"
-popup/item_1/id = 1
-script = SubResource("GDScript_k6fl5")
-
-[node name="QualitySection" type="Label" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer"]
-layout_mode = 2
-theme_override_colors/font_color = Color(0.683425, 0.916893, 1, 1)
-text = "Quality Settings"
-horizontal_alignment = 1
-
-[node name="QualityGridContainer" type="GridContainer" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer"]
-layout_mode = 2
-columns = 2
-
-[node name="ShadowSizeLabel" type="Label" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/QualityGridContainer"]
-layout_mode = 2
-size_flags_horizontal = 3
-theme_override_font_sizes/font_size = 16
-text = "Shadow Resolution:"
-
-[node name="ShadowSizeOptionButton" type="OptionButton" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/QualityGridContainer"]
-unique_name_in_owner = true
-layout_mode = 2
-size_flags_horizontal = 3
-theme_override_font_sizes/font_size = 16
-item_count = 6
-selected = 3
-popup/item_0/text = "Minimum (Fastest)"
-popup/item_0/id = 0
-popup/item_1/text = "Very Low (Faster)"
-popup/item_1/id = 1
-popup/item_2/text = "Low (Fast)"
-popup/item_2/id = 2
-popup/item_3/text = "Medium (Average)"
-popup/item_3/id = 3
-popup/item_4/text = "High (Slow)"
-popup/item_4/id = 4
-popup/item_5/text = "Ultra (Slowest)"
-popup/item_5/id = 5
-script = SubResource("GDScript_cph1u")
-
-[node name="ShadowFilterLabel" type="Label" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/QualityGridContainer"]
-layout_mode = 2
-size_flags_horizontal = 3
-theme_override_font_sizes/font_size = 16
-text = "Shadow Filtering:"
-
-[node name="ShadowFilterOptionButton" type="OptionButton" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/QualityGridContainer"]
-unique_name_in_owner = true
-layout_mode = 2
-size_flags_horizontal = 3
-theme_override_font_sizes/font_size = 16
-item_count = 6
-selected = 2
-popup/item_0/text = "Very Low (Fastest)"
-popup/item_0/id = 0
-popup/item_1/text = "Low (Faster)"
-popup/item_1/id = 1
-popup/item_2/text = "Medium (Fast)"
-popup/item_2/id = 2
-popup/item_3/text = "High (Average)"
-popup/item_3/id = 3
-popup/item_4/text = "Very High (Slow)"
-popup/item_4/id = 4
-popup/item_5/text = "Ultra (Slower)"
-popup/item_5/id = 5
-script = SubResource("GDScript_uhm5l")
-
-[node name="MeshLODLabel" type="Label" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/QualityGridContainer"]
-layout_mode = 2
-size_flags_horizontal = 3
-theme_override_font_sizes/font_size = 16
-text = "Model Quality:"
-
-[node name="MeshLODOptionButton" type="OptionButton" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/QualityGridContainer"]
-unique_name_in_owner = true
-layout_mode = 2
-size_flags_horizontal = 3
-theme_override_font_sizes/font_size = 16
-item_count = 4
-selected = 2
-popup/item_0/text = "Low (Faster)"
-popup/item_0/id = 0
-popup/item_1/text = "Medium (Fast)"
-popup/item_1/id = 1
-popup/item_2/text = "High (Average)"
-popup/item_2/id = 2
-popup/item_3/text = "Ultra (Slow)"
-popup/item_3/id = 3
-script = SubResource("GDScript_4cgn8")
-
-[node name="EnvironmentSection" type="Label" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer"]
-layout_mode = 2
-theme_override_colors/font_color = Color(0.683425, 0.916893, 1, 1)
-text = "Effect Settings"
-horizontal_alignment = 1
-
-[node name="EnvironmentGridContainer" type="GridContainer" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer"]
-layout_mode = 2
-columns = 2
-
-[node name="SDFGILabel" type="Label" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/EnvironmentGridContainer"]
-layout_mode = 2
-size_flags_horizontal = 3
-theme_override_font_sizes/font_size = 16
-text = "Global Illumination:"
-
-[node name="SDFGIOptionButton" type="OptionButton" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/EnvironmentGridContainer"]
-unique_name_in_owner = true
-layout_mode = 2
-size_flags_horizontal = 3
-theme_override_font_sizes/font_size = 16
-item_count = 3
-selected = 0
-popup/item_0/text = "Disabled (Fastest)"
-popup/item_0/id = 0
-popup/item_1/text = "Low (Average)"
-popup/item_1/id = 1
-popup/item_2/text = "High (Slow)"
-popup/item_2/id = 2
-script = SubResource("GDScript_xfygm")
-
-[node name="GlowLabel" type="Label" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/EnvironmentGridContainer"]
-layout_mode = 2
-size_flags_horizontal = 3
-theme_override_font_sizes/font_size = 16
-text = "Bloom:"
-
-[node name="GlowOptionButton" type="OptionButton" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/EnvironmentGridContainer"]
-unique_name_in_owner = true
-layout_mode = 2
-size_flags_horizontal = 3
-theme_override_font_sizes/font_size = 16
-item_count = 3
-selected = 0
-popup/item_0/text = "Disabled (Fastest)"
-popup/item_0/id = 0
-popup/item_1/text = "Low (Fast)"
-popup/item_1/id = 1
-popup/item_2/text = "High (Average)"
-popup/item_2/id = 2
-script = SubResource("GDScript_k3pso")
-
-[node name="SSAOLabel" type="Label" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/EnvironmentGridContainer"]
-layout_mode = 2
-size_flags_horizontal = 3
-theme_override_font_sizes/font_size = 16
-text = "Ambient Occlusion:"
-
-[node name="SSAOOptionButton" type="OptionButton" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/EnvironmentGridContainer"]
-unique_name_in_owner = true
-layout_mode = 2
-size_flags_horizontal = 3
-theme_override_font_sizes/font_size = 16
-item_count = 5
-selected = 0
-popup/item_0/text = "Disabled (Fastest)"
-popup/item_0/id = 0
-popup/item_1/text = "Very Low (Fast)"
-popup/item_1/id = 1
-popup/item_2/text = "Low (Fast)"
-popup/item_2/id = 2
-popup/item_3/text = "Medium (Average)"
-popup/item_3/id = 3
-popup/item_4/text = "High (Slow)"
-popup/item_4/id = 4
-script = SubResource("GDScript_0sqe1")
-
-[node name="SSReflectionsLabel" type="Label" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/EnvironmentGridContainer"]
-layout_mode = 2
-size_flags_horizontal = 3
-theme_override_font_sizes/font_size = 14
-text = "Screen-Space Reflections:"
-
-[node name="SSReflectionsOptionButton" type="OptionButton" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/EnvironmentGridContainer"]
-unique_name_in_owner = true
-layout_mode = 2
-size_flags_horizontal = 3
-theme_override_font_sizes/font_size = 16
-item_count = 4
-selected = 0
-popup/item_0/text = "Disabled (Fastest)"
-popup/item_0/id = 0
-popup/item_1/text = "Low (Average)"
-popup/item_1/id = 1
-popup/item_2/text = "Medium (Slow)"
-popup/item_2/id = 2
-popup/item_3/text = "High (Slower)"
-popup/item_3/id = 3
-script = SubResource("GDScript_cb82q")
-
-[node name="SSILLabel" type="Label" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/EnvironmentGridContainer"]
-layout_mode = 2
-size_flags_horizontal = 3
-theme_override_font_sizes/font_size = 16
-text = "Screen-Space Lighting:"
-
-[node name="SSILOptionButton" type="OptionButton" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/EnvironmentGridContainer"]
-unique_name_in_owner = true
-layout_mode = 2
-size_flags_horizontal = 3
-theme_override_font_sizes/font_size = 16
-item_count = 5
-selected = 0
-popup/item_0/text = "Disabled (Fastest)"
-popup/item_0/id = 0
-popup/item_1/text = "Very Low (Fast)"
-popup/item_1/id = 1
-popup/item_2/text = "Low (Average)"
-popup/item_2/id = 2
-popup/item_3/text = "Medium (Slow)"
-popup/item_3/id = 3
-popup/item_4/text = "High (Slower)"
-popup/item_4/id = 4
-script = SubResource("GDScript_qrf3h")
-
-[node name="VolumetricFogLabel" type="Label" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/EnvironmentGridContainer"]
-layout_mode = 2
-size_flags_horizontal = 3
-theme_override_font_sizes/font_size = 16
-text = "Volumetric Fog:"
-
-[node name="VolumetricFogOptionButton" type="OptionButton" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/EnvironmentGridContainer"]
-unique_name_in_owner = true
-layout_mode = 2
-size_flags_horizontal = 3
-theme_override_font_sizes/font_size = 16
-item_count = 3
-selected = 0
-popup/item_0/text = "Disabled (Fastest)"
-popup/item_0/id = 0
-popup/item_1/text = "Low (Fast)"
-popup/item_1/id = 1
-popup/item_2/text = "High (Average)"
-popup/item_2/id = 2
-script = SubResource("GDScript_80uen")
-
-[node name="AdjustmentSection" type="Label" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer"]
-visible = false
-layout_mode = 2
-theme_override_colors/font_color = Color(0.683425, 0.916893, 1, 1)
-text = "Adjustments"
-horizontal_alignment = 1
-
-[node name="AdjustmentsGridContainer" type="GridContainer" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer"]
-visible = false
-layout_mode = 2
-columns = 2
-
-[node name="BrightnessLabel" type="Label" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/AdjustmentsGridContainer"]
-layout_mode = 2
-size_flags_horizontal = 3
-theme_override_font_sizes/font_size = 16
-text = "Brightness:"
-
-[node name="BrightnessSlider" type="HSlider" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/AdjustmentsGridContainer"]
-layout_mode = 2
-size_flags_horizontal = 3
-size_flags_vertical = 1
-min_value = 0.5
-max_value = 2.0
-step = 0.01
-value = 1.0
-
-[node name="ContrastLabel" type="Label" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/AdjustmentsGridContainer"]
-layout_mode = 2
-size_flags_horizontal = 3
-theme_override_font_sizes/font_size = 16
-text = "Contrast:"
-
-[node name="ContrastSlider" type="HSlider" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/AdjustmentsGridContainer"]
-layout_mode = 2
-size_flags_horizontal = 3
-size_flags_vertical = 1
-min_value = 0.5
-max_value = 2.0
-step = 0.01
-value = 1.0
-
-[node name="SaturationLabel" type="Label" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/AdjustmentsGridContainer"]
-layout_mode = 2
-size_flags_horizontal = 3
-theme_override_font_sizes/font_size = 16
-text = "Saturation:"
-
-[node name="SaturationSlider" type="HSlider" parent="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/AdjustmentsGridContainer"]
-layout_mode = 2
-size_flags_horizontal = 3
-size_flags_vertical = 1
-min_value = 0.01
-max_value = 2.0
-step = 0.01
-value = 1.0
-
-[node name="MarginContainer" type="MarginContainer" parent="SettingsPanelContainer/VBoxContainer"]
-layout_mode = 2
-size_flags_horizontal = 3
-theme_override_constants/margin_left = 15
-theme_override_constants/margin_top = 10
-theme_override_constants/margin_right = 23
-theme_override_constants/margin_bottom = 15
-
-[node name="Buttons" type="HBoxContainer" parent="SettingsPanelContainer/VBoxContainer/MarginContainer"]
-layout_mode = 2
-
-[node name="Apply" type="Button" parent="SettingsPanelContainer/VBoxContainer/MarginContainer/Buttons"]
-layout_mode = 2
-size_flags_horizontal = 3
-text = "Apply
-"
-
-[node name="Reset" type="Button" parent="SettingsPanelContainer/VBoxContainer/MarginContainer/Buttons"]
-layout_mode = 2
-size_flags_horizontal = 3
-text = "Reset"
-
-[node name="Close" type="Button" parent="SettingsPanelContainer/VBoxContainer/MarginContainer/Buttons"]
-layout_mode = 2
-size_flags_horizontal = 3
-text = "Close"
+theme = ExtResource("5_unqvv")
[node name="MainPanelContainer" type="PanelContainer" parent="."]
-unique_name_in_owner = true
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
size_flags_horizontal = 0
+theme = ExtResource("5_unqvv")
theme_override_styles/panel = SubResource("StyleBoxFlat_c4ymk")
[node name="HBoxContainer" type="HBoxContainer" parent="MainPanelContainer"]
@@ -1994,48 +92,6 @@ size_flags_horizontal = 3
size_flags_stretch_ratio = 0.8
mouse_filter = 1
-[connection signal="join_server" from="MultiplayerPanelContainer" to="." method="_on_multiplayer_join_server"]
-[connection signal="start_server" from="MultiplayerPanelContainer" to="." method="_on_multiplayer_start_server"]
-[connection signal="pressed" from="MultiplayerPanelContainer/MarginContainer/VBoxContainer/TabContainer/Profile/MarginContainer/HBoxContainer/LeftBox/Top/Save" to="MultiplayerPanelContainer" method="_on_save_pressed"]
-[connection signal="pressed" from="MultiplayerPanelContainer/MarginContainer/VBoxContainer/TabContainer/Profile/MarginContainer/HBoxContainer/LeftBox/Bottom/MainMenu" to="." method="_on_main_menu_pressed"]
-[connection signal="pressed" from="MultiplayerPanelContainer/MarginContainer/VBoxContainer/TabContainer/Profile/MarginContainer/HBoxContainer/LeftBox/Bottom/Quit" to="." method="_on_quit_pressed"]
-[connection signal="pressed" from="MultiplayerPanelContainer/MarginContainer/VBoxContainer/TabContainer/Join/MarginContainer/HBoxContainer/LeftBox/Top/Join" to="MultiplayerPanelContainer" method="_on_join_pressed"]
-[connection signal="pressed" from="MultiplayerPanelContainer/MarginContainer/VBoxContainer/TabContainer/Join/MarginContainer/HBoxContainer/LeftBox/Bottom/MainMenu" to="." method="_on_main_menu_pressed"]
-[connection signal="pressed" from="MultiplayerPanelContainer/MarginContainer/VBoxContainer/TabContainer/Join/MarginContainer/HBoxContainer/LeftBox/Bottom/Quit" to="." method="_on_quit_pressed"]
-[connection signal="pressed" from="MultiplayerPanelContainer/MarginContainer/VBoxContainer/TabContainer/Host/MarginContainer/HBoxContainer/LeftBox/Top/Host" to="MultiplayerPanelContainer" method="_on_host_pressed"]
-[connection signal="pressed" from="MultiplayerPanelContainer/MarginContainer/VBoxContainer/TabContainer/Host/MarginContainer/HBoxContainer/LeftBox/Bottom/MainMenu" to="." method="_on_main_menu_pressed"]
-[connection signal="pressed" from="MultiplayerPanelContainer/MarginContainer/VBoxContainer/TabContainer/Host/MarginContainer/HBoxContainer/LeftBox/Bottom/Quit" to="." method="_on_quit_pressed"]
-[connection signal="text_changed" from="MultiplayerPanelContainer/MarginContainer/VBoxContainer/TabContainer/Host/MarginContainer/HBoxContainer/RightBox/ServerPort" to="MultiplayerPanelContainer/MarginContainer/VBoxContainer/TabContainer/Host/MarginContainer/HBoxContainer/RightBox/ServerPort" method="_on_text_changed"]
-[connection signal="pressed" from="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/Presets/VeryLowPreset" to="SettingsPanelContainer" method="_on_very_low_preset_pressed"]
-[connection signal="pressed" from="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/Presets/LowPreset" to="SettingsPanelContainer" method="_on_low_preset_pressed"]
-[connection signal="pressed" from="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/Presets/MediumPreset" to="SettingsPanelContainer" method="_on_medium_preset_pressed"]
-[connection signal="pressed" from="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/Presets/HighPreset" to="SettingsPanelContainer" method="_on_high_preset_pressed"]
-[connection signal="pressed" from="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/Presets/UltraPreset" to="SettingsPanelContainer" method="_on_ultra_preset_pressed"]
-[connection signal="item_selected" from="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/UIGridContainer/UIScaleOptionButton" to="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/UIGridContainer/UIScaleOptionButton" method="_on_item_selected"]
-[connection signal="value_changed" from="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ControlsGridContainer/SensitivityControls/SpinBox" to="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ControlsGridContainer/SensitivityControls" method="_on_spin_box_value_changed"]
-[connection signal="value_changed" from="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ControlsGridContainer/SensitivityControls/Slider" to="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ControlsGridContainer/SensitivityControls" method="_on_slider_value_changed"]
-[connection signal="value_changed" from="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer/FOVControls/SpinBox" to="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer/FOVControls" method="_on_spin_box_value_changed"]
-[connection signal="value_changed" from="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer/FOVControls/Slider" to="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer/FOVControls" method="_on_slider_value_changed"]
-[connection signal="value_changed" from="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer/QualitySlider" to="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer/QualitySlider" method="_on_value_changed"]
-[connection signal="item_selected" from="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer/FilterOptionButton" to="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer/FilterOptionButton" method="_on_item_selected"]
-[connection signal="value_changed" from="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer/FSRSharpnessSlider" to="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer/FSRSharpnessSlider" method="_on_value_changed"]
-[connection signal="item_selected" from="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer/WindowModeOptionButton" to="SettingsPanelContainer" method="_on_window_mode_option_button_item_selected"]
-[connection signal="item_selected" from="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer/VsyncOptionButton" to="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer/VsyncOptionButton" method="_on_item_selected"]
-[connection signal="item_selected" from="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer/TAAOptionButton" to="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer/TAAOptionButton" method="_on_item_selected"]
-[connection signal="item_selected" from="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer/MSAAOptionButton" to="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer/MSAAOptionButton" method="_on_item_selected"]
-[connection signal="item_selected" from="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer/FXAAOptionButton" to="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer/FXAAOptionButton" method="_on_item_selected"]
-[connection signal="item_selected" from="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/QualityGridContainer/ShadowSizeOptionButton" to="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/QualityGridContainer/ShadowSizeOptionButton" method="_on_item_selected"]
-[connection signal="item_selected" from="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/QualityGridContainer/ShadowFilterOptionButton" to="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/QualityGridContainer/ShadowFilterOptionButton" method="_on_item_selected"]
-[connection signal="item_selected" from="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/QualityGridContainer/MeshLODOptionButton" to="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/QualityGridContainer/MeshLODOptionButton" method="_on_item_selected"]
-[connection signal="item_selected" from="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/EnvironmentGridContainer/SDFGIOptionButton" to="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/EnvironmentGridContainer/SDFGIOptionButton" method="_on_item_selected"]
-[connection signal="item_selected" from="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/EnvironmentGridContainer/GlowOptionButton" to="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/EnvironmentGridContainer/GlowOptionButton" method="_on_item_selected"]
-[connection signal="item_selected" from="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/EnvironmentGridContainer/SSAOOptionButton" to="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/EnvironmentGridContainer/SSAOOptionButton" method="_on_item_selected"]
-[connection signal="item_selected" from="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/EnvironmentGridContainer/SSReflectionsOptionButton" to="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/EnvironmentGridContainer/SSReflectionsOptionButton" method="_on_item_selected"]
-[connection signal="item_selected" from="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/EnvironmentGridContainer/SSILOptionButton" to="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/EnvironmentGridContainer/SSILOptionButton" method="_on_item_selected"]
-[connection signal="item_selected" from="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/EnvironmentGridContainer/VolumetricFogOptionButton" to="SettingsPanelContainer/VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/EnvironmentGridContainer/VolumetricFogOptionButton" method="_on_item_selected"]
-[connection signal="pressed" from="SettingsPanelContainer/VBoxContainer/MarginContainer/Buttons/Apply" to="SettingsPanelContainer" method="_on_apply_pressed"]
-[connection signal="pressed" from="SettingsPanelContainer/VBoxContainer/MarginContainer/Buttons/Reset" to="SettingsPanelContainer" method="_on_reset_pressed"]
-[connection signal="pressed" from="SettingsPanelContainer/VBoxContainer/MarginContainer/Buttons/Close" to="SettingsPanelContainer" method="_on_close_pressed"]
[connection signal="pressed" from="MainPanelContainer/HBoxContainer/PanelContainer/MarginContainer/VBoxContainer/Demo" to="." method="_on_demo_pressed"]
[connection signal="pressed" from="MainPanelContainer/HBoxContainer/PanelContainer/MarginContainer/VBoxContainer/Multiplayer" to="." method="_on_multiplayer_pressed"]
[connection signal="pressed" from="MainPanelContainer/HBoxContainer/PanelContainer/MarginContainer/VBoxContainer/Settings" to="." method="_on_settings_pressed"]
diff --git a/tests/test_basics.gd b/interfaces/menus/boot/multiplayer/host_panel/host_panel.gd
similarity index 70%
rename from tests/test_basics.gd
rename to interfaces/menus/boot/multiplayer/host_panel/host_panel.gd
index 921df60..1cf104c 100644
--- a/tests/test_basics.gd
+++ b/interfaces/menus/boot/multiplayer/host_panel/host_panel.gd
@@ -12,10 +12,11 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-extends GutTest
+class_name HostPanel extends MarginContainer
-func test_projectile_class() -> void:
- var projectile : Projectile = Projectile.new()
- assert_true(projectile != null, "Projectile class should be instantiated")
- assert_true(projectile.speed == 78.4, "Projectile damage should be initialized to 78.4")
- projectile.free()
+@export var host_button : Button
+@export var menu_button : Button
+@export var quit_button : Button
+@export var server_port : LineEdit
+@export var map_selector : MapSelector
+@export var mode_selector : ModeSelector
diff --git a/interfaces/menus/boot/multiplayer/host_panel/host_panel.tscn b/interfaces/menus/boot/multiplayer/host_panel/host_panel.tscn
new file mode 100644
index 0000000..0ee7083
--- /dev/null
+++ b/interfaces/menus/boot/multiplayer/host_panel/host_panel.tscn
@@ -0,0 +1,97 @@
+[gd_scene load_steps=4 format=3 uid="uid://dv0lqek8bqyv"]
+
+[ext_resource type="Script" path="res://interfaces/menus/boot/multiplayer/host_panel/host_panel.gd" id="1_wnvng"]
+[ext_resource type="Script" path="res://interfaces/menus/boot/multiplayer/host_panel/map_selector.gd" id="2_kwlr3"]
+[ext_resource type="Script" path="res://interfaces/menus/boot/multiplayer/host_panel/mode_selector.gd" id="3_o6ecq"]
+
+[node name="HostPanel" type="MarginContainer" node_paths=PackedStringArray("host_button", "menu_button", "quit_button", "server_port", "map_selector", "mode_selector")]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+theme_override_constants/margin_left = 20
+theme_override_constants/margin_top = 20
+theme_override_constants/margin_right = 20
+theme_override_constants/margin_bottom = 20
+script = ExtResource("1_wnvng")
+host_button = NodePath("HBoxContainer/LeftBox/Top/Host")
+menu_button = NodePath("HBoxContainer/LeftBox/Bottom/Menu")
+quit_button = NodePath("HBoxContainer/LeftBox/Bottom/Quit")
+server_port = NodePath("HBoxContainer/RightBox/GridContainer/ServerPort")
+map_selector = NodePath("HBoxContainer/RightBox/GridContainer/MapSelector")
+mode_selector = NodePath("HBoxContainer/RightBox/GridContainer/ModeSelector")
+
+[node name="HBoxContainer" type="HBoxContainer" parent="."]
+layout_mode = 2
+theme_override_constants/separation = 20
+
+[node name="LeftBox" type="VBoxContainer" parent="HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_stretch_ratio = 0.25
+
+[node name="Top" type="VBoxContainer" parent="HBoxContainer/LeftBox"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_stretch_ratio = 0.25
+
+[node name="Host" type="Button" parent="HBoxContainer/LeftBox/Top"]
+layout_mode = 2
+text = "Host"
+
+[node name="Bottom" type="VBoxContainer" parent="HBoxContainer/LeftBox"]
+layout_mode = 2
+size_flags_vertical = 10
+
+[node name="Menu" type="Button" parent="HBoxContainer/LeftBox/Bottom"]
+layout_mode = 2
+size_flags_horizontal = 3
+text = "Main Menu
+"
+
+[node name="Quit" type="Button" parent="HBoxContainer/LeftBox/Bottom"]
+layout_mode = 2
+size_flags_horizontal = 3
+text = "Quit"
+
+[node name="RightBox" type="VBoxContainer" parent="HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="MapPreview" type="ColorRect" parent="HBoxContainer/RightBox"]
+layout_mode = 2
+
+[node name="GridContainer" type="GridContainer" parent="HBoxContainer/RightBox"]
+layout_mode = 2
+theme_override_constants/h_separation = 120
+columns = 2
+
+[node name="PortLabel" type="Label" parent="HBoxContainer/RightBox/GridContainer"]
+layout_mode = 2
+text = "Port"
+
+[node name="ServerPort" type="LineEdit" parent="HBoxContainer/RightBox/GridContainer"]
+layout_mode = 2
+placeholder_text = "9000"
+alignment = 2
+
+[node name="MapLabel" type="Label" parent="HBoxContainer/RightBox/GridContainer"]
+layout_mode = 2
+text = "Map"
+
+[node name="MapSelector" type="OptionButton" parent="HBoxContainer/RightBox/GridContainer"]
+layout_mode = 2
+clip_text = true
+allow_reselect = true
+script = ExtResource("2_kwlr3")
+
+[node name="ModeLabel" type="Label" parent="HBoxContainer/RightBox/GridContainer"]
+layout_mode = 2
+text = "Mode"
+
+[node name="ModeSelector" type="OptionButton" parent="HBoxContainer/RightBox/GridContainer"]
+layout_mode = 2
+script = ExtResource("3_o6ecq")
diff --git a/interfaces/menus/boot/multiplayer/host_panel/map_selector.gd b/interfaces/menus/boot/multiplayer/host_panel/map_selector.gd
new file mode 100644
index 0000000..4aeb2aa
--- /dev/null
+++ b/interfaces/menus/boot/multiplayer/host_panel/map_selector.gd
@@ -0,0 +1,21 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+class_name MapSelector extends OptionButton
+
+# Called when the node enters the scene tree for the first time.
+func _ready() -> void:
+ for map : PackedScene in MapsManager.maps:
+ var map_name : String = map._bundled.names[0]
+ add_item(map_name)
diff --git a/interfaces/menus/boot/multiplayer/host_panel/mode_selector.gd b/interfaces/menus/boot/multiplayer/host_panel/mode_selector.gd
new file mode 100644
index 0000000..b34dfed
--- /dev/null
+++ b/interfaces/menus/boot/multiplayer/host_panel/mode_selector.gd
@@ -0,0 +1,33 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+class_name ModeSelector extends OptionButton
+
+func format(enum_key: String) -> String:
+ # split enum value by underscores
+ var words : PackedStringArray = enum_key.split("_")
+ # capitalize first letter of each word
+ for i in range(words.size()):
+ words[i] = words[i].capitalize()
+ # join words with spaces
+ return " ".join(words)
+
+func _ready() -> void:
+ for mode:String in Multiplayer.Mode.keys():
+ add_item(format(mode))
+
+ # @NOTE: ctf, arena and ball are not implemented yet
+ set_item_disabled(2, true)
+ set_item_disabled(3, true)
+ set_item_disabled(4, true)
diff --git a/interfaces/menus/boot/multiplayer/join_panel/join_panel.gd b/interfaces/menus/boot/multiplayer/join_panel/join_panel.gd
new file mode 100644
index 0000000..0293403
--- /dev/null
+++ b/interfaces/menus/boot/multiplayer/join_panel/join_panel.gd
@@ -0,0 +1,20 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+class_name JoinPanel extends MarginContainer
+
+@export var join_button : Button
+@export var menu_button : Button
+@export var quit_button : Button
+@export var join_address : LineEdit
diff --git a/interfaces/menus/boot/multiplayer/join_panel/join_panel.tscn b/interfaces/menus/boot/multiplayer/join_panel/join_panel.tscn
new file mode 100644
index 0000000..3b64a9f
--- /dev/null
+++ b/interfaces/menus/boot/multiplayer/join_panel/join_panel.tscn
@@ -0,0 +1,62 @@
+[gd_scene load_steps=2 format=3 uid="uid://cfegls11p6wav"]
+
+[ext_resource type="Script" path="res://interfaces/menus/boot/multiplayer/join_panel/join_panel.gd" id="1_svm4h"]
+
+[node name="JoinPanel" type="MarginContainer" node_paths=PackedStringArray("join_button", "menu_button", "quit_button", "join_address")]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+theme_override_constants/margin_left = 20
+theme_override_constants/margin_top = 20
+theme_override_constants/margin_right = 20
+theme_override_constants/margin_bottom = 20
+script = ExtResource("1_svm4h")
+join_button = NodePath("HBoxContainer/LeftBox/Top/Join")
+menu_button = NodePath("HBoxContainer/LeftBox/Bottom/Menu")
+quit_button = NodePath("HBoxContainer/LeftBox/Bottom/Quit")
+join_address = NodePath("HBoxContainer/RightBox/JoinAddress")
+
+[node name="HBoxContainer" type="HBoxContainer" parent="."]
+layout_mode = 2
+theme_override_constants/separation = 20
+
+[node name="LeftBox" type="VBoxContainer" parent="HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_stretch_ratio = 0.25
+
+[node name="Top" type="VBoxContainer" parent="HBoxContainer/LeftBox"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_stretch_ratio = 0.25
+
+[node name="Join" type="Button" parent="HBoxContainer/LeftBox/Top"]
+layout_mode = 2
+text = "Join"
+
+[node name="Bottom" type="VBoxContainer" parent="HBoxContainer/LeftBox"]
+layout_mode = 2
+size_flags_vertical = 10
+
+[node name="Menu" type="Button" parent="HBoxContainer/LeftBox/Bottom"]
+layout_mode = 2
+size_flags_horizontal = 3
+text = "Main Menu
+"
+
+[node name="Quit" type="Button" parent="HBoxContainer/LeftBox/Bottom"]
+layout_mode = 2
+size_flags_horizontal = 3
+text = "Quit"
+
+[node name="RightBox" type="VBoxContainer" parent="HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="JoinAddress" type="LineEdit" parent="HBoxContainer/RightBox"]
+layout_mode = 2
+placeholder_text = "localhost"
diff --git a/interfaces/menus/boot/multiplayer/multiplayer_panel_container.gd b/interfaces/menus/boot/multiplayer/multiplayer_panel_container.gd
new file mode 100644
index 0000000..105af13
--- /dev/null
+++ b/interfaces/menus/boot/multiplayer/multiplayer_panel_container.gd
@@ -0,0 +1,98 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+class_name MultiplayerPanelContainer extends PanelContainer
+
+const DEFAULT_HOST : String = "localhost"
+const DEFAULT_PORT : int = 9000
+const CONFIG_FILE_PATH : String = "user://profile.cfg"
+
+var _join_address : RegEx = RegEx.new()
+var _registered_ports : RegEx = RegEx.new()
+var _config_file : ConfigFile = ConfigFile.new()
+
+signal menu_pressed # reuse signal for all tabs
+signal start_server(port : int, map : PackedScene, mode: Multiplayer.Mode, username: String)
+signal join_server(host : String, port : int)
+
+@export var tab_container : TabContainer
+@export var profile_panel : ProfilePanel
+@export var join_panel : JoinPanel
+@export var host_panel : HostPanel
+
+@onready var modal : Control = $Modal
+
+func _ready() -> void:
+ # see https://datatracker.ietf.org/doc/html/rfc1700
+ _registered_ports.compile(r'^(?:102[4-9]|10[3-9]\d|1[1-9]\d{2}|[2-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')
+ _join_address.compile(r'^(?[a-zA-Z0-9.-]+)(:(?:102[4-9]|10[3-9]\d|1[1-9]\d{2}|[2-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5]))?$')
+ # connect signals
+ join_panel.join_button.pressed.connect(_on_join_pressed)
+ host_panel.host_button.pressed.connect(_on_host_pressed)
+ for panel:Control in [profile_panel, host_panel, join_panel]:
+ panel.menu_button.pressed.connect(_on_menu_pressed)
+ panel.quit_button.pressed.connect(_on_quit_pressed)
+ # load configuration file
+ _load_config()
+
+func _unhandled_input(event: InputEvent) -> void:
+ if event.is_action_pressed("exit"):
+ modal.hide()
+
+func _load_config() -> void:
+ var error : Error = _config_file.load(CONFIG_FILE_PATH)
+ if error != OK:
+ return
+
+ profile_panel.profile_name.text = _config_file.get_value("profile", "name", "Newblood")
+
+func _on_save_pressed() -> void:
+ _config_file.set_value("profile", "name", profile_panel.profile_name.text)
+ _config_file.save(CONFIG_FILE_PATH)
+
+func _on_menu_pressed() -> void:
+ menu_pressed.emit()
+
+func _on_quit_pressed() -> void:
+ get_tree().quit()
+
+func _on_host_pressed() -> void:
+ var port : int = DEFAULT_PORT
+ # check for registered ports number matches
+ if host_panel.server_port.text:
+ var result : RegExMatch = _registered_ports.search(host_panel.server_port.text)
+ if result: # port is valid
+ port = int(result.get_string())
+ else: # port is not valid
+ push_warning("A valid port number in the range 1024-65535 is required.")
+ return
+
+ start_server.emit(
+ port,
+ MapsManager.maps[host_panel.map_selector.selected],
+ host_panel.mode_selector.selected,
+ profile_panel.profile_name.text
+ )
+
+func _on_join_pressed() -> void:
+ var addr : Array = [DEFAULT_HOST, DEFAULT_PORT]
+ # validate join address input
+ var result : RegExMatch = _join_address.search(join_panel.join_address.text)
+ if result: # address is valid
+ addr[0] = result.get_string("host")
+ var rport : String = result.get_string("port")
+ if rport: addr[1] = int(rport)
+
+ modal.show()
+ join_server.emit(addr[0], addr[1], profile_panel.profile_name.text)
diff --git a/interfaces/menus/boot/multiplayer/multiplayer_panel_container.tscn b/interfaces/menus/boot/multiplayer/multiplayer_panel_container.tscn
new file mode 100644
index 0000000..a7a4379
--- /dev/null
+++ b/interfaces/menus/boot/multiplayer/multiplayer_panel_container.tscn
@@ -0,0 +1,79 @@
+[gd_scene load_steps=6 format=3 uid="uid://7chfb47enqwm"]
+
+[ext_resource type="Script" path="res://interfaces/menus/boot/multiplayer/multiplayer_panel_container.gd" id="1_yyvwh"]
+[ext_resource type="PackedScene" uid="uid://cfegls11p6wav" path="res://interfaces/menus/boot/multiplayer/join_panel/join_panel.tscn" id="2_4vm27"]
+[ext_resource type="PackedScene" uid="uid://moey0w3677td" path="res://interfaces/menus/boot/multiplayer/profile_panel/profile_panel.tscn" id="2_aidbu"]
+[ext_resource type="PackedScene" uid="uid://dv0lqek8bqyv" path="res://interfaces/menus/boot/multiplayer/host_panel/host_panel.tscn" id="2_ameih"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_krqeq"]
+bg_color = Color(0.501961, 0.501961, 0.501961, 0.25098)
+
+[node name="MultiplayerPanelContainer" type="PanelContainer" node_paths=PackedStringArray("tab_container", "profile_panel", "join_panel", "host_panel")]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_styles/panel = SubResource("StyleBoxFlat_krqeq")
+script = ExtResource("1_yyvwh")
+tab_container = NodePath("MarginContainer/VBoxContainer/TabContainer")
+profile_panel = NodePath("MarginContainer/VBoxContainer/TabContainer/Profile/ProfilePanel")
+join_panel = NodePath("MarginContainer/VBoxContainer/TabContainer/Join/JoinPanel")
+host_panel = NodePath("MarginContainer/VBoxContainer/TabContainer/Host/HostPanel")
+
+[node name="MarginContainer" type="MarginContainer" parent="."]
+layout_mode = 2
+theme_override_constants/margin_left = 20
+theme_override_constants/margin_top = 20
+theme_override_constants/margin_right = 20
+theme_override_constants/margin_bottom = 20
+
+[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"]
+layout_mode = 2
+
+[node name="TabContainer" type="TabContainer" parent="MarginContainer/VBoxContainer"]
+layout_mode = 2
+size_flags_vertical = 3
+theme_override_constants/side_margin = 0
+
+[node name="Profile" type="TabBar" parent="MarginContainer/VBoxContainer/TabContainer"]
+layout_mode = 2
+theme_override_constants/h_separation = 0
+
+[node name="ProfilePanel" parent="MarginContainer/VBoxContainer/TabContainer/Profile" instance=ExtResource("2_aidbu")]
+layout_mode = 1
+
+[node name="Join" type="TabBar" parent="MarginContainer/VBoxContainer/TabContainer"]
+visible = false
+layout_mode = 2
+select_with_rmb = true
+
+[node name="JoinPanel" parent="MarginContainer/VBoxContainer/TabContainer/Join" instance=ExtResource("2_4vm27")]
+layout_mode = 1
+
+[node name="Host" type="TabBar" parent="MarginContainer/VBoxContainer/TabContainer"]
+visible = false
+layout_mode = 2
+select_with_rmb = true
+
+[node name="HostPanel" parent="MarginContainer/VBoxContainer/TabContainer/Host" instance=ExtResource("2_ameih")]
+layout_mode = 1
+
+[node name="Modal" type="Panel" parent="."]
+visible = false
+layout_mode = 2
+
+[node name="Label" type="Label" parent="Modal"]
+layout_mode = 1
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+offset_left = -59.5
+offset_top = -11.5
+offset_right = 59.5
+offset_bottom = 11.5
+grow_horizontal = 2
+grow_vertical = 2
+text = "CONNECTING..."
diff --git a/interfaces/menus/boot/multiplayer/profile_panel/profile_panel.gd b/interfaces/menus/boot/multiplayer/profile_panel/profile_panel.gd
new file mode 100644
index 0000000..de713c1
--- /dev/null
+++ b/interfaces/menus/boot/multiplayer/profile_panel/profile_panel.gd
@@ -0,0 +1,22 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+class_name ProfilePanel extends MarginContainer
+
+@export var create_button : Button
+@export var delete_button : Button
+@export var save_button : Button
+@export var menu_button : Button
+@export var quit_button : Button
+@export var profile_name : LineEdit
diff --git a/interfaces/menus/boot/multiplayer/profile_panel/profile_panel.tscn b/interfaces/menus/boot/multiplayer/profile_panel/profile_panel.tscn
new file mode 100644
index 0000000..01c33a2
--- /dev/null
+++ b/interfaces/menus/boot/multiplayer/profile_panel/profile_panel.tscn
@@ -0,0 +1,79 @@
+[gd_scene load_steps=2 format=3 uid="uid://moey0w3677td"]
+
+[ext_resource type="Script" path="res://interfaces/menus/boot/multiplayer/profile_panel/profile_panel.gd" id="1_e3jmb"]
+
+[node name="ProfilePanel" type="MarginContainer" node_paths=PackedStringArray("create_button", "delete_button", "save_button", "menu_button", "quit_button", "profile_name")]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+theme_override_constants/margin_left = 20
+theme_override_constants/margin_top = 20
+theme_override_constants/margin_right = 20
+theme_override_constants/margin_bottom = 20
+script = ExtResource("1_e3jmb")
+create_button = NodePath("HBoxContainer/LeftBox/Top/Create")
+delete_button = NodePath("HBoxContainer/LeftBox/Top/Delete")
+save_button = NodePath("HBoxContainer/LeftBox/Top/Save")
+menu_button = NodePath("HBoxContainer/LeftBox/Bottom/Menu")
+quit_button = NodePath("HBoxContainer/LeftBox/Bottom/Quit")
+profile_name = NodePath("HBoxContainer/RightBox/ProfileName")
+
+[node name="HBoxContainer" type="HBoxContainer" parent="."]
+layout_mode = 2
+theme_override_constants/separation = 20
+
+[node name="LeftBox" type="VBoxContainer" parent="HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_stretch_ratio = 0.25
+
+[node name="Top" type="VBoxContainer" parent="HBoxContainer/LeftBox"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_stretch_ratio = 0.25
+
+[node name="Create" type="Button" parent="HBoxContainer/LeftBox/Top"]
+layout_mode = 2
+disabled = true
+text = "Create"
+
+[node name="Delete" type="Button" parent="HBoxContainer/LeftBox/Top"]
+layout_mode = 2
+disabled = true
+text = "Delete"
+
+[node name="Save" type="Button" parent="HBoxContainer/LeftBox/Top"]
+layout_mode = 2
+text = "Save"
+
+[node name="Bottom" type="VBoxContainer" parent="HBoxContainer/LeftBox"]
+layout_mode = 2
+size_flags_vertical = 10
+
+[node name="Menu" type="Button" parent="HBoxContainer/LeftBox/Bottom"]
+layout_mode = 2
+size_flags_horizontal = 3
+text = "Main Menu
+"
+
+[node name="Quit" type="Button" parent="HBoxContainer/LeftBox/Bottom"]
+layout_mode = 2
+size_flags_horizontal = 3
+text = "Quit"
+
+[node name="RightBox" type="VBoxContainer" parent="HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="Label" type="Label" parent="HBoxContainer/RightBox"]
+layout_mode = 2
+text = "Current Profile:"
+
+[node name="ProfileName" type="LineEdit" parent="HBoxContainer/RightBox"]
+layout_mode = 2
+size_flags_horizontal = 3
+text = "Newblood"
diff --git a/interfaces/menus/boot/settings/scripts/filters.gd b/interfaces/menus/boot/settings/scripts/filters.gd
new file mode 100644
index 0000000..8811a12
--- /dev/null
+++ b/interfaces/menus/boot/settings/scripts/filters.gd
@@ -0,0 +1,32 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+extends OptionButton
+
+func _ready() -> void:
+ select(Settings.get_value("video", "filter"))
+
+func _on_item_selected(index: int) -> void:
+ Settings.set_value("video", "filter", index)
+ # Viewport scale mode setting.
+ if index == 0: # Bilinear (Fastest)
+ get_viewport().scaling_3d_mode = Viewport.SCALING_3D_MODE_BILINEAR
+ # FSR Sharpness is only effective when the scaling mode is FSR 1.0.
+ %FSRSharpnessLabel.visible = false
+ %FSRSharpnessSlider.visible = false
+ elif index == 1: # FSR 1.0 (Fast)
+ get_viewport().scaling_3d_mode = Viewport.SCALING_3D_MODE_FSR
+ # FSR Sharpness is only effective when the scaling mode is FSR 1.0.
+ %FSRSharpnessLabel.visible = true
+ %FSRSharpnessSlider.visible = true
diff --git a/interfaces/menus/boot/settings/scripts/fov.gd b/interfaces/menus/boot/settings/scripts/fov.gd
new file mode 100644
index 0000000..d2c2326
--- /dev/null
+++ b/interfaces/menus/boot/settings/scripts/fov.gd
@@ -0,0 +1,39 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+extends HBoxContainer
+
+@export var min_value : int = 70
+@export var max_value : int = 150
+@export var step : int = 1
+
+func _ready() -> void:
+ var value : int = Settings.get_value("video", "fov")
+ for control : Range in [$SpinBox, $Slider]:
+ control.min_value = min_value
+ control.max_value = max_value
+ control.step = step
+ control.value = value
+ _on_value_changed(value)
+
+func _on_value_changed(new_value : int) -> void:
+ Settings.set_value("video", "fov", new_value)
+#
+func _on_spin_box_value_changed(new_value : int) -> void:
+ _on_value_changed(new_value)
+ $Slider.value = new_value
+#
+func _on_slider_value_changed(new_value : int) -> void:
+ _on_value_changed(new_value)
+ $SpinBox.value = new_value
diff --git a/interfaces/menus/boot/settings/scripts/fsr_sharpness.gd b/interfaces/menus/boot/settings/scripts/fsr_sharpness.gd
new file mode 100644
index 0000000..88230ad
--- /dev/null
+++ b/interfaces/menus/boot/settings/scripts/fsr_sharpness.gd
@@ -0,0 +1,26 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+extends HSlider
+
+func _ready() -> void:
+ self.value = Settings.get_value("video", "fsr_sharpness")
+ self.value_changed.emit(self.value)
+
+func _on_value_changed(new_value : float) -> void:
+ Settings.set_value("video", "fsr_sharpness", new_value)
+ # Lower FSR sharpness values result in a sharper image.
+ # Invert the slider so that higher values result in a sharper image,
+ # which is generally expected from users.
+ get_viewport().fsr_sharpness = 2.0 - new_value
diff --git a/interfaces/menus/boot/settings/scripts/fxaa.gd b/interfaces/menus/boot/settings/scripts/fxaa.gd
new file mode 100644
index 0000000..bacef66
--- /dev/null
+++ b/interfaces/menus/boot/settings/scripts/fxaa.gd
@@ -0,0 +1,24 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+extends OptionButton
+
+func _ready() -> void:
+ select(Settings.get_value("video", "fxaa"))
+
+func _on_item_selected(index: int) -> void:
+ Settings.set_value("video", "fxaa", index)
+ # Fast approximate anti-aliasing. Much faster than MSAA (and works on alpha scissor edges),
+ # but blurs the whole scene rendering slightly.
+ get_viewport().screen_space_aa = int(index == 1) as Viewport.ScreenSpaceAA
diff --git a/interfaces/menus/boot/settings/scripts/glow.gd b/interfaces/menus/boot/settings/scripts/glow.gd
new file mode 100644
index 0000000..b5d2123
--- /dev/null
+++ b/interfaces/menus/boot/settings/scripts/glow.gd
@@ -0,0 +1,30 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+extends OptionButton
+
+func _ready() -> void:
+ select(Settings.get_value("environment", "glow"))
+
+func _on_item_selected(index: int) -> void:
+ Settings.set_value("environment", "glow", index)
+ ## This is a setting that is attached to the environment.
+ ## If your game requires you to change the environment,
+ ## then be sure to run this function again to make the setting effective.
+ if index == 0: # Disabled (default)
+ Game.environment.glow_enabled = false
+ elif index == 1: # Low
+ Game.environment.glow_enabled = true
+ elif index == 2: # High
+ Game.environment.glow_enabled = true
diff --git a/interfaces/menus/boot/settings/scripts/mesh_lod.gd b/interfaces/menus/boot/settings/scripts/mesh_lod.gd
new file mode 100644
index 0000000..0b23a37
--- /dev/null
+++ b/interfaces/menus/boot/settings/scripts/mesh_lod.gd
@@ -0,0 +1,32 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+extends OptionButton
+
+func _ready() -> void:
+ select(Settings.get_value("quality", "mesh_lod"))
+
+func _on_item_selected(index : int) -> void:
+ Settings.set_value("quality", "mesh_lod", index)
+ if index == 0: # Very Low
+ get_viewport().mesh_lod_threshold = 8.0
+ if index == 0: # Low
+ get_viewport().mesh_lod_threshold = 4.0
+ if index == 1: # Medium
+ get_viewport().mesh_lod_threshold = 2.0
+ if index == 2: # High (default)
+ get_viewport().mesh_lod_threshold = 1.0
+ if index == 3: # Ultra
+ # Always use highest LODs to avoid any form of pop-in.
+ get_viewport().mesh_lod_threshold = 0.0
diff --git a/interfaces/menus/boot/settings/scripts/mouse_sensitivity.gd b/interfaces/menus/boot/settings/scripts/mouse_sensitivity.gd
new file mode 100644
index 0000000..44a8dcd
--- /dev/null
+++ b/interfaces/menus/boot/settings/scripts/mouse_sensitivity.gd
@@ -0,0 +1,39 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+extends HBoxContainer
+
+@export var min_value : float = .01
+@export var max_value : float = 1.
+@export var step : float = .01
+
+func _ready() -> void:
+ var value : float = Settings.get_value("controls", "mouse_sensitivity")
+ for control : Range in [$SpinBox, $Slider]:
+ control.min_value = min_value
+ control.max_value = max_value
+ control.step = step
+ control.value = value
+ _on_value_changed(value)
+
+func _on_value_changed(new_value : float) -> void:
+ Settings.set_value("controls", "mouse_sensitivity", new_value)
+
+func _on_spin_box_value_changed(new_value : float) -> void:
+ _on_value_changed(new_value)
+ $Slider.value = new_value
+
+func _on_slider_value_changed(new_value : float) -> void:
+ _on_value_changed(new_value)
+ $SpinBox.value = new_value
diff --git a/interfaces/menus/boot/settings/scripts/msaa.gd b/interfaces/menus/boot/settings/scripts/msaa.gd
new file mode 100644
index 0000000..ca0c4a8
--- /dev/null
+++ b/interfaces/menus/boot/settings/scripts/msaa.gd
@@ -0,0 +1,31 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+extends OptionButton
+
+func _ready() -> void:
+ select(Settings.get_value("video", "msaa"))
+
+func _on_item_selected(index: int) -> void:
+ Settings.set_value("video", "msaa", index)
+ # Multi-sample anti-aliasing. High quality, but slow. It also does not smooth out the edges of
+ # transparent (alpha scissor) textures.
+ if index == 0: # Disabled (default)
+ get_viewport().msaa_3d = Viewport.MSAA_DISABLED
+ elif index == 1: # 2×
+ get_viewport().msaa_3d = Viewport.MSAA_2X
+ elif index == 2: # 4×
+ get_viewport().msaa_3d = Viewport.MSAA_4X
+ elif index == 3: # 8×
+ get_viewport().msaa_3d = Viewport.MSAA_8X
diff --git a/interfaces/menus/boot/settings/scripts/quality.gd b/interfaces/menus/boot/settings/scripts/quality.gd
new file mode 100644
index 0000000..1f16445
--- /dev/null
+++ b/interfaces/menus/boot/settings/scripts/quality.gd
@@ -0,0 +1,40 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+extends HBoxContainer
+
+@export var min_value : float = 0.25
+@export var max_value : float = 2
+@export var step : float = .05
+
+func _ready() -> void:
+ var value : float = Settings.get_value("video", "quality")
+ for control : Range in [$SpinBox, $Slider]:
+ control.min_value = min_value
+ control.max_value = max_value
+ control.step = step
+ control.value = value
+ get_viewport().scaling_3d_scale = value
+
+func _on_value_changed(new_value : float) -> void:
+ Settings.set_value("video", "quality", new_value)
+ get_viewport().scaling_3d_scale = new_value
+
+func _on_spin_box_value_changed(new_value : float) -> void:
+ _on_value_changed(new_value)
+ $Slider.value = new_value
+#
+func _on_slider_value_changed(new_value : float) -> void:
+ _on_value_changed(new_value)
+ $SpinBox.value = new_value
diff --git a/interfaces/menus/boot/settings/scripts/sdfgi.gd b/interfaces/menus/boot/settings/scripts/sdfgi.gd
new file mode 100644
index 0000000..6ceeb07
--- /dev/null
+++ b/interfaces/menus/boot/settings/scripts/sdfgi.gd
@@ -0,0 +1,32 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+extends OptionButton
+
+func _ready() -> void:
+ select(Settings.get_value("environment", "sdfgi"))
+
+func _on_item_selected(index: int) -> void:
+ Settings.set_value("environment", "sdfgi", index)
+ # This is a setting that is attached to the environment.
+ # If your game requires you to change the environment,
+ # then be sure to run this function again to make the setting effective.
+ if index == 0: # Disabled (default)
+ Game.environment.sdfgi_enabled = false
+ elif index == 1: # Low
+ Game.environment.sdfgi_enabled = true
+ RenderingServer.gi_set_use_half_resolution(true)
+ elif index == 2: # High
+ Game.environment.sdfgi_enabled = true
+ RenderingServer.gi_set_use_half_resolution(false)
diff --git a/interfaces/menus/boot/settings/scripts/shadow_filter.gd b/interfaces/menus/boot/settings/scripts/shadow_filter.gd
new file mode 100644
index 0000000..3ef834a
--- /dev/null
+++ b/interfaces/menus/boot/settings/scripts/shadow_filter.gd
@@ -0,0 +1,39 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+extends OptionButton
+
+func _ready() -> void:
+ select(Settings.get_value("quality", "shadow_filter"))
+
+func _on_item_selected(index : int) -> void:
+ Settings.set_value("quality", "shadow_filter", index)
+ if index == 0: # Very Low
+ RenderingServer.directional_soft_shadow_filter_set_quality(RenderingServer.SHADOW_QUALITY_HARD)
+ RenderingServer.positional_soft_shadow_filter_set_quality(RenderingServer.SHADOW_QUALITY_HARD)
+ if index == 1: # Low
+ RenderingServer.directional_soft_shadow_filter_set_quality(RenderingServer.SHADOW_QUALITY_SOFT_VERY_LOW)
+ RenderingServer.positional_soft_shadow_filter_set_quality(RenderingServer.SHADOW_QUALITY_SOFT_VERY_LOW)
+ if index == 2: # Medium (default)
+ RenderingServer.directional_soft_shadow_filter_set_quality(RenderingServer.SHADOW_QUALITY_SOFT_LOW)
+ RenderingServer.positional_soft_shadow_filter_set_quality(RenderingServer.SHADOW_QUALITY_SOFT_LOW)
+ if index == 3: # High
+ RenderingServer.directional_soft_shadow_filter_set_quality(RenderingServer.SHADOW_QUALITY_SOFT_MEDIUM)
+ RenderingServer.positional_soft_shadow_filter_set_quality(RenderingServer.SHADOW_QUALITY_SOFT_MEDIUM)
+ if index == 4: # Very High
+ RenderingServer.directional_soft_shadow_filter_set_quality(RenderingServer.SHADOW_QUALITY_SOFT_HIGH)
+ RenderingServer.positional_soft_shadow_filter_set_quality(RenderingServer.SHADOW_QUALITY_SOFT_HIGH)
+ if index == 5: # Ultra
+ RenderingServer.directional_soft_shadow_filter_set_quality(RenderingServer.SHADOW_QUALITY_SOFT_ULTRA)
+ RenderingServer.positional_soft_shadow_filter_set_quality(RenderingServer.SHADOW_QUALITY_SOFT_ULTRA)
diff --git a/interfaces/menus/boot/settings/scripts/shadow_size.gd b/interfaces/menus/boot/settings/scripts/shadow_size.gd
new file mode 100644
index 0000000..2e28717
--- /dev/null
+++ b/interfaces/menus/boot/settings/scripts/shadow_size.gd
@@ -0,0 +1,56 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+extends OptionButton
+
+func _ready() -> void:
+ select(Settings.get_value("quality", "shadow_size"))
+
+func _on_item_selected(index : int) -> void:
+ Settings.set_value("quality", "shadow_size", index)
+ if index == 0: # Minimum
+ RenderingServer.directional_shadow_atlas_set_size(512, true)
+ # Adjust shadow bias according to shadow resolution.
+ # Higher resultions can use a lower bias without suffering from shadow acne.
+ #Settings.set_value("quality", "shadow_bias", 0.06)
+ #directional_light.shadow_bias = Settings.get_value("quality", "shadow_bias")
+
+ # Disable positional (omni/spot) light shadows entirely to further improve performance.
+ # These often don't contribute as much to a scene compared to directional light shadows.
+ get_viewport().positional_shadow_atlas_size = 0
+ if index == 1: # Very Low
+ RenderingServer.directional_shadow_atlas_set_size(1024, true)
+ #Settings.set_value("quality", "shadow_bias", 0.04)
+ #directional_light.shadow_bias = Settings.get_value("quality", "shadow_bias")
+ get_viewport().positional_shadow_atlas_size = 1024
+ if index == 2: # Low
+ RenderingServer.directional_shadow_atlas_set_size(2048, true)
+ #Settings.set_value("quality", "shadow_bias", 0.03)
+ #directional_light.shadow_bias = Settings.get_value("quality", "shadow_bias")
+ get_viewport().positional_shadow_atlas_size = 2048
+ if index == 3: # Medium (default)
+ RenderingServer.directional_shadow_atlas_set_size(4096, true)
+ #Settings.set_value("quality", "shadow_bias", 0.02)
+ #directional_light.shadow_bias = Settings.get_value("quality", "shadow_bias")
+ get_viewport().positional_shadow_atlas_size = 4096
+ if index == 4: # High
+ RenderingServer.directional_shadow_atlas_set_size(8192, true)
+ #Settings.set_value("quality", "shadow_bias", 0.01)
+ #directional_light.shadow_bias = Settings.get_value("quality", "shadow_bias")
+ get_viewport().positional_shadow_atlas_size = 8192
+ if index == 5: # Ultra
+ RenderingServer.directional_shadow_atlas_set_size(16384, true)
+ #Settings.set_value("quality", "shadow_bias", 0.005)
+ #directional_light.shadow_bias = Settings.get_value("quality", "shadow_bias")
+ get_viewport().positional_shadow_atlas_size = 16384
diff --git a/interfaces/menus/boot/settings/scripts/ssao.gd b/interfaces/menus/boot/settings/scripts/ssao.gd
new file mode 100644
index 0000000..21d6b59
--- /dev/null
+++ b/interfaces/menus/boot/settings/scripts/ssao.gd
@@ -0,0 +1,38 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+extends OptionButton
+
+func _ready() -> void:
+ select(Settings.get_value("environment", "ssao"))
+
+func _on_item_selected(index: int) -> void:
+ Settings.set_value("environment", "ssao", index)
+ # This is a setting that is attached to the environment.
+ # If your game requires you to change the environment,
+ # then be sure to run this function again to make the setting effective.
+ if index == 0: # Disabled (default)
+ Game.environment.ssao_enabled = false
+ elif index == 1: # Very Low
+ Game.environment.ssao_enabled = true
+ RenderingServer.environment_set_ssao_quality(RenderingServer.ENV_SSAO_QUALITY_VERY_LOW, true, 0.5, 2, 50, 300)
+ elif index == 2: # Low
+ Game.environment.ssao_enabled = true
+ RenderingServer.environment_set_ssao_quality(RenderingServer.ENV_SSAO_QUALITY_VERY_LOW, true, 0.5, 2, 50, 300)
+ elif index == 3: # Medium
+ Game.environment.ssao_enabled = true
+ RenderingServer.environment_set_ssao_quality(RenderingServer.ENV_SSAO_QUALITY_MEDIUM, true, 0.5, 2, 50, 300)
+ elif index == 4: # High
+ Game.environment.ssao_enabled = true
+ RenderingServer.environment_set_ssao_quality(RenderingServer.ENV_SSAO_QUALITY_HIGH, true, 0.5, 2, 50, 300)
diff --git a/interfaces/menus/boot/settings/scripts/ssil.gd b/interfaces/menus/boot/settings/scripts/ssil.gd
new file mode 100644
index 0000000..02bbfcb
--- /dev/null
+++ b/interfaces/menus/boot/settings/scripts/ssil.gd
@@ -0,0 +1,38 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+extends OptionButton
+
+func _ready() -> void:
+ select(Settings.get_value("environment", "ssil"))
+
+func _on_item_selected(index: int) -> void:
+ Settings.set_value("environment", "ssil", index)
+ # This is a setting that is attached to the environment.
+ # If your game requires you to change the environment,
+ # then be sure to run this function again to make the setting effective.
+ if index == 0: # Disabled (default)
+ Game.environment.ssil_enabled = false
+ if index == 1: # Very Low
+ Game.environment.ssil_enabled = true
+ RenderingServer.environment_set_ssil_quality(RenderingServer.ENV_SSIL_QUALITY_VERY_LOW, true, 0.5, 4, 50, 300)
+ if index == 2: # Low
+ Game.environment.ssil_enabled = true
+ RenderingServer.environment_set_ssil_quality(RenderingServer.ENV_SSIL_QUALITY_LOW, true, 0.5, 4, 50, 300)
+ if index == 3: # Medium
+ Game.environment.ssil_enabled = true
+ RenderingServer.environment_set_ssil_quality(RenderingServer.ENV_SSIL_QUALITY_MEDIUM, true, 0.5, 4, 50, 300)
+ if index == 4: # High
+ Game.environment.ssil_enabled = true
+ RenderingServer.environment_set_ssil_quality(RenderingServer.ENV_SSIL_QUALITY_HIGH, true, 0.5, 4, 50, 300)
diff --git a/interfaces/menus/boot/settings/scripts/ssr.gd b/interfaces/menus/boot/settings/scripts/ssr.gd
new file mode 100644
index 0000000..e43ee32
--- /dev/null
+++ b/interfaces/menus/boot/settings/scripts/ssr.gd
@@ -0,0 +1,35 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+extends OptionButton
+
+func _ready() -> void:
+ select(Settings.get_value("environment", "ssr"))
+
+func _on_item_selected(index: int) -> void:
+ Settings.set_value("environment", "ssr", index)
+ # This is a setting that is attached to the environment.
+ # If your game requires you to change the environment,
+ # then be sure to run this function again to make the setting effective.
+ if index == 0: # Disabled (default)
+ Game.environment.ssr_enabled = false
+ elif index == 1: # Low
+ Game.environment.ssr_enabled = true
+ Game.environment.ssr_max_steps = 8
+ elif index == 2: # Medium
+ Game.environment.ssr_enabled = true
+ Game.environment.ssr_max_steps = 32
+ elif index == 3: # High
+ Game.environment.ssr_enabled = true
+ Game.environment.ssr_max_steps = 56
diff --git a/interfaces/menus/boot/settings/scripts/taa.gd b/interfaces/menus/boot/settings/scripts/taa.gd
new file mode 100644
index 0000000..355d3a8
--- /dev/null
+++ b/interfaces/menus/boot/settings/scripts/taa.gd
@@ -0,0 +1,26 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+extends OptionButton
+
+func _ready() -> void:
+ select(Settings.get_value("video", "taa"))
+
+func _on_item_selected(index: int) -> void:
+ Settings.set_value("video", "taa", index)
+ # Temporal antialiasing. Smooths out everything including specular aliasing,
+ # but can introduce ghosting artifacts and blurring in motion.
+ # Moderate performance cost.
+ # @WARNING: https://github.com/TokisanGames/Terrain3D/issues/302
+ # get_viewport().use_taa = index == 1
diff --git a/interfaces/menus/boot/settings/scripts/ui_scale.gd b/interfaces/menus/boot/settings/scripts/ui_scale.gd
new file mode 100644
index 0000000..5698299
--- /dev/null
+++ b/interfaces/menus/boot/settings/scripts/ui_scale.gd
@@ -0,0 +1,42 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+extends OptionButton
+
+func _ready() -> void:
+ self.selected = Settings.get_value("ui", "scale")
+ self.item_selected.emit(self.selected)
+
+func _on_item_selected(index: int) -> void:
+ Settings.set_value("ui", "scale", clamp(index, 0, 2))
+ # When the screen changes size, we need to update the 3D
+ # viewport quality setting. If we don't do this, the viewport will take
+ # the size from the main viewport.
+ var new_size := Vector2(
+ ProjectSettings.get_setting(&"display/window/size/viewport_width"),
+ ProjectSettings.get_setting(&"display/window/size/viewport_height")
+ )
+ # compute new size
+ if index == 0: # Smaller (66%)
+ new_size *= 1.5
+ elif index == 1: # Small (80%)
+ new_size *= 1.25
+ elif index == 2: # Medium (100%) (default)
+ new_size *= 1.
+ elif index == 3: # Large (133%)
+ new_size *= .75
+ elif index == 4: # Larger (200%)
+ new_size *= .5
+ # update scale
+ get_tree().root.set_content_scale_size(new_size)
diff --git a/interfaces/menus/boot/settings/scripts/volumetric_fog.gd b/interfaces/menus/boot/settings/scripts/volumetric_fog.gd
new file mode 100644
index 0000000..c03dd13
--- /dev/null
+++ b/interfaces/menus/boot/settings/scripts/volumetric_fog.gd
@@ -0,0 +1,30 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+extends OptionButton
+
+func _ready() -> void:
+ self.selected = Settings.get_value("environment", "volumetric_fog")
+ self.item_selected.emit(self.selected)
+
+func _on_item_selected(index: int) -> void:
+ Settings.set_value("environment", "volumetric_fog", index)
+ if index == 0: # Disabled (default)
+ Game.environment.volumetric_fog_enabled = false
+ if index == 1: # Low
+ Game.environment.volumetric_fog_enabled = true
+ RenderingServer.environment_set_volumetric_fog_filter_active(false)
+ if index == 2: # High
+ Game.environment.volumetric_fog_enabled = true
+ RenderingServer.environment_set_volumetric_fog_filter_active(true)
diff --git a/interfaces/menus/boot/settings/scripts/vsync.gd b/interfaces/menus/boot/settings/scripts/vsync.gd
new file mode 100644
index 0000000..d30cfe9
--- /dev/null
+++ b/interfaces/menus/boot/settings/scripts/vsync.gd
@@ -0,0 +1,33 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+extends OptionButton
+
+func _ready() -> void:
+ select(Settings.get_value("video", "vsync"))
+
+func _on_item_selected(index : int) -> void:
+ Settings.set_value("video", "vsync", index)
+ # Vsync is enabled by default.
+ # Vertical synchronization locks framerate and makes screen tearing not visible at the cost of
+ # higher input latency and stuttering when the framerate target is not met.
+ # Adaptive V-Sync automatically disables V-Sync when the framerate target is not met, and enables
+ # V-Sync otherwise. This prevents suttering and reduces input latency when the framerate target
+ # is not met, at the cost of visible tearing.
+ if index == 0: # Disabled (default)
+ DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_DISABLED)
+ elif index == 1: # Adaptive
+ DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_ADAPTIVE)
+ elif index == 2: # Enabled
+ DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_ENABLED)
diff --git a/interfaces/menus/boot/settings/scripts/window_mode.gd b/interfaces/menus/boot/settings/scripts/window_mode.gd
new file mode 100644
index 0000000..5a672fe
--- /dev/null
+++ b/interfaces/menus/boot/settings/scripts/window_mode.gd
@@ -0,0 +1,28 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+extends OptionButton
+
+func _ready() -> void:
+ var window_mode : DisplayServer.WindowMode = Settings.get_value("video", "window_mode")
+ select(DisplayServer.WINDOW_MODE_WINDOWED if OS.is_debug_build() else window_mode)
+
+func _on_item_selected(index : int) -> void:
+ Settings.set_value("video", "window_mode", clampi(index, 0, 2))
+ if index == 0: # Windowed (default)
+ get_tree().root.set_mode(Window.MODE_WINDOWED)
+ elif index == 1: # Fullscreen
+ get_tree().root.set_mode(Window.MODE_FULLSCREEN)
+ elif index == 2: # Exclusive Fullscreen
+ get_tree().root.set_mode(Window.MODE_EXCLUSIVE_FULLSCREEN)
diff --git a/interfaces/menus/boot/settings/settings_panel_container.gd b/interfaces/menus/boot/settings/settings_panel_container.gd
new file mode 100644
index 0000000..7b86f0e
--- /dev/null
+++ b/interfaces/menus/boot/settings/settings_panel_container.gd
@@ -0,0 +1,114 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+class_name SettingsPanelContainer extends PanelContainer
+
+signal closed
+
+# Quality presets.
+
+func _on_very_low_preset_pressed() -> void:
+ %TAAOptionButton.select(0)
+ %MSAAOptionButton.select(0)
+ %FXAAOptionButton.select(0)
+ %ShadowSizeOptionButton.select(0)
+ %ShadowFilterOptionButton.select(0)
+ %MeshLODOptionButton.select(0)
+ %SDFGIOptionButton.select(0)
+ %GlowOptionButton.select(0)
+ %SSAOOptionButton.select(0)
+ %SSReflectionsOptionButton.select(0)
+ %SSILOptionButton.select(0)
+ %VolumetricFogOptionButton.select(0)
+
+func _on_low_preset_pressed() -> void:
+ %TAAOptionButton.select(0)
+ %MSAAOptionButton.select(0)
+ %FXAAOptionButton.select(1)
+ %ShadowSizeOptionButton.select(1)
+ %ShadowFilterOptionButton.select(1)
+ %MeshLODOptionButton.select(1)
+ %SDFGIOptionButton.select(0)
+ %GlowOptionButton.select(0)
+ %SSAOOptionButton.select(0)
+ %SSReflectionsOptionButton.select(0)
+ %SSILOptionButton.select(0)
+ %VolumetricFogOptionButton.select(0)
+
+func _on_medium_preset_pressed() -> void:
+ %TAAOptionButton.select(1)
+ %MSAAOptionButton.select(0)
+ %FXAAOptionButton.select(0)
+ %ShadowSizeOptionButton.select(2)
+ %ShadowFilterOptionButton.select(2)
+ %MeshLODOptionButton.select(1)
+ %SDFGIOptionButton.select(1)
+ %GlowOptionButton.select(1)
+ %SSAOOptionButton.select(1)
+ %SSReflectionsOptionButton.select(1)
+ %SSILOptionButton.select(0)
+ %VolumetricFogOptionButton.select(1)
+
+func _on_high_preset_pressed() -> void:
+ %TAAOptionButton.select(1)
+ %MSAAOptionButton.select(0)
+ %FXAAOptionButton.select(0)
+ %ShadowSizeOptionButton.select(3)
+ %ShadowFilterOptionButton.select(3)
+ %MeshLODOptionButton.select(2)
+ %SDFGIOptionButton.select(1)
+ %GlowOptionButton.select(2)
+ %SSAOOptionButton.select(2)
+ %SSReflectionsOptionButton.select(2)
+ %SSILOptionButton.select(2)
+ %VolumetricFogOptionButton.select(2)
+
+func _on_ultra_preset_pressed() -> void:
+ %TAAOptionButton.select(1)
+ %MSAAOptionButton.select(1)
+ %FXAAOptionButton.select(0)
+ %ShadowSizeOptionButton.select(4)
+ %ShadowFilterOptionButton.select(4)
+ %MeshLODOptionButton.select(3)
+ %SDFGIOptionButton.select(2)
+ %GlowOptionButton.select(2)
+ %SSAOOptionButton.select(3)
+ %SSReflectionsOptionButton.select(3)
+ %SSILOptionButton.select(3)
+ %VolumetricFogOptionButton.select(2)
+
+func _on_apply_pressed() -> void:
+ Settings.save()
+
+func _on_reset_pressed() -> void:
+ Settings.reset()
+ # @TODO: reset also settings user interface to render actual state
+
+func _on_close_pressed() -> void:
+ hide()
+ closed.emit()
+
+# Adjustment settings.
+
+#func _on_brightness_slider_value_changed(value: float) -> void:
+ # The slider value is clamped between 0.5 and 4.
+ #world_environment.environment.set_adjustment_brightness(value)
+
+#func _on_contrast_slider_value_changed(value: float) -> void:
+ # The slider value is clamped between 0.5 and 4.
+ #world_environment.environment.set_adjustment_contrast(value)
+
+#func _on_saturation_slider_value_changed(value: float) -> void:
+ # The slider value is clamped between 0.5 and 10.
+ #world_environment.environment.set_adjustment_saturation(value)
diff --git a/interfaces/menus/boot/settings/settings_panel_container.tscn b/interfaces/menus/boot/settings/settings_panel_container.tscn
new file mode 100644
index 0000000..3bd6f84
--- /dev/null
+++ b/interfaces/menus/boot/settings/settings_panel_container.tscn
@@ -0,0 +1,704 @@
+[gd_scene load_steps=22 format=3 uid="uid://cvu66n4bm10w3"]
+
+[ext_resource type="Script" path="res://interfaces/menus/boot/settings/scripts/ui_scale.gd" id="1_b2atp"]
+[ext_resource type="Script" path="res://interfaces/menus/boot/settings/settings_panel_container.gd" id="1_c6l0i"]
+[ext_resource type="Script" path="res://interfaces/menus/boot/settings/scripts/mouse_sensitivity.gd" id="2_xrhb7"]
+[ext_resource type="Script" path="res://interfaces/menus/boot/settings/scripts/fov.gd" id="3_eemxr"]
+[ext_resource type="Script" path="res://interfaces/menus/boot/settings/scripts/quality.gd" id="4_n2pno"]
+[ext_resource type="Script" path="res://interfaces/menus/boot/settings/scripts/filters.gd" id="5_v6h66"]
+[ext_resource type="Script" path="res://interfaces/menus/boot/settings/scripts/fsr_sharpness.gd" id="6_ahtl5"]
+[ext_resource type="Script" path="res://interfaces/menus/boot/settings/scripts/window_mode.gd" id="7_rfyne"]
+[ext_resource type="Script" path="res://interfaces/menus/boot/settings/scripts/vsync.gd" id="8_tdbn8"]
+[ext_resource type="Script" path="res://interfaces/menus/boot/settings/scripts/taa.gd" id="9_ogr52"]
+[ext_resource type="Script" path="res://interfaces/menus/boot/settings/scripts/msaa.gd" id="10_hsx6v"]
+[ext_resource type="Script" path="res://interfaces/menus/boot/settings/scripts/fxaa.gd" id="11_qd7o2"]
+[ext_resource type="Script" path="res://interfaces/menus/boot/settings/scripts/shadow_size.gd" id="12_53fxd"]
+[ext_resource type="Script" path="res://interfaces/menus/boot/settings/scripts/shadow_filter.gd" id="13_tast2"]
+[ext_resource type="Script" path="res://interfaces/menus/boot/settings/scripts/mesh_lod.gd" id="14_gekxq"]
+[ext_resource type="Script" path="res://interfaces/menus/boot/settings/scripts/sdfgi.gd" id="15_yf3v0"]
+[ext_resource type="Script" path="res://interfaces/menus/boot/settings/scripts/glow.gd" id="16_pq7lj"]
+[ext_resource type="Script" path="res://interfaces/menus/boot/settings/scripts/ssao.gd" id="17_xi4oy"]
+[ext_resource type="Script" path="res://interfaces/menus/boot/settings/scripts/ssr.gd" id="18_dprjc"]
+[ext_resource type="Script" path="res://interfaces/menus/boot/settings/scripts/ssil.gd" id="19_myuy8"]
+[ext_resource type="Script" path="res://interfaces/menus/boot/settings/scripts/volumetric_fog.gd" id="20_wf7u8"]
+
+[node name="SettingsPanelContainer" type="PanelContainer"]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+size_flags_horizontal = 3
+script = ExtResource("1_c6l0i")
+
+[node name="VBoxContainer" type="VBoxContainer" parent="."]
+layout_mode = 2
+
+[node name="ScrollContainer" type="ScrollContainer" parent="VBoxContainer"]
+custom_minimum_size = Vector2(480, 0)
+layout_mode = 2
+size_flags_vertical = 3
+
+[node name="MarginContainer" type="MarginContainer" parent="VBoxContainer/ScrollContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_constants/margin_left = 15
+theme_override_constants/margin_top = 20
+theme_override_constants/margin_right = 15
+theme_override_constants/margin_bottom = 20
+
+[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/ScrollContainer/MarginContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+theme_override_constants/separation = 10
+
+[node name="PresetSection" type="Label" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer"]
+layout_mode = 2
+theme_override_colors/font_color = Color(0.682353, 0.917647, 1, 1)
+text = "Presets"
+horizontal_alignment = 1
+
+[node name="Presets" type="HBoxContainer" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer"]
+layout_mode = 2
+
+[node name="VeryLowPreset" type="Button" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/Presets"]
+layout_mode = 2
+size_flags_horizontal = 3
+text = "Very Low"
+
+[node name="LowPreset" type="Button" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/Presets"]
+layout_mode = 2
+size_flags_horizontal = 3
+text = "Low"
+
+[node name="MediumPreset" type="Button" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/Presets"]
+layout_mode = 2
+size_flags_horizontal = 3
+text = "Medium"
+
+[node name="HighPreset" type="Button" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/Presets"]
+layout_mode = 2
+size_flags_horizontal = 3
+text = "High"
+
+[node name="UltraPreset" type="Button" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/Presets"]
+layout_mode = 2
+size_flags_horizontal = 3
+text = "Ultra"
+
+[node name="HSeparator" type="HSeparator" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer"]
+layout_mode = 2
+
+[node name="UISection" type="Label" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer"]
+layout_mode = 2
+theme_override_colors/font_color = Color(0.683425, 0.916893, 1, 1)
+text = "User Interface Settings"
+horizontal_alignment = 1
+
+[node name="UIGridContainer" type="GridContainer" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+columns = 2
+
+[node name="UIScaleLabel" type="Label" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/UIGridContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_font_sizes/font_size = 16
+text = "UI Scale:"
+
+[node name="UIScaleOptionButton" type="OptionButton" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/UIGridContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_font_sizes/font_size = 16
+item_count = 5
+selected = 2
+popup/item_0/text = "Smaller (66%)"
+popup/item_0/id = 0
+popup/item_1/text = "Small (80%)"
+popup/item_1/id = 1
+popup/item_2/text = "Medium (100%)"
+popup/item_2/id = 2
+popup/item_3/text = "Large (133%)"
+popup/item_3/id = 3
+popup/item_4/text = "Larger (200%)"
+popup/item_4/id = 4
+script = ExtResource("1_b2atp")
+
+[node name="ControlsSection" type="Label" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer"]
+layout_mode = 2
+theme_override_colors/font_color = Color(0.683425, 0.916893, 1, 1)
+text = "Controls Settings"
+horizontal_alignment = 1
+
+[node name="ControlsGridContainer" type="GridContainer" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer"]
+layout_mode = 2
+columns = 2
+
+[node name="SensitivityLabel" type="Label" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ControlsGridContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+text = "Mouse sensitivity"
+
+[node name="SensitivityControls" type="HBoxContainer" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ControlsGridContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+script = ExtResource("2_xrhb7")
+
+[node name="SpinBox" type="SpinBox" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ControlsGridContainer/SensitivityControls"]
+layout_mode = 2
+
+[node name="Slider" type="HSlider" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ControlsGridContainer/SensitivityControls"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 4
+
+[node name="InvertedYLabel" type="Label" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ControlsGridContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+text = "Inverted Y"
+
+[node name="InvertedYCheckbox" type="CheckBox" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ControlsGridContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 0
+
+[node name="ViewportSection" type="Label" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer"]
+layout_mode = 2
+theme_override_colors/font_color = Color(0.683425, 0.916893, 1, 1)
+text = "Video Settings"
+horizontal_alignment = 1
+
+[node name="ViewportGridContainer" type="GridContainer" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer"]
+layout_mode = 2
+columns = 2
+
+[node name="FOVLabel" type="Label" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_font_sizes/font_size = 16
+text = "Field of View:"
+
+[node name="FOVControls" type="HBoxContainer" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+script = ExtResource("3_eemxr")
+
+[node name="SpinBox" type="SpinBox" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer/FOVControls"]
+layout_mode = 2
+min_value = 70.0
+max_value = 150.0
+value = 90.0
+
+[node name="Slider" type="HSlider" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer/FOVControls"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 4
+min_value = 70.0
+max_value = 150.0
+value = 90.0
+
+[node name="QualityLabel" type="Label" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_font_sizes/font_size = 16
+text = "Resolution Scale:"
+
+[node name="QualityControls" type="HBoxContainer" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+script = ExtResource("4_n2pno")
+
+[node name="SpinBox" type="SpinBox" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer/QualityControls"]
+layout_mode = 2
+
+[node name="Slider" type="HSlider" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer/QualityControls"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 4
+
+[node name="FilterLabel" type="Label" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_font_sizes/font_size = 16
+text = "Display Filter:"
+
+[node name="FilterOptionButton" type="OptionButton" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_font_sizes/font_size = 16
+item_count = 2
+selected = 0
+popup/item_0/text = "Bilinear (Fastest)"
+popup/item_0/id = 0
+popup/item_1/text = "FSR 1.0 (Fast)"
+popup/item_1/id = 1
+script = ExtResource("5_v6h66")
+
+[node name="FSRSharpnessLabel" type="Label" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_font_sizes/font_size = 16
+text = "FSR Sharpness:"
+
+[node name="FSRSharpnessSlider" type="HSlider" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 1
+max_value = 2.0
+step = 0.2
+script = ExtResource("6_ahtl5")
+
+[node name="WindowModeLabel" type="Label" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_font_sizes/font_size = 16
+text = "Window Mode:"
+
+[node name="WindowModeOptionButton" type="OptionButton" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_font_sizes/font_size = 16
+item_count = 3
+selected = 2
+popup/item_0/text = "Windowed"
+popup/item_0/id = 0
+popup/item_1/text = "Fullscreen"
+popup/item_1/id = 1
+popup/item_2/text = "Exclusive Fullscreen"
+popup/item_2/id = 2
+script = ExtResource("7_rfyne")
+
+[node name="VsyncLabel" type="Label" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_font_sizes/font_size = 16
+text = "V-Sync:"
+
+[node name="VsyncOptionButton" type="OptionButton" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_font_sizes/font_size = 16
+item_count = 3
+selected = 0
+popup/item_0/text = "Disabled"
+popup/item_0/id = 0
+popup/item_1/text = "Adaptive"
+popup/item_1/id = 1
+popup/item_2/text = "Enabled"
+popup/item_2/id = 2
+script = ExtResource("8_tdbn8")
+
+[node name="TAALabel" type="Label" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer"]
+visible = false
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_font_sizes/font_size = 16
+text = "Anti-Aliasing (TAA):"
+
+[node name="TAAOptionButton" type="OptionButton" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer"]
+unique_name_in_owner = true
+visible = false
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_font_sizes/font_size = 16
+item_count = 2
+selected = 0
+popup/item_0/text = "Disabled (Fastest)"
+popup/item_0/id = 0
+popup/item_1/text = "Enabled (Average)"
+popup/item_1/id = 1
+script = ExtResource("9_ogr52")
+
+[node name="MSAALabel" type="Label" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_font_sizes/font_size = 16
+text = "Anti-Aliasing (MSAA):"
+
+[node name="MSAAOptionButton" type="OptionButton" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_font_sizes/font_size = 16
+item_count = 4
+selected = 0
+popup/item_0/text = "Disabled (Fastest)"
+popup/item_0/id = 0
+popup/item_1/text = "2× (Average)"
+popup/item_1/id = 1
+popup/item_2/text = "4× (Slow)"
+popup/item_2/id = 2
+popup/item_3/text = "8× (Slower)"
+popup/item_3/id = 3
+script = ExtResource("10_hsx6v")
+
+[node name="FXAALabel" type="Label" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_font_sizes/font_size = 16
+text = "Anti-Aliasing (FXAA):"
+
+[node name="FXAAOptionButton" type="OptionButton" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_font_sizes/font_size = 16
+item_count = 2
+selected = 0
+popup/item_0/text = "Disabled (Fastest)"
+popup/item_0/id = 0
+popup/item_1/text = "Enabled (Fast)"
+popup/item_1/id = 1
+script = ExtResource("11_qd7o2")
+
+[node name="QualitySection" type="Label" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer"]
+layout_mode = 2
+theme_override_colors/font_color = Color(0.683425, 0.916893, 1, 1)
+text = "Quality Settings"
+horizontal_alignment = 1
+
+[node name="QualityGridContainer" type="GridContainer" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer"]
+layout_mode = 2
+columns = 2
+
+[node name="ShadowSizeLabel" type="Label" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/QualityGridContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_font_sizes/font_size = 16
+text = "Shadow Resolution:"
+
+[node name="ShadowSizeOptionButton" type="OptionButton" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/QualityGridContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_font_sizes/font_size = 16
+item_count = 6
+selected = 3
+popup/item_0/text = "Minimum (Fastest)"
+popup/item_0/id = 0
+popup/item_1/text = "Very Low (Faster)"
+popup/item_1/id = 1
+popup/item_2/text = "Low (Fast)"
+popup/item_2/id = 2
+popup/item_3/text = "Medium (Average)"
+popup/item_3/id = 3
+popup/item_4/text = "High (Slow)"
+popup/item_4/id = 4
+popup/item_5/text = "Ultra (Slowest)"
+popup/item_5/id = 5
+script = ExtResource("12_53fxd")
+
+[node name="ShadowFilterLabel" type="Label" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/QualityGridContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_font_sizes/font_size = 16
+text = "Shadow Filtering:"
+
+[node name="ShadowFilterOptionButton" type="OptionButton" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/QualityGridContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_font_sizes/font_size = 16
+item_count = 6
+selected = 2
+popup/item_0/text = "Very Low (Fastest)"
+popup/item_0/id = 0
+popup/item_1/text = "Low (Faster)"
+popup/item_1/id = 1
+popup/item_2/text = "Medium (Fast)"
+popup/item_2/id = 2
+popup/item_3/text = "High (Average)"
+popup/item_3/id = 3
+popup/item_4/text = "Very High (Slow)"
+popup/item_4/id = 4
+popup/item_5/text = "Ultra (Slower)"
+popup/item_5/id = 5
+script = ExtResource("13_tast2")
+
+[node name="MeshLODLabel" type="Label" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/QualityGridContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_font_sizes/font_size = 16
+text = "Model Quality:"
+
+[node name="MeshLODOptionButton" type="OptionButton" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/QualityGridContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_font_sizes/font_size = 16
+item_count = 4
+selected = 2
+popup/item_0/text = "Low (Faster)"
+popup/item_0/id = 0
+popup/item_1/text = "Medium (Fast)"
+popup/item_1/id = 1
+popup/item_2/text = "High (Average)"
+popup/item_2/id = 2
+popup/item_3/text = "Ultra (Slow)"
+popup/item_3/id = 3
+script = ExtResource("14_gekxq")
+
+[node name="EnvironmentSection" type="Label" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer"]
+layout_mode = 2
+theme_override_colors/font_color = Color(0.683425, 0.916893, 1, 1)
+text = "Effect Settings"
+horizontal_alignment = 1
+
+[node name="EnvironmentGridContainer" type="GridContainer" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer"]
+layout_mode = 2
+columns = 2
+
+[node name="SDFGILabel" type="Label" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/EnvironmentGridContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_font_sizes/font_size = 16
+text = "Global Illumination:"
+
+[node name="SDFGIOptionButton" type="OptionButton" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/EnvironmentGridContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_font_sizes/font_size = 16
+item_count = 3
+selected = 0
+popup/item_0/text = "Disabled (Fastest)"
+popup/item_0/id = 0
+popup/item_1/text = "Low (Average)"
+popup/item_1/id = 1
+popup/item_2/text = "High (Slow)"
+popup/item_2/id = 2
+script = ExtResource("15_yf3v0")
+
+[node name="GlowLabel" type="Label" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/EnvironmentGridContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_font_sizes/font_size = 16
+text = "Bloom:"
+
+[node name="GlowOptionButton" type="OptionButton" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/EnvironmentGridContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_font_sizes/font_size = 16
+item_count = 3
+selected = 0
+popup/item_0/text = "Disabled (Fastest)"
+popup/item_0/id = 0
+popup/item_1/text = "Low (Fast)"
+popup/item_1/id = 1
+popup/item_2/text = "High (Average)"
+popup/item_2/id = 2
+script = ExtResource("16_pq7lj")
+
+[node name="SSAOLabel" type="Label" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/EnvironmentGridContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_font_sizes/font_size = 16
+text = "Ambient Occlusion:"
+
+[node name="SSAOOptionButton" type="OptionButton" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/EnvironmentGridContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_font_sizes/font_size = 16
+item_count = 5
+selected = 0
+popup/item_0/text = "Disabled (Fastest)"
+popup/item_0/id = 0
+popup/item_1/text = "Very Low (Fast)"
+popup/item_1/id = 1
+popup/item_2/text = "Low (Fast)"
+popup/item_2/id = 2
+popup/item_3/text = "Medium (Average)"
+popup/item_3/id = 3
+popup/item_4/text = "High (Slow)"
+popup/item_4/id = 4
+script = ExtResource("17_xi4oy")
+
+[node name="SSReflectionsLabel" type="Label" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/EnvironmentGridContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_font_sizes/font_size = 14
+text = "Screen-Space Reflections:"
+
+[node name="SSReflectionsOptionButton" type="OptionButton" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/EnvironmentGridContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_font_sizes/font_size = 16
+item_count = 4
+selected = 0
+popup/item_0/text = "Disabled (Fastest)"
+popup/item_0/id = 0
+popup/item_1/text = "Low (Average)"
+popup/item_1/id = 1
+popup/item_2/text = "Medium (Slow)"
+popup/item_2/id = 2
+popup/item_3/text = "High (Slower)"
+popup/item_3/id = 3
+script = ExtResource("18_dprjc")
+
+[node name="SSILLabel" type="Label" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/EnvironmentGridContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_font_sizes/font_size = 16
+text = "Screen-Space Lighting:"
+
+[node name="SSILOptionButton" type="OptionButton" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/EnvironmentGridContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_font_sizes/font_size = 16
+item_count = 5
+selected = 0
+popup/item_0/text = "Disabled (Fastest)"
+popup/item_0/id = 0
+popup/item_1/text = "Very Low (Fast)"
+popup/item_1/id = 1
+popup/item_2/text = "Low (Average)"
+popup/item_2/id = 2
+popup/item_3/text = "Medium (Slow)"
+popup/item_3/id = 3
+popup/item_4/text = "High (Slower)"
+popup/item_4/id = 4
+script = ExtResource("19_myuy8")
+
+[node name="VolumetricFogLabel" type="Label" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/EnvironmentGridContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_font_sizes/font_size = 16
+text = "Volumetric Fog:"
+
+[node name="VolumetricFogOptionButton" type="OptionButton" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/EnvironmentGridContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_font_sizes/font_size = 16
+item_count = 3
+selected = 0
+popup/item_0/text = "Disabled (Fastest)"
+popup/item_0/id = 0
+popup/item_1/text = "Low (Fast)"
+popup/item_1/id = 1
+popup/item_2/text = "High (Average)"
+popup/item_2/id = 2
+script = ExtResource("20_wf7u8")
+
+[node name="AdjustmentSection" type="Label" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer"]
+visible = false
+layout_mode = 2
+theme_override_colors/font_color = Color(0.683425, 0.916893, 1, 1)
+text = "Adjustments"
+horizontal_alignment = 1
+
+[node name="AdjustmentsGridContainer" type="GridContainer" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer"]
+visible = false
+layout_mode = 2
+columns = 2
+
+[node name="BrightnessLabel" type="Label" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/AdjustmentsGridContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_font_sizes/font_size = 16
+text = "Brightness:"
+
+[node name="BrightnessSlider" type="HSlider" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/AdjustmentsGridContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 1
+min_value = 0.5
+max_value = 2.0
+step = 0.01
+value = 1.0
+
+[node name="ContrastLabel" type="Label" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/AdjustmentsGridContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_font_sizes/font_size = 16
+text = "Contrast:"
+
+[node name="ContrastSlider" type="HSlider" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/AdjustmentsGridContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 1
+min_value = 0.5
+max_value = 2.0
+step = 0.01
+value = 1.0
+
+[node name="SaturationLabel" type="Label" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/AdjustmentsGridContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_font_sizes/font_size = 16
+text = "Saturation:"
+
+[node name="SaturationSlider" type="HSlider" parent="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/AdjustmentsGridContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 1
+min_value = 0.01
+max_value = 2.0
+step = 0.01
+value = 1.0
+
+[node name="MarginContainer" type="MarginContainer" parent="VBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_constants/margin_left = 15
+theme_override_constants/margin_top = 10
+theme_override_constants/margin_right = 23
+theme_override_constants/margin_bottom = 15
+
+[node name="Buttons" type="HBoxContainer" parent="VBoxContainer/MarginContainer"]
+layout_mode = 2
+
+[node name="Save" type="Button" parent="VBoxContainer/MarginContainer/Buttons"]
+layout_mode = 2
+size_flags_horizontal = 3
+text = "Save"
+
+[node name="Reset" type="Button" parent="VBoxContainer/MarginContainer/Buttons"]
+layout_mode = 2
+size_flags_horizontal = 3
+text = "Reset"
+
+[node name="Close" type="Button" parent="VBoxContainer/MarginContainer/Buttons"]
+layout_mode = 2
+size_flags_horizontal = 3
+text = "Close"
+
+[connection signal="pressed" from="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/Presets/VeryLowPreset" to="." method="_on_very_low_preset_pressed"]
+[connection signal="pressed" from="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/Presets/LowPreset" to="." method="_on_low_preset_pressed"]
+[connection signal="pressed" from="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/Presets/MediumPreset" to="." method="_on_medium_preset_pressed"]
+[connection signal="pressed" from="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/Presets/HighPreset" to="." method="_on_high_preset_pressed"]
+[connection signal="pressed" from="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/Presets/UltraPreset" to="." method="_on_ultra_preset_pressed"]
+[connection signal="item_selected" from="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/UIGridContainer/UIScaleOptionButton" to="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/UIGridContainer/UIScaleOptionButton" method="_on_item_selected"]
+[connection signal="value_changed" from="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ControlsGridContainer/SensitivityControls/SpinBox" to="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ControlsGridContainer/SensitivityControls" method="_on_spin_box_value_changed"]
+[connection signal="value_changed" from="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ControlsGridContainer/SensitivityControls/Slider" to="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ControlsGridContainer/SensitivityControls" method="_on_slider_value_changed"]
+[connection signal="value_changed" from="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer/FOVControls/SpinBox" to="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer/FOVControls" method="_on_spin_box_value_changed"]
+[connection signal="value_changed" from="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer/FOVControls/Slider" to="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer/FOVControls" method="_on_slider_value_changed"]
+[connection signal="value_changed" from="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer/QualityControls/SpinBox" to="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer/QualityControls" method="_on_spin_box_value_changed"]
+[connection signal="value_changed" from="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer/QualityControls/Slider" to="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer/QualityControls" method="_on_slider_value_changed"]
+[connection signal="item_selected" from="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer/FilterOptionButton" to="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer/FilterOptionButton" method="_on_item_selected"]
+[connection signal="value_changed" from="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer/FSRSharpnessSlider" to="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer/FSRSharpnessSlider" method="_on_value_changed"]
+[connection signal="item_selected" from="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer/WindowModeOptionButton" to="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer/WindowModeOptionButton" method="_on_item_selected"]
+[connection signal="item_selected" from="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer/VsyncOptionButton" to="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer/VsyncOptionButton" method="_on_item_selected"]
+[connection signal="item_selected" from="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer/TAAOptionButton" to="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer/TAAOptionButton" method="_on_item_selected"]
+[connection signal="item_selected" from="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer/MSAAOptionButton" to="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer/MSAAOptionButton" method="_on_item_selected"]
+[connection signal="item_selected" from="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer/FXAAOptionButton" to="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/ViewportGridContainer/FXAAOptionButton" method="_on_item_selected"]
+[connection signal="item_selected" from="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/QualityGridContainer/ShadowSizeOptionButton" to="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/QualityGridContainer/ShadowSizeOptionButton" method="_on_item_selected"]
+[connection signal="item_selected" from="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/QualityGridContainer/ShadowFilterOptionButton" to="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/QualityGridContainer/ShadowFilterOptionButton" method="_on_item_selected"]
+[connection signal="item_selected" from="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/QualityGridContainer/MeshLODOptionButton" to="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/QualityGridContainer/MeshLODOptionButton" method="_on_item_selected"]
+[connection signal="item_selected" from="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/EnvironmentGridContainer/SDFGIOptionButton" to="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/EnvironmentGridContainer/SDFGIOptionButton" method="_on_item_selected"]
+[connection signal="item_selected" from="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/EnvironmentGridContainer/GlowOptionButton" to="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/EnvironmentGridContainer/GlowOptionButton" method="_on_item_selected"]
+[connection signal="item_selected" from="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/EnvironmentGridContainer/SSAOOptionButton" to="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/EnvironmentGridContainer/SSAOOptionButton" method="_on_item_selected"]
+[connection signal="item_selected" from="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/EnvironmentGridContainer/SSReflectionsOptionButton" to="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/EnvironmentGridContainer/SSReflectionsOptionButton" method="_on_item_selected"]
+[connection signal="item_selected" from="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/EnvironmentGridContainer/SSILOptionButton" to="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/EnvironmentGridContainer/SSILOptionButton" method="_on_item_selected"]
+[connection signal="item_selected" from="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/EnvironmentGridContainer/VolumetricFogOptionButton" to="VBoxContainer/ScrollContainer/MarginContainer/VBoxContainer/EnvironmentGridContainer/VolumetricFogOptionButton" method="_on_item_selected"]
+[connection signal="pressed" from="VBoxContainer/MarginContainer/Buttons/Save" to="." method="_on_apply_pressed"]
+[connection signal="pressed" from="VBoxContainer/MarginContainer/Buttons/Reset" to="." method="_on_reset_pressed"]
+[connection signal="pressed" from="VBoxContainer/MarginContainer/Buttons/Close" to="." method="_on_close_pressed"]
diff --git a/interfaces/scoreboard/components/deathmatch.gd b/interfaces/scoreboard/components/deathmatch.gd
new file mode 100644
index 0000000..03cec84
--- /dev/null
+++ b/interfaces/scoreboard/components/deathmatch.gd
@@ -0,0 +1,35 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+class_name DeathmatchScoringComponent extends Node
+
+@export var ON_KILL_SCORE := Vector3i(1,0,0)
+
+signal add_score(peer_id:int, score:Vector3i)
+
+## This method returns a new bound deathmatch scoring component to be added in tree.
+func _init(scoreboard:Scoreboard, players: Node) -> void:
+ add_score.connect(scoreboard.add_score)
+ players.child_entered_tree.connect(register)
+ players.child_exiting_tree.connect(unregister)
+
+func register(player: Player) -> void:
+ player.killed.connect(_on_player_killed)
+
+func unregister(player: Player) -> void:
+ player.killed.disconnect(_on_player_killed)
+
+func _on_player_killed(victim: Player, killer:int) -> void:
+ if victim.peer_id != killer:
+ add_score.emit(killer, ON_KILL_SCORE)
diff --git a/interfaces/scoreboard/components/rabbit.gd b/interfaces/scoreboard/components/rabbit.gd
new file mode 100644
index 0000000..ea492fc
--- /dev/null
+++ b/interfaces/scoreboard/components/rabbit.gd
@@ -0,0 +1,56 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+## This defines a rabbit scoring component.
+class_name RabbitScoringComponent extends Node
+
+const ON_GRAB_SCORE := Vector3i(1,0,0)
+const ON_HOLD_SCORE := Vector3i(1,0,0)
+const HOLD_SCORING_TIMER:float = 10.0 # seconds
+
+var timer:= Timer.new()
+
+signal add_score(peer_id:int, score:Vector3i)
+
+# @TODO: remove this variable by passing flag to signals `grabbed` and `dropped`
+# this if fine for rabbit but will be required for CTF
+var _flag: Flag
+
+# This method initializes a new rabbit scoring component.
+func _init(scoreboard:Scoreboard, flag:Flag) -> void:
+ timer.timeout.connect(_on_timer_timeout.bind(flag))
+ add_score.connect(scoreboard.add_score)
+ flag.grabbed.connect(_on_flag_grabbed)
+ flag.dropped.connect(_on_flag_dropped)
+ _flag = flag
+
+func _ready() -> void:
+ add_child(timer)
+
+func _on_flag_grabbed(carry: FlagCarryComponent) -> void:
+ assert(_flag)
+ # prevent chained grab scoring abuse
+ if _flag and (not _flag.last_carrier or _flag.last_carrier.owner.peer_id != carry.owner.peer_id):
+ # set new carrier for `_on_timer_timeout`
+ _flag.last_carrier = carry
+ add_score.emit(carry.owner.peer_id, ON_GRAB_SCORE)
+ timer.start(HOLD_SCORING_TIMER)
+
+func _on_flag_dropped(_carry: FlagCarryComponent) -> void:
+ assert(_flag)
+ timer.stop()
+
+func _on_timer_timeout(flag:Flag) -> void:
+ assert(_flag)
+ add_score.emit(flag.last_carrier.owner.peer_id, ON_HOLD_SCORE)
diff --git a/interfaces/scoreboard/ping_label.gd b/interfaces/scoreboard/ping_label.gd
new file mode 100644
index 0000000..d0b0b0c
--- /dev/null
+++ b/interfaces/scoreboard/ping_label.gd
@@ -0,0 +1,21 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+class_name PingLabel extends Label
+
+@export var entry : ScorePanelEntry
+
+func _on_sync(state: Dictionary) -> void:
+ if entry.peer_id in state: # update clients only
+ text = str(state[entry.peer_id])
diff --git a/interfaces/scoreboard/score_panel.gd b/interfaces/scoreboard/score_panel.gd
new file mode 100644
index 0000000..bfe055e
--- /dev/null
+++ b/interfaces/scoreboard/score_panel.gd
@@ -0,0 +1,80 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+## This defines a score panel to hold [ScorePanelEntry] instances.
+class_name ScorePanel extends Panel
+
+# The ScorePanelEntry packed scene to instantiate
+@export var _SCORE_PANEL_ENTRY:PackedScene
+## This is the panel title.
+@export var title:Label
+## This is the container for [ScorePanelEntry] child nodes.
+@export var entries:VBoxContainer
+
+# This is the iterator index cursor.
+var _iter_cursor:int = 0
+
+# This method is an iterator initializer.
+func _iter_init(_arg:Variant) -> bool:
+ _iter_cursor = 0 # reset
+ return _iter_cursor < entries.get_child_count()
+
+# This method checks if the iterator has a next value.
+func _iter_next(_arg:Variant) -> bool:
+ _iter_cursor += 1
+ return _iter_cursor < entries.get_child_count()
+
+# This method gets the next iterator value.
+func _iter_get(_arg:Variant) -> ScorePanelEntry:
+ return entries.get_child(_iter_cursor)
+
+func _to_string() -> String:
+ return "" % get_instance_id()
+
+## This method adds an entry to the panel.
+func add_entry(peer_id:int, username:String, score:Vector3i = Vector3i.ZERO) -> ScorePanelEntry:
+ var entry:ScorePanelEntry = _SCORE_PANEL_ENTRY.instantiate()
+ entry.name = str(peer_id)
+ entry.peer_id = peer_id
+ entries.add_child(entry)
+ entry.ping.text = "" # @TODO: get player ping
+ entry._username = username
+ entry._score = score
+ return entry
+
+## This method removes a [ScorePanelEntry] from the [ScorePanel].
+func remove_entry(entry:ScorePanelEntry) -> void:
+ entries.remove_child(entry)
+ entry.free()
+
+## This method returns a [ScorePanelEntry] from the [ScorePanel].
+func get_entry(index:int) -> ScorePanelEntry:
+ return entries.get_child(index)
+
+## This method removes a [ScorePanelEntry] from the [ScorePanel] by [param name]
+## and returns a boolean to indicate removal state.
+func remove_entry_by_name(entry_name:String) -> bool:
+ var path := NodePath(entry_name)
+ if entries.has_node(path):
+ var entry:ScorePanelEntry = entries.get_node(path)
+ remove_entry(entry)
+ return true
+ else:
+ return false
+
+func remove_entry_by_peer_id(peer_id:int) -> bool:
+ return remove_entry_by_name(str(peer_id))
+
+func size() -> int:
+ return entries.get_child_count()
diff --git a/interfaces/scoreboard/score_panel.tscn b/interfaces/scoreboard/score_panel.tscn
new file mode 100644
index 0000000..8765090
--- /dev/null
+++ b/interfaces/scoreboard/score_panel.tscn
@@ -0,0 +1,66 @@
+[gd_scene load_steps=4 format=3 uid="uid://p1q2lu7mtte1"]
+
+[ext_resource type="Script" path="res://interfaces/scoreboard/score_panel.gd" id="1_diy54"]
+[ext_resource type="PackedScene" uid="uid://bu186wanwk1kh" path="res://interfaces/scoreboard/score_panel_entry.tscn" id="2_54ms2"]
+
+[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_bhp7i"]
+properties/0/path = NodePath("MarginContainer/VBoxContainer/Title:text")
+properties/0/spawn = true
+properties/0/replication_mode = 2
+properties/1/path = NodePath("MarginContainer/VBoxContainer/Title:visible")
+properties/1/spawn = true
+properties/1/replication_mode = 2
+
+[node name="ScorePanel" type="Panel" node_paths=PackedStringArray("title", "entries")]
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+size_flags_horizontal = 3
+size_flags_vertical = 3
+mouse_filter = 2
+script = ExtResource("1_diy54")
+_SCORE_PANEL_ENTRY = ExtResource("2_54ms2")
+title = NodePath("MarginContainer/VBoxContainer/Title")
+entries = NodePath("MarginContainer/VBoxContainer/Entries")
+
+[node name="ScorePanelSync" type="MultiplayerSynchronizer" parent="."]
+replication_interval = 1.0
+delta_interval = 1.0
+replication_config = SubResource("SceneReplicationConfig_bhp7i")
+
+[node name="MarginContainer" type="MarginContainer" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_constants/margin_left = 10
+theme_override_constants/margin_top = 10
+theme_override_constants/margin_right = 10
+theme_override_constants/margin_bottom = 10
+
+[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"]
+layout_mode = 2
+
+[node name="Title" type="Label" parent="MarginContainer/VBoxContainer"]
+visible = false
+layout_mode = 2
+theme_type_variation = &"HeaderSmall"
+text = "PANEL TITLE"
+horizontal_alignment = 1
+vertical_alignment = 1
+uppercase = true
+
+[node name="Headers" parent="MarginContainer/VBoxContainer" instance=ExtResource("2_54ms2")]
+layout_mode = 2
+
+[node name="Entries" type="VBoxContainer" parent="MarginContainer/VBoxContainer"]
+unique_name_in_owner = true
+layout_mode = 2
+
+[node name="EntrySpawner" type="MultiplayerSpawner" parent="MarginContainer/VBoxContainer"]
+_spawnable_scenes = PackedStringArray("res://interfaces/scoreboard/score_panel_entry.tscn")
+spawn_path = NodePath("../Entries")
diff --git a/interfaces/scoreboard/score_panel_entry.gd b/interfaces/scoreboard/score_panel_entry.gd
new file mode 100644
index 0000000..26756b7
--- /dev/null
+++ b/interfaces/scoreboard/score_panel_entry.gd
@@ -0,0 +1,61 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+class_name ScorePanelEntry extends HBoxContainer
+
+@export var scoring: bool = true
+
+@export var _ping : int:
+ set = set_ping
+@export var _username : String:
+ set = set_username
+@export var _score : Vector3i = Vector3i.ZERO:
+ set = set_score
+
+@export var ping : PingLabel
+@export var username : Label
+@export var offense : Label
+@export var defense : Label
+@export var style : Label
+@export var score : Label
+
+var peer_id : int
+
+func get_total() -> int:
+ return _score.x + _score.y + _score.z
+
+func _to_string() -> String:
+ return "" % [name, get_instance_id()]
+
+# setters
+
+func set_username(new_username : String) -> void:
+ _username = new_username
+ username.text = new_username
+
+func set_ping(new_ping : int) -> void:
+ _ping = new_ping
+ ping.text = str(_ping)
+
+func set_score(new_score : Vector3i) -> void:
+ if scoring:
+ _score = new_score
+ offense.text = str(_score.x)
+ defense.text = str(_score.y)
+ style.text = str(_score.z)
+ score.text = str(get_total())
+
+# This is called when the Scoreboard.scoring_state_changed signal is emmitted
+func _on_scoring_state_changed(new_scoring: bool) -> void:
+ scoring = new_scoring
diff --git a/interfaces/scoreboard/score_panel_entry.tscn b/interfaces/scoreboard/score_panel_entry.tscn
new file mode 100644
index 0000000..bc69559
--- /dev/null
+++ b/interfaces/scoreboard/score_panel_entry.tscn
@@ -0,0 +1,92 @@
+[gd_scene load_steps=4 format=3 uid="uid://bu186wanwk1kh"]
+
+[ext_resource type="Script" path="res://interfaces/scoreboard/score_panel_entry.gd" id="1_ohfvg"]
+[ext_resource type="Script" path="res://interfaces/scoreboard/ping_label.gd" id="2_rlg8k"]
+
+[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_hpwcf"]
+properties/0/path = NodePath("Ping:text")
+properties/0/spawn = true
+properties/0/replication_mode = 2
+properties/1/path = NodePath("Username:text")
+properties/1/spawn = true
+properties/1/replication_mode = 2
+properties/2/path = NodePath("Offense:text")
+properties/2/spawn = true
+properties/2/replication_mode = 2
+properties/3/path = NodePath("Defense:text")
+properties/3/spawn = true
+properties/3/replication_mode = 2
+properties/4/path = NodePath("Style:text")
+properties/4/spawn = true
+properties/4/replication_mode = 2
+properties/5/path = NodePath("Score:text")
+properties/5/spawn = true
+properties/5/replication_mode = 2
+
+[node name="Entry" type="HBoxContainer" node_paths=PackedStringArray("ping", "username", "offense", "defense", "style", "score")]
+anchors_preset = 10
+anchor_right = 1.0
+offset_bottom = 17.0
+grow_horizontal = 2
+script = ExtResource("1_ohfvg")
+ping = NodePath("Ping")
+username = NodePath("Username")
+offense = NodePath("Offense")
+defense = NodePath("Defense")
+style = NodePath("Style")
+score = NodePath("Score")
+
+[node name="EntrySync" type="MultiplayerSynchronizer" parent="."]
+replication_interval = 0.5
+delta_interval = 0.5
+replication_config = SubResource("SceneReplicationConfig_hpwcf")
+
+[node name="Ping" type="Label" parent="." node_paths=PackedStringArray("entry")]
+custom_minimum_size = Vector2(64, 0)
+layout_mode = 2
+theme_override_font_sizes/font_size = 12
+text = "PING"
+horizontal_alignment = 1
+vertical_alignment = 1
+script = ExtResource("2_rlg8k")
+entry = NodePath("..")
+
+[node name="Username" type="Label" parent="."]
+custom_minimum_size = Vector2(128, 0)
+layout_mode = 2
+size_flags_horizontal = 3
+theme_override_font_sizes/font_size = 12
+text = "USERNAME"
+vertical_alignment = 1
+
+[node name="Offense" type="Label" parent="."]
+custom_minimum_size = Vector2(32, 0)
+layout_mode = 2
+theme_override_font_sizes/font_size = 12
+text = "O"
+horizontal_alignment = 1
+vertical_alignment = 1
+
+[node name="Defense" type="Label" parent="."]
+custom_minimum_size = Vector2(32, 0)
+layout_mode = 2
+theme_override_font_sizes/font_size = 12
+text = "D"
+horizontal_alignment = 1
+vertical_alignment = 1
+
+[node name="Style" type="Label" parent="."]
+custom_minimum_size = Vector2(32, 0)
+layout_mode = 2
+theme_override_font_sizes/font_size = 12
+text = "S"
+horizontal_alignment = 1
+vertical_alignment = 1
+
+[node name="Score" type="Label" parent="."]
+custom_minimum_size = Vector2(96, 0)
+layout_mode = 2
+theme_override_font_sizes/font_size = 12
+text = "SCORE"
+horizontal_alignment = 1
+vertical_alignment = 1
diff --git a/interfaces/scoreboard/scoreboard.gd b/interfaces/scoreboard/scoreboard.gd
index a8a46a0..f8ab08f 100644
--- a/interfaces/scoreboard/scoreboard.gd
+++ b/interfaces/scoreboard/scoreboard.gd
@@ -12,73 +12,118 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
+## This defines a scoreboard to track players, scores and teams.
+## It is a container for [ScorePanel] instances.
class_name Scoreboard extends Control
-@export var _entries : Dictionary = {}
+signal scoring_state_changed(new_scoring: bool)
-class ScoreboardEntry:
- var username : String
- var kills : int
- var score : int
- var username_label : Label = Label.new()
- var kills_label : Label = Label.new()
- var score_label : Label = Label.new()
+# The score panel scene to spawn.
+@export var _SCORE_PANEL: PackedScene
+## This is the container for [ScorePanel] child nodes.
+@export var panels: GridContainer
+## This controls whether [Entry] scores can be updated or not.
+@export var scoring: bool:
+ set = set_scoring
+
+func set_scoring(new_scoring: bool) -> void:
+ scoring = new_scoring
+ scoring_state_changed.emit(scoring)
-func _unhandled_input(event : InputEvent) -> void:
+func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("scoreboard"):
show()
elif event.is_action_released("scoreboard"):
hide()
+
+func _ready() -> void:
+ panels.child_entered_tree.connect(_on_score_panel_added)
+ panels.child_exiting_tree.connect(_on_score_panel_removed)
-func add_participant(participant : MatchParticipant) -> void:
- _add_scoreboard_entry.rpc(participant.player_id, participant.username)
+func _on_score_panel_added(panel: ScorePanel) -> void:
+ if not panel.entries.child_entered_tree.is_connected(_on_entry_added):
+ panel.entries.child_entered_tree.connect(_on_entry_added)
-func remove_participant(participant : MatchParticipant) -> void:
- _remove_scoreboard_entry.rpc(participant.player_id)
+func _on_score_panel_removed(panel: ScorePanel) -> void:
+ if panel.entries.child_entered_tree.is_connected(_on_entry_added):
+ panel.entries.child_entered_tree.disconnect(_on_entry_added)
+
+func _on_entry_added(entry: ScorePanelEntry) -> void:
+ if not scoring_state_changed.is_connected(entry._on_scoring_state_changed):
+ scoring_state_changed.connect(entry._on_scoring_state_changed)
+
+func _on_entry_removed(entry: ScorePanelEntry) -> void:
+ if scoring_state_changed.is_connected(entry._on_scoring_state_changed):
+ scoring_state_changed.disconnect(entry._on_scoring_state_changed)
-func increment_kill_count(participant : MatchParticipant) -> void:
- _entries[participant.player_id].kills += 1
+func _on_ping_sync(state: Dictionary) -> void:
+ for panel: ScorePanel in panels.get_children():
+ for entry in panel:
+ entry.ping._on_sync(state)
-func add_score_to_player(participant : MatchParticipant, amount : int) -> void:
- var player_id : int = participant.player_id
- var entry : ScoreboardEntry = _entries[player_id]
- _update_scoreboard_entry.rpc(player_id, entry.username, entry.kills, entry.score + amount)
+func _to_string() -> String:
+ return "" % get_instance_id()
-@rpc("any_peer", "call_remote", "reliable")
-func request_scoreboard_from_authority() -> void:
- if is_multiplayer_authority():
- var recipient_id : int = multiplayer.get_remote_sender_id()
- for entry_player_id : int in _entries:
- var entry : ScoreboardEntry = _entries[entry_player_id]
- _add_scoreboard_entry.rpc_id(recipient_id, entry_player_id, entry.username, entry.kills, entry.score)
+## This method adds a [ScorePanel] to the [Scoreboard] and returns a reference to the added [ScorePanel].
+func add_panel(panel_name : String = "") -> ScorePanel:
+ var score_panel: ScorePanel = _SCORE_PANEL.instantiate()
+ var score_panel_name := str(panel_name)
+ if score_panel_name.is_valid_identifier():
+ score_panel.name = score_panel_name
+ panels.add_child(score_panel)
+ panels.columns = 2 if panels.get_child_count() > 2 else panels.get_child_count()
+ return score_panel
-@rpc("authority", "call_local", "reliable")
-func _add_scoreboard_entry(player_id : int, username : String, kills : int = 0, score : int = 0) -> void:
- var new_entry : ScoreboardEntry = ScoreboardEntry.new()
- _entries[player_id] = new_entry
- %Scores.add_child(new_entry.username_label)
- %Scores.add_child(new_entry.kills_label)
- %Scores.add_child(new_entry.score_label)
- _update_scoreboard_entry(player_id, username, kills, score)
+## This method removes a [ScorePanel] from the [Scoreboard].
+func remove_panel(panel: ScorePanel) -> void:
+ panels.remove_child(panel)
+ panel.free()
-@rpc("authority", "call_local", "reliable")
-func _update_scoreboard_entry(player_id : int, username : String, kills : int, score : int) -> void:
- var entry : ScoreboardEntry = _entries[player_id]
- entry.username = username
- entry.kills = kills
- entry.score = score
- _update_scoreboard_entry_ui(player_id)
+## This method returns a [ScorePanel] from the [Scoreboard].
+func get_panel(index: int) -> ScorePanel:
+ return panels.get_child(index)
-@rpc("authority", "call_local", "reliable")
-func _remove_scoreboard_entry(player_id : int) -> void:
- var entry : ScoreboardEntry = _entries[player_id]
- entry.username_label.queue_free()
- entry.kills_label.queue_free()
- entry.score_label.queue_free()
- _entries.erase(player_id)
+## This method removes an entry by its node name in the tree.
+func remove_entry_by_name(entry_name: String) -> bool:
+ for panel : ScorePanel in panels.get_children():
+ if panel.remove_entry_by_name(entry_name):
+ return true
+ return false
-func _update_scoreboard_entry_ui(player_id : int) -> void:
- var entry : ScoreboardEntry = _entries[player_id]
- entry.username_label.text = entry.username
- entry.kills_label.text = str(entry.kills)
- entry.score_label.text = str(entry.score)
+func get_entry(peer_id: int) -> ScorePanelEntry:
+ for panel : ScorePanel in panels.get_children():
+ for entry in panel:
+ if entry.peer_id == peer_id:
+ return entry
+ return null
+
+## This method returns the score of the given [param peer_id].
+func get_score(peer_id: int) -> Vector3i:
+ var entry : ScorePanelEntry = get_entry(peer_id)
+ return entry._score if entry else Vector3i.ZERO
+
+## This method sets the score of the given [param peer_id].
+func set_score(peer_id:int, score:Vector3i) -> bool:
+ var entry : ScorePanelEntry = get_entry(peer_id)
+ if entry:
+ entry._score = score
+ return true
+ return false
+
+## This method adds to the score of the given [param peer_id].
+func add_score(peer_id:int, score:Vector3i) -> bool:
+ var entry : ScorePanelEntry = get_entry(peer_id)
+ if entry:
+ entry._score += score
+ return true
+ return false
+
+## This method resets the scores of all [Entry] nodes.
+func reset_scores() -> void:
+ for panel : ScorePanel in panels.get_children():
+ for entry in panel:
+ entry._score = Vector3.ZERO
+
+## This method returns the number of [ScorePanel] children.
+func size() -> int:
+ return panels.get_child_count()
diff --git a/interfaces/scoreboard/scoreboard.tscn b/interfaces/scoreboard/scoreboard.tscn
index 37b6e97..fcbf2c1 100644
--- a/interfaces/scoreboard/scoreboard.tscn
+++ b/interfaces/scoreboard/scoreboard.tscn
@@ -1,8 +1,16 @@
-[gd_scene load_steps=2 format=3 uid="uid://b8bosdd0o1lu7"]
+[gd_scene load_steps=5 format=3 uid="uid://b8bosdd0o1lu7"]
[ext_resource type="Script" path="res://interfaces/scoreboard/scoreboard.gd" id="1_k2wav"]
+[ext_resource type="Theme" uid="uid://bec7g0fax3c8o" path="res://interfaces/global_theme.tres" id="1_p81gx"]
+[ext_resource type="PackedScene" uid="uid://p1q2lu7mtte1" path="res://interfaces/scoreboard/score_panel.tscn" id="2_oa7t6"]
-[node name="Scoreboard" type="Control"]
+[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_jx1ww"]
+properties/0/path = NodePath(".:columns")
+properties/0/spawn = true
+properties/0/replication_mode = 2
+
+[node name="Scoreboard" type="Control" node_paths=PackedStringArray("panels")]
+visible = false
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
@@ -10,9 +18,12 @@ anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
+theme = ExtResource("1_p81gx")
script = ExtResource("1_k2wav")
+_SCORE_PANEL = ExtResource("2_oa7t6")
+panels = NodePath("MarginContainer/VBoxContainer/ScorePanels")
-[node name="Panel" type="Panel" parent="."]
+[node name="MarginContainer" type="MarginContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
@@ -20,45 +31,51 @@ anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
+theme_override_constants/margin_left = 12
+theme_override_constants/margin_top = 12
+theme_override_constants/margin_right = 12
+theme_override_constants/margin_bottom = 12
-[node name="MarginContainer" type="MarginContainer" parent="Panel"]
+[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"]
+layout_mode = 2
+mouse_filter = 2
+
+[node name="Header" type="Panel" parent="MarginContainer/VBoxContainer"]
+custom_minimum_size = Vector2(0, 48)
+layout_mode = 2
+
+[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Header"]
layout_mode = 1
-anchors_preset = 15
-anchor_right = 1.0
-anchor_bottom = 1.0
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+offset_left = -76.5
+offset_top = -19.5
+offset_right = 76.5
+offset_bottom = 19.5
grow_horizontal = 2
grow_vertical = 2
-mouse_filter = 2
-theme_override_constants/margin_left = 64
-theme_override_constants/margin_top = 64
-
-[node name="VBoxContainer" type="VBoxContainer" parent="Panel/MarginContainer"]
-layout_mode = 2
-mouse_filter = 2
-
-[node name="Label" type="Label" parent="Panel/MarginContainer/VBoxContainer"]
-layout_mode = 2
size_flags_horizontal = 4
theme_type_variation = &"HeaderLarge"
text = "Scoreboard"
+horizontal_alignment = 1
+vertical_alignment = 1
+uppercase = true
-[node name="Scores" type="GridContainer" parent="Panel/MarginContainer/VBoxContainer"]
-unique_name_in_owner = true
+[node name="ScorePanels" type="GridContainer" parent="MarginContainer/VBoxContainer"]
layout_mode = 2
-size_flags_horizontal = 4
+size_flags_vertical = 3
mouse_filter = 2
-theme_override_constants/h_separation = 32
-theme_override_constants/v_separation = 16
-columns = 3
-[node name="PlayerLabel" type="Label" parent="Panel/MarginContainer/VBoxContainer/Scores"]
-layout_mode = 2
-text = "Player"
+[node name="PanelSpawner" type="MultiplayerSpawner" parent="MarginContainer/VBoxContainer"]
+_spawnable_scenes = PackedStringArray("res://interfaces/scoreboard/score_panel.tscn")
+spawn_path = NodePath("../ScorePanels")
+spawn_limit = 4
-[node name="KillsLabel" type="Label" parent="Panel/MarginContainer/VBoxContainer/Scores"]
-layout_mode = 2
-text = "Kills"
-
-[node name="ScoreLabel" type="Label" parent="Panel/MarginContainer/VBoxContainer/Scores"]
-layout_mode = 2
-text = "Score"
+[node name="ScoreboardSync" type="MultiplayerSynchronizer" parent="MarginContainer/VBoxContainer"]
+root_path = NodePath("../ScorePanels")
+replication_interval = 1.0
+delta_interval = 1.0
+replication_config = SubResource("SceneReplicationConfig_jx1ww")
diff --git a/interfaces/waypoint/assets/waypoint.svg.import b/interfaces/waypoint/assets/waypoint.svg.import
index 600e8c8..e9f63c6 100644
--- a/interfaces/waypoint/assets/waypoint.svg.import
+++ b/interfaces/waypoint/assets/waypoint.svg.import
@@ -4,15 +4,16 @@ importer="texture"
type="CompressedTexture2D"
uid="uid://cpb6vpa0c74rl"
path.s3tc="res://.godot/imported/waypoint.svg-e7e029f470e2f863636e9426f3893ced.s3tc.ctex"
+path.etc2="res://.godot/imported/waypoint.svg-e7e029f470e2f863636e9426f3893ced.etc2.ctex"
metadata={
-"imported_formats": ["s3tc_bptc"],
+"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
[deps]
source_file="res://interfaces/waypoint/assets/waypoint.svg"
-dest_files=["res://.godot/imported/waypoint.svg-e7e029f470e2f863636e9426f3893ced.s3tc.ctex"]
+dest_files=["res://.godot/imported/waypoint.svg-e7e029f470e2f863636e9426f3893ced.s3tc.ctex", "res://.godot/imported/waypoint.svg-e7e029f470e2f863636e9426f3893ced.etc2.ctex"]
[params]
diff --git a/interfaces/waypoint/waypoint3d.tscn b/interfaces/waypoint/waypoint3d.tscn
index ed8c190..7ca55d5 100644
--- a/interfaces/waypoint/waypoint3d.tscn
+++ b/interfaces/waypoint/waypoint3d.tscn
@@ -17,7 +17,7 @@ line_spacing = 32.025
[node name="Sprite3D" type="Sprite3D" parent="."]
transform = Transform3D(0.05, 0, 0, 0, 0.05, 0, 0, 0, 0.05, 0, 0, 0)
offset = Vector2(0, 64)
-billboard = 2
+billboard = 1
no_depth_test = true
fixed_size = true
texture = ExtResource("1_502g6")
diff --git a/main.gd b/main.gd
index bc77d4d..251b703 100644
--- a/main.gd
+++ b/main.gd
@@ -14,50 +14,47 @@
# along with this program. If not, see .
extends Node3D
-@export_category("Modes")
+@export_category("Types")
@export var SINGLEPLAYER : PackedScene
@export var MULTIPLAYER : PackedScene
-func _ready() -> void:
- # only run server in headless
- if DisplayServer.get_name() == "headless":
- Global.type = MULTIPLAYER.instantiate()
- Global.type.start_server(9000, MapsManager.maps[MapsManager._rng.randi_range(0, len(MapsManager.maps) - 1)])
- return
- # connect boot menu signals
- $BootMenu.start_demo.connect(_start_demo)
- $BootMenu/MultiplayerPanelContainer.start_server.connect(_start_server)
- $BootMenu/MultiplayerPanelContainer.join_server.connect(_join_server)
- # do not set initial window mode for debug build
- if not OS.is_debug_build():
- DisplayServer.window_set_mode(Settings.get_value("video", "window_mode"))
+@onready var boot_menu : BootMenu = $BootMenu
-func _unhandled_input(event : InputEvent) -> void:
- if event.is_action_pressed("exit"):
- if Global.type is Multiplayer:
- $BootMenu._on_multiplayer_pressed()
- else:
- $BootMenu._on_main_menu_pressed()
- # reset game mode and get back to main menu
- Global.type = null
- if DisplayServer.get_name() != "headless":
- Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
- return
+func _ready() -> void:
+ # run server only in headless
+ if DisplayServer.get_name() == "headless":
+ Game.type = MULTIPLAYER.instantiate()
+ Game.type.start_server(9000, MapsManager.maps[randi_range(0, len(MapsManager.maps) - 1)])
+ return
+ Game.exit_pressed.connect(_on_game_exit_pressed)
+ boot_menu.start_demo.connect(_start_demo)
+ boot_menu.multiplayer_panel.start_server.connect(_start_server)
+ boot_menu.multiplayer_panel.join_server.connect(_join_server)
+
+func _on_game_exit_pressed() -> void:
+ Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
+ boot_menu.show()
func _start_demo() -> void:
- Global.type = SINGLEPLAYER.instantiate()
+ Game.type = SINGLEPLAYER.instantiate()
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
- $BootMenu.hide()
+ boot_menu.hide()
-func _start_server(port : int, nickname : String) -> void:
- Global.type = MULTIPLAYER.instantiate()
- Global.type.start_server(port, MapsManager.maps[$BootMenu/MultiplayerPanelContainer.map_selector.selected], nickname)
+func _start_server(port: int, map: PackedScene, mode: Multiplayer.Mode, username: String) -> void:
+ Game.type = MULTIPLAYER.instantiate()
+ Game.type.start_server(port, map, mode, username)
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
- $BootMenu.hide()
+ boot_menu.hide()
-func _join_server(host : String, port : int, nickname : String) -> void:
- Global.type = MULTIPLAYER.instantiate()
- Global.type.connected_to_server.connect($BootMenu/MultiplayerPanelContainer._on_connected_to_server)
- Global.type.connection_failed.connect($BootMenu/MultiplayerPanelContainer._on_connection_failed)
- Global.type.join_server(host, port, nickname)
+func _join_server(host : String, port : int, username : String) -> void:
+ Game.type = MULTIPLAYER.instantiate()
+ if not Game.type.multiplayer.connected_to_server.is_connected(_on_connected_to_server):
+ Game.type.multiplayer.connected_to_server.connect(_on_connected_to_server)
+ if not multiplayer.connection_failed.is_connected(boot_menu.multiplayer_panel.modal.hide):
+ multiplayer.connection_failed.connect(boot_menu.multiplayer_panel.modal.hide)
+ Game.type.join_server(host, port, username)
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
+
+func _on_connected_to_server() -> void:
+ boot_menu.multiplayer_panel.modal.hide()
+ boot_menu.hide()
diff --git a/main.tscn b/main.tscn
index ba29b11..3136123 100644
--- a/main.tscn
+++ b/main.tscn
@@ -1,8 +1,8 @@
[gd_scene load_steps=5 format=3 uid="uid://dr8salxjlpsba"]
[ext_resource type="Script" path="res://main.gd" id="1_opvjh"]
-[ext_resource type="PackedScene" uid="uid://boviiugcnfyrj" path="res://modes/singleplayer/demo.tscn" id="1_vgk6g"]
-[ext_resource type="PackedScene" uid="uid://bvwxfgygm2xb8" path="res://modes/multiplayer/multiplayer.tscn" id="2_iumx3"]
+[ext_resource type="PackedScene" uid="uid://boviiugcnfyrj" path="res://types/singleplayer/demo.tscn" id="1_vgk6g"]
+[ext_resource type="PackedScene" uid="uid://bvwxfgygm2xb8" path="res://types/multiplayer/multiplayer.tscn" id="2_iumx3"]
[ext_resource type="PackedScene" uid="uid://bjctlqvs33nqy" path="res://interfaces/menus/boot/boot.tscn" id="3_s8c8j"]
[node name="Main" type="Node3D"]
diff --git a/maps/components/deathmatch_scoring_component.gd b/maps/components/deathmatch_scoring_component.gd
deleted file mode 100644
index 042db76..0000000
--- a/maps/components/deathmatch_scoring_component.gd
+++ /dev/null
@@ -1,36 +0,0 @@
-# This file is part of open-fpsz.
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-class_name DeathmatchScoringComponent extends Node
-
-@export var ON_KILL_SCORE : int = 10
-
-# this is the node that contains all players, typically the one populated via MultiplayerSpawner
-@export var _players : Node
-@export var _scoreboard : Scoreboard
-
-# only subscribe once per player, per match
-func subscribe_player(player : Player) -> void:
- player.died.connect(_on_player_died)
-
-func unsubscribe_player(player : Player) -> void:
- player.died.disconnect(_on_player_died)
-
-func _on_player_died(player : Player, killer_id : int) -> void:
- if player.match_participant.player_id != killer_id:
- var node_name : String = str(killer_id)
- if _players.has_node(node_name):
- var killer : Player = _players.get_node(node_name)
- _scoreboard.increment_kill_count(killer.match_participant)
- _scoreboard.add_score_to_player(killer.match_participant, 10)
diff --git a/maps/components/rabbit_scoring_component.gd b/maps/components/rabbit_scoring_component.gd
deleted file mode 100644
index 7595ea1..0000000
--- a/maps/components/rabbit_scoring_component.gd
+++ /dev/null
@@ -1,53 +0,0 @@
-# This file is part of open-fpsz.
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-class_name RabbitScoringComponent extends Node
-
-@export var ON_GRAB_SCORE : int = 10
-@export var ON_HOLD_SCORE : int = 10
-@export var HOLD_SCORING_TIMER : float = 10.0 # seconds
-
-@export var _scoreboard : Scoreboard
-
-var _flag_carrier_scoring_timer : Timer = Timer.new()
-var _team_chasers : Team
-var _team_rabbit : Team
-
-func _ready() -> void:
- _flag_carrier_scoring_timer.wait_time = HOLD_SCORING_TIMER
- add_child(_flag_carrier_scoring_timer)
- _team_chasers = Team.new(0)
- _team_rabbit = Team.new(1)
-
-# setup only once per match, per flag (tested only with one flag so far)
-func setup(flag : Flag) -> void:
- _flag_carrier_scoring_timer.timeout.connect(_on_flag_carrier_scoring_timer_timeout.bind(flag))
- flag.grabbed.connect(_on_flag_grabbed)
- flag.regrabbed.connect(_on_flag_regrabbed)
- flag.dropped.connect(_on_flag_dropped)
-
-func _on_flag_grabbed(grabber : Player) -> void:
- grabber.match_participant.team_id = _team_rabbit.team_id
- _scoreboard.add_score_to_player(grabber.match_participant, ON_GRAB_SCORE)
- _flag_carrier_scoring_timer.start()
-
-func _on_flag_regrabbed(grabber : Player) -> void:
- grabber.match_participant.team_id = _team_rabbit.team_id
-
-func _on_flag_dropped(dropper : Player) -> void:
- dropper.match_participant.team_id = _team_chasers.team_id
- _flag_carrier_scoring_timer.stop()
-
-func _on_flag_carrier_scoring_timer_timeout(flag : Flag) -> void:
- _scoreboard.add_score_to_player(flag.last_carrier.match_participant, ON_HOLD_SCORE)
diff --git a/maps/desert/assets/boundaries.glb b/maps/desert/assets/boundaries.glb
new file mode 100644
index 0000000..b910588
Binary files /dev/null and b/maps/desert/assets/boundaries.glb differ
diff --git a/maps/desert/assets/boundaries.glb.import b/maps/desert/assets/boundaries.glb.import
new file mode 100644
index 0000000..64c74a2
--- /dev/null
+++ b/maps/desert/assets/boundaries.glb.import
@@ -0,0 +1,42 @@
+[remap]
+
+importer="scene"
+importer_version=1
+type="PackedScene"
+uid="uid://b40ts3kdv4in0"
+path="res://.godot/imported/boundaries.glb-55bd84dc617756c38b0a32e84b5db1c6.scn"
+
+[deps]
+
+source_file="res://maps/desert/assets/boundaries.glb"
+dest_files=["res://.godot/imported/boundaries.glb-55bd84dc617756c38b0a32e84b5db1c6.scn"]
+
+[params]
+
+nodes/root_type=""
+nodes/root_name=""
+nodes/apply_root_scale=true
+nodes/root_scale=1.0
+meshes/ensure_tangents=true
+meshes/generate_lods=true
+meshes/create_shadow_meshes=true
+meshes/light_baking=1
+meshes/lightmap_texel_size=0.2
+meshes/force_disable_compression=false
+skins/use_named_skins=true
+animation/import=true
+animation/fps=30
+animation/trimming=false
+animation/remove_immutable_tracks=true
+import_script/path=""
+_subresources={
+"nodes": {
+"PATH:Cube-concave": {
+"generate/physics": true,
+"physics/mask": 9,
+"physics/shape_type": 2
+}
+}
+}
+gltf/naming_version=1
+gltf/embedded_image_handling=1
diff --git a/maps/desert/assets/textures/ground054_alb_ht.png.import b/maps/desert/assets/textures/ground054_alb_ht.png.import
index ed56397..1510d87 100644
--- a/maps/desert/assets/textures/ground054_alb_ht.png.import
+++ b/maps/desert/assets/textures/ground054_alb_ht.png.import
@@ -4,15 +4,16 @@ importer="texture"
type="CompressedTexture2D"
uid="uid://cngjywcua4vv8"
path.bptc="res://.godot/imported/ground054_alb_ht.png-460d3ac5c09d7955ca61b8b57742b1a9.bptc.ctex"
+path.astc="res://.godot/imported/ground054_alb_ht.png-460d3ac5c09d7955ca61b8b57742b1a9.astc.ctex"
metadata={
-"imported_formats": ["s3tc_bptc"],
+"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
[deps]
source_file="res://maps/desert/assets/textures/ground054_alb_ht.png"
-dest_files=["res://.godot/imported/ground054_alb_ht.png-460d3ac5c09d7955ca61b8b57742b1a9.bptc.ctex"]
+dest_files=["res://.godot/imported/ground054_alb_ht.png-460d3ac5c09d7955ca61b8b57742b1a9.bptc.ctex", "res://.godot/imported/ground054_alb_ht.png-460d3ac5c09d7955ca61b8b57742b1a9.astc.ctex"]
[params]
diff --git a/maps/desert/assets/textures/ground054_nrm_rgh.png.import b/maps/desert/assets/textures/ground054_nrm_rgh.png.import
index 2fb2555..ef62d76 100644
--- a/maps/desert/assets/textures/ground054_nrm_rgh.png.import
+++ b/maps/desert/assets/textures/ground054_nrm_rgh.png.import
@@ -4,15 +4,16 @@ importer="texture"
type="CompressedTexture2D"
uid="uid://cbk7l6hs0yrcc"
path.bptc="res://.godot/imported/ground054_nrm_rgh.png-9ea209f1a7f6e48624db85005a037890.bptc.ctex"
+path.astc="res://.godot/imported/ground054_nrm_rgh.png-9ea209f1a7f6e48624db85005a037890.astc.ctex"
metadata={
-"imported_formats": ["s3tc_bptc"],
+"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
[deps]
source_file="res://maps/desert/assets/textures/ground054_nrm_rgh.png"
-dest_files=["res://.godot/imported/ground054_nrm_rgh.png-9ea209f1a7f6e48624db85005a037890.bptc.ctex"]
+dest_files=["res://.godot/imported/ground054_nrm_rgh.png-9ea209f1a7f6e48624db85005a037890.bptc.ctex", "res://.godot/imported/ground054_nrm_rgh.png-9ea209f1a7f6e48624db85005a037890.astc.ctex"]
[params]
diff --git a/maps/desert/assets/textures/rock029_alb_ht.png.import b/maps/desert/assets/textures/rock029_alb_ht.png.import
index 0153cc5..b568b06 100644
--- a/maps/desert/assets/textures/rock029_alb_ht.png.import
+++ b/maps/desert/assets/textures/rock029_alb_ht.png.import
@@ -4,15 +4,16 @@ importer="texture"
type="CompressedTexture2D"
uid="uid://dwk8islw7ebab"
path.bptc="res://.godot/imported/rock029_alb_ht.png-dd746789bc129bafaa8d1cc908de0e3e.bptc.ctex"
+path.astc="res://.godot/imported/rock029_alb_ht.png-dd746789bc129bafaa8d1cc908de0e3e.astc.ctex"
metadata={
-"imported_formats": ["s3tc_bptc"],
+"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
[deps]
source_file="res://maps/desert/assets/textures/rock029_alb_ht.png"
-dest_files=["res://.godot/imported/rock029_alb_ht.png-dd746789bc129bafaa8d1cc908de0e3e.bptc.ctex"]
+dest_files=["res://.godot/imported/rock029_alb_ht.png-dd746789bc129bafaa8d1cc908de0e3e.bptc.ctex", "res://.godot/imported/rock029_alb_ht.png-dd746789bc129bafaa8d1cc908de0e3e.astc.ctex"]
[params]
diff --git a/maps/desert/assets/textures/rock029_nrm_rgh.png.import b/maps/desert/assets/textures/rock029_nrm_rgh.png.import
index 0b1aa12..dc963df 100644
--- a/maps/desert/assets/textures/rock029_nrm_rgh.png.import
+++ b/maps/desert/assets/textures/rock029_nrm_rgh.png.import
@@ -4,15 +4,16 @@ importer="texture"
type="CompressedTexture2D"
uid="uid://lgfhdcsb2ryx"
path.bptc="res://.godot/imported/rock029_nrm_rgh.png-3bf029664d0f58a1c79abd2d6666ba90.bptc.ctex"
+path.astc="res://.godot/imported/rock029_nrm_rgh.png-3bf029664d0f58a1c79abd2d6666ba90.astc.ctex"
metadata={
-"imported_formats": ["s3tc_bptc"],
+"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
[deps]
source_file="res://maps/desert/assets/textures/rock029_nrm_rgh.png"
-dest_files=["res://.godot/imported/rock029_nrm_rgh.png-3bf029664d0f58a1c79abd2d6666ba90.bptc.ctex"]
+dest_files=["res://.godot/imported/rock029_nrm_rgh.png-3bf029664d0f58a1c79abd2d6666ba90.bptc.ctex", "res://.godot/imported/rock029_nrm_rgh.png-3bf029664d0f58a1c79abd2d6666ba90.astc.ctex"]
[params]
diff --git a/maps/desert/desert.tscn b/maps/desert/desert.tscn
index db183c0..fa88301 100644
--- a/maps/desert/desert.tscn
+++ b/maps/desert/desert.tscn
@@ -1,14 +1,19 @@
-[gd_scene load_steps=5 format=3 uid="uid://btlkog4b87p4x"]
+[gd_scene load_steps=7 format=3 uid="uid://btlkog4b87p4x"]
[ext_resource type="Terrain3DStorage" uid="uid://wgmg245njt8e" path="res://maps/desert/resources/storage.res" id="1_lmk23"]
[ext_resource type="Terrain3DMaterial" uid="uid://c3isipd4wqxpk" path="res://maps/desert/resources/material.tres" id="2_n44fh"]
-[ext_resource type="Terrain3DTextureList" uid="uid://d1j24k8sq8qpj" path="res://maps/desert/resources/textures.tres" id="3_w1yus"]
+[ext_resource type="Terrain3DTextureList" uid="uid://d1j24k8sq8qpj" path="res://maps/desert/resources/textures.tres" id="3_w6mwl"]
[ext_resource type="Environment" uid="uid://nw62ce5cglvs" path="res://maps/desert/resources/env.tres" id="4_m7p64"]
+[ext_resource type="PackedScene" uid="uid://b40ts3kdv4in0" path="res://maps/desert/assets/boundaries.glb" id="5_dyuqn"]
+
+[sub_resource type="ConcavePolygonShape3D" id="ConcavePolygonShape3D_wu2oa"]
+data = PackedVector3Array(-1024, -1024, 1024, -1024, 1024, -1024, -1024, 1024, 1024, -1024, -1024, 1024, -1024, -1024, -1024, -1024, 1024, -1024, -1024, -1024, -1024, 1024, 1024, -1024, -1024, 1024, -1024, -1024, -1024, -1024, 1024, -1024, -1024, 1024, 1024, -1024, 1024, -1024, -1024, 1024, 1024, 1024, 1024, 1024, -1024, 1024, -1024, -1024, 1024, -1024, 1024, 1024, 1024, 1024, 1024, -1024, 1024, -1024, 1024, 1024, 1024, 1024, 1024, 1024, -1024, 1024, -1024, -1024, 1024, -1024, 1024, 1024, -1024, -1024, -1024, 1024, -1024, 1024, 1024, -1024, -1024, -1024, -1024, -1024, -1024, -1024, 1024, 1024, -1024, 1024, 1024, 1024, -1024, -1024, 1024, 1024, -1024, 1024, -1024, 1024, 1024, -1024, 1024, 1024, 1024, -1024, 1024, 1024)
+backface_collision = true
[node name="Desert" type="Terrain3D"]
storage = ExtResource("1_lmk23")
material = ExtResource("2_n44fh")
-texture_list = ExtResource("3_w1yus")
+texture_list = ExtResource("3_w6mwl")
collision_layer = 2147483648
collision_mask = 2147483648
@@ -28,7 +33,16 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1024, 172.13, 1024)
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1161.42, 174.535, 909.901)
[node name="FlagStand" type="Marker3D" parent="."]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 895.356, 144.835, 888.261)
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 895.356, 145.367, 888.261)
[node name="Camera3D" type="Camera3D" parent="."]
-transform = Transform3D(-0.642788, 0.586824, -0.492404, 0, 0.642788, 0.766044, 0.766045, 0.492404, -0.413176, 800, 320, 846.595)
+transform = Transform3D(-0.642788, 0.586824, -0.492404, 0, 0.642788, 0.766044, 0.766044, 0.492404, -0.413176, 800, 320, 846.595)
+
+[node name="Boundaries" parent="." instance=ExtResource("5_dyuqn")]
+transform = Transform3D(0.99, 0, 0, 0, 0.99, 0, 0, 0, 0.99, 1024, 1024, 1024)
+visible = false
+
+[node name="CollisionShape3D" parent="Boundaries/Cube-concave/StaticBody3D" index="0"]
+shape = SubResource("ConcavePolygonShape3D_wu2oa")
+
+[editable path="Boundaries"]
diff --git a/maps/desert/resources/env.tres b/maps/desert/resources/env.tres
index 9aaa259..55a1b3d 100644
--- a/maps/desert/resources/env.tres
+++ b/maps/desert/resources/env.tres
@@ -14,7 +14,7 @@ sky_material = SubResource("ProceduralSkyMaterial_8mbvu")
background_mode = 2
sky = SubResource("Sky_mobku")
tonemap_mode = 3
-tonemap_exposure = 0.85
ssr_enabled = true
+fog_density = 0.001
volumetric_fog_density = 0.005
adjustment_brightness = 0.85
diff --git a/maps/desert/resources/material.tres b/maps/desert/resources/material.tres
index fec331b..e627db8 100644
--- a/maps/desert/resources/material.tres
+++ b/maps/desert/resources/material.tres
@@ -29,9 +29,9 @@ _shader_parameters = {
"auto_overlay_texture": 1,
"auto_slope": 1.0,
"blend_sharpness": 0.85,
-"dual_scale_far": 350.0,
+"dual_scale_far": 100.0,
"dual_scale_near": 0.0,
-"dual_scale_reduction": 1.0,
+"dual_scale_reduction": 0.8,
"dual_scale_texture": 1,
"height_blending": true,
"macro_variation1": Color(1, 1, 1, 1),
@@ -39,13 +39,14 @@ _shader_parameters = {
"noise1_angle": 0.0,
"noise1_offset": Vector2(0.5, 0.5),
"noise1_scale": 0.4,
-"noise2_scale": 0.5,
+"noise2_scale": 0.076,
"noise3_scale": 0.225,
"noise_texture": SubResource("NoiseTexture2D_esvkc"),
"tri_scale_reduction": 0.075,
+"vertex_normals_distance": null,
"world_noise_blend_far": 1.0,
"world_noise_blend_near": 0.75,
-"world_noise_height": 40.0,
+"world_noise_height": 50.0,
"world_noise_lod_distance": 500.0,
"world_noise_max_octaves": 6,
"world_noise_min_octaves": 5,
diff --git a/maps/desert/resources/storage.res b/maps/desert/resources/storage.res
index 7877177..57ad52c 100644
Binary files a/maps/desert/resources/storage.res and b/maps/desert/resources/storage.res differ
diff --git a/modes/multiplayer/multiplayer.gd b/modes/multiplayer/multiplayer.gd
deleted file mode 100644
index cd67f5f..0000000
--- a/modes/multiplayer/multiplayer.gd
+++ /dev/null
@@ -1,116 +0,0 @@
-# This file is part of open-fpsz.
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-class_name Multiplayer extends Node
-
-@export_category("Parameters")
-@export var PLAYER : PackedScene
-@export var FLAG : PackedScene
-@export var MAX_CLIENTS : int = 24
-@export var RESPAWN_TIME : float = 3.0
-
-@onready var players : Node = $Players
-@onready var objectives : Node = $Objectives
-@onready var map : Node = $Map
-@onready var scoreboard : Scoreboard = $Scoreboard
-
-signal connected_to_server
-signal connection_failed
-
-func start_server(port : int, map_scene : PackedScene, username : String = "mercury") -> void:
- # setup enet server peer
- var peer : ENetMultiplayerPeer = ENetMultiplayerPeer.new()
- peer.create_server(port, MAX_CLIENTS)
- multiplayer.peer_disconnected.connect(remove_player)
- multiplayer.multiplayer_peer = peer
-
- _load_map(map_scene)
-
- if DisplayServer.get_name() != "headless":
- add_player(1, username)
-
-func join_server(host : String, port : int, username : String) -> void:
- var peer : ENetMultiplayerPeer = ENetMultiplayerPeer.new()
- peer.create_client(host, port)
- multiplayer.connected_to_server.connect(_on_connected_to_server.bind(username))
- multiplayer.connection_failed.connect(_on_connection_failed)
- multiplayer.multiplayer_peer = peer
-
-func _on_connected_to_server(username : String) -> void:
- connected_to_server.emit()
- scoreboard.request_scoreboard_from_authority.rpc()
- _join_match.rpc(username)
-
-func _on_connection_failed() -> void:
- connection_failed.emit()
-
-func _on_player_died(player : Player, _killer_id : int) -> void:
- await get_tree().create_timer(RESPAWN_TIME).timeout
- respawn_player(player)
-
-func respawn_player(player : Player) -> void:
- var spawn_location : Vector3 = MapsManager.get_player_spawn().position
- player.respawn.rpc(spawn_location)
-
-func add_player(peer_id : int, username : String) -> void:
- var player : Player = PLAYER.instantiate()
- player.name = str(peer_id)
- player.died.connect(_on_player_died)
- players.add_child(player)
- player.match_participant.player_id = peer_id
- player.match_participant.team_id = ($RabbitScoringComponent as RabbitScoringComponent)._team_chasers.team_id
- player.match_participant.username = username
- player.global_position = MapsManager.get_player_spawn().position
- $DeathmatchScoringComponent.subscribe_player(player)
- scoreboard.add_participant(player.match_participant)
- print("Peer `%s` connected" % player.name)
-
-func remove_player(peer_id : int) -> void:
- var node_name : String = str(peer_id)
- if players.has_node(node_name):
- var player : Player = players.get_node(node_name)
- scoreboard.remove_participant(player.match_participant)
- player.died.disconnect(_on_player_died)
- $DeathmatchScoringComponent.unsubscribe_player(player)
- player.queue_free()
- print("Peer `%s` disconnected" % node_name)
-
-func _load_map(scene : PackedScene) -> void:
- var map_scene : Node = scene.instantiate()
- MapsManager.current_map = map_scene
- map_scene.ready.connect(_add_flag)
- map.add_child(map_scene)
-
-func _add_flag() -> void:
- var flag : Flag = FLAG.instantiate()
- $RabbitScoringComponent.setup(flag)
- objectives.add_child(flag)
- var flagstand : Marker3D = MapsManager.current_map.get_node("FlagStand")
- if flagstand:
- flag.global_position = flagstand.global_position
-
-# This method notifies the server that a player wants to join the match. It
-# takes a single [param username] parameter and is invoked remotely by clients.
-@rpc("any_peer", "reliable")
-func _join_match(username : String) -> void:
- if multiplayer.is_server():
- # add player to server with unique id and username
- add_player(multiplayer.get_remote_sender_id(), username)
-
-func _exit_tree() -> void:
- # @NOTE: The `is_multiplayer_authority` method in `_exit_tree` push an error
- # about the multiplayer instance not being the currently active one.
- # see https://github.com/godotengine/godot/issues/77723
- multiplayer.multiplayer_peer.close()
- multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new()
diff --git a/modes/multiplayer/multiplayer.tscn b/modes/multiplayer/multiplayer.tscn
deleted file mode 100644
index 7871900..0000000
--- a/modes/multiplayer/multiplayer.tscn
+++ /dev/null
@@ -1,44 +0,0 @@
-[gd_scene load_steps=7 format=3 uid="uid://bvwxfgygm2xb8"]
-
-[ext_resource type="Script" path="res://modes/multiplayer/multiplayer.gd" id="1_r1kd6"]
-[ext_resource type="PackedScene" uid="uid://cbhx1xme0sb7k" path="res://entities/player/player.tscn" id="2_og1vb"]
-[ext_resource type="PackedScene" uid="uid://c88l3h0ph00c7" path="res://entities/flag/flag.tscn" id="3_h0rie"]
-[ext_resource type="Script" path="res://maps/components/rabbit_scoring_component.gd" id="5_7woao"]
-[ext_resource type="PackedScene" uid="uid://b8bosdd0o1lu7" path="res://interfaces/scoreboard/scoreboard.tscn" id="5_uj0pp"]
-[ext_resource type="Script" path="res://maps/components/deathmatch_scoring_component.gd" id="6_iov4u"]
-
-[node name="Multiplayer" type="Node"]
-script = ExtResource("1_r1kd6")
-PLAYER = ExtResource("2_og1vb")
-FLAG = ExtResource("3_h0rie")
-
-[node name="Map" type="Node" parent="."]
-
-[node name="MapSpawner" type="MultiplayerSpawner" parent="."]
-_spawnable_scenes = PackedStringArray("res://maps/desert/desert.tscn")
-spawn_path = NodePath("../Map")
-spawn_limit = 1
-
-[node name="Players" type="Node" parent="."]
-
-[node name="PlayersSpawner" type="MultiplayerSpawner" parent="."]
-_spawnable_scenes = PackedStringArray("res://entities/player/player.tscn")
-spawn_path = NodePath("../Players")
-
-[node name="Objectives" type="Node" parent="."]
-
-[node name="ObjectivesSpawner" type="MultiplayerSpawner" parent="."]
-_spawnable_scenes = PackedStringArray("res://entities/flag/flag.tscn")
-spawn_path = NodePath("../Objectives")
-
-[node name="Scoreboard" parent="." instance=ExtResource("5_uj0pp")]
-visible = false
-
-[node name="RabbitScoringComponent" type="Node" parent="." node_paths=PackedStringArray("_scoreboard")]
-script = ExtResource("5_7woao")
-_scoreboard = NodePath("../Scoreboard")
-
-[node name="DeathmatchScoringComponent" type="Node" parent="." node_paths=PackedStringArray("_players", "_scoreboard")]
-script = ExtResource("6_iov4u")
-_players = NodePath("../Players")
-_scoreboard = NodePath("../Scoreboard")
diff --git a/project.godot b/project.godot
index 9f5432c..956c334 100644
--- a/project.godot
+++ b/project.godot
@@ -17,7 +17,7 @@ config/icon="res://icon.svg"
[autoload]
-Global="*res://systems/global.gd"
+Game="*res://systems/game.gd"
Settings="*res://systems/settings.gd"
MapsManager="*res://systems/MapsManager.tscn"
@@ -44,6 +44,10 @@ gdscript/warnings/function_used_as_property=2
gdscript/warnings/untyped_declaration=1
gdscript/warnings/static_called_on_instance=0
+[display]
+
+window/stretch/mode="canvas_items"
+
[editor]
movie_writer/disable_vsync=true
@@ -83,7 +87,7 @@ ski={
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":32,"key_label":0,"unicode":32,"echo":false,"script":null)
]
}
-jump_and_jet={
+secondary={
"deadzone": 0.5,
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":2,"canceled":false,"pressed":false,"double_click":false,"script":null)
]
@@ -108,12 +112,12 @@ mouse_mode={
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194332,"key_label":0,"unicode":0,"echo":false,"script":null)
]
}
-fire_primary={
+primary={
"deadzone": 0.5,
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":1,"canceled":false,"pressed":false,"double_click":false,"script":null)
]
}
-throw_flag={
+throw={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":70,"key_label":0,"unicode":102,"echo":false,"script":null)
]
@@ -123,14 +127,21 @@ scoreboard={
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194306,"key_label":0,"unicode":0,"echo":false,"script":null)
]
}
+respawn={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":75,"key_label":0,"unicode":107,"echo":false,"script":null)
+]
+}
[layer_names]
+3d_render/layer_2="ThirdPerson"
3d_physics/layer_3="Damage"
3d_physics/layer_4="Objective"
3d_physics/layer_32="WalkableSurface"
[physics]
+common/physics_jitter_fix=0.0
3d/physics_engine="JoltPhysics3D"
3d/default_gravity=19.6
diff --git a/systems/teams.gd b/resources/array_packed_scene.gd
similarity index 79%
rename from systems/teams.gd
rename to resources/array_packed_scene.gd
index f6f0a53..52f7374 100644
--- a/systems/teams.gd
+++ b/resources/array_packed_scene.gd
@@ -12,9 +12,9 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-class_name Team extends Object
+class_name ArrayPackedSceneResource extends Resource
-var team_id : int
+@export var _packed_scenes : Array[PackedScene] = []
-func _init(id : int) -> void:
- team_id = id
+func get_items() -> Array[PackedScene]:
+ return _packed_scenes
diff --git a/types/resources/array_packed_scene.tres b/resources/array_packed_scene.tres
similarity index 57%
rename from types/resources/array_packed_scene.tres
rename to resources/array_packed_scene.tres
index 163e7d4..4999b2a 100644
--- a/types/resources/array_packed_scene.tres
+++ b/resources/array_packed_scene.tres
@@ -1,7 +1,7 @@
[gd_resource type="Resource" script_class="ArrayPackedSceneResource" load_steps=2 format=3 uid="uid://cpnargstkucch"]
-[ext_resource type="Script" path="res://types/resources/array_packed_scene.gd" id="1_r1ygd"]
+[ext_resource type="Script" path="res://resources/array_packed_scene.gd" id="1_xcy3p"]
[resource]
-script = ExtResource("1_r1ygd")
+script = ExtResource("1_xcy3p")
_packed_scenes = Array[PackedScene]([])
diff --git a/systems/global.gd b/systems/game.gd
similarity index 86%
rename from systems/global.gd
rename to systems/game.gd
index 740b524..f02cd15 100644
--- a/systems/global.gd
+++ b/systems/game.gd
@@ -15,6 +15,10 @@
extends Node
signal type_changed(type : Node)
+signal exit_pressed
+
+## The default [Environment] for the game.
+var environment := Environment.new()
## This is the type currently used by the game.
var type : Node:
@@ -27,10 +31,16 @@ var type : Node:
# keep reference to new type
type = new_type
if type != null:
- get_tree().get_root().add_child(type)
+ get_tree().current_scene.add_child(type)
type_changed.emit(type)
+func quit() -> void:
+ get_tree().quit()
+
func _unhandled_input(event : InputEvent) -> void:
+ if event.is_action_pressed("exit"):
+ exit_pressed.emit()
+
# switch window mode
if event.is_action_pressed("window_mode"):
if DisplayServer.window_get_mode() == DisplayServer.WINDOW_MODE_FULLSCREEN:
diff --git a/systems/maps_manager.gd b/systems/maps_manager.gd
index 5812b03..2454aed 100644
--- a/systems/maps_manager.gd
+++ b/systems/maps_manager.gd
@@ -12,51 +12,26 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-## Manage environment settings and terrain maps within a scene.
extends Node
## The list of available maps.
+## @experimental
@export var maps : Array[PackedScene]
-## The default [Environment] used by the current scene in case the [Terrain3D]
-## node does not have a [WorldEnvironment] as one of its children.
-var environment := Environment.new()
-
-## Random number generator instance
-var _rng := RandomNumberGenerator.new()
-
-## Reference to the current terrain map in the scene
+## @experimental
var current_map : Terrain3D = null:
set(new_map):
if current_map != null:
current_map.queue_free()
current_map = new_map
- # forward user environment settings to current scene environment
- var world : World3D = get_viewport().find_world_3d()
- if world.environment:
- world.environment.sdfgi_enabled = MapsManager.environment.sdfgi_enabled
- world.environment.glow_enabled = MapsManager.environment.glow_enabled
- world.environment.ssao_enabled = MapsManager.environment.ssao_enabled
- world.environment.ssr_enabled = MapsManager.environment.ssr_enabled
- world.environment.ssr_max_steps = MapsManager.environment.ssr_max_steps
- world.environment.ssil_enabled = MapsManager.environment.ssil_enabled
- world.environment.volumetric_fog_enabled = MapsManager.environment.volumetric_fog_enabled
+## @experimental
func get_player_spawn() -> Node3D:
- if current_map == null:
- push_error("MapsManager.current_map is null")
+ if not current_map or not current_map.has_node("PlayerSpawns"):
return null
-
- if not current_map.has_node("PlayerSpawns"):
- push_warning("PlayerSpawns node not found in MapsManager.current_map")
- return null
-
- var player_spawns : Node = current_map.get_node("PlayerSpawns")
- var spawn_count : int = player_spawns.get_child_count()
- if spawn_count == 0:
- push_error("PlayerSpawns has no children (%s)" % current_map.name)
- return null
- var spawn_index : int = _rng.randi_range(0, spawn_count - 1)
- var random_spawn : Marker3D = player_spawns.get_child(spawn_index)
- return random_spawn
-
+ var spawns : Node = current_map.get_node("PlayerSpawns")
+ if spawns:
+ var child_count : int = spawns.get_child_count()
+ return spawns.get_child(
+ randi_range(0, child_count - 1 if child_count else 0))
+ return null
diff --git a/systems/settings.gd b/systems/settings.gd
index 0f1b0da..b693378 100644
--- a/systems/settings.gd
+++ b/systems/settings.gd
@@ -24,8 +24,8 @@ func _ready() -> void:
var err : Error = _file.load(self.path)
if err == ERR_FILE_CANT_OPEN:
push_warning("settings not found, using defaults")
- # apply state
- self.apply()
+ # save state
+ save()
func get_value(section: String, key: String, default: Variant = null) -> Variant:
return _file.get_value(section, key, default)
@@ -37,7 +37,7 @@ func reset() -> void:
_file.clear()
# video settings
_file.set_value("ui", "scale", 2)
- _file.set_value("video", "quality", 1.125)
+ _file.set_value("video", "quality", 1.)
_file.set_value("video", "filter", 0)
_file.set_value("video", "fsr_sharpness", 0)
_file.set_value("video", "window_mode", 2)
@@ -61,5 +61,5 @@ func reset() -> void:
_file.set_value("environment", "ssil", 0)
_file.set_value("environment", "volumetric_fog", 0)
-func apply() -> void:
+func save() -> void:
_file.save(self.path)
diff --git a/tests/test_deathmatch_scoring_component.gd b/tests/test_deathmatch_scoring_component.gd
deleted file mode 100644
index 027f902..0000000
--- a/tests/test_deathmatch_scoring_component.gd
+++ /dev/null
@@ -1,69 +0,0 @@
-# This file is part of open-fpsz.
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-extends GutTest
-
-var PLAYER : PackedScene = preload("res://entities/player/player.tscn")
-var SCOREBOARD : PackedScene = preload("res://interfaces/scoreboard/scoreboard.tscn")
-
-var _subject : DeathmatchScoringComponent
-var _scoreboard : Object
-var _victim : Player
-var _killer : Player
-
-func _setup_player(player_id : int) -> Player:
- var player : Player = PLAYER.instantiate()
- player.name = str(player_id)
- add_child(player) # we can't autofree in this test setup
- var participant : MatchParticipant = player.match_participant
- player.match_participant.player_id = player_id
- _scoreboard.add_participant(participant)
- _subject.subscribe_player(player)
- _subject._players = self
- return player
-
-func before_each() -> void:
- _subject = DeathmatchScoringComponent.new()
- _scoreboard = double(SCOREBOARD).instantiate()
- stub(_scoreboard, 'add_score_to_player')
- stub(_scoreboard, 'increment_kill_count')
- _subject._scoreboard = _scoreboard
- _victim = _setup_player(0)
- _killer = _setup_player(1)
-
-func after_each() -> void:
- _subject.unsubscribe_player(_victim)
- _subject.unsubscribe_player(_killer)
- _victim.free()
- _killer.free()
- _subject.free()
-
-func _kill_player(victim : Player, killer : Player) -> void:
- victim.died.emit(victim, killer.match_participant.player_id)
-
-func test_player_gets_no_score_for_self_kill() -> void:
- _kill_player(_victim, _victim)
- assert_not_called(_scoreboard, 'add_score_to_player')
- assert_not_called(_scoreboard, 'increment_kill_count')
-
-func test_killer_gets_score_after_kill() -> void:
- _kill_player(_victim, _killer)
- assert_called(_scoreboard, 'add_score_to_player', [_killer.match_participant, _subject.ON_KILL_SCORE])
- assert_called(_scoreboard, 'increment_kill_count', [_killer.match_participant])
-
-func test_killer_leaves_before_kill() -> void:
- remove_child(_killer)
- _kill_player(_victim, _killer)
- assert_not_called(_scoreboard, 'add_score_to_player')
- assert_not_called(_scoreboard, 'increment_kill_count')
diff --git a/tests/test_flag.gd b/tests/test_flag.gd
index b0a8055..2e5bff0 100644
--- a/tests/test_flag.gd
+++ b/tests/test_flag.gd
@@ -14,112 +14,47 @@
# along with this program. If not, see .
extends GutTest
-var FLAG : PackedScene = preload("res://entities/flag/flag.tscn")
-var PLAYER : PackedScene = preload("res://entities/player/player.tscn")
+const FLAG : PackedScene = preload("res://entities/flag/flag.tscn")
var _subject : Flag
+var _carry : FlagCarryComponent
func before_each() -> void:
_subject = FLAG.instantiate()
+ _carry = FlagCarryComponent.new()
watch_signals(_subject)
add_child_autofree(_subject)
+ add_child_autofree(_carry)
-func _is_mesh_visible() -> bool:
- var mesh : Node3D = _subject.find_child("Mesh")
- return mesh.is_visible()
+func test_flag_default_state() -> void:
+ assert_eq(_subject.state, Flag.FlagState.ON_STAND)
-func test_that_flag_is_on_stand_by_default() -> void:
- assert_eq(_subject.flag_state, Flag.FlagState.ON_STAND)
+func test_flag_default_visibility() -> void:
+ assert_true(_subject.is_visible())
-func test_that_mesh_is_visible_by_default() -> void:
- assert_true(_is_mesh_visible())
+func test_grab_flag() -> void:
+ _carry.grab(_subject)
+ assert_signal_emitted_with_parameters(_subject, 'grabbed', [_carry])
+ assert_eq(_subject.state, Flag.FlagState.TAKEN)
+ assert_eq(_subject.last_carrier, null)
+ assert_false(_subject.is_visible())
+
+func test_drop_flag() -> void:
+ test_grab_flag()
+ _carry.drop()
+ assert_signal_emitted_with_parameters(_subject, 'dropped', [_carry])
+ assert_eq(_subject.state, Flag.FlagState.DROPPED)
+ assert_eq(_subject.last_carrier, _carry)
+ assert_true(_subject.is_visible())
-func test_that_flag_on_stand_can_be_grabbed() -> void:
- _subject.flag_state = Flag.FlagState.ON_STAND
- assert_true(_subject.state != _subject.FlagState.TAKEN)
+func test_that_flag_grab_hides_flag() -> void:
+ test_grab_flag()
+ assert_false(_subject.is_visible())
-func test_that_dropped_flag_can_be_grabbed() -> void:
- _subject.flag_state = Flag.FlagState.DROPPED
- assert_true(_subject.state != _subject.FlagState.TAKEN)
-
-func test_that_taken_flag_cannot_be_grabbed() -> void:
- _subject.flag_state = Flag.FlagState.TAKEN
- assert_false(_subject.state != _subject.FlagState.TAKEN)
-
-func test_that_on_stand_flag_grab_emits_grab_signal_and_changes_state() -> void:
- _subject.flag_state = Flag.FlagState.ON_STAND
- var player : Player = PLAYER.instantiate()
- _subject.grab(player)
- assert_signal_emitted_with_parameters(_subject, 'grabbed', [player])
- assert_eq(_subject.flag_state, Flag.FlagState.TAKEN)
- assert_eq(_subject.last_carrier, player)
- player.free()
-
-func test_that_dropped_flag_grab_emits_grab_signal_and_changes_state() -> void:
- _subject.flag_state = Flag.FlagState.DROPPED
- var player : Player = PLAYER.instantiate()
- _subject.grab(player)
- assert_signal_emitted_with_parameters(_subject, 'grabbed', [player])
- assert_eq(_subject.flag_state, Flag.FlagState.TAKEN)
- assert_eq(_subject.last_carrier, player)
- player.free()
-
-func test_that_taken_flag_grab_does_not_emit_grab_signal_and_keeps_state() -> void:
- _subject.flag_state = Flag.FlagState.TAKEN
- _subject.grab(null) # doesn't matter that it's null in this case
- assert_signal_not_emitted(_subject, 'grabbed')
- assert_eq(_subject.flag_state, Flag.FlagState.TAKEN)
- assert_null(_subject.last_carrier)
-
-func test_that_flag_grab_hides_mesh() -> void:
- var player : Player = PLAYER.instantiate()
- _subject.grab(player)
- assert_false(_is_mesh_visible())
- player.free()
-
-func test_that_taken_flag_drop_emits_signal_and_changes_state() -> void:
- var player : Player = PLAYER.instantiate()
- _subject.grab(player)
- _subject.drop(player)
- assert_signal_emitted_with_parameters(_subject, 'dropped', [player])
- assert_eq(_subject.flag_state, Flag.FlagState.DROPPED)
- player.free()
-
-func test_that_on_stand_flag_drop_does_not_emit_signals_and_keeps_state() -> void:
- var player : Player = PLAYER.instantiate()
- _subject.drop(player)
- assert_signal_not_emitted(_subject, 'dropped')
- assert_eq(_subject.flag_state, Flag.FlagState.ON_STAND)
- player.free()
-
-func test_that_flag_drop_shows_mesh() -> void:
- var player : Player = PLAYER.instantiate()
- _subject.grab(player)
- _subject.drop(player)
- assert_true(_is_mesh_visible())
- player.free()
-
-func test_that_impossible_regrab_does_not_emit_regrabbed_signal() -> void:
- var player : Player = PLAYER.instantiate()
- _subject.grab(player)
- _subject.grab(player)
- assert_signal_not_emitted(_subject, 'regrabbed')
- player.free()
-
-func test_that_regrab_by_same_player_emits_regrabbed_signal() -> void:
- var player : Player = PLAYER.instantiate()
- _subject.grab(player)
- _subject.drop(player)
- _subject.grab(player)
- assert_signal_emitted_with_parameters(_subject, 'regrabbed', [player])
- player.free()
-
-func test_that_regrab_by_different_player_does_not_emit_regrabbed_signal() -> void:
- var player : Player = PLAYER.instantiate()
- var different_player : Player = PLAYER.instantiate()
- _subject.grab(player)
- _subject.drop(player)
- _subject.grab(different_player)
- assert_signal_not_emitted(_subject, 'regrabbed')
- player.free()
- different_player.free()
+func test_grab_taken_flag() -> void:
+ test_grab_flag()
+ var _new_carry = FlagCarryComponent.new()
+ add_child_autofree(_new_carry)
+ _new_carry.grab(_subject)
+ assert_null(_new_carry._flag)
+ assert_eq(_carry._flag, _subject)
diff --git a/tests/test_health.gd b/tests/test_health.gd
new file mode 100644
index 0000000..f337abf
--- /dev/null
+++ b/tests/test_health.gd
@@ -0,0 +1,50 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+extends GutTest
+
+var _subject:Health
+const TEST_MAX_VALUE:int = 255
+
+func before_each() -> void:
+ _subject = Health.new()
+ watch_signals(_subject)
+ autoqfree(_subject)
+
+func test_that_it_has_max_health_when_ready() -> void:
+ assert_eq(_subject.value, _subject.max_value)
+
+func test_that_it_takes_damage_from_opponent_team() -> void:
+ var amount:int = 42
+ _subject.damage(amount, -1)
+ assert_eq(_subject.value, TEST_MAX_VALUE - amount)
+
+func test_that_it_can_self_damage() -> void:
+ var amount:int = 42
+ _subject.damage(amount, 1)
+ assert_eq(_subject.value, TEST_MAX_VALUE - amount)
+
+func test_that_it_emits_health_changed_after_damage() -> void:
+ _subject.damage(42, -1)
+ assert_signal_emitted(_subject, 'updated')
+
+func test_that_it_emits_signals_when_health_value_is_min_value() -> void:
+ _subject.damage(TEST_MAX_VALUE, -1)
+ assert_signal_emitted_with_parameters(_subject, 'killed', [-1])
+ assert_signal_emitted(_subject, 'exhausted')
+
+func test_that_it_heals_fully() -> void:
+ _subject.value = 42
+ _subject.heal()
+ assert_eq(_subject.value, TEST_MAX_VALUE)
diff --git a/tests/test_health_component.gd b/tests/test_health_component.gd
deleted file mode 100644
index 3329dd5..0000000
--- a/tests/test_health_component.gd
+++ /dev/null
@@ -1,68 +0,0 @@
-# This file is part of open-fpsz.
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-extends GutTest
-
-var _subject : HealthComponent
-const TEST_MAX_HEALTH : float = 1.
-const TEST_PLAYER_ID : int = 1
-const TEST_TEAM_ID : int = 1
-
-func before_each() -> void:
- _subject = HealthComponent.new()
- watch_signals(_subject)
- _subject.max_health = TEST_MAX_HEALTH
- var participant : MatchParticipant = MatchParticipant.new()
- participant.player_id = TEST_PLAYER_ID
- participant.team_id = TEST_TEAM_ID
- _subject.match_participant = participant
- add_child_autofree(_subject)
-
-func after_each() -> void:
- _subject.match_participant.free()
-
-func test_that_it_has_max_health_when_ready() -> void:
- assert_eq(_subject.health, _subject.max_health)
-
-func test_that_it_takes_damage_from_opponent_team() -> void:
- var damage_amount : float = .1
- _subject.damage(damage_amount, -1, -1)
- assert_eq(_subject.health, TEST_MAX_HEALTH - damage_amount)
-
-func test_that_it_takes_no_damage_from_same_team() -> void:
- var damage_amount : float = .1
- _subject.damage(damage_amount, -1, TEST_TEAM_ID)
- assert_eq(_subject.health, TEST_MAX_HEALTH)
-
-func test_that_it_can_self_damage() -> void:
- var damage_amount : float = .1
- _subject.damage(damage_amount, TEST_PLAYER_ID, TEST_TEAM_ID)
- assert_eq(_subject.health, TEST_MAX_HEALTH - damage_amount)
-
-func test_that_it_emits_health_changed_after_damage() -> void:
- _subject.damage(1, -1, -1)
- assert_signal_emitted(_subject, 'health_changed')
-
-func test_that_it_emits_health_zeroed() -> void:
- _subject.damage(TEST_MAX_HEALTH, -1, -1)
- assert_signal_emitted_with_parameters(_subject, 'health_zeroed', [-1])
-
-func test_that_it_heals_fully() -> void:
- _subject.health = .1
- _subject.heal_full()
- assert_eq(_subject.health, TEST_MAX_HEALTH)
-
-func test_that_it_emits_health_changed_after_heal_full() -> void:
- _subject.heal_full()
- assert_signal_emitted(_subject, 'health_changed')
diff --git a/tests/test_scoreboard.gd b/tests/test_scoreboard.gd
index cfc58f8..aa372f9 100644
--- a/tests/test_scoreboard.gd
+++ b/tests/test_scoreboard.gd
@@ -14,41 +14,65 @@
# along with this program. If not, see .
extends GutTest
-var SCOREBOARD : PackedScene = preload("res://interfaces/scoreboard/scoreboard.tscn")
+const SCOREBOARD : PackedScene = preload("res://interfaces/scoreboard/scoreboard.tscn")
-var _subject : Scoreboard
+var _scoreboard : Scoreboard
func before_each() -> void:
- _subject = SCOREBOARD.instantiate()
- add_child_autofree(_subject)
+ _scoreboard = SCOREBOARD.instantiate()
+ add_child_autoqfree(_scoreboard)
-func test_that_new_scoreboard_is_empty() -> void:
- assert_eq(_subject._entries, {})
+func test_size() -> void:
+ assert_eq(_scoreboard.size(), 0)
-func test_that_added_entry_is_added_correctly() -> void:
- var participant : MatchParticipant = MatchParticipant.new()
- participant.username = "test_username"
- _subject.add_participant(participant)
- var entries : Array = _subject._entries.values()
- assert_eq(1, entries.size())
- var tested_entry : Scoreboard.ScoreboardEntry = entries[0]
- assert_eq("test_username", tested_entry.username)
- assert_eq(0, tested_entry.kills)
- assert_eq(0, tested_entry.score)
- participant.free()
+# panels
-func test_that_scores_are_added_correctly() -> void:
- var participant : MatchParticipant = MatchParticipant.new()
- _subject.add_participant(participant)
- _subject.add_score_to_player(participant, 10)
- var tested_entry : Scoreboard.ScoreboardEntry = _subject._entries.values()[0]
- assert_eq(10, tested_entry.score)
- participant.free()
+func test_add_panel() -> void:
+ _scoreboard.add_panel()
+ assert_eq(_scoreboard.size(), 1)
-func test_that_kill_counts_are_incremented_correctly() -> void:
- var participant : MatchParticipant = MatchParticipant.new()
- _subject.add_participant(participant)
- _subject.increment_kill_count(participant)
- var tested_entry : Scoreboard.ScoreboardEntry = _subject._entries.values()[0]
- assert_eq(1, tested_entry.kills)
- participant.free()
+func test_remove_panel() -> void:
+ var panel : ScorePanel = _scoreboard.add_panel()
+ _scoreboard.remove_panel(panel)
+ assert_eq(_scoreboard.size(), 0)
+
+func test_get_panel() -> void:
+ _scoreboard.add_panel()
+ var panel : ScorePanel = _scoreboard.get_panel(0)
+ assert_not_null(panel)
+
+# entries
+
+func test_panel_add_entry() -> void:
+ var panel : ScorePanel = _scoreboard.add_panel()
+ var entry : ScorePanelEntry = panel.add_entry(1, "mercury")
+ assert_not_null(entry)
+ assert_eq(panel.size(), 1)
+
+func test_panel_remove_entry() -> void:
+ var panel : ScorePanel = _scoreboard.add_panel()
+ var entry : ScorePanelEntry = panel.add_entry(1, "mercury")
+ assert_eq(panel.size(), 1)
+ panel.remove_entry(entry)
+ assert_eq(panel.size(), 0)
+
+func test_panel_remove_entry_by_name() -> void:
+ var panel : ScorePanel = _scoreboard.add_panel()
+ var entry : ScorePanelEntry = panel.add_entry(1, "mercury")
+ assert_eq(panel.size(), 1)
+ assert_true(panel.remove_entry_by_name(entry.name))
+ assert_eq(panel.size(), 0)
+
+func test_panel_remove_entry_by_peer_id() -> void:
+ var panel : ScorePanel = _scoreboard.add_panel()
+ panel.add_entry(1, "mercury")
+ assert_eq(panel.size(), 1)
+ assert_true(panel.remove_entry_by_peer_id(1))
+ assert_eq(panel.size(), 0)
+
+func test_panel_get_entry() -> void:
+ var panel : ScorePanel = _scoreboard.add_panel()
+ panel.add_entry(1, "mercury")
+ var entry : ScorePanelEntry = panel.get_entry(0)
+ assert_not_null(entry)
+ assert_eq(entry.name, "1")
diff --git a/tests/test_scoreboard_deathmatch_scoring.gd b/tests/test_scoreboard_deathmatch_scoring.gd
new file mode 100644
index 0000000..6593117
--- /dev/null
+++ b/tests/test_scoreboard_deathmatch_scoring.gd
@@ -0,0 +1,69 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+extends GutTest
+
+#var PLAYER : PackedScene = preload("res://entities/player/player.tscn")
+#var SCOREBOARD : PackedScene = preload("res://interfaces/scoreboard/scoreboard.tscn")
+#
+#var _subject : DeathmatchScoringComponent
+#var _scoreboard : Object
+#var _victim : Player
+#var _killer : Player
+
+#func _setup_player(player_id : int) -> Player:
+ #var player : Player = PLAYER.instantiate()
+ #player.name = str(player_id)
+ #add_child(player) # we can't autofree in this test setup
+ #var participant : MatchParticipant = player.match_participant
+ #player.match_participant.player_id = player_id
+ #_scoreboard.add_participant(participant)
+ #_subject.subscribe_player(player)
+ #_subject._players = self
+ #return player
+#
+#func before_each() -> void:
+ #_subject = DeathmatchScoringComponent.new()
+ #_scoreboard = double(SCOREBOARD).instantiate()
+ #stub(_scoreboard, 'add_score_to_player')
+ #stub(_scoreboard, 'increment_kill_count')
+ #_subject._scoreboard = _scoreboard
+ #_victim = _setup_player(0)
+ #_killer = _setup_player(1)
+#
+#func after_each() -> void:
+ #_subject.unsubscribe_player(_victim)
+ #_subject.unsubscribe_player(_killer)
+ #_victim.free()
+ #_killer.free()
+ #_subject.free()
+#
+#func _kill_player(victim : Player, killer : Player) -> void:
+ #victim.died.emit(victim, killer.match_participant.player_id)
+#
+#func test_player_gets_no_score_for_self_kill() -> void:
+ #_kill_player(_victim, _victim)
+ #assert_not_called(_scoreboard, 'add_score_to_player')
+ #assert_not_called(_scoreboard, 'increment_kill_count')
+#
+#func test_killer_gets_score_after_kill() -> void:
+ #_kill_player(_victim, _killer)
+ #assert_called(_scoreboard, 'add_score_to_player', [_killer.match_participant, _subject.ON_KILL_SCORE])
+ #assert_called(_scoreboard, 'increment_kill_count', [_killer.match_participant])
+#
+#func test_killer_leaves_before_kill() -> void:
+ #remove_child(_killer)
+ #_kill_player(_victim, _killer)
+ #assert_not_called(_scoreboard, 'add_score_to_player')
+ #assert_not_called(_scoreboard, 'increment_kill_count')
diff --git a/tests/test_teams.gd b/tests/test_teams.gd
new file mode 100644
index 0000000..0144135
--- /dev/null
+++ b/tests/test_teams.gd
@@ -0,0 +1,67 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+extends GutTest
+
+var _teams : Teams
+
+func before_each() -> void:
+ _teams = Teams.new()
+ autofree(_teams)
+
+func test_size() -> void:
+ assert_eq(_teams.size(), 0)
+
+func test_set_team() -> void:
+ var team_name : String = "team0"
+ _teams[team_name] = Team.new(team_name)
+ assert_eq(_teams.size(), 1)
+
+func test_get_team() -> void:
+ test_set_team()
+ var team0 : Team = _teams["team0"]
+ assert_not_null(team0)
+
+func test_add_team() -> void:
+ _teams.add_team("team0")
+ assert_eq(_teams.size(), 1)
+
+func test_erase_team() -> void:
+ test_add_team()
+ _teams.erase("team0")
+ assert_eq(_teams.size(), 0)
+
+func test_add_teams() -> void:
+ _teams.add_teams(["team0", "team1"])
+ assert_eq(_teams.size(), 2)
+
+# Team
+
+func test_team_size() -> void:
+ test_add_team()
+ var team : Team = _teams["team0"]
+ assert_eq(team.size(), 0)
+
+func test_team_signals() -> void:
+ test_add_team()
+ var team : Team = _teams["team0"]
+ watch_signals(team)
+ var player : Player = Player.new()
+ team.add(player.peer_id, player.username)
+ assert_signal_emitted(team, "player_added")
+ assert_eq(team.size(), 1)
+ team.erase(player.peer_id)
+ assert_signal_emitted(team, "player_erased")
+ assert_eq(team.size(), 0)
+ player.free()
diff --git a/tests/test_teams_scoreboard.gd b/tests/test_teams_scoreboard.gd
new file mode 100644
index 0000000..8e4bc63
--- /dev/null
+++ b/tests/test_teams_scoreboard.gd
@@ -0,0 +1,93 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+extends GutTest
+
+const SCOREBOARD : PackedScene = preload("res://interfaces/scoreboard/scoreboard.tscn")
+
+var _teams : Teams
+var _scoreboard : Scoreboard
+
+func before_each() -> void:
+ _teams = Teams.new()
+ _scoreboard = SCOREBOARD.instantiate()
+ autofree(_teams)
+ add_child_autoqfree(_scoreboard)
+ _teams.team_added.connect(_on_team_added)
+ _teams.team_erased.connect(_on_team_erased)
+ watch_signals(_teams)
+ watch_signals(_scoreboard)
+
+func _on_team_added(team_name: String) -> void:
+ var panel : ScorePanel = _scoreboard.add_panel(team_name)
+ var team : Team = _teams[team_name]
+ team.renamed.connect(panel.title.set_text)
+ team.player_added.connect(_on_team_player_added)
+ team.player_erased.connect(_on_team_player_erased)
+
+func _on_team_player_added(team_name: String, peer_id: int, username: String) -> void:
+ var panel : ScorePanel = _scoreboard.panels.get_node(team_name)
+ panel.add_entry(peer_id, username)
+
+func _on_team_player_erased(team_name: String, peer_id: int) -> void:
+ var panel : ScorePanel = _scoreboard.panels.get_node(team_name)
+ panel.remove_entry_by_peer_id(peer_id)
+
+func _on_team_erased(team_name: String) -> void:
+ var panel : ScorePanel = _scoreboard.panels.get_node(team_name)
+ _scoreboard.remove_panel(panel)
+
+func test_size() -> void:
+ assert_eq(_teams.size(), 0)
+ assert_eq(_scoreboard.size(), 0)
+
+func test_set_team() -> void:
+ var team_name : String = "team0"
+ _teams[team_name] = Team.new(team_name)
+ assert_signal_emitted(_teams, "team_added")
+ assert_eq(_teams.size(), 1)
+ assert_eq(_scoreboard.size(), 1)
+
+func test_get_team() -> void:
+ test_set_team()
+ var team0 : Team = _teams["team0"]
+ assert_not_null(team0)
+
+func test_add_team() -> void:
+ _teams.add_team("team0")
+ assert_signal_emitted(_teams, "team_added")
+ assert_eq(_teams.size(), 1)
+ assert_eq(_scoreboard.size(), 1)
+
+func test_erase_team() -> void:
+ test_add_team()
+ _teams.erase("team0")
+ assert_signal_emitted(_teams, "team_erased")
+ assert_eq(_teams.size(), 0)
+ assert_eq(_scoreboard.size(), 0)
+
+func test_team_signals() -> void:
+ test_add_team()
+ var team : Team = _teams["team0"]
+ watch_signals(team)
+ var player : Player = Player.new()
+ team.add(player.peer_id, player.username)
+ assert_signal_emitted(team, "player_added")
+ assert_eq(team.size(), 1)
+ assert_eq(_scoreboard.get_panel(0).size(), 1)
+ team.erase(player.peer_id)
+ assert_signal_emitted(team, "player_erased")
+ assert_eq(team.size(), 0)
+ assert_eq(_scoreboard.get_panel(0).size(), 0)
+ player.queue_free()
diff --git a/types/multiplayer/match.gd b/types/multiplayer/match.gd
new file mode 100644
index 0000000..ad8c8e4
--- /dev/null
+++ b/types/multiplayer/match.gd
@@ -0,0 +1,50 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+class_name Match extends Node
+
+enum MatchState {
+ READY,
+ RUNNING,
+ PAUSED,
+ ENDED
+}
+
+signal entered_state(state: MatchState)
+signal exiting_state(state: MatchState)
+
+@export var state : MatchState = MatchState.READY:
+ set = set_state
+
+func set_state(new_state: MatchState) -> void:
+ if state == new_state: return
+ exiting_state.emit(state)
+ state = new_state
+ entered_state.emit(state)
+
+func _ready() -> void:
+ pass # Replace with function body.
+
+func start() -> void:
+ state = MatchState.RUNNING
+
+func end() -> void:
+ state = MatchState.ENDED
+
+func pause() -> void:
+ #state = MatchState.PAUSED
+ push_warning("not implemented yet")
+
+func is_ready() -> bool:
+ return state == MatchState.READY
diff --git a/types/multiplayer/multiplayer.gd b/types/multiplayer/multiplayer.gd
new file mode 100644
index 0000000..aded247
--- /dev/null
+++ b/types/multiplayer/multiplayer.gd
@@ -0,0 +1,315 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+## This class defines the multiplayer game type.
+class_name Multiplayer extends Node
+
+signal connected_to_server
+signal connection_failed
+
+## Enumeration that defines supported game modes.
+enum Mode {
+ ## Free-for-all mode where players compete individually.
+ FREE_FOR_ALL,
+ ## Rabbit mode where players chase and protect a designated "rabbit" player.
+ RABBIT,
+ ## Capture the flag mode where teams compete to capture each other's flags.
+ CAPTURE_THE_FLAG,
+ ## Arena mode where players engage in team deathmatch battles.
+ ARENA,
+ ## Ball mode where teams aim to control the ball and score goals.
+ BALL
+}
+
+## The scoreboard to keep track of scores.
+@export var scoreboard : Scoreboard
+## The multiplayer mode.
+@export var mode : Mode = Mode.FREE_FOR_ALL
+## The current map node.
+@export var map : Node: set = set_map
+## The maximum number of clients.
+@export var MAX_CLIENTS := 24
+## The time it takes for a player to respawn when killed, secconds (s).
+@export var VICTIM_RESPAWN_TIME : float = 3.0
+## The total duration of a match, in secconds (s).
+@export var MATCH_DURATION := 1200
+## The total duration of the warmup phase of a match.
+@export var WARMUP_START_DURATION := 25
+
+@export_group("Spawned Scenes")
+@export var _PLAYER : PackedScene
+@export var _FLAG : PackedScene
+
+## This is the match timer.
+var timer:Timer = null
+
+## The [Teams] manager.
+@onready var teams : Teams = $Teams
+## The spawn root for [Player] nodes.
+@onready var players : Node = $Players
+## The spawn root for [Flag] nodes.
+@onready var objectives : Node = $Objectives
+## The spawn root for Map nodes.
+@onready var map_root : Node = $Map
+# The spawn root for the scoreboard node.
+@onready var _scoreboard_spawn_root : Node = $Scoreboard
+## The [Ping] manager.
+@onready var ping : Ping = $Ping
+
+func _unhandled_input(event : InputEvent) -> void:
+ if event.is_action_pressed("exit"):
+ if is_peer_connected():
+ _leave_match.rpc_id(get_multiplayer_authority())
+
+func is_peer_connected() -> bool:
+ if not multiplayer.multiplayer_peer or multiplayer.multiplayer_peer is OfflineMultiplayerPeer:
+ return false
+ return multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED
+
+func set_map(new_map: Node) -> void:
+ map = new_map
+ map_root.add_child(map)
+ MapsManager.current_map = map
+
+## This method starts a server.
+func start_server(port : int, map_scene : PackedScene, _mode: Mode = mode, username : String = "mercury") -> void:
+ # setup enet server peer
+ var peer : ENetMultiplayerPeer = ENetMultiplayerPeer.new()
+ peer.create_server(port, MAX_CLIENTS)
+ multiplayer.peer_connected.connect(_on_peer_connected)
+ multiplayer.peer_disconnected.connect(_on_peer_disconnected)
+ multiplayer.multiplayer_peer = peer
+
+ timer = Timer.new()
+ timer.one_shot = true
+ add_child(timer)
+ map = map_scene.instantiate()
+ mode = _mode
+
+ ping.synchronized.connect(scoreboard._on_ping_sync)
+
+ match mode:
+ Mode.RABBIT:
+ var flag : Flag = _FLAG.instantiate()
+ objectives.add_child(flag)
+ var flagstand : Marker3D = map.get_node("FlagStand")
+ if flagstand:
+ flag.global_position = flagstand.global_position
+
+ teams.team_added.connect(_on_team_added)
+ teams.team_erased.connect(_on_team_erased)
+ teams.add_teams(["rabbit", "chasers"])
+ flag.grabbed.connect(
+ func(carry:FlagCarryComponent) -> void:
+ carry.owner.hud.objective_label.set_visible(true)
+ switch_team(carry.owner.peer_id, "rabbit"))
+ flag.dropped.connect(
+ func(carry:FlagCarryComponent) -> void:
+ carry.owner.hud.objective_label.set_visible(false)
+ switch_team(carry.owner.peer_id, "chasers"))
+
+ _scoreboard_spawn_root.add_child(
+ RabbitScoringComponent.new(scoreboard, flag))
+ _scoreboard_spawn_root.add_child(
+ DeathmatchScoringComponent.new(scoreboard, players))
+
+ players.child_entered_tree.connect(func(_player:Player) -> void:
+ teams["chasers"].add(_player.peer_id, _player.username)
+ _player.damage.connect(_rabbit_damage_handler)
+ )
+
+ Mode.FREE_FOR_ALL:
+ scoreboard.add_panel()
+ _scoreboard_spawn_root.add_child(
+ DeathmatchScoringComponent.new(scoreboard, players))
+ players.child_entered_tree.connect(func(_player:Player) -> void:
+ var panel : ScorePanel = scoreboard.get_panel(0)
+ panel.add_entry(_player.peer_id, _player.username)
+ _player.damage.connect(_ffa_damage_handler)
+ )
+
+ # when a new player is added into tree
+ players.child_entered_tree.connect(func(_player:Player) -> void:
+ # make sure we have enough players to start the match
+ if players.get_child_count() > 1 and timer.is_stopped():
+
+ _start_match()
+ )
+
+ print("mode `%s` loaded" % Mode.keys()[mode])
+
+ if DisplayServer.get_name() != "headless":
+ add_player(1, username)
+
+func _rabbit_damage_handler(source: Node, target: Node, amount: float) -> void:
+ assert(target.find_children("*", "Health"))
+ if source == target or source.team_id != target.team_id:
+ target.health.damage.rpc(amount, source.peer_id)
+
+func _ffa_damage_handler(source: Node, target: Node, amount: float) -> void:
+ assert(target.find_children("*", "Health"))
+ target.health.damage.rpc(amount, source.peer_id)
+
+func _on_peer_connected(peer_id: int) -> void:
+ print("peer `%d` connected" % peer_id)
+
+func _on_peer_disconnected(peer_id : int) -> void:
+ print("peer `%d` disconnected" % peer_id)
+ if players.get_child_count() < 2:
+ # stop timer when there is not enough players
+ timer.stop()
+ var player: Player = players.get_child(0)
+ if player:
+ player.hud.timer_label.text = "Warmup"
+
+func _start_match() -> void:
+ scoreboard.scoring = true
+ timer.start(WARMUP_START_DURATION) # wait few seconds
+ await timer.timeout # wait for the starting countdown to finish
+ for player: Player in players.get_children():
+ if player.has_flag():
+ var flag: Flag = player.flag_carry_component._flag
+ player.flag_carry_component.drop(player.linear_velocity)
+ var flagstand : Marker3D = map.get_node("FlagStand")
+ if flagstand:
+ flag.global_position = flagstand.global_position
+ scoreboard.reset_scores()
+ timer.start(MATCH_DURATION) # restart timer with match duration
+ players.respawn() # respawn everyone
+ if not timer.timeout.is_connected(_on_post_match):
+ timer.timeout.connect(_on_post_match)
+
+func _on_post_match() -> void:
+ # disconnect handler for timer reuse
+ if timer.timeout.is_connected(_on_post_match):
+ timer.timeout.disconnect(_on_post_match)
+ # @TODO: display end of match stats with scoreboard data
+ scoreboard.reset_scores()
+ scoreboard.scoring = false
+ for player: Player in players.get_children():
+ if player.has_flag():
+ var flag: Flag = player.flag_carry_component._flag
+ player.flag_carry_component.drop(player.linear_velocity)
+ var flagstand : Marker3D = map.get_node("FlagStand")
+ if flagstand:
+ flag.global_position = flagstand.global_position
+ # restart match
+ _start_match()
+
+func join_server(host : String, port : int, username : String) -> void:
+ var peer : ENetMultiplayerPeer = ENetMultiplayerPeer.new()
+ peer.create_client(host, port)
+ multiplayer.connected_to_server.connect(_on_connected_to_server.bind(username))
+ multiplayer.connection_failed.connect(_on_connection_failed)
+ multiplayer.server_disconnected.connect(_on_server_disconnected)
+ multiplayer.multiplayer_peer = peer
+
+func _on_server_disconnected() -> void:
+ Game.exit_pressed.emit()
+ queue_free()
+
+func add_player(_peer_id : int, username : String) -> void:
+ var player : Player = _PLAYER.instantiate()
+ # @NOTE: MultiplayerSpawner needs valid names so we can use either
+ # `add_child(node, true)` or `player.name = str(_peer_id)`.
+ # > Unable to auto-spawn node with reserved name: @RigidBody3D@101.
+ # > Make sure to add your replicated scenes via 'add_child(node, true)'
+ # > to produce valid names.
+ player.name = str(_peer_id)
+ player.peer_id = _peer_id
+ player.username = username
+ # @NOTE: player scene requires both params prior being added to the tree
+ players.add_child(player)
+ # @TODO: get spawn location randomly instead of using points
+ player.global_position = MapsManager.get_player_spawn().global_position
+ player.killed.connect(_on_player_killed)
+
+## This method switch a [param peer_id] to the specified team along with its score.
+func switch_team(peer_id: int, to_team_name: String) -> void:
+ var score : Vector3i = scoreboard.get_score(peer_id)
+ teams.switch_team(peer_id, to_team_name)
+ scoreboard.set_score(peer_id, score)
+
+# This method is called when the [member Teams.team_addded] signal is emitted.
+func _on_team_added(team_name: String) -> void:
+ var panel : ScorePanel = scoreboard.add_panel(team_name)
+ panel.title.show()
+ var team : Team = teams[team_name]
+ panel.title.set_text(team_name)
+ team.renamed.connect(panel.title.set_text)
+ team.player_added.connect(_on_team_player_added)
+ team.player_erased.connect(_on_team_player_erased)
+
+# This method is called when the [member Teams.team_erased] signal is emitted.
+func _on_team_erased(team_name: String) -> void:
+ var panel : ScorePanel = scoreboard.panels.get_node(team_name)
+ scoreboard.remove_panel(panel)
+
+# This method is called when the [member Team.player_added] signal is emitted.
+func _on_team_player_added(team_name: String, peer_id: int, username : String = "newblood") -> void:
+ var panel : ScorePanel = scoreboard.panels.get_node(team_name)
+ panel.add_entry(peer_id, username)
+ var player:Player = players.get_node(str(peer_id))
+ player.team_id = teams[team_name].get_instance_id()
+
+# This method is called when the [member Teams.Team.player_erased] signal is emitted.
+func _on_team_player_erased(team_name: String, peer_id: int) -> void:
+ var panel : ScorePanel = scoreboard.panels.get_node(team_name)
+ panel.remove_entry_by_peer_id(peer_id)
+ var player:Player = players.get_node(str(peer_id))
+ player.team_id = -1
+
+func _on_connected_to_server(username : String) -> void:
+ connected_to_server.emit()
+ _join_match.rpc_id(1, username)
+
+func _on_connection_failed() -> void:
+ connection_failed.emit()
+
+func _on_player_killed(victim: Player, _killer:int) -> void:
+ await get_tree().create_timer(VICTIM_RESPAWN_TIME).timeout
+ var spawn: Node3D = MapsManager.get_player_spawn()
+ victim.respawn.rpc_id(1, spawn.global_position)
+
+# This method notifies the server that a player wants to join the match. It
+# takes a single [param username] parameter and is invoked remotely by clients.
+@rpc("any_peer", "call_remote", "reliable")
+func _join_match(username : String) -> void:
+ if multiplayer.is_server():
+ # add player to server with unique id and username
+ add_player(multiplayer.get_remote_sender_id(), username)
+
+@rpc("any_peer", "call_local", "reliable")
+func _leave_match() -> void:
+ if multiplayer.is_server():
+ var peer_id:int = multiplayer.get_remote_sender_id()
+ var player : Player = players.get_node(str(peer_id))
+ if player:
+ var team : Team = teams.get_peer_team(peer_id)
+ if team:
+ team.erase(peer_id)
+ players.remove_child(player)
+ player.queue_free()
+ _cleanup_peer.rpc_id(peer_id)
+
+@rpc("authority", "call_local", "reliable")
+func _cleanup_peer() -> void:
+ multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new()
+ queue_free()
+
+func _exit_tree() -> void:
+ # @NOTE: The `is_multiplayer_authority` method in `_extree` push an error
+ # about the multiplayer instance not being the currently active one.it_
+ # see https://github.com/godotengine/godot/issues/77723
+ multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new()
diff --git a/types/multiplayer/multiplayer.tscn b/types/multiplayer/multiplayer.tscn
new file mode 100644
index 0000000..d121b6d
--- /dev/null
+++ b/types/multiplayer/multiplayer.tscn
@@ -0,0 +1,55 @@
+[gd_scene load_steps=9 format=3 uid="uid://bvwxfgygm2xb8"]
+
+[ext_resource type="Script" path="res://types/multiplayer/multiplayer.gd" id="1_r1kd6"]
+[ext_resource type="Script" path="res://types/multiplayer/ping.gd" id="1_tafn1"]
+[ext_resource type="PackedScene" uid="uid://cbhx1xme0sb7k" path="res://entities/player/player.tscn" id="2_og1vb"]
+[ext_resource type="PackedScene" uid="uid://c88l3h0ph00c7" path="res://entities/flag/flag.tscn" id="3_h0rie"]
+[ext_resource type="PackedScene" uid="uid://b8bosdd0o1lu7" path="res://interfaces/scoreboard/scoreboard.tscn" id="4_rhwwf"]
+[ext_resource type="Script" path="res://types/multiplayer/teams.gd" id="5_op0if"]
+[ext_resource type="Script" path="res://types/multiplayer/players.gd" id="7_ycqj1"]
+
+[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_g11jg"]
+properties/0/path = NodePath(".:mode")
+properties/0/spawn = true
+properties/0/replication_mode = 2
+
+[node name="Multiplayer" type="Node" node_paths=PackedStringArray("scoreboard")]
+script = ExtResource("1_r1kd6")
+scoreboard = NodePath("Scoreboard/Scoreboard")
+_PLAYER = ExtResource("2_og1vb")
+_FLAG = ExtResource("3_h0rie")
+
+[node name="MultiplayerSync" type="MultiplayerSynchronizer" parent="."]
+replication_config = SubResource("SceneReplicationConfig_g11jg")
+
+[node name="Ping" type="Node" parent="."]
+script = ExtResource("1_tafn1")
+active = true
+sync_interval = 0.5
+
+[node name="Teams" type="Node" parent="."]
+script = ExtResource("5_op0if")
+
+[node name="Scoreboard" type="Node" parent="."]
+
+[node name="Scoreboard" parent="Scoreboard" instance=ExtResource("4_rhwwf")]
+
+[node name="Map" type="Node" parent="."]
+
+[node name="MapSpawner" type="MultiplayerSpawner" parent="."]
+_spawnable_scenes = PackedStringArray("res://maps/desert/desert.tscn")
+spawn_path = NodePath("../Map")
+spawn_limit = 1
+
+[node name="Players" type="Node" parent="."]
+script = ExtResource("7_ycqj1")
+
+[node name="PlayerSpawner" type="MultiplayerSpawner" parent="."]
+_spawnable_scenes = PackedStringArray("res://entities/player/player.tscn")
+spawn_path = NodePath("../Players")
+
+[node name="Objectives" type="Node" parent="."]
+
+[node name="ObjectiveSpawner" type="MultiplayerSpawner" parent="."]
+_spawnable_scenes = PackedStringArray("res://entities/flag/flag.tscn")
+spawn_path = NodePath("../Objectives")
diff --git a/types/multiplayer/ping.gd b/types/multiplayer/ping.gd
new file mode 100644
index 0000000..70f1a86
--- /dev/null
+++ b/types/multiplayer/ping.gd
@@ -0,0 +1,173 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+## This defines a ping manager.
+##
+## When added to your scene and connect the [member synchronized] signal to your
+## custom handler in order to receive ping updates on a regular basis (as
+## defined by [param ping_interval] and [param sync_interval]) for each peer.
+## [codeblock]
+## func _ready() -> void:
+## $Ping.synchronized.connect(_on_ping_sync)
+##
+## func _on_ping_sync(state: Dictionary) -> void:
+## for peer_id in state:
+## print("peer `%s` latency is %d ms" % [peer_id, state[peer_id]])
+## [/codeblock]
+class_name Ping extends Node
+
+## This class defines a ping information to keep track of ping results
+## and provide average ping.
+class PingInfo:
+ const SIZE := 21
+ var samples := PackedInt32Array()
+ var pos := 0
+
+ func _init() -> void:
+ samples.resize(SIZE)
+ for i in range(SIZE):
+ samples[i] = 0
+
+ func get_average() -> int:
+ var value := 0
+ for sample in samples:
+ value += sample
+ @warning_ignore("integer_division")
+ return value / SIZE if value > 0 else 0
+
+ func set_next(value: int) -> void:
+ samples[pos] = value
+ pos = (pos + 1) % SIZE
+
+## Emitted when the state is synchronized.
+signal synchronized(state : Dictionary)
+
+## Controls activation state of the ping feature.
+@export var active: bool = false
+## The time interval (in milliseconds) between ping requests sent by the server to measure client latency.
+@export_range(0.,1.,.01) var ping_interval: float = .2 # ms
+## The time interval (in milliseconds) between state synchronization updates sent to connected clients.
+@export_range(0.,1.,.01) var sync_interval: float = 1. # ms
+## The size of the ping history buffer used to store past ping times for each client.
+@export var ping_history: int = 10:
+ set = set_ping_history
+
+# internal variables
+var _clients := {}
+var _state := {}
+var _last_ping := 0
+var _pings := []
+var _last_sync := 0
+
+func _init() -> void:
+ clear_pings()
+
+func _ready() -> void:
+ multiplayer.peer_connected.connect(_add_peer)
+ multiplayer.peer_disconnected.connect(_del_peer)
+
+func _exit_tree() -> void:
+ multiplayer.peer_connected.disconnect(_add_peer)
+ multiplayer.peer_disconnected.disconnect(_del_peer)
+
+# synchronize state only on server
+func _process(_delta: float) -> void:
+ if not active \
+ or multiplayer.multiplayer_peer is OfflineMultiplayerPeer \
+ or multiplayer.multiplayer_peer.get_connection_status() \
+ != MultiplayerPeer.CONNECTION_CONNECTED \
+ or not multiplayer.is_server() \
+ or not multiplayer.has_multiplayer_peer():
+ return
+ # sync state at regular intervals
+ var now := Time.get_ticks_msec()
+ if sync_interval * 1000 > 0 and now >= _last_sync + sync_interval * 1000:
+ synchronize()
+ _last_sync = now
+ # check and update ping intervals
+ if now >= _pings[_last_ping] + ping_interval * 1000:
+ _last_ping = (_last_ping + 1) % ping_history
+ _pings[_last_ping] = now
+ _ping.rpc(now)
+
+## Clears the ping history array.
+func clear_pings() -> void:
+ _last_ping = 0
+ _pings.resize(ping_history)
+ _pings.fill(0)
+
+## Sets the size of the ping history buffer and clears existing ping data.
+func set_ping_history(value: int) -> void:
+ if value < 1:
+ return
+ ping_history = value
+ clear_pings()
+
+## Adds a new peer to the registry
+func _add_peer(peer_id: int) -> void:
+ _clients[peer_id] = PingInfo.new()
+
+## Deletes a peer from the registry
+func _del_peer(peer_id: int) -> void:
+ _clients.erase(peer_id)
+
+@rpc("unreliable")
+func _ping(time: int) -> void:
+ _pong.rpc_id(1, time)
+
+@rpc("any_peer")
+func _pong(time: int) -> void:
+ if not multiplayer.is_server():
+ return
+ # get id of the peer sending the pong
+ var peer_id: int = multiplayer.get_remote_sender_id()
+ # check if peer exists in the registered clients dictionary
+ if not _clients.has(peer_id):
+ return
+ # init variables
+ var now := Time.get_ticks_msec()
+ var last := (_last_ping + 1) % ping_history
+ var found := ping_history * ping_interval * 1000
+ # search related ping in history
+ for i in range(ping_history):
+ # check if current ping matches received pong
+ if time == _pings[last]:
+ found = _pings[last]
+ break
+ # move to next ping in history
+ last = (last + 1) % ping_history
+ # compute round-trip time (RTT) and update ping info for this peer
+ _clients[peer_id].set_next((now - found) / 2)
+
+## This function sends the current state of ping information to all connected peers
+## and emits a signal indicating that the state has been synchronized.
+func synchronize() -> void:
+ # check if server is active and has multiplayer peers connected
+ if not active or not multiplayer.has_multiplayer_peer() or not multiplayer.is_server():
+ return
+ # clear current state
+ _state.clear()
+ # iterate over registered clients
+ for peer_id:int in _clients:
+ # set client average ping state
+ _state[peer_id] = _clients[peer_id].get_average()
+ # send state to connected peers
+ _synchronize.rpc(_state)
+ # emit signal to indicate that synchronization has occured
+ synchronized.emit(_state)
+
+@rpc("call_local", "unreliable")
+func _synchronize(state: Dictionary) -> void:
+ _state = state
+ synchronized.emit(_state)
diff --git a/types/multiplayer/players.gd b/types/multiplayer/players.gd
new file mode 100644
index 0000000..b38f223
--- /dev/null
+++ b/types/multiplayer/players.gd
@@ -0,0 +1,21 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+extends Node
+
+func respawn() -> void:
+ for player : Player in get_children():
+ var spawn : Node3D = MapsManager.get_player_spawn()
+ if spawn:
+ player.respawn.rpc_id(get_multiplayer_authority(), spawn.global_position)
diff --git a/types/multiplayer/team.gd b/types/multiplayer/team.gd
new file mode 100644
index 0000000..81f45ec
--- /dev/null
+++ b/types/multiplayer/team.gd
@@ -0,0 +1,85 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+## This class defines a Team.
+class_name Team extends RefCounted
+
+## Emitted when the [Team] is renamed.
+signal renamed(team_name: String)
+
+## Emitted when a peer_id is added to the [Team].
+signal player_added(team_name: String, peer_id: int, username: String)
+
+## Emitted when a peer_id is erased from the [Team].
+signal player_erased(team_name: String, peer_id: int)
+
+var name : String:
+ set(new_name):
+ name = new_name
+ renamed.emit(name)
+
+var players: Dictionary = {}
+
+## Constructor.
+func _init(team_name: String, team_players: Dictionary = {}) -> void:
+ name = team_name
+ for peer_id : int in team_players:
+ add(peer_id, team_players[peer_id])
+
+# This is the iterator index cursor.
+var _iter_cursor : int = 0
+
+# This method is an iterator initializer.
+func _iter_init(_arg : Variant) -> bool:
+ _iter_cursor = 0 # reset
+ print(_iter_cursor < players.size())
+ return _iter_cursor < players.size()
+
+# This method checks if the iterator has a next value.
+func _iter_next(_arg : Variant) -> bool:
+ _iter_cursor += 1
+ return _iter_cursor < players.size()
+
+# This method gets the next iterator value.
+func _iter_get(_arg : Variant) -> int:
+ return players.keys()[_iter_cursor]
+
+## Add [param peer_id] to the [Team] if not already present.
+func add(peer_id: int, username : String = "") -> void:
+ if peer_id not in players:
+ players[peer_id] = username
+ player_added.emit(name, peer_id, username)
+
+## Erase [param peer_id] from the [Team].
+func erase(peer_id: int) -> bool:
+ if players.erase(peer_id):
+ player_erased.emit(name, peer_id)
+ return true
+ return false
+
+## This method pops out the related [param peer_id] string.
+func pop(peer_id: int) -> String:
+ var value : String = players[peer_id]
+ erase(peer_id)
+ return value
+
+## Returns the size of the team.
+func size() -> int:
+ return players.size()
+
+func serialize() -> Variant:
+ return [name, players]
+
+func _to_string() -> String:
+ return "" % [name, get_instance_id()]
diff --git a/types/multiplayer/teams.gd b/types/multiplayer/teams.gd
new file mode 100644
index 0000000..fd00886
--- /dev/null
+++ b/types/multiplayer/teams.gd
@@ -0,0 +1,144 @@
+# This file is part of open-fpsz.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+## This class defines a Teams manager
+class_name Teams extends Node
+
+## Emitted when a [Team] is created.
+signal team_added(team_name : String)
+
+## Emitted when a [Team] is removed.
+signal team_erased(team_name : String)
+
+## Emitted when a [Player] is added to a [Team].
+signal player_added(team_name: String, peer_id: int, username: String)
+
+## Emitted when a [Player] is removed from a [Team].
+signal player_erased(team_name: String, peer_id: int)
+
+# The synced teams state.
+@export var _state : Dictionary = {}:
+ get = get_state, set = set_state
+
+func get_state() -> Dictionary:
+ for team : Team in _teams.values():
+ _state[team.name] = team.players
+ return _state
+
+func set_state(new_state: Dictionary) -> void:
+ _state = new_state
+
+# The internal dictionary of teams.
+var _teams : Dictionary = {}
+
+## Retrieves an [Array] of [int] by [param team_name].
+## [codeblock]
+## var teams := Teams.new()
+## teams.add_team("Phoenix")
+## print(teams["Phoenix"]) # this calls `_get`
+## [/codeblock]
+func _get(team_name : StringName) -> Variant:
+ return _teams.get(team_name)
+
+## Sets a [Team] by [param name] with an optional [param default].
+## [codeblock]
+## var teams := Teams.new()
+## teams["Phoenix"] = Teams.Team.new("Phoenix") # this calls `_set`
+## teams["Phoenix"] = null # refcount for Team is now 0
+## [/codeblock]
+func _set(key : StringName, team : Variant) -> bool:
+ if team == null:
+ return erase(key)
+ if team is Team:
+ _teams[key] = team
+ team_added.emit(key)
+ return true
+ return false
+
+## This method retrieves the array of managed [Team] names.
+func keys() -> Array:
+ return _teams.keys()
+
+## This method retrieves the array of managed [Team].
+func values() -> Array:
+ return _teams.values()
+
+# This is the iterator index cursor.
+var _iter_cursor : int = 0
+
+# This method is an iterator initializer.
+func _iter_init(_arg : Variant) -> bool:
+ _iter_cursor = 0 # reset
+ return _iter_cursor < len(_teams)
+
+# This method checks if the iterator has a next value.
+func _iter_next(_arg : Variant) -> bool:
+ _iter_cursor += 1
+ return _iter_cursor < len(_teams)
+
+# This method gets the next iterator value.
+func _iter_get(_arg : Variant) -> Team:
+ return _teams.values()[_iter_cursor]
+
+## This method adds a new [Team] into the manager.
+func add_team(key: String) -> bool:
+ if not _teams.has(key):
+ var team : Team = Team.new(key)
+ team.player_added.connect(_on_team_player_added)
+ team.player_erased.connect(_on_team_player_erased)
+ return _set(key, team)
+ return false
+
+## This method adds new [Team] in batch from [param team_names].
+func add_teams(team_names: Array[String]) -> Node:
+ for team_name in team_names:
+ add_team(team_name)
+ return self
+
+func get_peer_team(peer_id : int) -> Team:
+ for team : Team in _teams.values():
+ if peer_id in team.players:
+ return team
+ return null
+
+func erase(team_name: String) -> bool:
+ var res: bool = _teams.erase(team_name)
+ if res:
+ team_erased.emit(team_name)
+ return res
+
+func erase_player(peer_id: int) -> void:
+ for team : Team in _teams.values():
+ if team.erase(peer_id):
+ break
+
+## The number of [Team].
+func size() -> int:
+ return len(_teams)
+
+## This method eases peer team switching by moving them to the specified [param team_name].
+## It does not do anything if the peer is not already added to a [Team], see [method Team.add].
+func switch_team(peer_id: int, team_name: String) -> void:
+ var lhs : Team = get_peer_team(peer_id)
+ var rhs : Team = _get(team_name)
+ if lhs and rhs:
+ rhs.add(peer_id, lhs.pop(peer_id))
+
+# This method runs when a Team emits `player_added` signal
+func _on_team_player_added(team_name: String, peer_id: int, username: String) -> void:
+ player_added.emit(team_name, peer_id, username) # emit manager signal
+
+# This method runs when a Team emits `player_erased` signal
+func _on_team_player_erased(team_name: String, peer_id: int) -> void:
+ player_erased.emit(team_name, peer_id) # emit manager signal
diff --git a/types/resources/array_packed_scene.gd b/types/resources/array_packed_scene.gd
deleted file mode 100644
index cf2a2ee..0000000
--- a/types/resources/array_packed_scene.gd
+++ /dev/null
@@ -1,6 +0,0 @@
-class_name ArrayPackedSceneResource extends Resource
-
-@export var _packed_scenes : Array[PackedScene] = []
-
-func get_items() -> Array[PackedScene]:
- return _packed_scenes
diff --git a/modes/singleplayer/demo.gd b/types/singleplayer/demo.gd
similarity index 56%
rename from modes/singleplayer/demo.gd
rename to types/singleplayer/demo.gd
index 5a23b69..b96744f 100644
--- a/modes/singleplayer/demo.gd
+++ b/types/singleplayer/demo.gd
@@ -14,26 +14,22 @@
# along with this program. If not, see .
class_name Singleplayer extends Node
-@export var FLAG : PackedScene
-
-@onready var player_node : Player = $Player
+@onready var player : Player = $Player
+@onready var flag : Flag = $Flag
+@onready var map : Node = $Desert
func _ready() -> void:
- player_node.died.connect(respawn_player)
- player_node.match_participant.player_id = 1
- MapsManager.current_map = $Desert
- _add_flag()
-
-func respawn_player(player : Player, _killer_id : int) -> void:
- var spawn : Marker3D = MapsManager.get_player_spawn()
- if spawn:
- player.respawn(spawn.position)
- else:
- push_error("MapsManager.get_player_spawn could not find a spawn marker")
-
-func _add_flag() -> void:
- var flag : Flag = FLAG.instantiate()
- add_child(flag)
- var flagstand : Marker3D = MapsManager.current_map.get_node("FlagStand")
+ player.health.exhausted.connect(_on_player_dead)
+ var flagstand : Marker3D = map.get_node("FlagStand")
if flagstand:
flag.global_position = flagstand.position
+
+func _unhandled_input(event: InputEvent) -> void:
+ if event.is_action_pressed("exit"):
+ queue_free()
+
+func _on_player_dead(_player : Player) -> void:
+ var spawns : Node = map.get_node("PlayerSpawns")
+ var spawn : Marker3D = spawns.get_child(
+ randi_range(0, spawns.get_child_count() - 1))
+ player.respawn(spawn.global_position)
diff --git a/modes/singleplayer/demo.tscn b/types/singleplayer/demo.tscn
similarity index 70%
rename from modes/singleplayer/demo.tscn
rename to types/singleplayer/demo.tscn
index f5b73c5..42e61eb 100644
--- a/modes/singleplayer/demo.tscn
+++ b/types/singleplayer/demo.tscn
@@ -1,6 +1,6 @@
[gd_scene load_steps=7 format=3 uid="uid://boviiugcnfyrj"]
-[ext_resource type="Script" path="res://modes/singleplayer/demo.gd" id="1_kkjqs"]
+[ext_resource type="Script" path="res://types/singleplayer/demo.gd" id="1_kkjqs"]
[ext_resource type="PackedScene" uid="uid://cbhx1xme0sb7k" path="res://entities/player/player.tscn" id="2_6wbjq"]
[ext_resource type="PackedScene" uid="uid://c88l3h0ph00c7" path="res://entities/flag/flag.tscn" id="4_1j2pw"]
[ext_resource type="PackedScene" uid="uid://btlkog4b87p4x" path="res://maps/desert/desert.tscn" id="4_dogmv"]
@@ -13,13 +13,15 @@ absorbent = true
[node name="Demo" type="Node"]
script = ExtResource("1_kkjqs")
-FLAG = ExtResource("4_1j2pw")
[node name="Player" parent="." instance=ExtResource("2_6wbjq")]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 589.786, 209.119, 853.632)
+transform = Transform3D(-0.000506087, 0, -1, 0, 1, 0, 1, 0, -0.000506087, 612.497, 211.292, 853.632)
physics_material_override = SubResource("PhysicsMaterial_c5jqv")
[node name="Desert" parent="." instance=ExtResource("4_dogmv")]
-transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.0566101, 1456.67, 0.597462)
[node name="DummyTarget" parent="." instance=ExtResource("5_euor2")]
+transform = Transform3D(-0.997996, 0, -0.0632782, 0, 1, 0, 0.0632782, 0, -0.997996, 901.962, 145.367, 882.65)
+
+[node name="Flag" parent="." instance=ExtResource("4_1j2pw")]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 895.356, 145.367, 888.261)