-
Notifications
You must be signed in to change notification settings - Fork 199
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
250 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
immortal-cravings | ||
================ | ||
|
||
.. dfhack-tool:: | ||
:summary: Allow immortals to satisfy their cravings for food and drink. | ||
:tags: fort gameplay | ||
|
||
When enabled, this script watches your fort for units that have no physiological | ||
need to eat or drink but still have personality needs that can only be satisfied | ||
by eating or drinking (e.g. necromancers). This enables those units to help | ||
themselves to a drink or a meal when they crave one and are not otherwise | ||
occupied. | ||
|
||
Usage | ||
----- | ||
|
||
``enable immortal-cravings`` | ||
``disable immortal-cravings`` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,231 @@ | ||
--@enable = true | ||
--@module = true | ||
|
||
local idle = reqscript('idle-crafting') | ||
local repeatutil = require("repeat-util") | ||
--- utility functions | ||
|
||
---3D city metric | ||
---@param p1 df.coord | ||
---@param p2 df.coord | ||
---@return number | ||
function distance(p1, p2) | ||
return math.max(math.abs(p1.x - p2.x), math.abs(p1.y - p2.y)) + math.abs(p1.z - p2.z) | ||
end | ||
|
||
---find closest accessible item in an item vector | ||
---@generic T : df.item | ||
---@param pos df.coord | ||
---@param item_vector T[] | ||
---@param is_good? fun(item: T): boolean | ||
---@return T? | ||
local function findClosest(pos, item_vector, is_good) | ||
local closest = nil | ||
local dclosest = -1 | ||
for _,item in ipairs(item_vector) do | ||
if not item.flags.in_job and (not is_good or is_good(item)) then | ||
local x, y, z = dfhack.items.getPosition(item) | ||
local pitem = xyz2pos(x, y, z) | ||
local ditem = distance(pos, pitem) | ||
if dfhack.maps.canWalkBetween(pos, pitem) and (not closest or ditem < dclosest) then | ||
closest = item | ||
dclosest = ditem | ||
end | ||
end | ||
end | ||
return closest | ||
end | ||
|
||
---find a drink | ||
---@param pos df.coord | ||
---@return df.item_drinkst|nil | ||
local function get_closest_drink(pos) | ||
local is_good = function (drink) | ||
local container = dfhack.items.getContainer(drink) | ||
return container and df.item_barrelst:is_instance(container) | ||
end | ||
return findClosest(pos, df.global.world.items.other.DRINK, is_good) | ||
end | ||
|
||
---find some prepared meal | ||
---@return df.item_foodst? | ||
local function get_closest_meal(pos) | ||
---@param meal df.item_foodst | ||
local function is_good(meal) | ||
return meal.flags.rotten == false | ||
end | ||
return findClosest(pos, df.global.world.items.other.FOOD, is_good) | ||
end | ||
|
||
---create a Drink job for the given unit | ||
---@param unit df.unit | ||
local function goDrink(unit) | ||
local drink = get_closest_drink(unit.pos) | ||
if not drink then | ||
-- print('no accessible drink found') | ||
return | ||
end | ||
local job = idle.make_job() | ||
job.job_type = df.job_type.DrinkItem | ||
job.flags.special = true | ||
local dx, dy, dz = dfhack.items.getPosition(drink) | ||
job.pos = xyz2pos(dx, dy, dz) | ||
if not dfhack.job.attachJobItem(job, drink, df.job_item_ref.T_role.Other, -1, -1) then | ||
error('could not attach drink') | ||
return | ||
end | ||
dfhack.job.addWorker(job, unit) | ||
local name = dfhack.TranslateName(dfhack.units.getVisibleName(unit)) | ||
print(dfhack.df2console('immortal-cravings: %s is getting a drink'):format(name)) | ||
end | ||
|
||
---create Eat job for the given unit | ||
---@param unit df.unit | ||
local function goEat(unit) | ||
local meal = get_closest_meal(unit.pos) | ||
if not meal then | ||
-- print('no accessible meals found') | ||
return | ||
end | ||
local job = idle.make_job() | ||
job.job_type = df.job_type.Eat | ||
job.flags.special = true | ||
local dx, dy, dz = dfhack.items.getPosition(meal) | ||
job.pos = xyz2pos(dx, dy, dz) | ||
if not dfhack.job.attachJobItem(job, meal, df.job_item_ref.T_role.Other, -1, -1) then | ||
error('could not attach meal') | ||
return | ||
end | ||
dfhack.job.addWorker(job, unit) | ||
local name = dfhack.TranslateName(dfhack.units.getVisibleName(unit)) | ||
print(dfhack.df2console('immortal-cravings: %s is getting something to eat'):format(name)) | ||
end | ||
|
||
--- script logic | ||
|
||
local GLOBAL_KEY = 'immortal-cravings' | ||
|
||
enabled = enabled or false | ||
function isEnabled() | ||
return enabled | ||
end | ||
|
||
local function persist_state() | ||
dfhack.persistent.saveSiteData(GLOBAL_KEY, { | ||
enabled=enabled, | ||
}) | ||
end | ||
|
||
--- Load the saved state of the script | ||
local function load_state() | ||
-- load persistent data | ||
local persisted_data = dfhack.persistent.getSiteData(GLOBAL_KEY, {}) | ||
enabled = persisted_data.enabled or false | ||
end | ||
|
||
DrinkAlcohol = df.need_type['DrinkAlcohol'] | ||
EatGoodMeal = df.need_type['EatGoodMeal'] | ||
|
||
---@type integer[] | ||
watched = {} | ||
|
||
threshold = -9000 | ||
|
||
---unit loop: check for idle watched units and create eat/drink jobs for them | ||
local function unit_loop() | ||
-- print(('immortal-cravings: running unit loop (%d watched units)'):format(#watched)) | ||
---@type integer[] | ||
local kept = {} | ||
for _, unit_id in ipairs(watched) do | ||
local unit = df.unit.find(unit_id) | ||
if unit and not (unit.flags1.caged or unit.flags1.chained) then | ||
if not idle.unitIsAvailable(unit) then | ||
table.insert(kept, unit.id) | ||
else | ||
-- | ||
for _, need in ipairs(unit.status.current_soul.personality.needs) do | ||
if need.id == DrinkAlcohol and need.focus_level < threshold then | ||
goDrink(unit) | ||
goto next_unit | ||
elseif need.id == EatGoodMeal and need.focus_level < threshold then | ||
goEat(unit) | ||
goto next_unit | ||
end | ||
end | ||
end | ||
else | ||
-- print('immortal-cravings: unit gone or caged') | ||
end | ||
::next_unit:: | ||
end | ||
watched = kept | ||
if #watched == 0 then | ||
-- print('immortal-cravings: no more watched units, cancelling unit loop') | ||
repeatutil.cancel(GLOBAL_KEY .. '-unit') | ||
end | ||
end | ||
|
||
---main loop: look for citizens with personality needs for food/drink but w/o physiological need | ||
local function main_loop() | ||
print('immortal-cravings watching:') | ||
watched = {} | ||
for _, unit in ipairs(dfhack.units.getCitizens()) do | ||
if unit.curse.add_tags1.NO_DRINK or unit.curse.add_tags1.NO_EAT then | ||
for _, need in ipairs(unit.status.current_soul.personality.needs) do | ||
if need.id == DrinkAlcohol and need.focus_level < threshold or | ||
need.id == EatGoodMeal and need.focus_level < threshold | ||
then | ||
table.insert(watched, unit.id) | ||
print(' '..dfhack.df2console(dfhack.TranslateName(dfhack.units.getVisibleName(unit)))) | ||
goto next_unit | ||
end | ||
end | ||
end | ||
::next_unit:: | ||
end | ||
|
||
if #watched > 0 then | ||
repeatutil.scheduleUnlessAlreadyScheduled(GLOBAL_KEY..'-unit', 59, 'ticks', unit_loop) | ||
end | ||
end | ||
|
||
local function start() | ||
if enabled then | ||
repeatutil.scheduleUnlessAlreadyScheduled(GLOBAL_KEY..'-main', 4003, 'ticks', main_loop) | ||
end | ||
end | ||
|
||
local function stop() | ||
repeatutil.cancel(GLOBAL_KEY..'-main') | ||
repeatutil.cancel(GLOBAL_KEY..'-unit') | ||
end | ||
|
||
|
||
|
||
-- script action | ||
|
||
--- Handles automatic loading | ||
dfhack.onStateChange[GLOBAL_KEY] = function(sc) | ||
if sc == SC_MAP_UNLOADED then | ||
enabled = false | ||
return | ||
end | ||
|
||
if sc ~= SC_MAP_LOADED or df.global.gamemode ~= df.game_mode.DWARF then | ||
return | ||
end | ||
|
||
load_state() | ||
start() | ||
end | ||
|
||
if dfhack_flags.enable then | ||
if dfhack_flags.enable_state then | ||
enabled = true | ||
start() | ||
else | ||
enabled = false | ||
stop() | ||
end | ||
persist_state() | ||
end |