diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..d2c0a84
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,13 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Rider ignored files
+/contentModel.xml
+/modules.xml
+/projectSettingsUpdater.xml
+/.idea.tgcc.iml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/encodings.xml b/.idea/encodings.xml
new file mode 100644
index 0000000..df87cf9
--- /dev/null
+++ b/.idea/encodings.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/.idea/indexLayout.xml b/.idea/indexLayout.xml
new file mode 100644
index 0000000..7b08163
--- /dev/null
+++ b/.idea/indexLayout.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/ai/agents/base/ai_base.gd b/core/ai/agents/base/ai_base.gd
new file mode 100644
index 0000000..96dcc0d
--- /dev/null
+++ b/core/ai/agents/base/ai_base.gd
@@ -0,0 +1,51 @@
+extends CharacterBody3D
+
+class_name AIBase
+
+@export var speed: float = 4.0
+var _enable_state_machine: bool = true
+@export var enable_state_machine: bool:
+ set(value):
+ _enable_state_machine = value
+ toggle_enable_state_machine()
+ get:
+ return _enable_state_machine
+
+
+@onready var patrol_radius_shape: CollisionShape3D = $%PatrolRadiusShape
+@onready var nav_agent: NavigationAgent3D = $%NavigationAgent3D
+@onready var state_machine: StateMachine = $%StateMachine
+
+
+func _ready() -> void:
+ randomize()
+ toggle_enable_state_machine()
+
+func toggle_enable_state_machine() -> void:
+ state_machine.enable = _enable_state_machine
+
+func _physics_process(delta: float) -> void:
+ if !_enable_state_machine:
+ return
+
+ state_machine.physics_process(delta)
+
+ if nav_agent.is_navigation_finished():
+ velocity = Vector3.ZERO
+ move_and_slide()
+ return
+
+ var next_point = nav_agent.get_next_path_position()
+ var direction = global_position.direction_to(next_point)
+
+ velocity.x = direction.x * speed
+ velocity.z = direction.z * speed
+
+ move_and_slide()
+
+func navigate_to_random_point() -> void:
+ var nav_map_rid = get_world_3d().get_navigation_map()
+ var patrol_radius = patrol_radius_shape.shape.radius
+ var target_point_in_space = global_position + Vector3(randf_range(-1, 1), 0, randf_range(-1, 1)).normalized() * randf_range(0, patrol_radius)
+ var closest_valid_point = NavigationServer3D.map_get_closest_point(nav_map_rid, target_point_in_space)
+ nav_agent.target_position = closest_valid_point
diff --git a/core/ai/agents/base/ai_base.gd.uid b/core/ai/agents/base/ai_base.gd.uid
new file mode 100644
index 0000000..ea72f6d
--- /dev/null
+++ b/core/ai/agents/base/ai_base.gd.uid
@@ -0,0 +1 @@
+uid://b30p1yqojbbbk
diff --git a/core/ai/agents/base/ai_base.tscn b/core/ai/agents/base/ai_base.tscn
new file mode 100644
index 0000000..2143094
--- /dev/null
+++ b/core/ai/agents/base/ai_base.tscn
@@ -0,0 +1,44 @@
+[gd_scene format=3 uid="uid://clx701xdwelgx"]
+
+[ext_resource type="Script" uid="uid://b30p1yqojbbbk" path="res://core/ai/agents/base/ai_base.gd" id="1_4d1nn"]
+[ext_resource type="Script" uid="uid://ps3vlu2qvmop" path="res://core/ai/framework/state_machine.gd" id="2_q1hg3"]
+[ext_resource type="Script" uid="uid://nga8qx56iwgu" path="res://core/ai/agents/base/idle_state.gd" id="3_26faq"]
+[ext_resource type="Script" uid="uid://bngfthvt04ivv" path="res://core/ai/agents/base/patrol_state.gd" id="4_lim44"]
+
+[sub_resource type="CapsuleMesh" id="CapsuleMesh_mh3lg"]
+
+[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_mh3lg"]
+
+[sub_resource type="SphereShape3D" id="SphereShape3D_lim44"]
+radius = 20.0
+
+[node name="AiBase" type="CharacterBody3D" unique_id=1228675528]
+script = ExtResource("1_4d1nn")
+
+[node name="MeshInstance3D" type="MeshInstance3D" parent="." unique_id=214482161]
+mesh = SubResource("CapsuleMesh_mh3lg")
+
+[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=92811823]
+shape = SubResource("CapsuleShape3D_mh3lg")
+
+[node name="NavigationAgent3D" type="NavigationAgent3D" parent="." unique_id=1370024449]
+unique_name_in_owner = true
+
+[node name="StateMachine" type="Node" parent="." unique_id=1286404264 node_paths=PackedStringArray("initial_state")]
+unique_name_in_owner = true
+script = ExtResource("2_q1hg3")
+initial_state = NodePath("IdleState")
+
+[node name="IdleState" type="Node" parent="StateMachine" unique_id=763668255]
+script = ExtResource("3_26faq")
+state_id = &"idle"
+
+[node name="PatrolState" type="Node" parent="StateMachine" unique_id=2054606629]
+script = ExtResource("4_lim44")
+state_id = &"patrol"
+
+[node name="PatrolRadiusShape" type="CollisionShape3D" parent="." unique_id=1379515938]
+unique_name_in_owner = true
+shape = SubResource("SphereShape3D_lim44")
+disabled = true
+debug_color = Color(1, 1, 0, 1)
diff --git a/core/ai/agents/base/idle_state.gd b/core/ai/agents/base/idle_state.gd
new file mode 100644
index 0000000..8e5062b
--- /dev/null
+++ b/core/ai/agents/base/idle_state.gd
@@ -0,0 +1,26 @@
+extends State
+
+const PATROL_STATE_ID: StringName = &"patrol"
+
+@export var wait_time: float = 3.0
+
+var runtime_timer: Timer
+
+func enter() -> void:
+ runtime_timer = Timer.new()
+ runtime_timer.name = "IdleTimer"
+ add_child(runtime_timer)
+ runtime_timer.one_shot = true
+ runtime_timer.timeout.connect(_on_timer_timeout)
+ runtime_timer.start(wait_time)
+
+func exit() -> void:
+ if runtime_timer:
+ if runtime_timer.is_connected("timeout", _on_timer_timeout):
+ runtime_timer.timeout.disconnect(_on_timer_timeout)
+
+ runtime_timer.queue_free()
+ runtime_timer = null
+
+func _on_timer_timeout() -> void:
+ transitioned.emit(self, PATROL_STATE_ID)
diff --git a/core/ai/agents/base/idle_state.gd.uid b/core/ai/agents/base/idle_state.gd.uid
new file mode 100644
index 0000000..41fe475
--- /dev/null
+++ b/core/ai/agents/base/idle_state.gd.uid
@@ -0,0 +1 @@
+uid://nga8qx56iwgu
diff --git a/core/ai/agents/base/patrol_state.gd b/core/ai/agents/base/patrol_state.gd
new file mode 100644
index 0000000..0a1fdd7
--- /dev/null
+++ b/core/ai/agents/base/patrol_state.gd
@@ -0,0 +1,12 @@
+extends State
+
+const IDLE_STATE_ID: StringName = &"idle"
+
+@onready var agent: AIBase = owner
+
+func enter() -> void:
+ agent.navigate_to_random_point()
+
+func physics_update(_delta) -> void:
+ if agent.nav_agent.is_navigation_finished():
+ transitioned.emit(self, IDLE_STATE_ID)
diff --git a/core/ai/agents/base/patrol_state.gd.uid b/core/ai/agents/base/patrol_state.gd.uid
new file mode 100644
index 0000000..4bdcccd
--- /dev/null
+++ b/core/ai/agents/base/patrol_state.gd.uid
@@ -0,0 +1 @@
+uid://bngfthvt04ivv
diff --git a/core/ai/framework/state.gd b/core/ai/framework/state.gd
new file mode 100644
index 0000000..b24ae72
--- /dev/null
+++ b/core/ai/framework/state.gd
@@ -0,0 +1,20 @@
+extends Node
+
+class_name State
+
+@export var state_id: StringName = &""
+
+@warning_ignore("unused_signal")
+signal transitioned(state: State, new_state_id: StringName)
+
+func enter() -> void:
+ pass
+
+func exit() -> void:
+ pass
+
+func update(_delta) -> void:
+ pass
+
+func physics_update(_delta) -> void:
+ pass
diff --git a/core/ai/framework/state.gd.uid b/core/ai/framework/state.gd.uid
new file mode 100644
index 0000000..7f2faac
--- /dev/null
+++ b/core/ai/framework/state.gd.uid
@@ -0,0 +1 @@
+uid://dpso7abi5ftyw
diff --git a/core/ai/framework/state_machine.gd b/core/ai/framework/state_machine.gd
new file mode 100644
index 0000000..0a91f3d
--- /dev/null
+++ b/core/ai/framework/state_machine.gd
@@ -0,0 +1,61 @@
+extends Node
+
+class_name StateMachine
+
+@export var initial_state: State
+
+var current_state: State
+var states: Dictionary[StringName, State] = {}
+var is_initialized: bool = false
+var _enabled: bool = true
+var enable: bool:
+ set(value):
+ _enabled = value
+ _set_enabled(value)
+ get:
+ return _enabled
+
+func _ready() -> void:
+ _initialize_states()
+ _set_enabled(enable)
+
+func _initialize_states() -> void:
+ if is_initialized:
+ return
+
+ for state in get_children():
+ if state is State:
+ states[state.state_id] = state
+ state.transitioned.connect(_on_state_transition)
+
+ is_initialized = true
+
+func _set_enabled(value: bool) -> void:
+ _enabled = value
+
+ if not _enabled:
+ return
+
+ _initialize_states()
+
+ if initial_state and current_state == null:
+ current_state = initial_state
+ current_state.enter()
+
+func physics_process(delta) -> void:
+ if current_state and _enabled:
+ current_state.physics_update(delta)
+
+func _on_state_transition(state: State, new_state_id: StringName) -> void:
+ if state != current_state:
+ return
+
+ var new_state = states.get(new_state_id)
+ if not new_state:
+ return
+
+ if current_state:
+ current_state.exit()
+
+ current_state = new_state
+ current_state.enter()
diff --git a/core/ai/framework/state_machine.gd.uid b/core/ai/framework/state_machine.gd.uid
new file mode 100644
index 0000000..fc3d12e
--- /dev/null
+++ b/core/ai/framework/state_machine.gd.uid
@@ -0,0 +1 @@
+uid://ps3vlu2qvmop
diff --git a/core/audio/audio_manager.gd b/core/audio/audio_manager.gd
new file mode 100644
index 0000000..2d0a6f0
--- /dev/null
+++ b/core/audio/audio_manager.gd
@@ -0,0 +1,31 @@
+extends Node
+
+const MIN_VOLUME: float = 0.0
+const MAX_VOLUME: float = 1.0
+
+func _ready() -> void:
+ load_save_data()
+
+func load_save_data() -> void:
+ for bus_name in GameState.save_data.audio_bus_volumes:
+ _apply_audio_bus_volume(bus_name, GameState.save_data.audio_bus_volumes[bus_name])
+
+func set_audio_bus_volume(bus_name: StringName, linear_value: float) -> void:
+ var normalized_bus_name := str(bus_name)
+ var clamped_value := clampf(linear_value, MIN_VOLUME, MAX_VOLUME)
+
+ GameState.save_data.audio_bus_volumes[normalized_bus_name] = clamped_value
+ _apply_audio_bus_volume(normalized_bus_name, clamped_value)
+ GameState.save_game()
+
+func get_audio_bus_volume(bus_name: StringName, default_value: float = 1.0) -> float:
+ return float(GameState.save_data.audio_bus_volumes.get(str(bus_name), default_value))
+
+func _apply_audio_bus_volume(bus_name: String, linear_value: float) -> void:
+ var bus_index := AudioServer.get_bus_index(bus_name)
+ if bus_index == -1:
+ return
+
+ var clamped_value := clampf(linear_value, MIN_VOLUME, MAX_VOLUME)
+ AudioServer.set_bus_volume_db(bus_index, linear_to_db(clamped_value))
+ AudioServer.set_bus_mute(bus_index, is_zero_approx(clamped_value))
diff --git a/core/audio/audio_manager.gd.uid b/core/audio/audio_manager.gd.uid
new file mode 100644
index 0000000..5d5a0c2
--- /dev/null
+++ b/core/audio/audio_manager.gd.uid
@@ -0,0 +1 @@
+uid://dcttbbavtwtsg
diff --git a/core/audio/settings/audio_option.gd b/core/audio/settings/audio_option.gd
new file mode 100644
index 0000000..1c386ff
--- /dev/null
+++ b/core/audio/settings/audio_option.gd
@@ -0,0 +1,56 @@
+extends VBoxContainer
+
+class_name AudioOption
+
+const MIN_VOLUME: float = 0.0
+const MAX_VOLUME: float = 1.0
+
+@export var bus_name: StringName = &"Master"
+@export var label_text: String = "Master Volume"
+@export_range(0.0, 1.0, 0.01) var default_volume: float = 1.0
+
+@onready var label: Label = $Label
+@onready var slider: HSlider = $HSlider
+
+func _ready() -> void:
+ slider.min_value = MIN_VOLUME
+ slider.max_value = MAX_VOLUME
+ slider.step = 0.01
+ slider.value_changed.connect(_on_slider_value_changed)
+ refresh()
+
+func refresh() -> void:
+ label.text = label_text
+
+ if not has_bus():
+ slider.set_value_no_signal(default_volume)
+ slider.editable = false
+ label.text = "%s (bus missing)" % label_text
+ return
+
+ slider.editable = true
+ var saved_volume := AudioManager.get_audio_bus_volume(bus_name, default_volume)
+ slider.set_value_no_signal(saved_volume)
+ apply_current_value()
+
+func reset_to_default() -> void:
+ slider.value = default_volume
+
+func apply_current_value() -> void:
+ _set_bus_volume(slider.value)
+
+func has_bus() -> bool:
+ return get_bus_index() != -1
+
+func get_bus_index() -> int:
+ return AudioServer.get_bus_index(bus_name)
+
+func _set_bus_volume(linear_value: float) -> void:
+ if not has_bus():
+ return
+
+ var clamped_value := clampf(linear_value, MIN_VOLUME, MAX_VOLUME)
+ AudioManager.set_audio_bus_volume(bus_name, clamped_value)
+
+func _on_slider_value_changed(value: float) -> void:
+ _set_bus_volume(value)
diff --git a/core/audio/settings/audio_option.gd.uid b/core/audio/settings/audio_option.gd.uid
new file mode 100644
index 0000000..97b1806
--- /dev/null
+++ b/core/audio/settings/audio_option.gd.uid
@@ -0,0 +1 @@
+uid://bnh1y560h55vc
diff --git a/core/audio/settings/audio_option.tscn b/core/audio/settings/audio_option.tscn
new file mode 100644
index 0000000..c339962
--- /dev/null
+++ b/core/audio/settings/audio_option.tscn
@@ -0,0 +1,14 @@
+[gd_scene format=3 uid="uid://cescrovsjlwke"]
+
+[ext_resource type="Script" uid="uid://bnh1y560h55vc" path="res://core/audio/settings/audio_option.gd" id="1_orafl"]
+
+[node name="AudioOption" type="VBoxContainer" unique_id=1509773712]
+custom_minimum_size = Vector2(300, 0)
+script = ExtResource("1_orafl")
+
+[node name="Label" type="Label" parent="." unique_id=781845786]
+layout_mode = 2
+text = "Master Volume"
+
+[node name="HSlider" type="HSlider" parent="." unique_id=1286896253]
+layout_mode = 2
diff --git a/core/audio/settings/settings_menu.gd b/core/audio/settings/settings_menu.gd
new file mode 100644
index 0000000..ea9501e
--- /dev/null
+++ b/core/audio/settings/settings_menu.gd
@@ -0,0 +1,36 @@
+extends Control
+
+class_name SettingsMenu
+
+@onready var audio_options_container: VBoxContainer = $%AudioOptionsContainer
+@onready var reset_button: Button = $%ResetToDefault_Button
+@onready var close_button: Button = $%Close_Button
+
+func _ready() -> void:
+ reset_button.pressed.connect(_on_reset_to_default_button_pressed)
+ close_button.pressed.connect(_on_close_button_pressed)
+ _refresh_audio_options()
+
+func _get_audio_options() -> Array[AudioOption]:
+ var audio_options: Array[AudioOption] = []
+
+ for child in audio_options_container.get_children():
+ if child is AudioOption:
+ audio_options.append(child)
+
+ return audio_options
+
+func _refresh_audio_options() -> void:
+ for audio_option in _get_audio_options():
+ audio_option.refresh()
+
+func _apply_current_audio_values() -> void:
+ for audio_option in _get_audio_options():
+ audio_option.apply_current_value()
+
+func _on_reset_to_default_button_pressed() -> void:
+ for audio_option in _get_audio_options():
+ audio_option.reset_to_default()
+
+func _on_close_button_pressed() -> void:
+ visible = false
diff --git a/core/audio/settings/settings_menu.gd.uid b/core/audio/settings/settings_menu.gd.uid
new file mode 100644
index 0000000..e0d0e1c
--- /dev/null
+++ b/core/audio/settings/settings_menu.gd.uid
@@ -0,0 +1 @@
+uid://cvr25q1blyxg
diff --git a/core/audio/settings/settings_menu.tscn b/core/audio/settings/settings_menu.tscn
new file mode 100644
index 0000000..8143580
--- /dev/null
+++ b/core/audio/settings/settings_menu.tscn
@@ -0,0 +1,74 @@
+[gd_scene format=3 uid="uid://caqf471x0kttc"]
+
+[ext_resource type="Script" uid="uid://cvr25q1blyxg" path="res://core/audio/settings/settings_menu.gd" id="1_4lekq"]
+[ext_resource type="PackedScene" uid="uid://cescrovsjlwke" path="res://core/audio/settings/audio_option.tscn" id="1_hwcco"]
+
+[node name="SettingsMenu" type="Control" unique_id=1639777294]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("1_4lekq")
+
+[node name="PanelContainer" type="PanelContainer" parent="." unique_id=315821242]
+layout_mode = 1
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+offset_left = -79.5
+offset_top = -110.0
+offset_right = 79.5
+offset_bottom = 110.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="MarginContainer" type="MarginContainer" parent="PanelContainer" unique_id=648635784]
+layout_mode = 2
+size_flags_vertical = 0
+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="AudioOptionsContainer" type="VBoxContainer" parent="PanelContainer/MarginContainer" unique_id=824023653]
+unique_name_in_owner = true
+layout_mode = 2
+theme_override_constants/separation = 12
+
+[node name="MasterVolume_AudioOption" parent="PanelContainer/MarginContainer/AudioOptionsContainer" unique_id=1509773712 instance=ExtResource("1_hwcco")]
+layout_mode = 2
+
+[node name="HSeparator" type="HSeparator" parent="PanelContainer/MarginContainer/AudioOptionsContainer" unique_id=1053430754]
+layout_mode = 2
+
+[node name="MusicVolume_AudioOption" parent="PanelContainer/MarginContainer/AudioOptionsContainer" unique_id=690924616 instance=ExtResource("1_hwcco")]
+layout_mode = 2
+bus_name = &"Music"
+label_text = "Music Volume"
+
+[node name="HSeparator2" type="HSeparator" parent="PanelContainer/MarginContainer/AudioOptionsContainer" unique_id=453716295]
+layout_mode = 2
+
+[node name="SFXVolume_AudioOption" parent="PanelContainer/MarginContainer/AudioOptionsContainer" unique_id=2096291127 instance=ExtResource("1_hwcco")]
+layout_mode = 2
+bus_name = &"SFX"
+label_text = "SFX Volume"
+
+[node name="HSeparator3" type="HSeparator" parent="PanelContainer/MarginContainer/AudioOptionsContainer" unique_id=498553283]
+layout_mode = 2
+
+[node name="ResetToDefault_Button" type="Button" parent="PanelContainer/MarginContainer/AudioOptionsContainer" unique_id=720420423]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 0
+text = "Reset To Default"
+
+[node name="Close_Button" type="Button" parent="PanelContainer/MarginContainer/AudioOptionsContainer" unique_id=1137906684]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 0
+text = "Close"
diff --git a/core/game_state/game_state.gd b/core/game_state/game_state.gd
new file mode 100644
index 0000000..ffe6322
--- /dev/null
+++ b/core/game_state/game_state.gd
@@ -0,0 +1,32 @@
+extends Node
+
+const SAVE_PATH: String = "user://savegame.json"
+
+var save_data: SaveGameData = SaveGameData.new()
+var is_loaded: bool = false
+
+func _ready() -> void:
+ load_game()
+
+func save_game() -> void:
+ var save_file = FileAccess.open(SAVE_PATH, FileAccess.WRITE)
+ var json_string = JSON.stringify(save_data.to_dictionary())
+ save_file.store_line(json_string)
+
+func load_game() -> void:
+ if not FileAccess.file_exists(SAVE_PATH):
+ save_data = SaveGameData.new()
+ is_loaded = true
+ return
+
+ var file = FileAccess.open(SAVE_PATH, FileAccess.READ)
+ var json_string = file.get_as_text()
+
+ var json = JSON.new()
+ var parse_result = json.parse(json_string)
+
+ save_data = SaveGameData.new()
+ if parse_result == OK and json.data is Dictionary:
+ save_data = SaveGameData.from_dictionary(json.data)
+
+ is_loaded = true
diff --git a/core/game_state/game_state.gd.uid b/core/game_state/game_state.gd.uid
new file mode 100644
index 0000000..8f8015c
--- /dev/null
+++ b/core/game_state/game_state.gd.uid
@@ -0,0 +1 @@
+uid://b17g2w2g101o6
diff --git a/core/game_state/save_game_data.gd b/core/game_state/save_game_data.gd
new file mode 100644
index 0000000..8a1cf10
--- /dev/null
+++ b/core/game_state/save_game_data.gd
@@ -0,0 +1,34 @@
+class_name SaveGameData
+extends Resource
+
+var unlocked_collectibles_ids: Array[StringName] = []
+var saved_photos: Array[String] = []
+var audio_bus_volumes: Dictionary[String, float] = {}
+
+func to_dictionary() -> Dictionary:
+ var serialized_collectible_ids: Array[String] = []
+ for collectible_id in unlocked_collectibles_ids:
+ serialized_collectible_ids.append(str(collectible_id))
+
+ return {
+ "unlocked_collectibles_ids": serialized_collectible_ids,
+ "saved_photos": saved_photos,
+ "audio_bus_volumes": audio_bus_volumes,
+ }
+
+static func from_dictionary(data: Dictionary) -> SaveGameData:
+ var save_game_data := SaveGameData.new()
+
+ if data.has("unlocked_collectibles_ids"):
+ for collectible_id in data["unlocked_collectibles_ids"]:
+ save_game_data.unlocked_collectibles_ids.append(StringName(str(collectible_id)))
+
+ if data.has("saved_photos"):
+ for photo_path in data["saved_photos"]:
+ save_game_data.saved_photos.append(str(photo_path))
+
+ if data.has("audio_bus_volumes") and data["audio_bus_volumes"] is Dictionary:
+ for bus_name in data["audio_bus_volumes"]:
+ save_game_data.audio_bus_volumes[str(bus_name)] = float(data["audio_bus_volumes"][bus_name])
+
+ return save_game_data
diff --git a/core/game_state/save_game_data.gd.uid b/core/game_state/save_game_data.gd.uid
new file mode 100644
index 0000000..b994a29
--- /dev/null
+++ b/core/game_state/save_game_data.gd.uid
@@ -0,0 +1 @@
+uid://bxmvs842r82vl
diff --git a/core/photo_mode/data/collectible_library.gd b/core/photo_mode/data/collectible_library.gd
new file mode 100644
index 0000000..7aede1f
--- /dev/null
+++ b/core/photo_mode/data/collectible_library.gd
@@ -0,0 +1,4 @@
+class_name CollectibleLibrary
+extends Resource
+
+@export var collectibles: Array[CollectibleResource]
diff --git a/core/photo_mode/data/collectible_library.gd.uid b/core/photo_mode/data/collectible_library.gd.uid
new file mode 100644
index 0000000..4a5e341
--- /dev/null
+++ b/core/photo_mode/data/collectible_library.gd.uid
@@ -0,0 +1 @@
+uid://hhuufh87skq5
diff --git a/core/photo_mode/data/collectible_library.tres b/core/photo_mode/data/collectible_library.tres
new file mode 100644
index 0000000..0ee1fe2
--- /dev/null
+++ b/core/photo_mode/data/collectible_library.tres
@@ -0,0 +1,31 @@
+[gd_resource type="Resource" script_class="CollectibleLibrary" format=3 uid="uid://c6pkpvsvimafb"]
+
+[ext_resource type="Script" uid="uid://bj34o0org55ei" path="res://core/photo_mode/data/collectible_resource.gd" id="1_3cgvf"]
+[ext_resource type="Script" uid="uid://hhuufh87skq5" path="res://core/photo_mode/data/collectible_library.gd" id="2_ggu10"]
+[ext_resource type="Texture2D" uid="uid://cx8d233y32kmu" path="res://icon.svg" id="2_pxmdy"]
+
+[sub_resource type="Resource" id="Resource_3cgvf"]
+script = ExtResource("1_3cgvf")
+id = &"cane"
+title = "Cane"
+image = ExtResource("2_pxmdy")
+metadata/_custom_type_script = "uid://bj34o0org55ei"
+
+[sub_resource type="Resource" id="Resource_ggu10"]
+script = ExtResource("1_3cgvf")
+id = &"gatto"
+title = "Gatto"
+image = ExtResource("2_pxmdy")
+metadata/_custom_type_script = "uid://bj34o0org55ei"
+
+[sub_resource type="Resource" id="Resource_pxmdy"]
+script = ExtResource("1_3cgvf")
+id = &"farfalla"
+title = "Farfalla"
+image = ExtResource("2_pxmdy")
+metadata/_custom_type_script = "uid://bj34o0org55ei"
+
+[resource]
+script = ExtResource("2_ggu10")
+collectibles = Array[ExtResource("1_3cgvf")]([SubResource("Resource_3cgvf"), SubResource("Resource_ggu10"), SubResource("Resource_pxmdy")])
+metadata/_custom_type_script = "uid://hhuufh87skq5"
diff --git a/core/photo_mode/data/collectible_resource.gd b/core/photo_mode/data/collectible_resource.gd
new file mode 100644
index 0000000..90e2d4d
--- /dev/null
+++ b/core/photo_mode/data/collectible_resource.gd
@@ -0,0 +1,6 @@
+class_name CollectibleResource
+extends Resource
+
+@export var id: StringName
+@export var title: String
+@export var image: Texture2D
diff --git a/core/photo_mode/data/collectible_resource.gd.uid b/core/photo_mode/data/collectible_resource.gd.uid
new file mode 100644
index 0000000..698ed09
--- /dev/null
+++ b/core/photo_mode/data/collectible_resource.gd.uid
@@ -0,0 +1 @@
+uid://bj34o0org55ei
diff --git a/core/photo_mode/managers/collection_manager.gd b/core/photo_mode/managers/collection_manager.gd
new file mode 100644
index 0000000..193b28a
--- /dev/null
+++ b/core/photo_mode/managers/collection_manager.gd
@@ -0,0 +1,60 @@
+extends Node
+
+signal on_collectible_unlocked(collectible_id: StringName)
+signal on_photo_saved(filePath: String)
+
+var collectibles_library: CollectibleLibrary = preload("res://core/photo_mode/data/collectible_library.tres")
+
+var unlocked_collectibles_ids: Array[StringName] = []
+var saved_photos: Array[String] = []
+
+func _ready() -> void:
+ load_save_data()
+
+func unlock_collectible(collectible_id: StringName) -> void:
+ if not collectible_id in unlocked_collectibles_ids:
+ var new_collectible = get_collectible_by_id(collectible_id)
+ if new_collectible:
+ unlocked_collectibles_ids.append(collectible_id)
+ sync_save_data()
+ on_collectible_unlocked.emit(collectible_id)
+
+func get_collectible_by_id(id: StringName) -> CollectibleResource:
+ for collectible in collectibles_library.collectibles:
+ if collectible.id == id:
+ return collectible
+ return null
+
+func get_all_collectibles() -> Array[CollectibleResource]:
+ return collectibles_library.collectibles
+
+func get_unlocked_collectible_ids() -> Array[StringName]:
+ return unlocked_collectibles_ids
+
+func save_photo_to_disk_async() -> void:
+ await RenderingServer.frame_post_draw
+ var image = get_viewport().get_texture().get_image()
+
+ if not DirAccess.dir_exists_absolute("user://photos"):
+ DirAccess.make_dir_absolute("user://photos")
+
+ var timestamp = str(Time.get_unix_time_from_system())
+ var file_path = "user://photos/photo_" + timestamp + ".png"
+ image.save_png(file_path)
+
+ saved_photos.append(file_path)
+ sync_save_data()
+ on_photo_saved.emit(file_path)
+
+func sync_save_data() -> void:
+ GameState.save_data.unlocked_collectibles_ids = unlocked_collectibles_ids.duplicate()
+ GameState.save_data.saved_photos = saved_photos.duplicate()
+
+func load_save_data() -> void:
+ unlocked_collectibles_ids = GameState.save_data.unlocked_collectibles_ids.duplicate()
+ saved_photos = GameState.save_data.saved_photos.duplicate()
+
+ if unlocked_collectibles_ids.is_empty() and saved_photos.is_empty():
+ unlocked_collectibles_ids.append(&"cane")
+
+ sync_save_data()
diff --git a/core/photo_mode/managers/collection_manager.gd.uid b/core/photo_mode/managers/collection_manager.gd.uid
new file mode 100644
index 0000000..39979fc
--- /dev/null
+++ b/core/photo_mode/managers/collection_manager.gd.uid
@@ -0,0 +1 @@
+uid://c3kq1qddpm8tf
diff --git a/core/photo_mode/runtime/collectible.gd b/core/photo_mode/runtime/collectible.gd
new file mode 100644
index 0000000..08772f7
--- /dev/null
+++ b/core/photo_mode/runtime/collectible.gd
@@ -0,0 +1,6 @@
+extends Area3D
+
+@export var collectible_data: CollectibleResource
+
+func _ready() -> void:
+ add_to_group("collectible")
diff --git a/core/photo_mode/runtime/collectible.gd.uid b/core/photo_mode/runtime/collectible.gd.uid
new file mode 100644
index 0000000..3f51b53
--- /dev/null
+++ b/core/photo_mode/runtime/collectible.gd.uid
@@ -0,0 +1 @@
+uid://62d14boivr3g
diff --git a/core/photo_mode/runtime/collectible.tscn b/core/photo_mode/runtime/collectible.tscn
new file mode 100644
index 0000000..12b1c10
--- /dev/null
+++ b/core/photo_mode/runtime/collectible.tscn
@@ -0,0 +1,11 @@
+[gd_scene format=3 uid="uid://bmkxt6btcx8qr"]
+
+[ext_resource type="Script" uid="uid://62d14boivr3g" path="res://core/photo_mode/runtime/collectible.gd" id="1_xse8c"]
+
+[sub_resource type="BoxShape3D" id="BoxShape3D_xse8c"]
+
+[node name="Collectible" type="Area3D" unique_id=1229019813]
+script = ExtResource("1_xse8c")
+
+[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=1479480920]
+shape = SubResource("BoxShape3D_xse8c")
diff --git a/core/photo_mode/runtime/photo_mode_controller.gd b/core/photo_mode/runtime/photo_mode_controller.gd
new file mode 100644
index 0000000..d28d40d
--- /dev/null
+++ b/core/photo_mode/runtime/photo_mode_controller.gd
@@ -0,0 +1,94 @@
+extends Node3D
+
+@export_group("Speed")
+@export var pan_speed: float = 5.0
+@export var rotation_speed: float = 0.003
+
+@export_group("Bounds")
+@export var movement_bounds: AABB
+
+@export_group("Ref")
+@export var rotation_target: Node3D
+
+@onready var camera: Camera3D = $%Camera3D
+
+var total_yaw: float = 0.0
+var total_pitch: float = 0.0
+var orbit_distance: float = 0.0
+var current_pan: Vector2 = Vector2.ZERO
+var is_active: bool = false
+
+func _ready() -> void:
+ total_yaw = global_rotation.y
+ total_pitch = global_rotation.x
+
+ if rotation_target:
+ orbit_distance = global_position.distance_to(rotation_target.global_position)
+
+func _unhandled_input(event: InputEvent) -> void:
+ if event.is_action_pressed("toggle_photo_mode"):
+ is_active = !is_active
+
+ get_tree().paused = is_active
+
+ if !is_active:
+ Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
+ else:
+ Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
+
+ if event is InputEventMouseMotion and is_active:
+ total_yaw -= event.relative.x * rotation_speed
+
+ if event.is_action_pressed("take_photo"):
+ take_photo_async()
+
+func _process(delta: float) -> void:
+ if not rotation_target:
+ return
+
+
+ var pan_input = Input.get_vector("photo_pan_left", "photo_pan_right", "photo_pan_up", "photo_pan_down")
+ var rot_basis = Basis.from_euler(Vector3(total_pitch, total_yaw, 0))
+
+ if pan_input != Vector2.ZERO and is_active:
+ current_pan.x += pan_input.x * pan_speed * delta
+ current_pan.y += -pan_input.y * pan_speed * delta
+
+ if movement_bounds.size != Vector3.ZERO:
+ current_pan.x = clampf(current_pan.x, movement_bounds.position.x, movement_bounds.position.x + movement_bounds.size.x)
+ current_pan.y = clampf(current_pan.y, movement_bounds.position.y, movement_bounds.position.y + movement_bounds.size.y)
+
+ var right_dir = rot_basis.x.normalized()
+ var up_dir = rot_basis.y.normalized()
+
+ var final_pan_offset = (right_dir * current_pan.x) + (up_dir * current_pan.y)
+
+ var orbit_pos = rotation_target.global_position + (rot_basis * Vector3(0, 0, orbit_distance))
+
+ global_position = orbit_pos + final_pan_offset
+ global_basis = rot_basis
+
+func take_photo_async() -> void:
+ if !is_active:
+ return
+
+ var targets = get_tree().get_nodes_in_group("collectible")
+ var space_state = get_world_3d().direct_space_state
+
+ await CollectionManager.save_photo_to_disk_async()
+
+ for target in targets:
+ if not target.collectible_data:
+ continue
+
+ if camera.is_position_in_frustum(target.global_position):
+ var query = PhysicsRayQueryParameters3D.create(camera.global_position, target.global_position)
+ query.collide_with_areas = true
+ query.collide_with_bodies = true
+
+ var result = space_state.intersect_ray(query)
+
+ if result and result.collider == target:
+ CollectionManager.unlock_collectible(target.collectible_data.id)
+
+ GameState.save_game()
diff --git a/core/photo_mode/runtime/photo_mode_controller.gd.uid b/core/photo_mode/runtime/photo_mode_controller.gd.uid
new file mode 100644
index 0000000..aba9819
--- /dev/null
+++ b/core/photo_mode/runtime/photo_mode_controller.gd.uid
@@ -0,0 +1 @@
+uid://d7ln6iru6mq6
diff --git a/core/photo_mode/runtime/photo_mode_controller.tscn b/core/photo_mode/runtime/photo_mode_controller.tscn
new file mode 100644
index 0000000..d048e34
--- /dev/null
+++ b/core/photo_mode/runtime/photo_mode_controller.tscn
@@ -0,0 +1,10 @@
+[gd_scene format=3 uid="uid://vni5kjalum6d"]
+
+[ext_resource type="Script" uid="uid://d7ln6iru6mq6" path="res://core/photo_mode/runtime/photo_mode_controller.gd" id="1_uhpya"]
+
+[node name="PhotoModeController" type="Node3D" unique_id=695158870]
+process_mode = 3
+script = ExtResource("1_uhpya")
+
+[node name="Camera3D" type="Camera3D" parent="." unique_id=1369039645]
+unique_name_in_owner = true
diff --git a/core/photo_mode/ui/collectible_gallery.gd b/core/photo_mode/ui/collectible_gallery.gd
new file mode 100644
index 0000000..ea1a87b
--- /dev/null
+++ b/core/photo_mode/ui/collectible_gallery.gd
@@ -0,0 +1,34 @@
+extends Control
+
+@export var collectible_ui_scene: PackedScene
+
+@onready var grid_container: GridContainer = $%CollectibleGrid
+
+func _ready() -> void:
+ CollectionManager.on_collectible_unlocked.connect(_unlock_collectible)
+ setup()
+
+func setup() -> void:
+ var unlocked_collectible_ids = CollectionManager.get_unlocked_collectible_ids()
+ var collectibles = CollectionManager.get_all_collectibles()
+
+ for child in grid_container.get_children():
+ child.queue_free()
+
+ for collectible in collectibles:
+ _add_collectible(collectible)
+ if unlocked_collectible_ids.has(collectible.id):
+ _unlock_collectible(collectible.id)
+
+func on_collectible_unlocked(collectible_id: StringName) -> void:
+ _unlock_collectible(collectible_id)
+
+func _add_collectible(collectible: CollectibleResource) -> void:
+ var collectible_ui: CollectibleUI = collectible_ui_scene.instantiate()
+ collectible_ui.setup(collectible)
+ grid_container.add_child(collectible_ui)
+
+func _unlock_collectible(collectible_id: StringName) -> void:
+ for collectible_ui in grid_container.get_children():
+ if collectible_ui.collectible_resource.id == collectible_id:
+ collectible_ui.unlock()
diff --git a/core/photo_mode/ui/collectible_gallery.gd.uid b/core/photo_mode/ui/collectible_gallery.gd.uid
new file mode 100644
index 0000000..2ec65a4
--- /dev/null
+++ b/core/photo_mode/ui/collectible_gallery.gd.uid
@@ -0,0 +1 @@
+uid://dq3qtcrdnikl7
diff --git a/core/photo_mode/ui/collectible_gallery.tscn b/core/photo_mode/ui/collectible_gallery.tscn
new file mode 100644
index 0000000..27851d6
--- /dev/null
+++ b/core/photo_mode/ui/collectible_gallery.tscn
@@ -0,0 +1,50 @@
+[gd_scene format=3 uid="uid://bvw086glfpcba"]
+
+[ext_resource type="Script" uid="uid://dq3qtcrdnikl7" path="res://core/photo_mode/ui/collectible_gallery.gd" id="1_67tug"]
+[ext_resource type="PackedScene" uid="uid://dp7dvfauh5rpx" path="res://core/photo_mode/ui/collectible_ui.tscn" id="2_or234"]
+
+[node name="CollectibleGallery" type="Control" unique_id=354419843]
+layout_mode = 3
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("1_67tug")
+collectible_ui_scene = ExtResource("2_or234")
+
+[node name="PanelContainer" type="PanelContainer" parent="." unique_id=640179265]
+layout_mode = 1
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+offset_left = -350.0
+offset_top = -350.0
+offset_right = 350.0
+offset_bottom = 350.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="MarginContainer" type="MarginContainer" parent="PanelContainer" unique_id=125147979]
+layout_mode = 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="ScrollContainer" type="ScrollContainer" parent="PanelContainer/MarginContainer" unique_id=348576028]
+layout_mode = 2
+draw_focus_border = true
+
+[node name="CollectibleGrid" type="GridContainer" parent="PanelContainer/MarginContainer/ScrollContainer" unique_id=1187865988]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 6
+size_flags_vertical = 2
+theme_override_constants/h_separation = 12
+theme_override_constants/v_separation = 12
+columns = 3
diff --git a/core/photo_mode/ui/collectible_ui.gd b/core/photo_mode/ui/collectible_ui.gd
new file mode 100644
index 0000000..453f528
--- /dev/null
+++ b/core/photo_mode/ui/collectible_ui.gd
@@ -0,0 +1,22 @@
+extends Control
+
+class_name CollectibleUI
+
+@onready var label: Label = $%CollectibleName
+@onready var texture_rect: TextureRect = $%CollectibleTexture
+var collectible_resource: CollectibleResource
+
+func setup(collectible: CollectibleResource) -> void:
+ collectible_resource = collectible
+ if !label:
+ label = $%CollectibleName
+ label.text = collectible_resource.title
+ if collectible.image:
+ if !texture_rect:
+ texture_rect = $%CollectibleTexture
+ texture_rect.texture = collectible_resource.image
+
+func unlock() -> void:
+ if !texture_rect:
+ texture_rect = $%CollectibleTexture
+ texture_rect.set_modulate(Color(1,1,1,1))
diff --git a/core/photo_mode/ui/collectible_ui.gd.uid b/core/photo_mode/ui/collectible_ui.gd.uid
new file mode 100644
index 0000000..d9ba065
--- /dev/null
+++ b/core/photo_mode/ui/collectible_ui.gd.uid
@@ -0,0 +1 @@
+uid://dxh8e1n16qjpp
diff --git a/core/photo_mode/ui/collectible_ui.tscn b/core/photo_mode/ui/collectible_ui.tscn
new file mode 100644
index 0000000..8242ea3
--- /dev/null
+++ b/core/photo_mode/ui/collectible_ui.tscn
@@ -0,0 +1,49 @@
+[gd_scene format=3 uid="uid://dp7dvfauh5rpx"]
+
+[ext_resource type="Script" uid="uid://dxh8e1n16qjpp" path="res://core/photo_mode/ui/collectible_ui.gd" id="1_tu5nx"]
+[ext_resource type="Texture2D" uid="uid://cx8d233y32kmu" path="res://icon.svg" id="1_x3wje"]
+
+[node name="CollectibleUI" type="Control" unique_id=201865221]
+custom_minimum_size = Vector2(200, 200)
+layout_mode = 3
+anchors_preset = 0
+offset_right = 200.0
+offset_bottom = 200.0
+script = ExtResource("1_tu5nx")
+
+[node name="Panel" type="PanelContainer" parent="." unique_id=1372516531]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="MarginContainer" type="MarginContainer" parent="Panel" unique_id=1141643525]
+layout_mode = 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="VBoxContainer" type="VBoxContainer" parent="Panel/MarginContainer" unique_id=1841409877]
+layout_mode = 2
+alignment = 1
+
+[node name="CollectibleName" type="Label" parent="Panel/MarginContainer/VBoxContainer" unique_id=117774359]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 0
+text = "Collectible"
+horizontal_alignment = 1
+
+[node name="HSeparator" type="HSeparator" parent="Panel/MarginContainer/VBoxContainer" unique_id=1345822244]
+layout_mode = 2
+
+[node name="CollectibleTexture" type="TextureRect" parent="Panel/MarginContainer/VBoxContainer" unique_id=506844785]
+unique_name_in_owner = true
+modulate = Color(0.16206557, 0.1620656, 0.16206557, 1)
+layout_mode = 2
+size_flags_vertical = 3
+texture = ExtResource("1_x3wje")
+stretch_mode = 3
diff --git a/core/photo_mode/ui/photo.gd b/core/photo_mode/ui/photo.gd
new file mode 100644
index 0000000..5740c30
--- /dev/null
+++ b/core/photo_mode/ui/photo.gd
@@ -0,0 +1,11 @@
+extends Control
+
+class_name Photo
+
+@onready var texture_rect: TextureRect = $%PhotoTexture
+
+func setup(texture: Texture) -> void:
+ if texture:
+ if !texture_rect:
+ texture_rect = $%PhotoTexture
+ texture_rect.texture = texture
diff --git a/core/photo_mode/ui/photo.gd.uid b/core/photo_mode/ui/photo.gd.uid
new file mode 100644
index 0000000..37f3929
--- /dev/null
+++ b/core/photo_mode/ui/photo.gd.uid
@@ -0,0 +1 @@
+uid://dsey5dvc11vpq
diff --git a/core/photo_mode/ui/photo.tscn b/core/photo_mode/ui/photo.tscn
new file mode 100644
index 0000000..615e857
--- /dev/null
+++ b/core/photo_mode/ui/photo.tscn
@@ -0,0 +1,38 @@
+[gd_scene format=3 uid="uid://cvus62qkop3qi"]
+
+[ext_resource type="Script" uid="uid://dsey5dvc11vpq" path="res://core/photo_mode/ui/photo.gd" id="1_u0arp"]
+
+[node name="Photo" type="Control" unique_id=201865221]
+custom_minimum_size = Vector2(200, 200)
+layout_mode = 3
+anchors_preset = 0
+offset_right = 200.0
+offset_bottom = 200.0
+script = ExtResource("1_u0arp")
+
+[node name="ColorRect" type="ColorRect" parent="." unique_id=1208008621]
+layout_mode = 0
+offset_right = 200.0
+offset_bottom = 200.0
+
+[node name="MarginContainer" type="MarginContainer" parent="ColorRect" unique_id=1141643525]
+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 = 12
+theme_override_constants/margin_top = 12
+theme_override_constants/margin_right = 12
+theme_override_constants/margin_bottom = 12
+
+[node name="Panel" type="PanelContainer" parent="ColorRect/MarginContainer" unique_id=1372516531]
+layout_mode = 2
+
+[node name="PhotoTexture" type="TextureRect" parent="ColorRect/MarginContainer/Panel" unique_id=506844785]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_vertical = 3
+expand_mode = 1
+stretch_mode = 6
diff --git a/core/photo_mode/ui/photo_gallery.gd b/core/photo_mode/ui/photo_gallery.gd
new file mode 100644
index 0000000..9ffd1e4
--- /dev/null
+++ b/core/photo_mode/ui/photo_gallery.gd
@@ -0,0 +1,27 @@
+extends Control
+
+@export var photo_scene: PackedScene
+
+@onready var grid_container: GridContainer = $%PhotoGrid
+
+func _ready() -> void:
+ CollectionManager.on_photo_saved.connect(_create_photo_thumbnail)
+ load_gallery()
+
+func load_gallery() -> void:
+ for child in grid_container.get_children():
+ child.queue_free()
+
+ var saved_photos = CollectionManager.saved_photos
+
+ for file_path in saved_photos:
+ if FileAccess.file_exists(file_path):
+ _create_photo_thumbnail(file_path)
+
+func _create_photo_thumbnail(file_path: String) -> void:
+ var image = Image.load_from_file(file_path)
+ if image:
+ var photo: Photo = photo_scene.instantiate()
+ var texture = ImageTexture.create_from_image(image)
+ photo.setup(texture)
+ grid_container.add_child(photo)
diff --git a/core/photo_mode/ui/photo_gallery.gd.uid b/core/photo_mode/ui/photo_gallery.gd.uid
new file mode 100644
index 0000000..74980db
--- /dev/null
+++ b/core/photo_mode/ui/photo_gallery.gd.uid
@@ -0,0 +1 @@
+uid://k4iqrlkbjppl
diff --git a/core/photo_mode/ui/photo_gallery.tscn b/core/photo_mode/ui/photo_gallery.tscn
new file mode 100644
index 0000000..64c94a7
--- /dev/null
+++ b/core/photo_mode/ui/photo_gallery.tscn
@@ -0,0 +1,50 @@
+[gd_scene format=3 uid="uid://b6r787sik5yil"]
+
+[ext_resource type="Script" uid="uid://k4iqrlkbjppl" path="res://core/photo_mode/ui/photo_gallery.gd" id="1_bp5uf"]
+[ext_resource type="PackedScene" uid="uid://cvus62qkop3qi" path="res://core/photo_mode/ui/photo.tscn" id="2_45wok"]
+
+[node name="PhotoGallery" type="Control" unique_id=354419843]
+layout_mode = 3
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("1_bp5uf")
+photo_scene = ExtResource("2_45wok")
+
+[node name="PanelContainer" type="PanelContainer" parent="." unique_id=640179265]
+layout_mode = 1
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+offset_left = -350.0
+offset_top = -350.0
+offset_right = 350.0
+offset_bottom = 350.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="MarginContainer" type="MarginContainer" parent="PanelContainer" unique_id=125147979]
+layout_mode = 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="ScrollContainer" type="ScrollContainer" parent="PanelContainer/MarginContainer" unique_id=348576028]
+layout_mode = 2
+draw_focus_border = true
+
+[node name="PhotoGrid" type="GridContainer" parent="PanelContainer/MarginContainer/ScrollContainer" unique_id=1187865988]
+unique_name_in_owner = true
+layout_mode = 2
+size_flags_horizontal = 6
+size_flags_vertical = 2
+theme_override_constants/h_separation = 12
+theme_override_constants/v_separation = 12
+columns = 3
diff --git a/core/radio/radio.gd b/core/radio/radio.gd
new file mode 100644
index 0000000..0cdaf0f
--- /dev/null
+++ b/core/radio/radio.gd
@@ -0,0 +1,31 @@
+extends AudioStreamPlayer
+
+class_name Radio
+
+@export var playlist: Array[AudioStream] = []
+var current_track_index: int = 0
+
+func _ready():
+ finished.connect(next_track)
+
+func play_track(index: int = current_track_index):
+ if playlist.is_empty():
+ return
+
+ current_track_index = posmod(index, playlist.size())
+ stream = playlist[current_track_index]
+ play()
+ stream_paused = false
+
+func next_track():
+ play_track(current_track_index + 1)
+
+func prev_track():
+ play_track(current_track_index - 1)
+
+func toggle_pause():
+ stream_paused = !stream_paused
+
+func stop_radio():
+ stop()
+ stream_paused = false
diff --git a/core/radio/radio.gd.uid b/core/radio/radio.gd.uid
new file mode 100644
index 0000000..16144af
--- /dev/null
+++ b/core/radio/radio.gd.uid
@@ -0,0 +1 @@
+uid://bwrgh6xlvhqch
diff --git a/core/radio/radio.tscn b/core/radio/radio.tscn
new file mode 100644
index 0000000..40f7519
--- /dev/null
+++ b/core/radio/radio.tscn
@@ -0,0 +1,7 @@
+[gd_scene format=3 uid="uid://cpeyt1dgrtglc"]
+
+[ext_resource type="Script" uid="uid://bwrgh6xlvhqch" path="res://core/radio/radio.gd" id="1_2akjj"]
+
+[node name="Radio" type="AudioStreamPlayer" unique_id=1234112225]
+bus = &"Music"
+script = ExtResource("1_2akjj")
diff --git a/default_bus_layout.tres b/default_bus_layout.tres
new file mode 100644
index 0000000..3003eca
--- /dev/null
+++ b/default_bus_layout.tres
@@ -0,0 +1,15 @@
+[gd_resource type="AudioBusLayout" format=3 uid="uid://cqvx62pwvvboj"]
+
+[resource]
+bus/1/name = &"Music"
+bus/1/solo = false
+bus/1/mute = false
+bus/1/bypass_fx = false
+bus/1/volume_db = 0.0
+bus/1/send = &"Master"
+bus/2/name = &"SFX"
+bus/2/solo = false
+bus/2/mute = false
+bus/2/bypass_fx = false
+bus/2/volume_db = 0.0
+bus/2/send = &"Master"
diff --git a/docs/gyms/ai/gym_ai.tscn b/docs/gyms/ai/gym_ai.tscn
new file mode 100644
index 0000000..faa1cc8
--- /dev/null
+++ b/docs/gyms/ai/gym_ai.tscn
@@ -0,0 +1,92 @@
+[gd_scene format=3 uid="uid://dqvrhiqgkd3w1"]
+
+[ext_resource type="PackedScene" uid="uid://clx701xdwelgx" path="res://core/ai/agents/base/ai_base.tscn" id="1_a2xtd"]
+
+[sub_resource type="NavigationMesh" id="NavigationMesh_optuv"]
+vertices = PackedVector3Array(10.984741, 0.25012434, -1.0140381, 11.234741, 0.25012434, 0.23596191, 19.484741, 0.25012434, 0.23596191, 19.484741, 0.25012434, -19.514038, 9.234741, 0.25012434, -1.0140381, -19.515259, 0.25012434, -19.514038, -19.515259, 0.25012434, -0.014038086, 8.984741, 0.25012434, -0.014038086, 10.984741, 0.25012434, 1.2359619, 19.484741, 0.25012434, 19.235962, 9.234741, 0.25012434, 1.2359619, -19.515259, 0.25012434, 19.235962)
+polygons = [PackedInt32Array(1, 0, 2), PackedInt32Array(2, 0, 3), PackedInt32Array(6, 5, 4), PackedInt32Array(4, 5, 3), PackedInt32Array(3, 0, 4), PackedInt32Array(4, 7, 6), PackedInt32Array(1, 2, 8), PackedInt32Array(8, 2, 9), PackedInt32Array(6, 10, 11), PackedInt32Array(11, 10, 9), PackedInt32Array(10, 8, 9), PackedInt32Array(6, 7, 10)]
+
+[sub_resource type="PlaneMesh" id="PlaneMesh_optuv"]
+
+[sub_resource type="BoxShape3D" id="BoxShape3D_xtjak"]
+size = Vector3(1.9942131, 0.09710693, 1.984024)
+
+[sub_resource type="BoxMesh" id="BoxMesh_lmjyn"]
+
+[sub_resource type="BoxShape3D" id="BoxShape3D_lmjyn"]
+
+[sub_resource type="Environment" id="Environment_lmjyn"]
+
+[node name="GymAI" type="Node3D" unique_id=868339787]
+
+[node name="NavigationRegion3D" type="NavigationRegion3D" parent="." unique_id=774907858]
+navigation_mesh = SubResource("NavigationMesh_optuv")
+
+[node name="Floor" type="MeshInstance3D" parent="NavigationRegion3D" unique_id=1976415311]
+transform = Transform3D(20, 0, 0, 0, 20, 0, 0, 0, 20, 0, -0.83473957, 0)
+mesh = SubResource("PlaneMesh_optuv")
+
+[node name="StaticBody3D" type="StaticBody3D" parent="NavigationRegion3D/Floor" unique_id=2007477719]
+
+[node name="CollisionShape3D" type="CollisionShape3D" parent="NavigationRegion3D/Floor/StaticBody3D" unique_id=1446628130]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.0036563873, -0.009703338, -0.00868988)
+shape = SubResource("BoxShape3D_xtjak")
+
+[node name="Obstacle" type="MeshInstance3D" parent="NavigationRegion3D" unique_id=710701082]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 10.148018, 0.06508446, 0)
+mesh = SubResource("BoxMesh_lmjyn")
+
+[node name="StaticBody3D" type="StaticBody3D" parent="NavigationRegion3D/Obstacle" unique_id=67152580]
+
+[node name="CollisionShape3D" type="CollisionShape3D" parent="NavigationRegion3D/Obstacle/StaticBody3D" unique_id=1663742898]
+shape = SubResource("BoxShape3D_lmjyn")
+
+[node name="AIBase" parent="." unique_id=1228675528 instance=ExtResource("1_a2xtd")]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.48246834, 0.21202153, 0.11262059)
+
+[node name="Camera3D" type="Camera3D" parent="." unique_id=1148396057]
+transform = Transform3D(-2.0613477e-08, 0.8818225, -0.47158146, 3.854569e-08, 0.47158146, 0.8818225, 1, -3.5527137e-15, -4.371139e-08, -18.21907, 28.71355, 0)
+
+[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=1772424900]
+environment = SubResource("Environment_lmjyn")
+
+[node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=269230449]
+transform = Transform3D(1, 0, 0, 0, 0.17952333, 0.98375374, 0, -0.98375374, 0.17952333, 0, 18.920979, 0)
+
+[node name="Control" type="Control" parent="." unique_id=1072240389]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="PanelContainer" type="PanelContainer" parent="Control" unique_id=1647272132]
+layout_mode = 0
+offset_left = 50.0
+offset_top = 50.0
+offset_right = 400.0
+offset_bottom = 827.0
+
+[node name="Label" type="Label" parent="Control/PanelContainer" unique_id=272421245]
+custom_minimum_size = Vector2(1, 1)
+layout_mode = 2
+size_flags_vertical = 1
+text = "System Logic:
+- The NPC uses a finite state machine to switch between behaviors.
+- State logic runs during physics updates, while movement is handled by the agent through NavigationAgent3D.
+
+AI States:
+- IdleState: The agent waits in place for the duration defined by wait_time.
+- PatrolState: The agent selects a valid random destination and moves toward it.
+
+Core Components:
+- NavigationAgent3D: Calculates the path and reports when the destination has been reached.
+- PatrolRadiusShape: Defines the random movement area used when choosing the next patrol point.
+- StateMachine: Initializes the available states and handles transitions between them.
+
+Parameters:
+- Speed: Movement speed of the agent.
+- Wait Time: Delay in seconds on the IdleState node.
+- Area Radius: Radius of the SphereShape3D used by PatrolRadiusShape."
+autowrap_mode = 2
diff --git a/docs/gyms/photo_mode/gym_photo_mode.gd b/docs/gyms/photo_mode/gym_photo_mode.gd
new file mode 100644
index 0000000..e92bfd2
--- /dev/null
+++ b/docs/gyms/photo_mode/gym_photo_mode.gd
@@ -0,0 +1,7 @@
+extends Node3D
+
+@onready var gallery: Control = $%Gallery
+
+
+func _on_button_gallery_pressed() -> void:
+ gallery.visible = !gallery.visible
diff --git a/docs/gyms/photo_mode/gym_photo_mode.gd.uid b/docs/gyms/photo_mode/gym_photo_mode.gd.uid
new file mode 100644
index 0000000..b17c53f
--- /dev/null
+++ b/docs/gyms/photo_mode/gym_photo_mode.gd.uid
@@ -0,0 +1 @@
+uid://7oglx4br38d5
diff --git a/docs/gyms/photo_mode/gym_photo_mode.tscn b/docs/gyms/photo_mode/gym_photo_mode.tscn
new file mode 100644
index 0000000..7549329
--- /dev/null
+++ b/docs/gyms/photo_mode/gym_photo_mode.tscn
@@ -0,0 +1,138 @@
+[gd_scene format=3 uid="uid://bwc8slnbu7dmn"]
+
+[ext_resource type="Script" uid="uid://7oglx4br38d5" path="res://docs/gyms/photo_mode/gym_photo_mode.gd" id="1_cl1h5"]
+[ext_resource type="PackedScene" uid="uid://vni5kjalum6d" path="res://core/photo_mode/runtime/photo_mode_controller.tscn" id="2_q5kgc"]
+[ext_resource type="PackedScene" uid="uid://bmkxt6btcx8qr" path="res://core/photo_mode/runtime/collectible.tscn" id="3_734hf"]
+[ext_resource type="Texture2D" uid="uid://c3grftlmap4q5" path="res://tgcc/chunk/prop/tree/leaf1_alpha.png" id="4_00joj"]
+[ext_resource type="Script" uid="uid://bj34o0org55ei" path="res://core/photo_mode/data/collectible_resource.gd" id="5_crfij"]
+[ext_resource type="PackedScene" uid="uid://bvw086glfpcba" path="res://core/photo_mode/ui/collectible_gallery.tscn" id="6_6aab5"]
+[ext_resource type="PackedScene" uid="uid://b6r787sik5yil" path="res://core/photo_mode/ui/photo_gallery.tscn" id="7_tb1ii"]
+
+[sub_resource type="Environment" id="Environment_48mla"]
+
+[sub_resource type="PlaneMesh" id="PlaneMesh_hh1ka"]
+
+[sub_resource type="BoxShape3D" id="BoxShape3D_ynvi2"]
+size = Vector3(1.9942131, 0.09710693, 1.984024)
+
+[sub_resource type="Resource" id="Resource_ynvi2"]
+script = ExtResource("5_crfij")
+id = &"gatto"
+title = "Gatto"
+image = ExtResource("4_00joj")
+metadata/_custom_type_script = "uid://bj34o0org55ei"
+
+[sub_resource type="BoxMesh" id="BoxMesh_hh1ka"]
+
+[node name="GymPhotoMode" type="Node3D" unique_id=1314600330]
+script = ExtResource("1_cl1h5")
+
+[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=213183287]
+environment = SubResource("Environment_48mla")
+
+[node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=1274975837]
+transform = Transform3D(1, 0, 0, 0, 0.17952333, 0.98375374, 0, -0.98375374, 0.17952333, 0, 18.920979, 0)
+
+[node name="Floor" type="MeshInstance3D" parent="." unique_id=958865480]
+transform = Transform3D(20, 0, 0, 0, 20, 0, 0, 0, 20, 0, -0.83473957, 0)
+mesh = SubResource("PlaneMesh_hh1ka")
+
+[node name="StaticBody3D" type="StaticBody3D" parent="Floor" unique_id=2096983240]
+
+[node name="CollisionShape3D" type="CollisionShape3D" parent="Floor/StaticBody3D" unique_id=188185045]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.0036563873, -0.009703338, -0.00868988)
+shape = SubResource("BoxShape3D_ynvi2")
+
+[node name="PhotoModeController" parent="." unique_id=695158870 node_paths=PackedStringArray("rotation_target") instance=ExtResource("2_q5kgc")]
+transform = Transform3D(1, 0, 0, 0, 0.49999994, 0.8660254, 0, -0.8660254, 0.49999994, 0, 30, 20)
+movement_bounds = AABB(-5, -5, -5, 10, 10, 10)
+rotation_target = NodePath("../Node3D")
+
+[node name="Node3D" type="Node3D" parent="." unique_id=570786250]
+transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 10, 0)
+
+[node name="Node3D2" type="Node3D" parent="." unique_id=15037296]
+
+[node name="Collectible" parent="Node3D2" unique_id=1229019813 instance=ExtResource("3_734hf")]
+collectible_data = SubResource("Resource_ynvi2")
+
+[node name="MeshInstance3D" type="MeshInstance3D" parent="Node3D2" unique_id=553040412]
+mesh = SubResource("BoxMesh_hh1ka")
+
+[node name="Control" type="Control" parent="." unique_id=1802496396]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+mouse_filter = 2
+
+[node name="ButtonGallery" type="Button" parent="Control" unique_id=539195532]
+layout_mode = 0
+offset_left = 50.0
+offset_top = 50.0
+offset_right = 163.0
+offset_bottom = 81.0
+text = "Menu"
+
+[node name="Gallery" type="HSplitContainer" parent="Control" unique_id=1388833682]
+unique_name_in_owner = true
+visible = false
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = 420.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="CollectibleGallery" parent="Control/Gallery" unique_id=354419843 instance=ExtResource("6_6aab5")]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="PhotoGallery" parent="Control/Gallery" unique_id=263414560 instance=ExtResource("7_tb1ii")]
+layout_mode = 2
+size_flags_horizontal = 3
+
+[node name="PanelContainer" type="PanelContainer" parent="Control" unique_id=2015953740]
+layout_mode = 0
+offset_left = 50.0
+offset_top = 150.0
+offset_right = 400.0
+offset_bottom = 927.0
+
+[node name="ScrollContainer" type="ScrollContainer" parent="Control/PanelContainer" unique_id=1474258594]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="Control/PanelContainer/ScrollContainer" unique_id=1982569320]
+custom_minimum_size = Vector2(1, 1)
+layout_mode = 2
+size_flags_horizontal = 3
+text = "System Logic:
+- The system allows players to toggle a free camera mode to photograph and collect museum items.
+- Game Pause: Activating the mode pauses the SceneTree, freezing the world while allowing the camera to remain functional.
+- Taking a photo saves the captured image to disk and checks which collectibles are visible and unobstructed before unlocking them.
+- CollectionManager acts as a global singleton that tracks unlocked collectibles, saved photos, and the data used by the UI.
+
+Photo Mode Actions:
+- Toggle Mode(P): Switches between normal gameplay and the photo camera, capturing or releasing the mouse, and toggles the global pause state.
+- Rotate(Mouse): The camera orbits around the configured target while photo mode is active.
+- Pan(Input Actions): The camera can be moved horizontally and vertically within the configured bounds.
+- Capture(F): Saves the current frame and performs frustum and raycast checks against collectibles in view.
+
+Core Components:
+- PhotoModeController: Manages camera inputs, movement logic, and the photo-taking process.
+- Collectible: An Area3D node placed on objects to make them identifiable by the camera.
+- CollectionManager: Handles the logic for unlocking items and provides data to the UI.
+- CollectibleGallery: Displays all collectibles and visually highlights the unlocked ones.
+- PhotoGallery: Displays the photos saved by the player during the session.
+
+Parameters:
+- Pan & Rotation Speed: Defines how fast the camera moves and rotates.
+- Movement Bounds: An AABB that restricts the camera's panning area.
+- Collectible Data: Resource files containing the ID, title, and image for each item.
+- Library: A central list of all CollectibleResources registered in the game."
+autowrap_mode = 2
+
+[connection signal="pressed" from="Control/ButtonGallery" to="." method="_on_button_gallery_pressed"]
diff --git a/docs/gyms/radio/gym_radio.gd b/docs/gyms/radio/gym_radio.gd
new file mode 100644
index 0000000..d905bf0
--- /dev/null
+++ b/docs/gyms/radio/gym_radio.gd
@@ -0,0 +1,35 @@
+extends Node3D
+
+@onready var radio: Radio = $%Radio
+@onready var button_pause_resume: Button = $%Button_Pause_Resume
+@onready var settings_menu: SettingsMenu = $%SettingsMenu
+
+func _on_button_play_pressed() -> void:
+ if !radio.playing:
+ radio.play_track()
+
+
+func _on_button_pause_pressed() -> void:
+ radio.toggle_pause()
+ if !button_pause_resume:
+ button_pause_resume = $%Button_Pause_Resume
+ if radio.stream_paused:
+ button_pause_resume.text = "Resume"
+ else:
+ button_pause_resume.text = "Pause"
+
+
+func _on_button_stop_pressed() -> void:
+ radio.stop_radio()
+
+
+func _on_button_next_pressed() -> void:
+ radio.next_track()
+
+
+func _on_button_previous_pressed() -> void:
+ radio.prev_track()
+
+
+func _on_button_settings_menu_pressed() -> void:
+ settings_menu.visible = !settings_menu.visible
diff --git a/docs/gyms/radio/gym_radio.gd.uid b/docs/gyms/radio/gym_radio.gd.uid
new file mode 100644
index 0000000..37098fa
--- /dev/null
+++ b/docs/gyms/radio/gym_radio.gd.uid
@@ -0,0 +1 @@
+uid://ew4b78u5tkiu
diff --git a/docs/gyms/radio/gym_radio.tscn b/docs/gyms/radio/gym_radio.tscn
new file mode 100644
index 0000000..7289baa
--- /dev/null
+++ b/docs/gyms/radio/gym_radio.tscn
@@ -0,0 +1,104 @@
+[gd_scene format=3 uid="uid://bdkdhn1oallke"]
+
+[ext_resource type="PackedScene" uid="uid://cpeyt1dgrtglc" path="res://core/radio/radio.tscn" id="1_8yomj"]
+[ext_resource type="Script" uid="uid://ew4b78u5tkiu" path="res://docs/gyms/radio/gym_radio.gd" id="1_h5r8c"]
+[ext_resource type="AudioStream" uid="uid://70cc8she43re" path="res://docs/gyms/radio/lofi_01.ogg" id="3_ltpvc"]
+[ext_resource type="PackedScene" uid="uid://caqf471x0kttc" path="res://core/audio/settings/settings_menu.tscn" id="4_8pw2j"]
+
+[node name="GymRadio" type="Node3D" unique_id=204859062]
+script = ExtResource("1_h5r8c")
+
+[node name="Radio" parent="." unique_id=1234112225 instance=ExtResource("1_8yomj")]
+unique_name_in_owner = true
+playlist = Array[AudioStream]([ExtResource("3_ltpvc")])
+
+[node name="Control" type="Control" parent="." unique_id=1232082176]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="SettingsMenu" parent="Control" unique_id=1639777294 instance=ExtResource("4_8pw2j")]
+unique_name_in_owner = true
+visible = false
+layout_mode = 1
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+
+[node name="VBoxContainer2" type="VBoxContainer" parent="Control" unique_id=2092627240]
+layout_mode = 1
+anchors_preset = 9
+anchor_bottom = 1.0
+offset_right = 229.0
+grow_vertical = 2
+
+[node name="VBoxContainer" type="VBoxContainer" parent="Control/VBoxContainer2" unique_id=2097365775]
+layout_mode = 2
+
+[node name="Button_Play" type="Button" parent="Control/VBoxContainer2/VBoxContainer" unique_id=1422696340]
+layout_mode = 2
+text = "Play"
+
+[node name="Button_Pause_Resume" type="Button" parent="Control/VBoxContainer2/VBoxContainer" unique_id=2016690081]
+unique_name_in_owner = true
+layout_mode = 2
+text = "Pause"
+
+[node name="Button_Stop" type="Button" parent="Control/VBoxContainer2/VBoxContainer" unique_id=61861870]
+layout_mode = 2
+text = "Stop"
+
+[node name="Button_Next" type="Button" parent="Control/VBoxContainer2/VBoxContainer" unique_id=957606385]
+layout_mode = 2
+text = "Next"
+
+[node name="Button_Previous" type="Button" parent="Control/VBoxContainer2/VBoxContainer" unique_id=929072518]
+layout_mode = 2
+text = "Previous"
+
+[node name="Button_SettingsMenu" type="Button" parent="Control/VBoxContainer2/VBoxContainer" unique_id=431867154]
+layout_mode = 2
+text = "SettingsMenu"
+
+[node name="PanelContainer" type="PanelContainer" parent="Control/VBoxContainer2" unique_id=1558442050]
+layout_mode = 2
+size_flags_vertical = 3
+
+[node name="ScrollContainer" type="ScrollContainer" parent="Control/VBoxContainer2/PanelContainer" unique_id=7789304]
+layout_mode = 2
+
+[node name="Label" type="Label" parent="Control/VBoxContainer2/PanelContainer/ScrollContainer" unique_id=2043293564]
+custom_minimum_size = Vector2(1, 1)
+layout_mode = 2
+size_flags_horizontal = 3
+size_flags_vertical = 1
+text = "System Logic:
+- The system is an extension of AudioStreamPlayer designed to manage sequential audio playback.
+- It automatically triggers the next track in the sequence when the current one finishes via the finished signal.
+
+Core Functionality:
+- Playlist Management: Stores a list of AudioStream resources and tracks the current song index.
+- Track Navigation: Allows cycling forward and backward through the playlist with index wrapping (loops back to start/end).
+- Playback Control: Supports jumping to specific tracks, toggling the pause state, and stopping the stream.
+- Audio Settings: If the Music bus exists, the Settings menu can be used to test and adjust playback volume.
+
+Key Methods:
+- play_track: Loads a specific stream from the playlist and begins playback.
+- next_track / prev_track: Increments or decrements the index to navigate the playlist.
+- toggle_pause: Resumes or pauses the current stream without resetting its position.
+
+Adjustable Parameters:
+- Playlist: An Array in the inspector where you can assign one or more AudioStream resources."
+autowrap_mode = 2
+
+[connection signal="pressed" from="Control/VBoxContainer2/VBoxContainer/Button_Play" to="." method="_on_button_play_pressed"]
+[connection signal="pressed" from="Control/VBoxContainer2/VBoxContainer/Button_Pause_Resume" to="." method="_on_button_pause_pressed"]
+[connection signal="pressed" from="Control/VBoxContainer2/VBoxContainer/Button_Stop" to="." method="_on_button_stop_pressed"]
+[connection signal="pressed" from="Control/VBoxContainer2/VBoxContainer/Button_Next" to="." method="_on_button_next_pressed"]
+[connection signal="pressed" from="Control/VBoxContainer2/VBoxContainer/Button_Previous" to="." method="_on_button_previous_pressed"]
+[connection signal="pressed" from="Control/VBoxContainer2/VBoxContainer/Button_SettingsMenu" to="." method="_on_button_settings_menu_pressed"]
diff --git a/docs/gyms/radio/lofi_01.ogg b/docs/gyms/radio/lofi_01.ogg
new file mode 100644
index 0000000..e465826
Binary files /dev/null and b/docs/gyms/radio/lofi_01.ogg differ
diff --git a/docs/gyms/radio/lofi_01.ogg.import b/docs/gyms/radio/lofi_01.ogg.import
new file mode 100644
index 0000000..025a2c9
--- /dev/null
+++ b/docs/gyms/radio/lofi_01.ogg.import
@@ -0,0 +1,19 @@
+[remap]
+
+importer="oggvorbisstr"
+type="AudioStreamOggVorbis"
+uid="uid://70cc8she43re"
+path="res://.godot/imported/lofi_01.ogg-0aacd597266806080412021811923091.oggvorbisstr"
+
+[deps]
+
+source_file="res://docs/gyms/radio/lofi_01.ogg"
+dest_files=["res://.godot/imported/lofi_01.ogg-0aacd597266806080412021811923091.oggvorbisstr"]
+
+[params]
+
+loop=false
+loop_offset=0
+bpm=0
+beat_count=0
+bar_beats=4
diff --git a/project.godot b/project.godot
index 23acade..b1a9573 100644
--- a/project.godot
+++ b/project.godot
@@ -19,6 +19,9 @@ config/icon="uid://bfar1kk3pgq8f"
[autoload]
UIEvents="*uid://dehu28iq27mbn"
+GameState="*uid://b17g2w2g101o6"
+CollectionManager="*uid://c3kq1qddpm8tf"
+AudioManager="*uid://dcttbbavtwtsg"
[display]
@@ -34,6 +37,39 @@ weather_vegetables_node=""
wind_node="Materials to apply wind"
weather_node=""
+[input]
+
+photo_pan_left={
+"deadzone": 0.2,
+"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":65,"key_label":0,"unicode":65,"location":0,"echo":false,"script":null)
+]
+}
+photo_pan_right={
+"deadzone": 0.2,
+"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":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null)
+]
+}
+photo_pan_up={
+"deadzone": 0.2,
+"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":87,"key_label":0,"unicode":119,"location":0,"echo":false,"script":null)
+]
+}
+photo_pan_down={
+"deadzone": 0.2,
+"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":83,"key_label":0,"unicode":115,"location":0,"echo":false,"script":null)
+]
+}
+take_photo={
+"deadzone": 0.2,
+"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,"location":0,"echo":false,"script":null)
+]
+}
+toggle_photo_mode={
+"deadzone": 0.2,
+"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":80,"key_label":0,"unicode":112,"location":0,"echo":false,"script":null)
+]
+}
+
[physics]
3d/physics_engine="Jolt Physics"