diff --git a/client/content/considering.json b/client/content/considering.json new file mode 100644 index 0000000..2704bbd --- /dev/null +++ b/client/content/considering.json @@ -0,0 +1,9 @@ +{ + "considering": [ + "The Master considers your words.", + "Somewhere, dice are gathered.", + "The tale weighs its next turn.", + "The dark shifts, thinking.", + "A page is turned, unhurried." + ] +} diff --git a/client/scripts/ui/considering_phrases.gd b/client/scripts/ui/considering_phrases.gd new file mode 100644 index 0000000..2e397d8 --- /dev/null +++ b/client/scripts/ui/considering_phrases.gd @@ -0,0 +1,41 @@ +class_name ConsideringPhrases +extends RefCounted +## Authored "the GM is thinking" lines (charter §13 — even loading is content). +## Pure: cycles a phrase pool in order on a running index that is instance state +## (spec decision 4 — the indicator reuses one instance so the first phrase varies +## turn to turn without randomness). A missing/malformed file degrades to one +## hardcoded constant, so the indicator itself cannot fail. + +const PATH := "res://content/considering.json" +const LAST_RESORT := "The Master considers your words." + +var _pool: Array = [] +var _index: int = 0 + + +func _init(path := PATH) -> void: + _pool = _load(path) + + +func next() -> String: + if _pool.is_empty(): + return LAST_RESORT + var phrase := str(_pool[_index % _pool.size()]) + _index += 1 + return phrase + + +static func _load(path: String) -> Array: + if not FileAccess.file_exists(path): + return [] + var text := FileAccess.get_file_as_string(path) + # Instance parse() (not JSON.parse_string) so malformed input returns an + # Error code instead of the engine error the test config promotes to a fail. + 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("considering", []) + return lines if typeof(lines) == TYPE_ARRAY else [] diff --git a/client/tests/unit/test_considering_phrases.gd b/client/tests/unit/test_considering_phrases.gd new file mode 100644 index 0000000..db2c92d --- /dev/null +++ b/client/tests/unit/test_considering_phrases.gd @@ -0,0 +1,33 @@ +extends "res://addons/gut/test.gd" + +const ConsideringPhrases = preload("res://scripts/ui/considering_phrases.gd") + + +func test_missing_file_returns_last_resort(): + var p = ConsideringPhrases.new("res://content/does_not_exist.json") + assert_eq(p.next(), ConsideringPhrases.LAST_RESORT) + assert_eq(p.next(), ConsideringPhrases.LAST_RESORT) + + +func test_cycles_pool_in_order_and_wraps(): + # Real authored file has at least 3 lines; assert order + wrap on it. + var p = ConsideringPhrases.new() # default path — real content + var first = p.next() + var second = p.next() + assert_ne(first, "") + assert_ne(first, second) # advances, not stuck + # Drain a full cycle back to `first` to prove it wraps (bounded loop). + var seen := [first, second] + for i in range(20): + var n = p.next() + if n == first: + return # wrapped — success + seen.append(n) + assert_true(false, "did not wrap within 22 calls: %s" % [seen]) + + +func test_index_persists_across_calls_not_reset(): + var p = ConsideringPhrases.new() + var a = p.next() + var b = p.next() + assert_ne(a, b) # second call did not re-return the first phrase