|
| 1 | +--@enable = true |
| 2 | +--@module = true |
| 3 | + |
| 4 | +local idle = reqscript('idle-crafting') |
| 5 | +local repeatutil = require("repeat-util") |
| 6 | +--- utility functions |
| 7 | + |
| 8 | +---3D city metric |
| 9 | +---@param p1 df.coord |
| 10 | +---@param p2 df.coord |
| 11 | +---@return number |
| 12 | +function distance(p1, p2) |
| 13 | + return math.max(math.abs(p1.x - p2.x), math.abs(p1.y - p2.y)) + math.abs(p1.z - p2.z) |
| 14 | +end |
| 15 | + |
| 16 | +---find closest accessible item in an item vector |
| 17 | +---@generic T : df.item |
| 18 | +---@param pos df.coord |
| 19 | +---@param item_vector T[] |
| 20 | +---@param is_good? fun(item: T): boolean |
| 21 | +---@return T? |
| 22 | +local function findClosest(pos, item_vector, is_good) |
| 23 | + local closest = nil |
| 24 | + local dclosest = -1 |
| 25 | + for _,item in ipairs(item_vector) do |
| 26 | + if not item.flags.in_job and (not is_good or is_good(item)) then |
| 27 | + local pitem = xyz2pos(dfhack.items.getPosition(item)) |
| 28 | + local ditem = distance(pos, pitem) |
| 29 | + if dfhack.maps.canWalkBetween(pos, pitem) and (not closest or ditem < dclosest) then |
| 30 | + closest = item |
| 31 | + dclosest = ditem |
| 32 | + end |
| 33 | + end |
| 34 | + end |
| 35 | + return closest |
| 36 | +end |
| 37 | + |
| 38 | +---find a drink |
| 39 | +---@param pos df.coord |
| 40 | +---@return df.item_drinkst? |
| 41 | +local function get_closest_drink(pos) |
| 42 | + local is_good = function (drink) |
| 43 | + local container = dfhack.items.getContainer(drink) |
| 44 | + return container and container:isFoodStorage() |
| 45 | + end |
| 46 | + return findClosest(pos, df.global.world.items.other.DRINK, is_good) |
| 47 | +end |
| 48 | + |
| 49 | +---find some prepared meal |
| 50 | +---@return df.item_foodst? |
| 51 | +local function get_closest_meal(pos) |
| 52 | + ---@param meal df.item_foodst |
| 53 | + local function is_good(meal) |
| 54 | + if meal.flags.rotten then |
| 55 | + return false |
| 56 | + else |
| 57 | + local container = dfhack.items.getContainer(meal) |
| 58 | + return not container or container:isFoodStorage() |
| 59 | + end |
| 60 | + end |
| 61 | + return findClosest(pos, df.global.world.items.other.FOOD, is_good) |
| 62 | +end |
| 63 | + |
| 64 | +---create a Drink job for the given unit |
| 65 | +---@param unit df.unit |
| 66 | +local function goDrink(unit) |
| 67 | + local drink = get_closest_drink(unit.pos) |
| 68 | + if not drink then |
| 69 | + -- print('no accessible drink found') |
| 70 | + return |
| 71 | + end |
| 72 | + local job = idle.make_job() |
| 73 | + job.job_type = df.job_type.DrinkItem |
| 74 | + job.flags.special = true |
| 75 | + local dx, dy, dz = dfhack.items.getPosition(drink) |
| 76 | + job.pos = xyz2pos(dx, dy, dz) |
| 77 | + if not dfhack.job.attachJobItem(job, drink, df.job_item_ref.T_role.Other, -1, -1) then |
| 78 | + error('could not attach drink') |
| 79 | + return |
| 80 | + end |
| 81 | + dfhack.job.addWorker(job, unit) |
| 82 | + local name = dfhack.TranslateName(dfhack.units.getVisibleName(unit)) |
| 83 | + print(dfhack.df2console('immortal-cravings: %s is getting a drink'):format(name)) |
| 84 | +end |
| 85 | + |
| 86 | +---create Eat job for the given unit |
| 87 | +---@param unit df.unit |
| 88 | +local function goEat(unit) |
| 89 | + local meal = get_closest_meal(unit.pos) |
| 90 | + if not meal then |
| 91 | + -- print('no accessible meals found') |
| 92 | + return |
| 93 | + end |
| 94 | + local job = idle.make_job() |
| 95 | + job.job_type = df.job_type.Eat |
| 96 | + job.flags.special = true |
| 97 | + local dx, dy, dz = dfhack.items.getPosition(meal) |
| 98 | + job.pos = xyz2pos(dx, dy, dz) |
| 99 | + if not dfhack.job.attachJobItem(job, meal, df.job_item_ref.T_role.Other, -1, -1) then |
| 100 | + error('could not attach meal') |
| 101 | + return |
| 102 | + end |
| 103 | + dfhack.job.addWorker(job, unit) |
| 104 | + local name = dfhack.TranslateName(dfhack.units.getVisibleName(unit)) |
| 105 | + print(dfhack.df2console('immortal-cravings: %s is getting something to eat'):format(name)) |
| 106 | +end |
| 107 | + |
| 108 | +--- script logic |
| 109 | + |
| 110 | +local GLOBAL_KEY = 'immortal-cravings' |
| 111 | + |
| 112 | +enabled = enabled or false |
| 113 | +function isEnabled() |
| 114 | + return enabled |
| 115 | +end |
| 116 | + |
| 117 | +local function persist_state() |
| 118 | + dfhack.persistent.saveSiteData(GLOBAL_KEY, { |
| 119 | + enabled=enabled, |
| 120 | + }) |
| 121 | +end |
| 122 | + |
| 123 | +--- Load the saved state of the script |
| 124 | +local function load_state() |
| 125 | + -- load persistent data |
| 126 | + local persisted_data = dfhack.persistent.getSiteData(GLOBAL_KEY, {}) |
| 127 | + enabled = persisted_data.enabled or false |
| 128 | +end |
| 129 | + |
| 130 | +DrinkAlcohol = df.need_type.DrinkAlcohol |
| 131 | +EatGoodMeal = df.need_type.EatGoodMeal |
| 132 | + |
| 133 | +---@type integer[] |
| 134 | +watched = watched or {} |
| 135 | + |
| 136 | +local threshold = -9000 |
| 137 | + |
| 138 | +---unit loop: check for idle watched units and create eat/drink jobs for them |
| 139 | +local function unit_loop() |
| 140 | + -- print(('immortal-cravings: running unit loop (%d watched units)'):format(#watched)) |
| 141 | + ---@type integer[] |
| 142 | + local kept = {} |
| 143 | + for _, unit_id in ipairs(watched) do |
| 144 | + local unit = df.unit.find(unit_id) |
| 145 | + if |
| 146 | + not unit or not dfhack.units.isActive(unit) or |
| 147 | + unit.flags1.caged or unit.flags1.chained |
| 148 | + then |
| 149 | + goto next_unit |
| 150 | + end |
| 151 | + if not idle.unitIsAvailable(unit) then |
| 152 | + table.insert(kept, unit.id) |
| 153 | + else |
| 154 | + -- unit is available for jobs; satisfy one of its needs |
| 155 | + for _, need in ipairs(unit.status.current_soul.personality.needs) do |
| 156 | + if need.id == DrinkAlcohol and need.focus_level < threshold then |
| 157 | + goDrink(unit) |
| 158 | + break |
| 159 | + elseif need.id == EatGoodMeal and need.focus_level < threshold then |
| 160 | + goEat(unit) |
| 161 | + break |
| 162 | + end |
| 163 | + end |
| 164 | + end |
| 165 | + ::next_unit:: |
| 166 | + end |
| 167 | + watched = kept |
| 168 | + if #watched == 0 then |
| 169 | + -- print('immortal-cravings: no more watched units, cancelling unit loop') |
| 170 | + repeatutil.cancel(GLOBAL_KEY .. '-unit') |
| 171 | + end |
| 172 | +end |
| 173 | + |
| 174 | +---main loop: look for citizens with personality needs for food/drink but w/o physiological need |
| 175 | +local function main_loop() |
| 176 | + -- print('immortal-cravings watching:') |
| 177 | + watched = {} |
| 178 | + for _, unit in ipairs(dfhack.units.getCitizens()) do |
| 179 | + if unit.curse.add_tags1.NO_DRINK or unit.curse.add_tags1.NO_EAT then |
| 180 | + for _, need in ipairs(unit.status.current_soul.personality.needs) do |
| 181 | + if need.id == DrinkAlcohol and need.focus_level < threshold or |
| 182 | + need.id == EatGoodMeal and need.focus_level < threshold |
| 183 | + then |
| 184 | + table.insert(watched, unit.id) |
| 185 | + -- print(' '..dfhack.df2console(dfhack.TranslateName(dfhack.units.getVisibleName(unit)))) |
| 186 | + goto next_unit |
| 187 | + end |
| 188 | + end |
| 189 | + end |
| 190 | + ::next_unit:: |
| 191 | + end |
| 192 | + |
| 193 | + if #watched > 0 then |
| 194 | + repeatutil.scheduleUnlessAlreadyScheduled(GLOBAL_KEY..'-unit', 59, 'ticks', unit_loop) |
| 195 | + end |
| 196 | +end |
| 197 | + |
| 198 | +local function start() |
| 199 | + if enabled then |
| 200 | + repeatutil.scheduleUnlessAlreadyScheduled(GLOBAL_KEY..'-main', 4003, 'ticks', main_loop) |
| 201 | + end |
| 202 | +end |
| 203 | + |
| 204 | +local function stop() |
| 205 | + repeatutil.cancel(GLOBAL_KEY..'-main') |
| 206 | + repeatutil.cancel(GLOBAL_KEY..'-unit') |
| 207 | +end |
| 208 | + |
| 209 | + |
| 210 | + |
| 211 | +-- script action |
| 212 | + |
| 213 | +--- Handles automatic loading |
| 214 | +dfhack.onStateChange[GLOBAL_KEY] = function(sc) |
| 215 | + if sc == SC_MAP_UNLOADED then |
| 216 | + enabled = false |
| 217 | + -- repeat-util will cancel the loops on unload |
| 218 | + return |
| 219 | + end |
| 220 | + |
| 221 | + if sc ~= SC_MAP_LOADED or df.global.gamemode ~= df.game_mode.DWARF then |
| 222 | + return |
| 223 | + end |
| 224 | + |
| 225 | + load_state() |
| 226 | + start() |
| 227 | +end |
| 228 | + |
| 229 | +if dfhack_flags.enable then |
| 230 | + if dfhack_flags.enable_state then |
| 231 | + enabled = true |
| 232 | + start() |
| 233 | + else |
| 234 | + enabled = false |
| 235 | + stop() |
| 236 | + end |
| 237 | + persist_state() |
| 238 | +end |
0 commit comments