Add mixer/accessibility settings, caption/reduce-motion hooks, audio volume clamping, a 24-NPC performance smoke and shortened coat/dress stride for hem clipping. Record the explicit Godot 4.3-CI/4.7-local matrix and the 0.9 production scope. Headless 250/250, Python 94/94, 24-NPC budget smoke passes.
105 lines
3.3 KiB
GDScript
105 lines
3.3 KiB
GDScript
extends Node
|
|
## Autoload for local persistence (JSON in user://). Wrapped in an autoload so UI
|
|
## nodes never touch the filesystem directly.
|
|
|
|
const SHIFT_RESULTS_FILE := "shift_results.json"
|
|
const SETTINGS_FILE := "settings.json"
|
|
const CAMPAIGN_FILE := "campaign.json"
|
|
const SHIFT_STATE_FILE := "shift_state.json"
|
|
|
|
func _load_json(file_name: String) -> Variant:
|
|
var path := "user://" + file_name
|
|
if not FileAccess.file_exists(path):
|
|
return null
|
|
var text := FileAccess.get_file_as_string(path)
|
|
if text.is_empty():
|
|
return null
|
|
var parsed = JSON.parse_string(text)
|
|
return parsed
|
|
|
|
func _save_json(file_name: String, data: Variant) -> Error:
|
|
var path := "user://" + file_name
|
|
var temp := path + ".tmp"
|
|
var text := JSON.stringify(data, "\t")
|
|
var f := FileAccess.open(temp, FileAccess.WRITE)
|
|
if f == null:
|
|
return FileAccess.get_open_error()
|
|
f.store_string(text)
|
|
f.close()
|
|
# Replace the target only after the complete JSON has been written, so a
|
|
# crash cannot leave a partial save in place.
|
|
return DirAccess.rename_absolute(ProjectSettings.globalize_path(temp),
|
|
ProjectSettings.globalize_path(path))
|
|
|
|
## Load previous shift results. Returns Array of Dictionaries or [].
|
|
func load_shift_results() -> Array:
|
|
var data = _load_json(SHIFT_RESULTS_FILE)
|
|
if data is Array:
|
|
return data
|
|
return []
|
|
|
|
## Append one shift result and save. Returns Error.
|
|
func save_shift_result(result: Dictionary) -> Error:
|
|
var results := load_shift_results()
|
|
results.append(result)
|
|
return _save_json(SHIFT_RESULTS_FILE, results)
|
|
|
|
## Campaign/session progression (separate from settings and shift history).
|
|
func load_campaign() -> Dictionary:
|
|
var data = _load_json(CAMPAIGN_FILE)
|
|
return data if data is Dictionary else {}
|
|
|
|
func save_campaign(data: Dictionary) -> Error:
|
|
return _save_json(CAMPAIGN_FILE, data)
|
|
|
|
func reset_campaign() -> Error:
|
|
return _save_json(CAMPAIGN_FILE, {})
|
|
|
|
## Mid-shift checkpoint. Kept separate so a completed shift history is never
|
|
## overwritten by a resumable snapshot.
|
|
func load_shift_state() -> Dictionary:
|
|
var data = _load_json(SHIFT_STATE_FILE)
|
|
return data if data is Dictionary else {}
|
|
|
|
func save_shift_state(data: Dictionary) -> Error:
|
|
return _save_json(SHIFT_STATE_FILE, data)
|
|
|
|
func clear_shift_state() -> Error:
|
|
return _save_json(SHIFT_STATE_FILE, {})
|
|
|
|
## Defaults for player settings. Kept in code so a missing or partial
|
|
## settings.json still produces a usable config.
|
|
const DEFAULT_SETTINGS := {
|
|
"look_sensitivity": 0.0022,
|
|
"hud_font_scale": 1.0,
|
|
"invert_y": false,
|
|
"head_bob": true,
|
|
"input_bindings": {},
|
|
"ai_difficulty": "normal",
|
|
"tutorial_completed": false,
|
|
"tutorial_skipped": false,
|
|
"master_volume": 1.0,
|
|
"sfx_volume": 1.0,
|
|
"ambience_volume": 1.0,
|
|
"reduce_motion": false,
|
|
"captions_enabled": true,
|
|
}
|
|
|
|
## Load player settings, merged over the defaults. Returns a Dictionary.
|
|
func load_settings() -> Dictionary:
|
|
var merged: Dictionary = DEFAULT_SETTINGS.duplicate(true)
|
|
var data = _load_json(SETTINGS_FILE)
|
|
if data is Dictionary:
|
|
for key in data.keys():
|
|
if merged.has(key):
|
|
merged[key] = data[key]
|
|
return merged
|
|
|
|
## Persist player settings. Returns Error.
|
|
func save_settings(settings: Dictionary) -> Error:
|
|
var merged: Dictionary = DEFAULT_SETTINGS.duplicate(true)
|
|
for key in settings.keys():
|
|
if merged.has(key):
|
|
merged[key] = settings[key]
|
|
return _save_json(SETTINGS_FILE, merged)
|