class_name MoveApplier extends RefCounted ## Writes validated §6 moves to game state. Town-NPC disposition lives in ## GameState.npc_dispositions (NOT CanonLog.party — that is companions only). ## Called by the conversation caller after NpcService.speak, so state writes ## stay explicit (§2). Unknown move names are ignored (they never pass the ## validator, but this stays defensive). const MAX_DELTA := 15 # one line cannot swing standing wholesale (§7 spirit) const HOSTILE_FLOOR := -100 static func apply(moves: Array, game_state, canon_log, content_db, npc_id: String) -> void: for move in moves: var name: String = move.get("name", "") var args: Array = move.get("args", []) match name: "offer_quest": var q: String = str(args[0]) var qd: Dictionary = content_db.quest(q) canon_log.add_quest(q, qd.get("name", ""), qd.get("objective", "")) "reveal": game_state.mark_revealed(str(args[0])) "give_item": var i: String = str(args[0]) game_state.grant(i, 1) if not Currency.is_currency(i): game_state.mark_gift_given(i) # the one-shot gate is for real items only "accept_item": game_state.take(str(args[0]), 1) "adjust_disposition": var delta: int = clampi(int(str(args[0])), -MAX_DELTA, MAX_DELTA) var cur: int = int(game_state.npc_dispositions.get(npc_id, 0)) game_state.set_npc_disposition(npc_id, cur + delta) "become_hostile": game_state.set_npc_disposition(npc_id, HOSTILE_FLOOR) _: pass # refuse, end_conversation, and anything unknown: no state