3
3
4
4
local idle = reqscript (' idle-crafting' )
5
5
local repeatutil = require (" repeat-util" )
6
+
6
7
--- utility functions
7
8
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
+
8
18
--- 3D city metric
9
19
--- @param p1 df.coord
10
20
--- @param p2 df.coord
@@ -13,26 +23,40 @@ function distance(p1, p2)
13
23
return math.max (math.abs (p1 .x - p2 .x ), math.abs (p1 .y - p2 .y )) + math.abs (p1 .z - p2 .z )
14
24
end
15
25
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
+
16
45
--- find closest accessible item in an item vector
17
46
--- @generic T : df.item
18
47
--- @param pos df.coord
19
48
--- @param item_vector T[]
20
49
--- @param is_good ? fun ( item : T ): boolean
21
50
--- @return T ?
22
51
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
52
+ local function metric (item )
53
+ if not is_good or is_good (item ) then
27
54
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
33
56
end
57
+ return nil
34
58
end
35
- return closest
59
+ return findBest ( item_vector , metric , true )
36
60
end
37
61
38
62
--- find a drink
41
65
local function get_closest_drink (pos )
42
66
local is_good = function (drink )
43
67
local container = dfhack .items .getContainer (drink )
44
- return container and container :isFoodStorage ()
68
+ return not drink . flags . in_job and container and container :isFoodStorage ()
45
69
end
46
70
return findClosest (pos , df .global .world .items .other .DRINK , is_good )
47
71
end
48
72
49
- --- find some prepared meal
73
+ --- find available meal with highest per-portion value
50
74
--- @return df.item_foodst ?
51
- local function get_closest_meal (pos )
75
+ local function get_best_meal (pos )
76
+
52
77
--- @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
56
82
else
83
+ -- check that meal is either on the ground or in food storage (and not in a backpack)
57
84
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
59
90
end
60
91
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 )
62
94
end
63
95
64
96
--- create a Drink job for the given unit
86
118
--- create Eat job for the given unit
87
119
--- @param unit df.unit
88
120
local 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
91
123
-- print('no accessible meals found')
92
124
return
93
125
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
+
94
137
local job = idle .make_job ()
95
138
job .job_type = df .job_type .Eat
96
139
job .flags .special = true
@@ -105,6 +148,25 @@ local function goEat(unit)
105
148
print (dfhack .df2console (' immortal-cravings: %s is getting something to eat' ):format (name ))
106
149
end
107
150
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
+
108
170
--- script logic
109
171
110
172
local GLOBAL_KEY = ' immortal-cravings'
@@ -137,7 +199,7 @@ local threshold = -9000
137
199
138
200
--- unit loop: check for idle watched units and create eat/drink jobs for them
139
201
local 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 ))
141
203
--- @type integer[]
142
204
local kept = {}
143
205
for _ , unit_id in ipairs (watched ) do
@@ -148,7 +210,8 @@ local function unit_loop()
148
210
then
149
211
goto next_unit
150
212
end
151
- if not idle .unitIsAvailable (unit ) then
213
+ if not unitIsAvailable (unit ) then
214
+ debug (" immortal-cravings: skipping busy" .. dfhack .units .getReadableName (unit ))
152
215
table.insert (kept , unit .id )
153
216
else
154
217
-- unit is available for jobs; satisfy one of its needs
@@ -166,7 +229,7 @@ local function unit_loop()
166
229
end
167
230
watched = kept
168
231
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' )
170
233
repeatutil .cancel (GLOBAL_KEY .. ' -unit' )
171
234
end
172
235
end
@@ -178,18 +241,21 @@ end
178
241
179
242
--- main loop: look for citizens with personality needs for food/drink but w/o physiological need
180
243
local function main_loop ()
181
- -- print ('immortal-cravings watching:')
244
+ debug (' immortal-cravings watching:' )
182
245
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
185
251
goto next_unit
186
252
end
187
253
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
190
256
then
191
257
table.insert (watched , unit .id )
192
- -- print (' '..dfhack.df2console(dfhack.units.getReadableName(unit)))
258
+ debug (' ' .. dfhack .df2console (dfhack .units .getReadableName (unit )))
193
259
goto next_unit
194
260
end
195
261
end
0 commit comments