feat(client): ConsideringPhrases — authored rotating think-lines (§13)
This commit is contained in:
9
client/content/considering.json
Normal file
9
client/content/considering.json
Normal file
@@ -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."
|
||||
]
|
||||
}
|
||||
41
client/scripts/ui/considering_phrases.gd
Normal file
41
client/scripts/ui/considering_phrases.gd
Normal file
@@ -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 []
|
||||
33
client/tests/unit/test_considering_phrases.gd
Normal file
33
client/tests/unit/test_considering_phrases.gd
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user