Finding 1 (important): test_same_seed_yields_an_identical_character only compared two construct() runs against each other, so swapping dex/con in Attributes.IDS still passed the whole suite while every persisted seed would silently become a different character. Added a golden-vector test that pins one fixed seed to its exact rolled attributes and hidden Luck, so reordering Attributes.IDS or moving the Luck roll fails loudly instead of silently rewriting every saved character. Proved it: swapped IDS, watched the golden vector fail, restored, confirmed green. Also: _validate_spend no longer lets a negative value mask the over-pool check (continue after each error instead of falling through to `total += v`); non-integer spend values (float/bool/string) are now rejected instead of coerced; a missing or non-integer seed is now a validation error instead of silently defaulting to 0. GameState's header comment no longer advertises "stats" it no longer holds. 243 -> 248 tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QYa9u7Kdxv5gX4AnwWexy8
81 lines
2.3 KiB
GDScript
81 lines
2.3 KiB
GDScript
class_name GameState
|
||
extends RefCounted
|
||
## Luck-centric game state (spec decision 5). The SOLE home of numeric Luck,
|
||
## the character sheet, world-NPC dispositions, and inventory — none of which
|
||
## enter the canon log (charter §7/§2). The log reads only luck_descriptor()
|
||
## from here.
|
||
|
||
var luck: int = 0
|
||
var luck_base: int = 0
|
||
var sheet: CharacterSheet = null # the whole character (§2). Built by NewGame.
|
||
var npc_dispositions: Dictionary = {} # world-npc id -> int (−100..100)
|
||
var inventory: Dictionary = {} # item_id -> qty (NEVER money — see purse_copper)
|
||
var purse_copper: int = 0 # ALL money, in copper. One integer (§4.1 of the spec).
|
||
var revealed_topics: Dictionary = {} # topic_id -> true
|
||
var gifts_given: Dictionary = {} # item_id -> true (idempotency for give_item)
|
||
|
||
|
||
func set_luck(v: int) -> void:
|
||
luck = clampi(v, Luck.MIN, Luck.MAX)
|
||
|
||
|
||
func drift_luck(delta: int) -> void:
|
||
luck = Luck.drift(luck, luck_base, delta)
|
||
|
||
|
||
func luck_descriptor() -> String:
|
||
return Luck.descriptor(luck)
|
||
|
||
|
||
func set_npc_disposition(id: String, v: int) -> void:
|
||
npc_dispositions[id] = clampi(v, -100, 100)
|
||
|
||
|
||
func add_item(item_id: String, qty: int) -> void:
|
||
inventory[item_id] = int(inventory.get(item_id, 0)) + qty
|
||
|
||
|
||
func remove_item(item_id: String, qty: int) -> void:
|
||
var left := int(inventory.get(item_id, 0)) - qty
|
||
if left > 0:
|
||
inventory[item_id] = left
|
||
else:
|
||
inventory.erase(item_id)
|
||
|
||
|
||
func add_copper(amount: int) -> void:
|
||
purse_copper = maxi(0, purse_copper + amount)
|
||
|
||
|
||
func grant(item_id: String, qty: int) -> void:
|
||
# The single routing point (with take): currency lands in the purse, every
|
||
# other item in the inventory dict. Money is never an inventory row, so it
|
||
# can never surface in an inventory grid — exclusion by construction.
|
||
if Currency.is_currency(item_id):
|
||
add_copper(Currency.value(item_id) * qty)
|
||
else:
|
||
add_item(item_id, qty)
|
||
|
||
|
||
func take(item_id: String, qty: int) -> void:
|
||
if Currency.is_currency(item_id):
|
||
add_copper(-Currency.value(item_id) * qty)
|
||
else:
|
||
remove_item(item_id, qty)
|
||
|
||
|
||
func mark_revealed(topic_id: String) -> void:
|
||
revealed_topics[topic_id] = true
|
||
|
||
|
||
func is_revealed(topic_id: String) -> bool:
|
||
return revealed_topics.has(topic_id)
|
||
|
||
|
||
func mark_gift_given(item_id: String) -> void:
|
||
gifts_given[item_id] = true
|
||
|
||
|
||
func is_gift_given(item_id: String) -> bool:
|
||
return gifts_given.has(item_id)
|