33
44local idle = reqscript (' idle-crafting' )
55local repeatutil = require (" repeat-util" )
6+
67--- utility functions
78
9+ local verbose = false
10+ --- conditional printing of debug messages
11+ --- @param message string
12+ local function debug (message )
13+ if verbose then
14+ print (message )
15+ end
16+ end
17+
818--- 3D city metric
919--- @param p1 df.coord
1020--- @param p2 df.coord
@@ -13,26 +23,40 @@ function distance(p1, p2)
1323 return math.max (math.abs (p1 .x - p2 .x ), math.abs (p1 .y - p2 .y )) + math.abs (p1 .z - p2 .z )
1424end
1525
26+ --- maybe a candidate for utils.lua?
27+ --- find best available item in an item vector (according to some metric)
28+ --- @generic T : df.item
29+ --- @param item_vector T[]
30+ --- @param metric fun ( item : T ): number ?
31+ --- @return T ?
32+ function findBest (item_vector , metric , smallest )
33+ local best = nil
34+ local mbest = nil
35+ for _ , item in ipairs (item_vector ) do
36+ mitem = metric (item )
37+ if mitem and (not best or (smallest and mitem < mbest or mitem > mbest )) then
38+ best = item
39+ mbest = mitem
40+ end
41+ end
42+ return best
43+ end
44+
1645--- find closest accessible item in an item vector
1746--- @generic T : df.item
1847--- @param pos df.coord
1948--- @param item_vector T[]
2049--- @param is_good ? fun ( item : T ): boolean
2150--- @return T ?
2251local 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
52+ local function metric (item )
53+ if not is_good or is_good (item ) then
2754 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
55+ return dfhack .maps .canWalkBetween (pos , pitem ) and distance (pos , pitem ) or nil
3356 end
57+ return nil
3458 end
35- return closest
59+ return findBest ( item_vector , metric , true )
3660end
3761
3862--- find a drink
4165local function get_closest_drink (pos )
4266 local is_good = function (drink )
4367 local container = dfhack .items .getContainer (drink )
44- return container and container :isFoodStorage ()
68+ return not drink . flags . in_job and container and container :isFoodStorage ()
4569 end
4670 return findClosest (pos , df .global .world .items .other .DRINK , is_good )
4771end
4872
49- --- find some prepared meal
73+ --- find available meal with highest per-portion value
5074--- @return df.item_foodst ?
51- local function get_closest_meal (pos )
75+ local function get_best_meal (pos )
76+
5277 --- @param meal df.item_foodst
53- local function is_good (meal )
54- if meal .flags .rotten then
55- return false
78+ local function portion_value (meal )
79+ local accessible = dfhack .maps .canWalkBetween (pos ,xyz2pos (dfhack .items .getPosition (meal )))
80+ if meal .flags .in_job or meal .flags .rotten or not accessible then
81+ return nil
5682 else
83+ -- check that meal is either on the ground or in food storage (and not in a backpack)
5784 local container = dfhack .items .getContainer (meal )
58- return not container or container :isFoodStorage ()
85+ if not container or container :isFoodStorage () then
86+ return dfhack .items .getValue (meal ) / meal .stack_size
87+ else
88+ return nil
89+ end
5990 end
6091 end
61- return findClosest (pos , df .global .world .items .other .FOOD , is_good )
92+
93+ return findBest (df .global .world .items .other .FOOD , portion_value )
6294end
6395
6496--- create a Drink job for the given unit
86118--- create Eat job for the given unit
87119--- @param unit df.unit
88120local function goEat (unit )
89- local meal = get_closest_meal (unit .pos )
90- if not meal then
121+ local meal_stack = get_best_meal (unit .pos )
122+ if not meal_stack then
91123 -- print('no accessible meals found')
92124 return
93125 end
126+
127+ --- @type df.item | df.item_foodst
128+ local meal
129+ if meal_stack .stack_size > 1 then
130+ meal = meal_stack :splitStack (1 , true )
131+ meal :categorize (true )
132+ else
133+ meal = meal_stack
134+ end
135+ dfhack .items .setOwner (meal , unit )
136+
94137 local job = idle .make_job ()
95138 job .job_type = df .job_type .Eat
96139 job .flags .special = true
@@ -105,6 +148,25 @@ local function goEat(unit)
105148 print (dfhack .df2console (' immortal-cravings: %s is getting something to eat' ):format (name ))
106149end
107150
151+ --- unit is ready to take jobs (will interrupt social activities)
152+ --- @param unit df.unit
153+ --- @return boolean
154+ function unitIsAvailable (unit )
155+ if unit .job .current_job then
156+ return false
157+ elseif # unit .individual_drills > 0 then
158+ return false
159+ elseif unit .flags1 .caged or unit .flags1 .chained then
160+ return false
161+ elseif unit .military .squad_id ~= - 1 then
162+ local squad = df .squad .find (unit .military .squad_id )
163+ -- this lookup should never fail
164+ --- @diagnostic disable-next-line : need-check-nil
165+ return # squad .orders == 0 and squad .activity == - 1
166+ end
167+ return true
168+ end
169+
108170--- script logic
109171
110172local GLOBAL_KEY = ' immortal-cravings'
@@ -137,7 +199,7 @@ local threshold = -9000
137199
138200--- unit loop: check for idle watched units and create eat/drink jobs for them
139201local function unit_loop ()
140- -- print (('immortal-cravings: running unit loop (%d watched units)'):format(#watched))
202+ debug ((' immortal-cravings: running unit loop (%d watched units)' ):format (# watched ))
141203 --- @type integer[]
142204 local kept = {}
143205 for _ , unit_id in ipairs (watched ) do
@@ -148,7 +210,8 @@ local function unit_loop()
148210 then
149211 goto next_unit
150212 end
151- if not idle .unitIsAvailable (unit ) then
213+ if not unitIsAvailable (unit ) then
214+ debug (" immortal-cravings: skipping busy" .. dfhack .units .getReadableName (unit ))
152215 table.insert (kept , unit .id )
153216 else
154217 -- unit is available for jobs; satisfy one of its needs
@@ -166,7 +229,7 @@ local function unit_loop()
166229 end
167230 watched = kept
168231 if # watched == 0 then
169- -- print ('immortal-cravings: no more watched units, cancelling unit loop')
232+ debug (' immortal-cravings: no more watched units, cancelling unit loop' )
170233 repeatutil .cancel (GLOBAL_KEY .. ' -unit' )
171234 end
172235end
@@ -178,18 +241,21 @@ end
178241
179242--- main loop: look for citizens with personality needs for food/drink but w/o physiological need
180243local function main_loop ()
181- -- print ('immortal-cravings watching:')
244+ debug (' immortal-cravings watching:' )
182245 watched = {}
183- for _ , unit in ipairs (dfhack .units .getCitizens ()) do
184- if not is_active_caste_flag (unit , ' NO_DRINK' ) and not is_active_caste_flag (unit , ' NO_EAT' ) then
246+ for _ , unit in ipairs (dfhack .units .getCitizens (false , false )) do
247+ if
248+ not (is_active_caste_flag (unit , ' NO_DRINK' ) or is_active_caste_flag (unit , ' NO_EAT' )) or
249+ unit .counters2 .stomach_content > 0
250+ then
185251 goto next_unit
186252 end
187253 for _ , need in ipairs (unit .status .current_soul .personality .needs ) do
188- if need .id == DrinkAlcohol and need .focus_level < threshold or
189- need .id == EatGoodMeal and need .focus_level < threshold
254+ if need .id == DrinkAlcohol and need .focus_level < threshold or
255+ need .id == EatGoodMeal and need .focus_level < threshold
190256 then
191257 table.insert (watched , unit .id )
192- -- print (' '..dfhack.df2console(dfhack.units.getReadableName(unit)))
258+ debug (' ' .. dfhack .df2console (dfhack .units .getReadableName (unit )))
193259 goto next_unit
194260 end
195261 end
0 commit comments