From 32fa37212444be02be723c3539b82ce093bdb71b Mon Sep 17 00:00:00 2001 From: Phillip Tarrant Date: Fri, 10 Jul 2026 09:59:54 -0500 Subject: [PATCH] =?UTF-8?q?feat(client):=20FallbackLibrary=20+=20authored?= =?UTF-8?q?=20narrator=20fallback=20(=C2=A713)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- client/content/fallback/narrator.json | 5 +++ client/scripts/net/fallback_library.gd | 37 +++++++++++++++++++ client/scripts/net/fallback_library.gd.uid | 1 + client/tests/unit/test_fallback_library.gd | 33 +++++++++++++++++ .../tests/unit/test_fallback_library.gd.uid | 1 + 5 files changed, 77 insertions(+) create mode 100644 client/content/fallback/narrator.json create mode 100644 client/scripts/net/fallback_library.gd create mode 100644 client/scripts/net/fallback_library.gd.uid create mode 100644 client/tests/unit/test_fallback_library.gd create mode 100644 client/tests/unit/test_fallback_library.gd.uid diff --git a/client/content/fallback/narrator.json b/client/content/fallback/narrator.json new file mode 100644 index 0000000..a77b885 --- /dev/null +++ b/client/content/fallback/narrator.json @@ -0,0 +1,5 @@ +{ + "narrator": [ + "The rain keeps on. The room ahead is dark and colder than it should be, and whatever is in it has been there a while." + ] +} diff --git a/client/scripts/net/fallback_library.gd b/client/scripts/net/fallback_library.gd new file mode 100644 index 0000000..408a7be --- /dev/null +++ b/client/scripts/net/fallback_library.gd @@ -0,0 +1,37 @@ +class_name FallbackLibrary +extends RefCounted +## Authored degraded-DM fallback text (charter §13). The client shows one of +## these lines on ANY failure, in the Narrator's voice — never an error. If the +## content file is missing or malformed the library returns a hardcoded +## constant, so the fallback itself cannot fail. + +const PATH := "res://content/fallback/narrator.json" +const LAST_RESORT := "The chamber is cold. Something waits in the dark." + +var _lines: Array = [] + + +func _init(path := PATH) -> void: + _lines = _load(path) + + +func narrator_line() -> String: + return str(_lines[0]) if not _lines.is_empty() else LAST_RESORT + + +static func _load(path: String) -> Array: + if not FileAccess.file_exists(path): + return [] + var text := FileAccess.get_file_as_string(path) + # Use the instance API, not JSON.parse_string(): the static convenience + # method pushes an engine-level error on malformed input, which is noisy + # and (in this project's test config) turns a handled failure into a + # reported test error. The instance parse() just returns an Error code. + var json := JSON.new() + if json.parse(text) != OK: + return [] + var data = json.get_data() + if typeof(data) != TYPE_DICTIONARY: + return [] + var lines = data.get("narrator", []) + return lines if typeof(lines) == TYPE_ARRAY else [] diff --git a/client/scripts/net/fallback_library.gd.uid b/client/scripts/net/fallback_library.gd.uid new file mode 100644 index 0000000..341a94b --- /dev/null +++ b/client/scripts/net/fallback_library.gd.uid @@ -0,0 +1 @@ +uid://b8jhu0e4yg6tf diff --git a/client/tests/unit/test_fallback_library.gd b/client/tests/unit/test_fallback_library.gd new file mode 100644 index 0000000..e517993 --- /dev/null +++ b/client/tests/unit/test_fallback_library.gd @@ -0,0 +1,33 @@ +extends "res://addons/gut/test.gd" + +const FallbackLibrary = preload("res://scripts/net/fallback_library.gd") + + +func test_loads_authored_line_from_default_file(): + var lib = FallbackLibrary.new() + # The authored line is distinct from LAST_RESORT, so a match proves the file was read. + assert_ne(lib.narrator_line(), FallbackLibrary.LAST_RESORT) + assert_true(lib.narrator_line().length() > 0) + + +func test_missing_file_uses_last_resort(): + var lib = FallbackLibrary.new("res://content/fallback/does_not_exist.json") + assert_eq(lib.narrator_line(), FallbackLibrary.LAST_RESORT) + + +func test_malformed_file_uses_last_resort(): + var path := "user://bad_fallback.json" + var f := FileAccess.open(path, FileAccess.WRITE) + f.store_string("{ this is not valid json") + f.close() + var lib = FallbackLibrary.new(path) + assert_eq(lib.narrator_line(), FallbackLibrary.LAST_RESORT) + + +func test_empty_list_uses_last_resort(): + var path := "user://empty_fallback.json" + var f := FileAccess.open(path, FileAccess.WRITE) + f.store_string('{"narrator": []}') + f.close() + var lib = FallbackLibrary.new(path) + assert_eq(lib.narrator_line(), FallbackLibrary.LAST_RESORT) diff --git a/client/tests/unit/test_fallback_library.gd.uid b/client/tests/unit/test_fallback_library.gd.uid new file mode 100644 index 0000000..19a8c5f --- /dev/null +++ b/client/tests/unit/test_fallback_library.gd.uid @@ -0,0 +1 @@ +uid://bs7jxst7fmyqx