forked from DFHack/scripts
-
Notifications
You must be signed in to change notification settings - Fork 1
/
assign-attributes.lua
290 lines (251 loc) · 11.4 KB
/
assign-attributes.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
-- Change the attributes of a unit.
--@ module = true
local help = [====[
assign-attributes
=================
A script to change the physical and mental attributes of a unit.
Attributes are divided into tiers from -4 to 4. Tier 0 is the
standard level and represents the average values for that attribute,
tier 4 is the maximum level, and tier -4 is the minimum level.
An example of the attribute "Strength":
==== ===================
Tier Description
==== ===================
4 unbelievably strong
3 mighty
2 very strong
1 strong
0 (no description)
-1 weak
-2 very weak
-3 unquestionably weak
-4 unfathomably weak
==== ===================
For more information:
https://dwarffortresswiki.org/index.php/DF2014:Attribute
Usage:
``-help``:
print the help page.
``-unit <UNIT_ID>``:
the target unit ID. If not present, the
currently selected unit will be the target.
``-attributes [ <ATTRIBUTE> <TIER> <ATTRIBUTE> <TIER> <...> ]``:
the list of the attributes to modify and their tiers.
The valid attribute names can be found in the wiki:
https://dwarffortresswiki.org/index.php/DF2014:Attribute
(substitute any space with underscores); tiers range from -4
to 4. There must be a space before and after each square
bracket.
``-reset``:
reset all attributes to the average level (tier 0).
If both this option and a list of attributes/tiers
are present, the unit attributes will be reset
and then the listed attributes will be modified.
Example::
assign-attributes -reset -attributes [ STRENGTH 2 AGILITY -1 SPATIAL_SENSE -1 ]
This will reset all attributes to a neutral value and will set the following
values (if the currently selected unit is a dwarf):
* Strength: a random value between 1750 and 1999 (tier 2);
* Agility: a random value between 401 and 650 (tier -1);
* Spatial sense: a random value between 1043 and 1292 (tier -1).
The final result will be: "She is very strong, but she is clumsy.
She has a questionable spatial sense."
]====]
local utils = require("utils")
local valid_args = utils.invert({
'help',
'unit',
'attributes',
'reset',
})
-- ----------------------------------------------- UTILITY FUNCTIONS ------------------------------------------------ --
function print_yellow(text)
dfhack.color(COLOR_YELLOW)
print(text)
dfhack.color(-1)
end
-- ----------------------------------------------- GENERATE MEDIANS ------------------------------------------------- --
--- Retrieve and store the medians for the given race. Default race is DWARF.
local function generate_medians_table(race)
assert(not race or type(race) == "number" or type(race) == "string")
race = race or "DWARF"
local medians = {
PHYSICAL = {},
MENTAL = {}
}
-- populate the medians table with the attributes and their standard "human" value
do
for _, physical_attribute in ipairs(df.physical_attribute_type) do
medians.PHYSICAL[physical_attribute] = 1000
end
for _, mental_attribute in ipairs(df.mental_attribute_type) do
medians.MENTAL[mental_attribute] = 1000
end
end
-- now update those medians that are different from the standard median (i.e. the ones listed in the creature raws)
do
local creatures = df.global.world.raws.creatures.all
local creature
if type(race) == "number" then
creature = creatures[race]
else
_, creature = utils.linear_index(creatures, race, "creature_id")
end
assert(creature ~= nil)
for _, raw in ipairs(creature.raws) do
if string.match(raw.value, "PHYS_ATT_RANGE") then
local token_parts = raw.value:split(":")
-- part 1 *2* 3 4 5 *6* 7 8 9
-- [PHYS_ATT_RANGE:STRENGTH:450:950:1150:1250:1350:1550:2250]
medians.PHYSICAL[token_parts[2]] = tonumber(token_parts[6])
elseif string.match(raw.value, "MENT_ATT_RANGE") then
local token_parts = raw.value:split(":")
-- part 1 *2* 3 4 5 *6* 7 8 9
-- [MENT_ATT_RANGE:PATIENCE:450:950:1150:1250:1350:1550:2250]
medians.MENTAL[token_parts[2]] = tonumber(token_parts[6])
end
end
end
return medians
end
-- ----------------------------------------------- ASSIGN ATTRIBUTES ------------------------------------------------ --
--- Assign the given attributes to a unit, resetting all the other attributes if requested.
--- :attributes: nil, or a table. The fields have the attribute token as key and the tier as value.
--- :unit: a valid unit id, a df.unit object, or nil. If nil, the currently selected unit will be targeted.
--- :reset: boolean, or nil.
function assign(attributes, unit, reset)
assert(not attributes or type(attributes) == "table")
assert(not unit or type(unit) == "number" or df.unit:is_instance(unit))
assert(not reset or type(reset) == "boolean")
attributes = attributes or {}
reset = reset or false
if type(unit) == "number" then
unit = df.unit.find(tonumber(unit)) --luacheck:retype
else
unit = unit or dfhack.gui.getSelectedUnit(true)
end
if not unit then
qerror("No unit found.")
end
local medians = generate_medians_table(unit.race)
--- Given an attribute and a tier, calculate a random value within the limits of the tier and return it.
--- Tiers are normally 250 points apart, so they are 249 points wide, with the exception of tier 0 and tier 4:
--- tier 0 minimum value is 249 points below the median, and its maximum value is 249 points above the median;
--- tier 4 maximum value is theoretically 5000, but each unit has a maximum value equal to:
--- starting_value + max(starting_value, median)
--- where starting_value is the value of the attribute before any training.
--- The formula to calculate tier boundaries is:
--- median + (tier * 250).
--- For tiers 1, 2, 3 and 4, the formula returns the minimum value of the tier; for tiers -1, -2, -3, -4, the
--- formula returns the maximum value of the tier. Given that all tiers but tier 0 and tier 4 are 250 points
--- apart, for tiers -4, -3, -2, -1, 1, 2 and 3 we can add (for positive tiers) or subtract (for negative tiers)
--- up to 249 points to the formula result and still remain in the same tier. For tier 0, we can add or
--- subtract 249 points to the median value. For tier 4, we could add points up to the maximum value, but we won't
--- do it for simplicity's sake and we will treat tier 4 as any other tier.
local function convert_tier_to_value(attribute, tier)
assert(type(attribute) == "string")
assert(type(tier) == "number" and tier >= -4 or tier <= 4)
-- decide if we need to add or subtract
local sign = 0
if tier > 0 then
sign = 1
elseif tier < 0 then
sign = -1
else
-- tier 0: decide randomly if we'll add or subtract
local pos_or_neg = { 1, -1 }
sign = pos_or_neg[math.random(2)]
end
local random_offset = math.random(0, 249) * sign
local median = medians.PHYSICAL[attribute] or medians.MENTAL[attribute]
local value = math.max(0, median + tier * 250 + random_offset)
-- calculate the new max_value, using the new value as the starting value
local max_value = value + math.max(value, median)
return value, max_value
end
-- reset attributes around the median value
if reset then
for attribute_token, attribute_obj in pairs(unit.body.physical_attrs) do
local v, max_v = convert_tier_to_value(attribute_token, 0)
attribute_obj.value = v
attribute_obj.max_value = max_v
end
for attribute_token, attribute_obj in pairs(unit.status.current_soul.mental_attrs) do
local v, max_v = convert_tier_to_value(attribute_token, 0)
attribute_obj.value = v
attribute_obj.max_value = max_v
end
end
-- assign new attributes
for attribute, tier in pairs(attributes) do
attribute = attribute:upper()
if medians.PHYSICAL[attribute] then
if tier >= -4 and tier <= 4 then
local v, max_v = convert_tier_to_value(attribute, tier)
unit.body.physical_attrs[attribute].value = v
unit.body.physical_attrs[attribute].max_value = max_v
else
print_yellow("WARNING: tier out of range for attribute '" .. attribute .. "'. Skipping...")
end
elseif medians.MENTAL[attribute] then
if tier >= -4 and tier <= 4 then -- code repetition, but I think the warning message it's clearer this way
local v, max_v = convert_tier_to_value(attribute, tier)
unit.status.current_soul.mental_attrs[attribute].value = v
unit.status.current_soul.mental_attrs[attribute].max_value = max_v
else
print_yellow("WARNING: tier out of range for attribute '" .. attribute .. "'. Skipping...")
end
else
print_yellow("WARNING: '" .. attribute .. "' is not a valid attribute. Skipping...")
end
end
end
-- ------------------------------------------------------ MAIN ------------------------------------------------------ --
local function main(...)
local args = utils.processArgs({ ... }, valid_args)
if args.help then
print(help)
return
end
local unit
if args.unit then
unit = tonumber(args.unit)
if not unit then
qerror("'" .. args.unit .. "' is not a valid unit ID.")
end
end
local reset = false
if args.reset then
reset = true
end
-- parse attributes list
local attributes = {}
if args.attributes then
local i = 1
while i <= #args.attributes do
local v = args.attributes[i]
-- v can be an attribute name but it can also be a tier value, so we have to check
if not tonumber(v) then
-- assume it's a valid attribute name, for now
local attribute_name = tostring(v):upper()
-- then try to get the tier value
local tier_str = args.attributes[i + 1]
if not tier_str then
-- we reached the end of the attributes list
qerror("Missing tier value after '" .. v .. "'.")
end
local tier_int = tonumber(tier_str)
if not tier_int then
qerror("'" .. tier_str .. "' is not a valid number.")
end
-- assume the tier value is in range, for now
attributes[attribute_name] = tier_int
end
i = i + 1 -- skip next arg because we already consumed it
end
end
assign(attributes, unit, reset)
end
if not dfhack_flags.module then
main(...)
end