-
Notifications
You must be signed in to change notification settings - Fork 193
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
new tool: immortal-cravings #1301
base: master
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could you also add this tool to the control panel registry ( |
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`` | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we don't usually individually document the disable command |
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) | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this seems like a good candidate to move to |
||||||||||
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) | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you can combine this into local pitem = xyz2pos(dfhack.items.getPosition(item)) |
||||||||||
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) | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. also pots. don't forget about pots! I think what you want is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good that I tested this change: It appears that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Update: no, backpacks are not food storage. The change you suggested was for drinks, but the same check should also be done when looking for meals. ^^ |
||||||||||
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 | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||||||
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'] | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
|
||||||||||
---@type integer[] | ||||||||||
watched = {} | ||||||||||
|
||||||||||
threshold = -9000 | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. these should be local or "protected" globals ( There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||||||
|
||||||||||
---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 | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. opportunity to reduce code nesting here if not unit or unit.flags1.caged or units.flags1.chained then
goto next_unit
end There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. also, unit should be skipped if dead or not active (e.g. left map on a raid) |
||||||||||
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 | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. these |
||||||||||
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:') | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this prints in this function are probably too spammy. the prints for when actions are actually taken is enough |
||||||||||
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 | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe add a comment here explaining that repeat-util will cancel the scheduled callbacks for you |
||||||||||
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could you move this up into the new "Future" section?