Skip to content

Commit

Permalink
Merge pull request #1301 from chdoc/immortal-cravings
Browse files Browse the repository at this point in the history
new tool: immortal-cravings
  • Loading branch information
myk002 authored Nov 12, 2024
2 parents ff8f711 + edc7d40 commit 80e1a98
Show file tree
Hide file tree
Showing 4 changed files with 259 additions and 0 deletions.
1 change: 1 addition & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Template for new versions:

## New Tools
- `fix/wildlife`: prevent wildlife from getting stuck when trying to exit the map. This fix needs to be enabled manually in `gui/control-panel` on the Bug Fixes tab since not all players want this bug to be fixed.
- `immortal-cravings`: allow immortals to satisfy their cravings for food and drink

## New Features
- `force`: support the ``Wildlife`` event to allow additional wildlife to enter the map
Expand Down
19 changes: 19 additions & 0 deletions docs/immortal-cravings.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
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
238 changes: 238 additions & 0 deletions immortal-cravings.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
--@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 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?
local function get_closest_drink(pos)
local is_good = function (drink)
local container = dfhack.items.getContainer(drink)
return container and container:isFoodStorage()
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)
if meal.flags.rotten then
return false
else
local container = dfhack.items.getContainer(meal)
return not container or container:isFoodStorage()
end
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 = watched or {}

local 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
not unit or not dfhack.units.isActive(unit) or
unit.flags1.caged or unit.flags1.chained
then
goto next_unit
end
if not idle.unitIsAvailable(unit) then
table.insert(kept, unit.id)
else
-- unit is available for jobs; satisfy one of its needs
for _, need in ipairs(unit.status.current_soul.personality.needs) do
if need.id == DrinkAlcohol and need.focus_level < threshold then
goDrink(unit)
break
elseif need.id == EatGoodMeal and need.focus_level < threshold then
goEat(unit)
break
end
end
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
-- repeat-util will cancel the loops on unload
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
1 change: 1 addition & 0 deletions internal/control-panel/registry.lua
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ COMMANDS_BY_IDX = {
{command='fastdwarf', group='gameplay', mode='enable'},
{command='hermit', group='gameplay', mode='enable'},
{command='hide-tutorials', group='gameplay', mode='system_enable'},
{command='immortal-cravings', group='gameplay', mode='enable'},
{command='light-aquifers-only', group='gameplay', mode='run'},
{command='misery', group='gameplay', mode='enable'},
{command='orders-reevaluate', help_command='orders', group='gameplay', mode='repeat',
Expand Down

0 comments on commit 80e1a98

Please sign in to comment.