10const SETTINGS_PATH :=
"user://settings.cfg"
11const THEME_CATALOG_PATH :=
"res://data/themes/default.json"
12const CUSTOM_THEME_CATALOG_PATH :=
"user://custom_themes.json"
13const LOW_POWER_PRESET_PATH :=
"res://data/system/low_power_presets.json"
14const WINDOW_SIZE_OPTIONS := {
15 "Phone 390 x 844": Vector2i(390, 844),
16 "Small 480 x 800": Vector2i(480, 800),
17 "Square 800 x 800": Vector2i(800, 800),
18 "Desktop 1280 x 720": Vector2i(1280, 720)
20const TEXT_SIZE_OPTIONS := {
25const DEFAULT_THEME_OPTIONS := {
27 "High Contrast":
"high_contrast"
29const TEXT_SIZE_PRESETS := {
30 "small": {
"body": 15,
"caption": 13,
"button": 16,
"title": 25,
"list": 15},
31 "medium": {
"body": 17,
"caption": 15,
"button": 18,
"title": 31,
"list": 17},
32 "large": {
"body": 21,
"caption": 18,
"button": 22,
"title": 37,
"list": 21}
35signal settings_changed(settings:Dictionary)
38 "window_size": Vector2i(800, 800),
40 "master_volume": 0.85,
43 "master_muted": false,
47 "text_size":
"medium",
48 "low_power_mode": false,
51var theme_options := DEFAULT_THEME_OPTIONS.duplicate(true)
52var theme_typography := {}
53var low_power_profiles := {}
59 load_low_power_profiles()
61 apply_window_settings()
65func load_settings() -> void:
66 var file = ConfigFile.new()
67 if file.load(SETTINGS_PATH) != OK:
69 settings[
"window_size"] = file.get_value(
"display",
"window_size", settings[
"window_size"])
70 settings[
"fullscreen"] = bool(file.get_value(
"display",
"fullscreen", settings[
"fullscreen"]))
71 settings[
"theme"] = str(file.get_value(
"display",
"theme", settings[
"theme"]))
72 if not theme_options.values().has(settings[
"theme"]):
73 settings[
"theme"] =
"dark"
74 settings[
"text_size"] = str(file.get_value(
"display",
"text_size", settings[
"text_size"]))
75 settings[
"low_power_mode"] = bool(file.get_value(
"display",
"low_power_mode", settings[
"low_power_mode"]))
76 settings[
"master_volume"] = float(file.get_value(
"audio",
"master_volume", settings[
"master_volume"]))
77 settings[
"music_volume"] = float(file.get_value(
"audio",
"music_volume", settings[
"music_volume"]))
78 settings[
"sfx_volume"] = float(file.get_value(
"audio",
"sfx_volume", settings[
"sfx_volume"]))
79 settings[
"master_muted"] = bool(file.get_value(
"audio",
"master_muted", settings[
"master_muted"]))
80 settings[
"music_muted"] = bool(file.get_value(
"audio",
"music_muted", settings[
"music_muted"]))
81 settings[
"sfx_muted"] = bool(file.get_value(
"audio",
"sfx_muted", settings[
"sfx_muted"]))
82 settings[
"input_remaps"] = file.get_value(
"input",
"remaps", settings[
"input_remaps"])
86func save_settings() -> void:
87 var file = ConfigFile.new()
88 file.set_value(
"display",
"window_size", settings[
"window_size"])
89 file.set_value(
"display",
"fullscreen", settings[
"fullscreen"])
90 file.set_value(
"display",
"theme", settings[
"theme"])
91 file.set_value(
"display",
"text_size", settings[
"text_size"])
92 file.set_value(
"display",
"low_power_mode", settings[
"low_power_mode"])
93 file.set_value(
"audio",
"master_volume", settings[
"master_volume"])
94 file.set_value(
"audio",
"music_volume", settings[
"music_volume"])
95 file.set_value(
"audio",
"sfx_volume", settings[
"sfx_volume"])
96 file.set_value(
"audio",
"master_muted", settings[
"master_muted"])
97 file.set_value(
"audio",
"music_muted", settings[
"music_muted"])
98 file.set_value(
"audio",
"sfx_muted", settings[
"sfx_muted"])
99 file.set_value(
"input",
"remaps", settings[
"input_remaps"])
100 file.save(SETTINGS_PATH)
104func get_settings() -> Dictionary:
105 return settings.duplicate(true)
109func get_window_size_options() -> Dictionary:
110 return WINDOW_SIZE_OPTIONS.duplicate(true)
114func get_text_size_options() -> Dictionary:
115 return TEXT_SIZE_OPTIONS.duplicate(true)
119func get_theme_options() -> Dictionary:
120 if theme_options.is_empty():
122 return theme_options.duplicate(true)
126func load_low_power_profiles(file_path:String = LOW_POWER_PRESET_PATH) -> void:
127 var loaded_profiles := {}
128 var file = FileAccess.open(file_path, FileAccess.READ)
130 var data = JSON.parse_string(file.get_as_text())
131 if data
is Dictionary
and data.get(
"profiles", {})
is Dictionary:
132 for profile_id
in data.get(
"profiles", {}).keys():
133 var profile = data.get(
"profiles", {}).get(profile_id)
134 if profile
is Dictionary:
135 loaded_profiles[str(profile_id)] = profile.duplicate(true)
136 if loaded_profiles.is_empty():
137 loaded_profiles[
"default"] = _default_low_power_profile()
138 low_power_profiles = loaded_profiles
142func get_low_power_profile(profile_id:String =
"default") -> Dictionary:
143 if low_power_profiles.is_empty():
144 load_low_power_profiles()
145 var profile:Dictionary = low_power_profiles.get(profile_id, low_power_profiles.get(
"default", _default_low_power_profile()))
as Dictionary
146 return profile.duplicate(true)
150func load_theme_options(file_path:String = THEME_CATALOG_PATH) -> void:
152 var typography_defaults := _default_typography()
153 var loaded_typography := {}
154 for catalog_path
in _theme_catalog_paths(file_path):
155 var file = FileAccess.open(catalog_path, FileAccess.READ)
158 var data = JSON.parse_string(file.get_as_text())
159 if data
is Dictionary:
160 if data.get(
"typography", {})
is Dictionary:
161 typography_defaults = _merge_dict(typography_defaults, data.get(
"typography", {}))
162 var themes = data.get(
"themes", {})
163 if themes
is Dictionary:
164 for theme_id
in themes.keys():
165 var definition = themes[theme_id]
166 var label := str(theme_id).capitalize()
167 if definition
is Dictionary:
168 label = str(definition.get(
"name", label))
169 var theme_typography_definition = definition.get(
"typography", {})
170 if theme_typography_definition
is Dictionary:
171 loaded_typography[str(theme_id)] = _merge_dict(typography_defaults, theme_typography_definition)
172 loaded[label] = str(theme_id)
173 if loaded.is_empty():
174 loaded = DEFAULT_THEME_OPTIONS.duplicate(true)
175 theme_options = loaded
176 for theme_id
in theme_options.values():
177 if not loaded_typography.has(theme_id):
178 loaded_typography[theme_id] = typography_defaults.duplicate(true)
179 theme_typography = loaded_typography
182func _theme_catalog_paths(primary_path:String) -> Array:
183 var paths := [primary_path]
184 if primary_path == THEME_CATALOG_PATH
and FileAccess.file_exists(CUSTOM_THEME_CATALOG_PATH):
185 paths.append(CUSTOM_THEME_CATALOG_PATH)
190func get_text_size_preset(size_id:String =
"") -> Dictionary:
191 var resolved_id = str(settings[
"text_size"])
if size_id ==
"" else size_id
192 var theme_id := str(settings.get(
"theme",
"dark"))
193 return get_theme_text_size_preset(theme_id, resolved_id)
197func get_theme_text_size_preset(theme_id:String =
"", size_id:String =
"") -> Dictionary:
198 if theme_typography.is_empty():
200 var resolved_theme := str(settings.get(
"theme",
"dark"))
if theme_id ==
"" else theme_id
201 var resolved_size := str(settings.get(
"text_size",
"medium"))
if size_id ==
"" else size_id
202 var typography = theme_typography.get(resolved_theme, _default_typography())
203 var base_sizes:Dictionary = typography.get(
"base_sizes", TEXT_SIZE_PRESETS[
"medium"])
204 var scale_presets:Dictionary = typography.get(
"scale_presets", {
"small": 0.88,
"medium": 1.0,
"large": 1.22})
205 var scale := float(scale_presets.get(resolved_size, 1.0))
207 for key
in TEXT_SIZE_PRESETS[
"medium"].keys():
208 preset[key] = max(1, int(round(float(base_sizes.get(key, TEXT_SIZE_PRESETS[
"medium"][key])) * scale)))
209 preset[
"button_height_multiplier"] = float(typography.get(
"button_height_multiplier", 2.4))
210 preset[
"font_paths"] = typography.get(
"font_paths", {})
215func set_window_size(value:Vector2i) -> void:
216 settings[
"window_size"] = value
217 apply_window_settings()
221func set_window_size_by_label(label:String) -> void:
222 if not WINDOW_SIZE_OPTIONS.has(label):
224 set_window_size(WINDOW_SIZE_OPTIONS[label])
228func set_fullscreen(value:bool) -> void:
229 settings[
"fullscreen"] = value
230 apply_window_settings()
235func set_text_size(value:String) -> void:
236 if not TEXT_SIZE_PRESETS.has(value):
238 settings[
"text_size"] = value
242func set_text_size_by_label(label:String) -> void:
243 if not TEXT_SIZE_OPTIONS.has(label):
245 set_text_size(TEXT_SIZE_OPTIONS[label])
249func set_theme(value:String) -> void:
250 if not get_theme_options().values().has(value):
252 settings[
"theme"] = value
256func set_theme_by_label(label:String) -> void:
257 var options = get_theme_options()
258 if not options.has(label):
260 set_theme(options[label])
264func set_low_power_mode(value:bool) -> void:
265 settings[
"low_power_mode"] = value
270func is_low_power_mode() -> bool:
271 return bool(settings.get(
"low_power_mode", false))
275func set_audio_value(key:String, value) -> void:
276 if not settings.has(key):
278 settings[key] = value
283func set_input_remaps(value:Dictionary) -> void:
284 settings[
"input_remaps"] = value.duplicate(true)
289func get_input_remaps() -> Dictionary:
290 return settings.get(
"input_remaps", {}).duplicate(true)
294func apply_text_size(root:Node) -> void:
297 var preset = get_text_size_preset()
298 _apply_text_size_recursive(root, preset)
301func apply_window_settings() -> void:
302 if bool(settings[
"fullscreen"]):
303 DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN)
305 DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
306 DisplayServer.window_set_size(settings[
"window_size"])
309func _commit() -> void:
311 settings_changed.emit(get_settings())
314func _apply_text_size_recursive(node:Node, preset:Dictionary) -> void:
318 var node_name = str(node.name).to_lower()
319 var size_key =
"body"
320 if node_name.contains(
"title"):
322 elif node_name.contains(
"description")
or node_name.contains(
"info")
or node_name.contains(
"subtitle")
or node_name.contains(
"status")
or node_name.contains(
"details")
or node_name.contains(
"preview"):
324 var font_role:String = size_key
325 var font_size := int(preset[size_key])
326 if node.has_meta(
"max_font_size"):
327 font_size = min(font_size, int(node.get_meta(
"max_font_size")))
328 if node.has_meta(
"text_size_role"):
329 var role := str(node.get_meta(
"text_size_role"))
332 font_size = int(preset[role])
333 if node.has_meta(
"max_font_size"):
334 font_size = min(font_size, int(node.get_meta(
"max_font_size")))
335 node.add_theme_font_size_override(
"font_size", font_size)
336 _apply_font_override(node, preset, font_role)
337 if node
is Button
or node
is OptionButton
or node
is CheckButton:
338 var button_font_size := int(preset[
"button"])
339 if node.has_meta(
"max_font_size"):
340 button_font_size = min(button_font_size, int(node.get_meta(
"max_font_size")))
341 node.add_theme_font_size_override(
"font_size", button_font_size)
342 _apply_font_override(node, preset,
"button")
344 node.custom_minimum_size.y = max(float(node.custom_minimum_size.y), float(button_font_size) * float(preset.get(
"button_height_multiplier", 2.4)))
345 if node
is AcceptDialog:
346 node.add_theme_font_size_override(
"font_size", int(preset[
"body"]))
347 if node.get_ok_button() != null:
348 node.get_ok_button().add_theme_font_size_override(
"font_size", int(preset[
"button"]))
349 node.get_ok_button().custom_minimum_size.y = max(float(node.get_ok_button().custom_minimum_size.y), float(preset[
"button"]) * 2.4)
350 if node.get_cancel_button() != null:
351 node.get_cancel_button().add_theme_font_size_override(
"font_size", int(preset[
"button"]))
352 node.get_cancel_button().custom_minimum_size.y = max(float(node.get_cancel_button().custom_minimum_size.y), float(preset[
"button"]) * 2.4)
354 node.add_theme_font_size_override(
"font_size", int(preset[
"list"]))
355 if node
is RichTextLabel:
356 node.add_theme_font_size_override(
"normal_font_size", int(preset[
"body"]))
357 for child
in node.get_children():
358 _apply_text_size_recursive(child, preset)
361func _apply_font_override(node:Control, preset:Dictionary, role:String) -> void:
362 var font_paths = preset.get(
"font_paths", {})
363 if not font_paths
is Dictionary:
365 var path := str(font_paths.get(role, font_paths.get(
"default",
"")))
368 if not ResourceLoader.exists(path):
370 var font = font_cache.get(path)
373 font_cache[path] = font
375 node.add_theme_font_override(
"font", font)
378func _default_typography() -> Dictionary:
380 "font_paths": {
"default":
"",
"title":
"",
"button":
"",
"mono":
""},
381 "base_sizes": TEXT_SIZE_PRESETS[
"medium"].duplicate(true),
382 "scale_presets": {
"small": 0.88,
"medium": 1.0,
"large": 1.22},
383 "button_height_multiplier": 2.4
387func _default_low_power_profile() -> Dictionary:
389 "audio": {
"queue_next_music_track": false},
391 "max_field_powerups": 1,
392 "powerup_spawn_pace_multiplier": 0.55,
393 "minimum_powerup_spawn_pace": 0.25,
394 "powerup_lifetime": 8.0,
395 "held_powerup_icon_size": 22,
396 "show_active_effect_text": false
399 "prefer_static_effects": true,
400 "background_animation_rate": 0.0,
401 "decorative_effect_limit": 0
404 "warm_optional_screens": false,
405 "preserve_source_images": true
410func _merge_dict(base:Dictionary, override:Dictionary) -> Dictionary:
411 var merged := base.duplicate(true)
412 for key
in override.keys():
413 if merged.get(key)
is Dictionary
and override[key]
is Dictionary:
414 merged[key] = _merge_dict(merged[key], override[key])
416 merged[key] = override[key]