DEMI-PROJECT-PACKAGE ð{"files":[{"hash":"sha256:ac6ba87fbcaf8ed7c7e404b749a28a070a0aa97c8ae5bb7f257b49ec81d40bdd","path":"README.md","size":2268},{"hash":"sha256:032544b502f419d6f5e927f0867957b2a5339d103fc0658caa6b65af5802d72d","path":"scripts/demi/gameplay/interactions.lua","size":1362},{"hash":"sha256:9ff07e9d6969d84480f771090bc96e082d9f1d34fdd15ebf9078be12ab809bf7","path":"scripts/demi/gameplay/pickups.lua","size":857},{"hash":"sha256:789d64b457971dde325e1a5ca01c7e5c9b6b5504d9da94b2d95a7d64b6d6c967","path":"tests/interactions_test.lua","size":1265}],"format_version":1,"manifest":{"dependencies":{"demi.gameplay.events":"^1.0.0"},"engine":"^0.1.0","exported_events":["interaction_selected","pickup_collected"],"files":["README.md","scripts/demi/gameplay/interactions.lua","scripts/demi/gameplay/pickups.lua","tests/interactions_test.lua"],"format_version":1,"name":"demi.gameplay.interactions","public_modules":["demi.gameplay.interactions","demi.gameplay.pickups"],"version":"1.1.0"},"package_type":"DemiProjectPackage"}# Interactions and pickups Package: `demi.gameplay.interactions`. Selects an interaction from game-supplied candidates and tracks single-use pickups. Exports `demi.gameplay.interactions` and `demi.gameplay.pickups`; depends on `demi.gameplay.events`. See [package installation](../../README.md). ```lua local Events = require("demi.gameplay.events") local Interactions = require("demi.gameplay.interactions") local Pickups = require("demi.gameplay.pickups") local events = Events.new() local interactions = Interactions.new(events) local pickups = Pickups.new(events) pickups:add("coin-1", { item = "coin", count = 3 }) interactions:set("coin-1", { distance = 1, prompt = "Collect" }) local unsubscribe = events:on("interaction_selected", function(value) if pickups:collect(value.entity, "player") then interactions:remove(value.entity) end end) assert(interactions:confirm(2)) events:flush() assert(not pickups:collect("coin-1", "player")) unsubscribe() ``` `best(max_distance)` filters disabled/out-of-range candidates, then chooses highest priority, shortest distance, and lexicographically smallest ID. Update candidates with `set`; it replaces their fields, so supply all values you need to retain. `confirm` queues `interaction_selected` and returns `true, id`, or `false` when no candidate qualifies. Confirmation does not remove the candidate, so repeated confirmations can queue repeated events. `collect(id, collector)` removes an enabled pickup immediately and queues `pickup_collected` with `pickup`, `collector`, `item`, `count`, and `payload`. Later collections fail unless the game adds that pickup again. Collection does not check distance or inventory capacity. The game owns the bus and chooses when to flush. Events emitted by listeners are drained by the same flush; in this example that includes `pickup_collected`. Subscribe to it to grant items or update presentation, and unsubscribe when the listener owner is destroyed. Neither module reads input, queries physics, draws prompts, modifies inventory, or destroys scene entities. The game supplies distance/enabled state and handles those effects. From the repository root, test with: ```sh ./build/linux-debug/demi package test packages/sources/demi.gameplay.interactions ``` local Interactions = {} Interactions.__index = Interactions function Interactions.new(events) return setmetatable({ events = assert(events), candidates = {} }, Interactions) end function Interactions:set(id, values) assert(type(id) == "string") values = values or {} self.candidates[id] = { id = id, priority = values.priority or 0, distance = values.distance or math.huge, enabled = values.enabled ~= false, prompt = values.prompt, payload = values.payload } end function Interactions:remove(id) self.candidates[id] = nil end function Interactions:best(max_distance) local result for _, value in pairs(self.candidates) do if value.enabled and value.distance <= (max_distance or math.huge) and (not result or value.priority > result.priority or value.priority == result.priority and value.distance < result.distance or value.priority == result.priority and value.distance == result.distance and value.id < result.id) then result = value end end return result end function Interactions:confirm(max_distance) local selected = self:best(max_distance) if not selected or not self.candidates[selected.id] or not selected.enabled then return false end self.events:emit("interaction_selected", { entity = selected.id, payload = selected.payload }) return true, selected.id end return Interactions local Pickups = {} Pickups.__index = Pickups function Pickups.new(events) return setmetatable({ events = assert(events), entries = {} }, Pickups) end function Pickups:add(id, values) assert(type(id) == "string") values = values or {} self.entries[id] = { id = id, item = values.item, count = values.count or 1, enabled = values.enabled ~= false, payload = values.payload, } end function Pickups:remove(id) self.entries[id] = nil end function Pickups:collect(id, collector) local pickup = self.entries[id] if not pickup or not pickup.enabled then return false end pickup.enabled = false self.entries[id] = nil self.events:emit("pickup_collected", { pickup = id, collector = collector, item = pickup.item, count = pickup.count, payload = pickup.payload, }) return true end return Pickups local Test = require("demi.test") local Events = require("demi.gameplay.events") local Interactions = require("demi.gameplay.interactions") Test.case("ties use priority distance then stable id", function() local interactions = Interactions.new(Events.new()) interactions:set("b", { priority = 2, distance = 4 }) interactions:set("a", { priority = 2, distance = 4 }) interactions:set("near-low", { priority = 1, distance = 1 }) Test.equal(interactions:best(5).id, "a") end) Test.case("disabled candidate cannot be confirmed", function() local interactions = Interactions.new(Events.new()) interactions:set("door", { enabled = false, distance = 0 }) Test.equal(interactions:confirm(1), false) end) Test.case("pickup collection is single use", function() local Events = require("demi.gameplay.events") local Pickups = require("demi.gameplay.pickups") local events, collected = Events.new(), 0 events:on("pickup_collected", function(value) collected = collected + value.count end) local pickups = Pickups.new(events) pickups:add("coin-1", { item = "coin", count = 3 }) Test.equal(pickups:collect("coin-1", "player"), true) Test.equal(pickups:collect("coin-1", "player"), false) events:flush() Test.equal(collected, 3) end)