49 lines
1.8 KiB
GDScript
49 lines
1.8 KiB
GDScript
class_name NpcService
|
|
extends RefCounted
|
|
## The client /npc/speak loop (§6). Posts persona-free request (npc_id +
|
|
## disposition + client-computed available_moves + utterance) through an injected
|
|
## transport, degrades to an authored NPC fallback on ANY failure, extracts tags
|
|
## with TagExtractor, validates moves against available_moves (MoveValidator),
|
|
## and returns an NpcResult. It applies NO state — the caller runs MoveApplier,
|
|
## keeping state writes explicit (§2).
|
|
|
|
const ENDING_MOVES := ["end_conversation", "become_hostile"]
|
|
|
|
var _transport: DmTransport
|
|
var _fallback: FallbackLibrary
|
|
|
|
|
|
func _init(transport: DmTransport, fallback: FallbackLibrary) -> void:
|
|
_transport = transport
|
|
_fallback = fallback
|
|
|
|
|
|
func speak(npc_id: String, utterance: String, canon_log: CanonLog,
|
|
disposition: int, available_moves: Array) -> NpcResult:
|
|
var resp: DmResponse = await _transport.post_json("/npc/speak", {
|
|
"canon_log": canon_log.to_dict(),
|
|
"npc_id": npc_id,
|
|
"disposition": disposition,
|
|
"available_moves": available_moves,
|
|
"utterance": utterance,
|
|
})
|
|
if not resp.transport_ok or resp.status != 200 \
|
|
or typeof(resp.body) != TYPE_DICTIONARY or not resp.body.has("prose"):
|
|
return NpcResult.fallback(_fallback.npc_line())
|
|
|
|
var extracted := TagExtractor.extract(str(resp.body["prose"]))
|
|
var moves: Array = extracted["moves"].duplicate()
|
|
# Fold the [ADJUST_DISPOSITION:n] alias into the move stream so the validator
|
|
# sees one uniform shape (§ tag convention).
|
|
for delta in extracted["dispositions"]:
|
|
moves.append({"name": "adjust_disposition", "args": [str(delta)]})
|
|
|
|
var checked := MoveValidator.validate(moves, available_moves)
|
|
var ends := false
|
|
for m in checked["valid"]:
|
|
if m["name"] in ENDING_MOVES:
|
|
ends = true
|
|
return NpcResult.new(
|
|
extracted["clean_text"], extracted["facts"],
|
|
checked["valid"], checked["dropped"], ends, false)
|