diff --git a/client/scripts/npc/move_applier.gd b/client/scripts/npc/move_applier.gd index 1aeaa0c..5b22c64 100644 --- a/client/scripts/npc/move_applier.gd +++ b/client/scripts/npc/move_applier.gd @@ -23,10 +23,11 @@ static func apply(moves: Array, game_state, canon_log, content_db, npc_id: Strin game_state.mark_revealed(str(args[0])) "give_item": var i: String = str(args[0]) - game_state.add_item(i, 1) - game_state.mark_gift_given(i) + 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.remove_item(str(args[0]), 1) + 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)) diff --git a/client/scripts/npc/npc_content.gd b/client/scripts/npc/npc_content.gd index 6c5bc4a..c4752d8 100644 --- a/client/scripts/npc/npc_content.gd +++ b/client/scripts/npc/npc_content.gd @@ -21,12 +21,20 @@ static func available_moves(npc_dict: Dictionary, game_state, canon_log) -> Arra moves.append("reveal(%s)" % t) for i in caps.get("giveable_items", []): - if not game_state.is_gift_given(i): + # Currency is exempt from the one-shot gift gate — an NPC can pay you twice. + if Currency.is_currency(i) or not game_state.is_gift_given(i): moves.append("give_item(%s)" % i) for item_id in game_state.inventory.keys(): if int(game_state.inventory[item_id]) > 0: moves.append("accept_item(%s)" % item_id) + # Money is not in the inventory dict, so it needs its own offer. Only the + # denominations the purse can actually pay: an NPC cannot ask a party holding + # 47c for a gold coin. + for denom in Currency.VALUES: + if game_state.purse_copper >= Currency.value(denom): + moves.append("accept_item(%s)" % denom) + moves.append_array(UNIVERSAL) return moves diff --git a/client/tests/unit/test_move_applier.gd b/client/tests/unit/test_move_applier.gd index 2a653c9..3fd940e 100644 --- a/client/tests/unit/test_move_applier.gd +++ b/client/tests/unit/test_move_applier.gd @@ -44,9 +44,40 @@ func test_become_hostile_sets_floor(): func test_accept_item_removes_from_inventory(): var gs = GameState.new() - gs.add_item("coin", 2) - MoveApplier.apply([_m("accept_item", ["coin"])], gs, CanonLog.new(), _content(), "fenn") - assert_eq(gs.inventory.get("coin", 0), 1) + gs.add_item("worn_shortsword", 2) + MoveApplier.apply([_m("accept_item", ["worn_shortsword"])], gs, CanonLog.new(), _content(), "fenn") + assert_eq(gs.inventory.get("worn_shortsword", 0), 1) + + +func test_give_item_currency_pays_the_purse(): + var gs = GameState.new() + MoveApplier.apply([_m("give_item", ["gold"])], gs, CanonLog.new(), _content(), "fenn") + assert_eq(gs.purse_copper, 10000) + assert_eq(gs.inventory.size(), 0, "money is never an inventory row") + + +func test_give_item_currency_is_not_a_one_shot_gift(): + # gifts_given is a GLOBAL one-shot keyed by item id. If money were marked, an + # NPC could hand over coin exactly once, ever, for the whole campaign. + var gs = GameState.new() + MoveApplier.apply([_m("give_item", ["gold"])], gs, CanonLog.new(), _content(), "fenn") + MoveApplier.apply([_m("give_item", ["gold"])], gs, CanonLog.new(), _content(), "fenn") + assert_eq(gs.purse_copper, 20000) + assert_false(gs.is_gift_given("gold"), "currency is exempt from the one-shot gate") + + +func test_give_item_real_item_still_marks_the_one_shot_gate(): + var gs = GameState.new() + MoveApplier.apply([_m("give_item", ["amulet"])], gs, CanonLog.new(), _content(), "fenn") + assert_eq(gs.inventory.get("amulet", 0), 1) + assert_true(gs.is_gift_given("amulet")) + + +func test_accept_item_currency_spends_the_purse(): + var gs = GameState.new() + gs.grant("gold", 1) + MoveApplier.apply([_m("accept_item", ["silver"])], gs, CanonLog.new(), _content(), "fenn") + assert_eq(gs.purse_copper, 9900) func test_refuse_and_end_are_noops(): diff --git a/client/tests/unit/test_npc_content.gd b/client/tests/unit/test_npc_content.gd index e4f7bf1..4914477 100644 --- a/client/tests/unit/test_npc_content.gd +++ b/client/tests/unit/test_npc_content.gd @@ -39,6 +39,39 @@ func test_revealed_topic_drops_out(): func test_accept_item_offered_per_held_item(): var gs = GameState.new() - gs.add_item("coin", 2) + gs.add_item("worn_shortsword", 2) var moves = NpcContent.available_moves(_fenn(), gs, CanonLog.new()) - assert_true("accept_item(coin)" in moves) + assert_true("accept_item(worn_shortsword)" in moves) + + +func test_a_broke_party_is_asked_for_nothing(): + var moves = NpcContent.available_moves(_fenn(), GameState.new(), CanonLog.new()) + for denom in ["copper", "silver", "gold"]: + assert_false("accept_item(%s)" % denom in moves, "an empty purse cannot pay a %s" % denom) + + +func test_accept_item_denomination_gated_on_affordability(): + var gs = GameState.new() + gs.grant("copper", 347) + var moves = NpcContent.available_moves(_fenn(), gs, CanonLog.new()) + assert_true("accept_item(copper)" in moves) + assert_true("accept_item(silver)" in moves) + assert_false("accept_item(gold)" in moves, "347c cannot pay a gold coin") + + +func test_a_gold_coin_is_asked_for_once_affordable(): + var gs = GameState.new() + gs.grant("gold", 1) + var moves = NpcContent.available_moves(_fenn(), gs, CanonLog.new()) + assert_true("accept_item(gold)" in moves) + + +func test_currency_gift_is_offered_repeatedly(): + var npc := _fenn() + npc["capabilities"]["giveable_items"] = ["copper", "amulet"] + var gs = GameState.new() + gs.mark_gift_given("copper") + gs.mark_gift_given("amulet") + var moves = NpcContent.available_moves(npc, gs, CanonLog.new()) + assert_true("give_item(copper)" in moves, "money is not a one-shot gift") + assert_false("give_item(amulet)" in moves, "a real item still is")