Skip to content

Commit 80e1a98

Browse files
authored
Merge pull request #1301 from chdoc/immortal-cravings
new tool: immortal-cravings
2 parents ff8f711 + edc7d40 commit 80e1a98

File tree

4 files changed

+259
-0
lines changed

4 files changed

+259
-0
lines changed

changelog.txt

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Template for new versions:
2828

2929
## New Tools
3030
- `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.
31+
- `immortal-cravings`: allow immortals to satisfy their cravings for food and drink
3132

3233
## New Features
3334
- `force`: support the ``Wildlife`` event to allow additional wildlife to enter the map

docs/immortal-cravings.rst

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
immortal-cravings
2+
=================
3+
4+
.. dfhack-tool::
5+
:summary: Allow immortals to satisfy their cravings for food and drink.
6+
:tags: fort gameplay
7+
8+
When enabled, this script watches your fort for units that have no physiological
9+
need to eat or drink but still have personality needs that can only be satisfied
10+
by eating or drinking (e.g. necromancers). This enables those units to help
11+
themselves to a drink or a meal when they crave one and are not otherwise
12+
occupied.
13+
14+
Usage
15+
-----
16+
17+
::
18+
19+
enable immortal-cravings

immortal-cravings.lua

+238
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
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

internal/control-panel/registry.lua

+1
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ COMMANDS_BY_IDX = {
119119
{command='fastdwarf', group='gameplay', mode='enable'},
120120
{command='hermit', group='gameplay', mode='enable'},
121121
{command='hide-tutorials', group='gameplay', mode='system_enable'},
122+
{command='immortal-cravings', group='gameplay', mode='enable'},
122123
{command='light-aquifers-only', group='gameplay', mode='run'},
123124
{command='misery', group='gameplay', mode='enable'},
124125
{command='orders-reevaluate', help_command='orders', group='gameplay', mode='repeat',

0 commit comments

Comments
 (0)