extends GutTest ## Ground loot and the item actions that move things on and off it. ## ## Everything here goes through [method SimWorld.queue_input] rather than ## calling the private handlers, because the whole point of putting item actions ## in the input frame is that they are subject to the same rules as movement. ## A test that reached past that would prove nothing about what a client can ## actually do. const ME := 1 const THEM := 2 var world: SimWorld func before_each() -> void: world = SimWorld.new(7) var me := world.add_player(ME, "me") me.pos = Vector2.ZERO me.spawn_grace = 0 ## One tick of held input. Ticks have to advance or queue_input discards the ## frame as a duplicate, which is exactly what it should do. func _hold(peer: int, ticks: int, buttons: int, slot: int = 0) -> void: for _i in ticks: var frames: Array[InputFrame] = [ InputFrame.make(world.tick + 1, Vector2.ZERO, 0.0, buttons, slot)] world.queue_input(peer, frames) world.step() ## Press and release, so the next press is a fresh edge. func _tap(peer: int, buttons: int, slot: int = 0) -> void: _hold(peer, 1, buttons, slot) _hold(peer, 1, 0, slot) func _events_of(kind: int) -> Array: var out := [] for ev in world.events: if int(ev["t"]) == kind: out.append(ev) return out # --- Picking up ------------------------------------------------------------- func test_pressing_interact_next_to_an_item_takes_it() -> void: world.spawn_loot(Items.HEALTH_POTION, Vector2(20.0, 0.0)) _tap(ME, InputFrame.BTN_INTERACT) assert_eq(world.players[ME].inventory[0], Items.HEALTH_POTION) assert_eq(world.loot.size(), 0, "and it leaves the floor") assert_eq(_events_of(SimEvent.Type.ITEM_PICKED_UP).size(), 1) func test_an_item_out_of_reach_is_not_taken() -> void: world.spawn_loot(Items.HEALTH_POTION, Vector2(SimConfig.LOOT_PICKUP_RADIUS + 20.0, 0.0)) _tap(ME, InputFrame.BTN_INTERACT) assert_eq(world.players[ME].free_slot(), 0, "nothing should have been picked up") assert_eq(world.loot.size(), 1) ## Interact is held down while walking around, and the client repeats the last ## few frames every tick. Level-triggered, standing on a pile would hoover it up ## in four ticks. func test_holding_interact_takes_exactly_one_item() -> void: for i in 3: world.spawn_loot(Items.HEALTH_POTION, Vector2(float(i) * 4.0, 0.0)) _hold(ME, 30, InputFrame.BTN_INTERACT) assert_eq(world.loot.size(), 2, "two items should still be on the floor") func test_releasing_and_pressing_again_takes_the_next_one() -> void: world.spawn_loot(Items.HEALTH_POTION, Vector2(4.0, 0.0)) world.spawn_loot(Items.HEALTH_POTION, Vector2(8.0, 0.0)) _tap(ME, InputFrame.BTN_INTERACT) _tap(ME, InputFrame.BTN_INTERACT) assert_eq(world.loot.size(), 0) ## Nothing is destroyed by a full bag. Silently deleting the item would be the ## kind of loss a player cannot be compensated for. func test_a_full_inventory_leaves_the_item_on_the_floor() -> void: var me := world.players[ME] for _i in SimConfig.INVENTORY_SLOTS: me.add_item(Items.WARDENS_RATION) world.spawn_loot(Items.HEALTH_POTION, Vector2(10.0, 0.0)) _tap(ME, InputFrame.BTN_INTERACT) assert_eq(world.loot.size(), 1, "the item must survive a failed pickup") assert_eq(_events_of(SimEvent.Type.ITEM_PICKED_UP).size(), 0) ## The portal and pickup share the interact key. A full bag must not leave a ## player standing on the portal unable to use it. func test_a_failed_pickup_does_not_block_the_portal() -> void: world.portal_enabled = true world.portal_pos = Vector2.ZERO var me := world.players[ME] for _i in SimConfig.INVENTORY_SLOTS: me.add_item(Items.WARDENS_RATION) world.spawn_loot(Items.HEALTH_POTION, Vector2(10.0, 0.0)) _hold(ME, 1, InputFrame.BTN_INTERACT) assert_gt(_events_of(SimEvent.Type.PORTAL_USED).size(), 0, "the portal should still answer when the pickup could not happen") # --- Using ------------------------------------------------------------------ func test_using_a_potion_heals_and_consumes_it() -> void: var me := world.players[ME] me.add_item(Items.HEALTH_POTION) me.hp = 10 _tap(ME, InputFrame.BTN_USE, 0) assert_gt(me.hp, 10, "the potion should have healed") assert_eq(me.inventory[0], Items.NONE, "and been consumed") assert_eq(_events_of(SimEvent.Type.ITEM_USED).size(), 1) ## A mistimed keypress must not cost a potion. Refusing the use is the only way ## to make that true, since nothing else in the game asks for confirmation. func test_a_potion_at_full_health_is_refused_rather_than_wasted() -> void: var me := world.players[ME] me.add_item(Items.HEALTH_POTION) me.hp = me.max_hp _tap(ME, InputFrame.BTN_USE, 0) assert_eq(me.inventory[0], Items.HEALTH_POTION, "it should still be there") ## Refilling the slot mid-hold is what makes this test bite. Checking only that ## one potion was spent proves nothing: a level-triggered bug empties the slot ## on the first tick and then finds nothing left to spend either way. func test_holding_the_use_key_spends_exactly_one_potion() -> void: var me := world.players[ME] me.add_item(Items.HEALTH_POTION) me.hp = 1 _hold(ME, 5, InputFrame.BTN_USE, 0) assert_eq(me.inventory[0], Items.NONE, "the first press should drink it") # Same key still down, same slot, a fresh potion in it. me.inventory[0] = Items.HEALTH_POTION me.hp = 1 _hold(ME, 20, InputFrame.BTN_USE, 0) assert_eq(me.inventory[0], Items.HEALTH_POTION, "a held key is one action, however many frames carry it") ## Tapping a second slot while the first is still held is a real second action, ## so the slot has to be part of the edge and not just the button bit. func test_a_different_slot_while_held_is_a_second_action() -> void: var me := world.players[ME] me.add_item(Items.WARDENS_RATION) me.add_item(Items.WARDENS_RATION) _hold(ME, 3, InputFrame.BTN_USE, 0) _hold(ME, 3, InputFrame.BTN_USE, 1) assert_eq(me.inventory[0], Items.NONE) assert_eq(me.inventory[1], Items.NONE) ## The ration has no effect, and is still spent. "Does nothing" has to mean a ## completed transaction, or it proves nothing about the path it exists to test. func test_a_useless_item_is_still_consumed() -> void: var me := world.players[ME] me.add_item(Items.WARDENS_RATION) me.hp = me.max_hp _tap(ME, InputFrame.BTN_USE, 0) assert_eq(me.inventory[0], Items.NONE) func test_using_an_empty_slot_does_nothing() -> void: _tap(ME, InputFrame.BTN_USE, 2) assert_eq(_events_of(SimEvent.Type.ITEM_USED).size(), 0) ## The slot index comes off the wire, so a hostile value has to be inert rather ## than merely unusual. func test_a_slot_index_off_the_end_is_harmless() -> void: var me := world.players[ME] me.add_item(Items.HEALTH_POTION) _tap(ME, InputFrame.BTN_USE, 200) _tap(ME, InputFrame.BTN_DROP, 250) assert_eq(me.inventory[0], Items.HEALTH_POTION) assert_eq(world.loot.size(), 0) # --- Dropping --------------------------------------------------------------- func test_dropping_puts_the_item_back_on_the_floor() -> void: var me := world.players[ME] me.add_item(Items.HEALTH_POTION) _tap(ME, InputFrame.BTN_DROP, 0) assert_eq(me.inventory[0], Items.NONE) assert_eq(world.loot.size(), 1) assert_eq(_events_of(SimEvent.Type.ITEM_DROPPED).size(), 1) ## Same refill trick as the use test, for the same reason. func test_holding_the_drop_key_drops_exactly_one_item() -> void: var me := world.players[ME] me.add_item(Items.HEALTH_POTION) _hold(ME, 5, InputFrame.BTN_DROP, 0) me.inventory[0] = Items.WARDENS_RATION _hold(ME, 20, InputFrame.BTN_DROP, 0) assert_eq(me.inventory[0], Items.WARDENS_RATION, "holding drop must not empty the bag one slot per tick") assert_eq(world.loot.size(), 1) func test_what_you_drop_can_be_taken_by_someone_else() -> void: var them := world.add_player(THEM, "them") them.pos = Vector2(10.0, 0.0) them.spawn_grace = 0 world.players[ME].add_item(Items.HEALTH_POTION) _tap(ME, InputFrame.BTN_DROP, 0) _tap(THEM, InputFrame.BTN_INTERACT) assert_eq(them.inventory[0], Items.HEALTH_POTION, "a dropped item is world-shared, whatever it was before") ## An instanced item becomes shared the moment it is dropped. That is the point ## of being able to drop things: a trophy you do not want should be able to ## reach someone who does. func test_dropping_an_instanced_item_makes_it_shared() -> void: world.players[ME].add_item(Items.WARDENS_RATION) _tap(ME, InputFrame.BTN_DROP, 0) for l in world.loot.values(): assert_eq(l.owner_peer, 0) func test_dropping_an_empty_slot_does_nothing() -> void: _tap(ME, InputFrame.BTN_DROP, 1) assert_eq(world.loot.size(), 0) # --- Visibility ------------------------------------------------------------- func test_instanced_loot_cannot_be_taken_by_anyone_else() -> void: world.spawn_loot(Items.WARDENS_RATION, Vector2(8.0, 0.0), THEM) _tap(ME, InputFrame.BTN_INTERACT) assert_eq(world.loot.size(), 1, "it is not mine to take") assert_eq(world.players[ME].free_slot(), 0) func test_instanced_loot_can_be_taken_by_its_owner() -> void: world.spawn_loot(Items.WARDENS_RATION, Vector2(8.0, 0.0), ME) _tap(ME, InputFrame.BTN_INTERACT) assert_eq(world.players[ME].inventory[0], Items.WARDENS_RATION) ## Nobody else can see it, so leaving it behind would be an entity the instance ## carries around invisibly until it closes. func test_leaving_takes_your_instanced_loot_with_you() -> void: world.spawn_loot(Items.WARDENS_RATION, Vector2(8.0, 0.0), ME) world.spawn_loot(Items.HEALTH_POTION, Vector2(8.0, 0.0)) world.remove_player(ME) assert_eq(world.loot.size(), 1, "the shared item stays") for l in world.loot.values(): assert_eq(l.owner_peer, 0) # --- Drops ------------------------------------------------------------------ func test_a_boss_kill_drops_a_shared_potion_and_a_ration_each() -> void: var them := world.add_player(THEM, "them") them.pos = Vector2(60.0, 0.0) var boss := world.spawn_boss(Content.warden()) world._damage_boss(boss.def.max_hp * 2) var shared := 0 var mine := 0 var theirs := 0 for l in world.loot.values(): if l.owner_peer == 0: shared += 1 elif l.owner_peer == ME: mine += 1 elif l.owner_peer == THEM: theirs += 1 assert_eq(shared, 1, "one potion for the party to divide") assert_eq(mine, 1, "and a ration each") assert_eq(theirs, 1) func test_a_dead_player_earns_no_instanced_drop() -> void: var them := world.add_player(THEM, "them") them.alive = false var boss := world.spawn_boss(Content.warden()) world._damage_boss(boss.def.max_hp * 2) for l in world.loot.values(): assert_ne(l.owner_peer, THEM, "you have to be alive for the kill") ## Rare means rare: a run should be survivable on what it hands you, never ## comfortably. Checked statistically rather than exactly, because the roll uses ## the world's RNG and pinning the exact count would pin the RNG. func test_a_trash_enemy_drops_a_potion_only_sometimes() -> void: var kills := 400 var dropped := 0 for i in kills: var solo := SimWorld.new(i * 31 + 5) var e := solo.spawn_enemy(Content.drifter(), Vector2.ZERO) solo._damage_enemy(e, e.def.max_hp * 2) dropped += solo.loot.size() assert_gt(dropped, 0, "trash has to drop something eventually") assert_lt(dropped, kills / 2, "if most kills drop a potion, potions are not worth a slot") func test_the_practice_dummy_drops_nothing() -> void: var e := world.spawn_enemy(Content.dummy(), Vector2(100.0, 0.0)) world._damage_enemy(e, e.def.max_hp * 2) assert_eq(world.loot.size(), 0, "the hub target is not a loot piƱata") # --- Housekeeping ----------------------------------------------------------- ## Only the hub can realistically reach the cap -- dungeons close and take ## their litter with them -- but unbounded growth in the one world that never ## closes is worth a ceiling. func test_ground_loot_is_capped() -> void: for i in SimConfig.MAX_LOOT_PER_INSTANCE + 20: world.spawn_loot(Items.HEALTH_POTION, Vector2(float(i), 200.0)) assert_lte(world.loot.size(), SimConfig.MAX_LOOT_PER_INSTANCE) func test_loot_ids_never_collide_with_actor_ids() -> void: var e := world.spawn_enemy(Content.turret(), Vector2(100.0, 100.0)) var l := world.spawn_loot(Items.HEALTH_POTION, Vector2.ZERO) assert_ne(l.id, e.id, "loot shares the actor id space, so it must share the counter") func test_an_unknown_item_cannot_be_put_on_the_floor() -> void: assert_null(world.spawn_loot(&"phlogiston", Vector2.ZERO)) ## The inventory belongs to the character, not to the room. Walking into a ## dungeon with the potions you were carrying is the entire point of carrying ## them. func test_inventory_survives_an_instance_transition() -> void: var me := world.players[ME] me.add_item(Items.HEALTH_POTION) me.reset_for_instance(Vector2(50.0, 50.0), SimConfig.SPAWN_GRACE_TICKS) assert_eq(me.inventory[0], Items.HEALTH_POTION) ## A replica never decides anything, loot included. This is the same guarantee ## the rest of the simulation makes, checked for the newest actor type. func test_a_replica_never_creates_loot_of_its_own() -> void: var replica := SimWorld.new(7) replica.authoritative = false replica.add_player(ME, "me") var e := replica.spawn_enemy(Content.drifter(), Vector2(10.0, 0.0)) e.hp = 1 for _i in 300: replica.step() assert_eq(replica.loot.size(), 0, "a client must never invent an item for itself to pick up")