DEMI-PROJECT-PACKAGE 3{"files":[{"hash":"sha256:3065937c7cb4e2018eb4daa1569505b0ad7d2f1ec84c4c4e7a67fd7b8c3de103","path":"LICENSE","size":1492},{"hash":"sha256:bcb861563e52805ed420b1289bdc6ab7120438b85830e6ef6cd38f9de1165564","path":"README.md","size":1947},{"hash":"sha256:dff3d4b7b334d71ebf796d928d895fbe1c8a9fd2e3552d65f52852375275a469","path":"scripts/demi/gameplay/needs.lua","size":2658},{"hash":"sha256:1ff99245ed86638eb1fc963f960eb131a39f37bd85836037e26eccfc4706042f","path":"tests/needs_test.lua","size":1827}],"format_version":1,"manifest":{"dependencies":{},"engine":"^0.1.0","exported_events":[],"files":["LICENSE","README.md","scripts/demi/gameplay/needs.lua","tests/needs_test.lua"],"format_version":1,"name":"demi.gameplay.needs","public_modules":["demi.gameplay.needs"],"version":"0.1.0"},"package_type":"DemiProjectPackage"}BSD 3-Clause License Copyright (c) 2026, JEAPI Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # Character Needs Named, bounded needs for survival games, NPC routines and colony simulations. The module is independent of scenes, physics and UI. Higher values mean a need is better satisfied: use nutrition rather than hunger, or stamina rather than fatigue. ```lua local Needs = require("demi.gameplay.needs") local needs = Needs.new({ stamina = { value = 80, low = 25, recovered = 90 }, nutrition = { value = 70, low = 30, recovered = 75 }, }) -- Rates are units per second. Only listed needs change. needs:update(dt, { stamina = -0.4, nutrition = -0.15 }) if needs:urgent() then -- Ask the game's scheduler for a break. end -- After the game accepts a meal transaction: local restored = needs:change("nutrition", 60) ``` `Needs.new(definitions)` copies named definitions. Defaults are maximum 100, initial value equal to maximum, low threshold 25% and recovered threshold 90%. Values clamp to `[0, maximum]`. Recovery must exceed the low threshold. `get(name)` reads a value; `change(name, amount)` returns the actual applied change. `update(dt, rates)` validates the entire request before changing any value. Invalid names, nonfinite inputs and negative time are errors. `urgent()` is true when any need is at or below its low threshold. `recovered()` is true when every need has reached its recovery threshold. Keep a break active until recovery: separate thresholds avoid switching tasks back and forth near a single threshold. The game owns that task state. The game also owns drain rates, food/water costs, habitat requirements, animation, HUD presentation and consequences of depletion. No resource is consumed and no entity is moved by this package. It does not implement save storage. The Colony Builder example uses this package for its engineer's stamina, nutrition and suit oxygen, while keeping habitat policy in `colony/needs.lua`. Run `demi package test packages/sources/demi.gameplay.needs` for isolated tests. -- Named, bounded needs. Higher values mean the need is better satisfied. -- Games own recovery activities, resource costs, HUD and scheduling decisions. local Needs = {} Needs.__index = Needs local function finite(value) return type(value) == "number" and value == value and math.abs(value) < math.huge end function Needs.new(definitions) assert(type(definitions) == "table", "need definitions must be a table") local self = setmetatable({ entries = {} }, Needs) for name, definition in pairs(definitions) do assert(type(name) == "string" and name ~= "", "need names must be nonempty strings") assert(type(definition) == "table", "each need requires a definition") local maximum = definition.maximum or 100 local value = definition.value or maximum local low = definition.low or maximum * 0.25 local recovered = definition.recovered or maximum * 0.9 assert(finite(maximum) and maximum > 0, "maximum must be positive and finite") assert(finite(value), "initial value must be finite") assert(finite(low) and low >= 0 and low <= maximum, "low threshold must be within bounds") assert( finite(recovered) and recovered > low and recovered <= maximum, "recovered threshold must exceed low and be within bounds" ) self.entries[name] = { value = math.max(0, math.min(maximum, value)), maximum = maximum, low = low, recovered = recovered, } end return self end function Needs:entry(name) return assert(self.entries[name], "unknown need: " .. tostring(name)) end function Needs:get(name) return self:entry(name).value end function Needs:change(name, amount) assert(finite(amount), "need change must be finite") local entry = self:entry(name) local previous = entry.value entry.value = math.max(0, math.min(entry.maximum, previous + amount)) return entry.value - previous end function Needs:update(dt, rates) assert(finite(dt) and dt >= 0, "dt must be nonnegative and finite") assert(type(rates) == "table", "rates must be a table") -- Validate the whole request before mutating any meter. for name, rate in pairs(rates) do self:entry(name) assert(finite(rate) and finite(rate * dt), "need rates must be finite") end for name, rate in pairs(rates) do self:change(name, rate * dt) end end function Needs:urgent() for _, entry in pairs(self.entries) do if entry.value <= entry.low then return true end end return false end function Needs:recovered() for _, entry in pairs(self.entries) do if entry.value < entry.recovered then return false end end return true end return Needs local Test = require("demi.test") local Needs = require("demi.gameplay.needs") Test.case("needs drain, clamp and report actual changes", function() local needs = Needs.new({ stamina = { value = 80 }, food = { value = 60 } }) needs:update(10, { stamina = -2, food = -1 }) Test.equal(needs:get("stamina"), 60) Test.equal(needs:get("food"), 50) Test.equal(needs:change("stamina", -100), -60) Test.equal(needs:change("stamina", 120), 100) end) Test.case("separate low and recovery thresholds prevent immediate task churn", function() local needs = Needs.new({ oxygen = { value = 25, low = 25, recovered = 95 } }) Test.truthy(needs:urgent()) needs:change("oxygen", 1) Test.equal(needs:urgent(), false) Test.equal(needs:recovered(), false) needs:update(10, { oxygen = 10 }) Test.truthy(needs:recovered()) end) Test.case("instances and caller definitions are independent", function() local definitions = { stamina = { value = 50 } } local first, second = Needs.new(definitions), Needs.new(definitions) definitions.stamina.value = 0 first:change("stamina", -30) Test.equal(second:get("stamina"), 50) Test.equal(first:get("stamina"), 20) end) Test.case("invalid updates reject without partial mutation", function() local needs = Needs.new({ stamina = { value = 50 }, oxygen = { value = 50 } }) Test.equal( pcall(function() needs:update(1, { stamina = -1, oxygen = math.huge }) end), false ) Test.equal(needs:get("stamina"), 50) Test.equal(needs:get("oxygen"), 50) Test.equal( pcall(function() needs:update(-1, {}) end), false ) Test.equal( pcall(function() needs:change("unknown", 1) end), false ) Test.equal( pcall(function() Needs.new({ stamina = { low = 90, recovered = 50 } }) end), false ) end)