I am working on a script to display information about a unit such as birth day (civ members/pets), max age (currently as an average), body size estimate, milkable, and grazer info. I have it respecting assumed identities for the birth/age/name and can display animals with/without "Stray".
However, I am having some trouble figuring out how to determine that a unit is supposed to have "Corpse" in the name. Black Mamba Man Corpse comes out as Black Mamba Man. Since the zombie looks very similar to vampires in code I don't know what to base the decision to tack on "corpse" to the name/profession. I am also curious as to how many other name/profession modifications I might need for units such as necromancers, ghosts, demons, clowns, husks, etc
I would also like to show "missing" units as missing with perhaps the time they were reported missing, but I am uncertain if the game records that date. If it also recorded a last known location I might want to allow centering on that location.
I am also currently trying to print the basic description provided by the caste definition but am finding it difficult to find the printable width of my lua screen / widget so that I can insert appropriate NEWLINE breaks. I am probably trying to perform that logic in the wrong place (:init of my framed screen class). I think I ought to break up the info chunks into their own individual widget labels too.
I am currently calling it more-unit-info.lua and binding it to Alt-I but eventually it might be able to duplicate the "thoughts and preferences" screen so maybe another name would be more appropriate.
local gui = require 'gui'
local widgets =require 'gui.widgets'
local utils = require 'utils'
function getUnit_byID(id) -- get unit by id from units.all via binsearch
if type(id) == 'number' then
-- (vector,key,field,cmpfun,min,max) { item/nil , found true/false , idx/insert at }
return utils.binsearch(df.global.world.units.all,id,'id')
end
end
function getUnit_byVS(silent) -- by view screen mode
silent = silent or false
-- if not world loaded, return nil ?
local u,tmp -- u: the unit to return ; tmp: temporary for intermediate tests/return values
local v = dfhack.gui.getCurViewscreen()
u = dfhack.gui.getSelectedUnit(true) -- supports gui scripts/plugin that provide a hook for getSelectedUnit()
if u then
return u
-- else: contexts not currently supported by dfhack.gui.getSelectedUnit()
elseif df.viewscreen_dwarfmodest:is_instance(v) then
tmp = df.global.ui.main.mode
if tmp == 17 or tmp == 42 or tmp == 43 then
-- context: @dwarfmode/QueryBuiding/Some/Cage -- (q)uery cage
-- context: @dwarfmode/ZonesPenInfo/AssignUnit -- i (zone) -> pe(N)
-- context: @dwarfmode/ZonesPitInfo -- i (zone) -> (P)it
u = df.global.ui_building_assign_units[df.global.ui_building_item_cursor]
elseif df.global.ui.follow_unit ~= -1 then
-- context: follow unit mode
u = getUnit_byID(df.global.ui.follow_unit)
end -- end viewscreen_dwarfmodest
elseif df.viewscreen_petst:is_instance(v) then
-- context: @pet/List/Unit -- z (status) -> animals
if v.mode == 0 then -- List
if not v.is_vermin[v.cursor] then
u = v.animal[v.cursor].unit
end
--elseif v.mode = 1 then -- training knowledge (no unit reference)
elseif v.mode == 2 then -- select trainer
u = v.trainer_unit[v.trainer_cursor]
end
elseif df.viewscreen_layer_workshop_profilest:is_instance(v) then
-- context: @layer_workshop_profile/Unit -- (q)uery workshop -> (P)rofile -- df.global.ui.main.mode == 17
u = v.workers[v.layer_objects[0].cursor]
elseif df.viewscreen_layer_overall_healthst:is_instance(v) then
-- context @layer_overall_health/Units -- z -> health
u = v.unit[v.layer_objects[0].cursor]
elseif df.viewscreen_layer_militaryst:is_instance(v) then
-- layer_objects[0: squads list; 1: positions list; 2: candidates list]
-- page 0:positions/assignments 1:alerts 2:equipment 3:uniforms 4:supplies 5:ammunition
if v.page == 0 and v.layer_objects[2].enabled and v.layer_objects[2].active then
-- context: @layer_military/Positions/Position/Candidates -- m -> Candidates
u = v.positions.candidates[v.layer_objects[2].cursor]
elseif (v.page == 0 or v.page == 2) and v.layer_objects[1].enabled and v.layer_objects[1].active then
-- context: @layer_military/Positions/Position -- m -> Positions
-- context: @layer_military/Equip/Customize/View/Position -- m -> (e)quip -> Positions
-- context: @layer_military/Equip/Uniform/Positions -- m -> (e)quip -> assign (U)niforms -> Positions
u = v.positions.assigned[v.layer_objects[1].cursor]
end
elseif df.viewscreen_layer_noblelistst:is_instance(v) then
if v.mode == 0 then
-- context: @layer_noblelist/List -- (n)obles
u = v.info[v.layer_objects[v.mode].cursor].unit
elseif v.mode == 1 then
-- context: @layer_noblelist/Appoint -- (n)obles -> (r)eplace
u = v.candidates[v.layer_objects[v.mode].cursor].unit
end
elseif df.viewscreen_unitst:is_instance(v) then
-- @unit -- (v)unit -> z ; loo(k) -> enter ; (n)obles -> enter ; others
u = v.unit
elseif df.viewscreen_customize_unitst:is_instance(v) then
-- @customize_unit -- @unit -> y
u = v.unit
elseif df.viewscreen_layer_unit_healthst:is_instance(v) then
-- @layer_unit_health -- @unit -> h ; @layer_overall_health/Units -> enter
if df.viewscreen_layer_overall_healthst:is_instance(v.parent) then
-- context @layer_overall_health/Units -- z (status)-> health
u = v.parent.unit[v.parent.layer_objects[0].cursor]
elseif df.viewscreen_unitst:is_instance(v.parent) then
-- @customize_unit -- (v)unit -> z ; loo(k) -> enter ; etc
u = v.parent.unit
end
elseif df.viewscreen_textviewerst:is_instance(v) then
-- @textviewer -- @unit -> enter (thoughts and preferences)
if df.viewscreen_unitst:is_instance(v.parent) then
-- @customize_unit -- @unit -> enter (thoughts and preferences)
u = v.parent.unit
elseif df.viewscreen_itemst:is_instance(v.parent) then
tmp = v.parent.entry_ref[v.parent.cursor_pos]
if df.general_ref_unit:is_instance(tmp) then -- general_ref_unit and derived ; general_ref_contains_unitst ; others?
u = getUnit_byID(tmp.unit_id)
end
end
end -- switch viewscreen
if not u and not silent then
dfhack.printerr('No unit is selected in the UI or context not supported.')
end
return u
end -- getUnit_byVS()
local months = {
'Granite',
'Slate',
'Felsite',
'Hematite',
'Malachite',
'Galena',
'Limestone',
'Sandstone',
'Timber',
'Moonstone',
'Opal',
'Obsidian',
}
local pronouns = {
[0]='She',
[1]='He',
[2]='It',
}
--http://lua-users.org/wiki/StringRecipes ----------
function str2FirstUpper(str)
return str:gsub("^%l", string.upper)
end
--------------------------------------------------
--http://lua-users.org/wiki/StringRecipes ----------
local function tchelper(first, rest)
return first:upper()..rest:lower()
end
-- Add extra characters to the pattern if you need to. _ and ' are
-- found in the middle of identifiers and English words.
-- We must also put %w_' into [%w_'] to make it handle normal stuff
-- and extra stuff the same.
-- This also turns hex numbers into, eg. 0Xa7d4
function str2TitleCase(str)
return str:gsub("(%a)([%w_']*)", tchelper)
end
--------------------------------------------------
--isBlank suggestion by http://stackoverflow.com/a/10330861
function isBlank(x)
-- returns not match_begin, not match_end => false false ?
-- returns not not {match_begin, match_end} => true ?
-- returns not not nil => false
return not not tostring(x):find("^%s*$")
end
function Age(tbltxt,unit)
local civ_id = df.global.ui.civ_id
local cur_year = df.global.cur_year
local cur_year_tick = df.global.cur_year_tick
local caste = df.global.world.raws.creatures.all[unit.race].caste[unit.caste]
local ident = dfhack.units.getIdentity(unit)
if not dfhack.units.isDead(unit) then
if unit.civ_id == civ_id then
local y,m,d,t
if ident then
y = ident.birth_year
t = ident.birth_second
m = math.floor (t / 33600) + 1
d = math.floor ( (t % 33600) / 1200 ) + 1
else
y = unit.relations.birth_year
t = unit.relations.birth_time
m = math.floor (t / 33600) + 1
d = math.floor ( (t % 33600) / 1200 ) + 1
end
if d == 11 or d == 12 or d == 13 then
d = tostring(d)..'th'
elseif d % 10 == 1 then
d = tostring(d)..'st'
elseif d % 10 == 2 then
d = tostring(d)..'nd'
elseif d % 10 == 3 then
d = tostring(d)..'rd'
else
d = tostring(d)..'th'
end
local age_y = cur_year - y
local age = ''
if age_y == 0 then
local age_m = math.floor( (cur_year_tick - t) / 33600 )
if age_m == 0 then
age = 'a newborn'
elseif age_m == 1 then
age = '1 month old'
else
age = tostring(age_m)..' months old'
end
elseif age_y == 1 then
age = '1 year old'
elseif age_y > 1 then
age = tostring(age_y)..' years old'
end
local pronoun = pronouns[unit.sex] or 'It'
local blurb = { text=pronoun..' is '..age..', born on the '..d..' of '..months[m]..' in the year '..tostring(y)..PERIOD,
pen=dfhack.pen.parse{fg=COLOR_YELLOW,bg=0}}
table.insert(tbltxt,blurb)
table.insert(tbltxt,NEWLINE)
table.insert(tbltxt,NEWLINE)
pronoun = str2FirstUpper( caste.caste_name[1] )
local maxage = math.floor( (caste.misc.maxage_max + caste.misc.maxage_min)/2 )
if caste.misc.maxage_max == -1 then
maxage = ' die of unnatural causes.'
elseif maxage == 0 then
maxage = ' die at a very young age.'
elseif maxage == 1 then
maxage = ' live about '..tostring(maxage)..' year.'
else
maxage = ' live about '..tostring(maxage)..' years.'
end
--' is expected to '..
blurb = { text=pronoun..maxage,
pen=dfhack.pen.parse{fg=COLOR_DARKGREY,bg=0}}
table.insert(tbltxt,blurb)
table.insert(tbltxt,NEWLINE)
table.insert(tbltxt,NEWLINE)
else
local pronoun = str2FirstUpper( caste.caste_name[1] )
local maxage = math.floor( (caste.misc.maxage_max + caste.misc.maxage_min)/2 )
if caste.misc.maxage_max == -1 then
maxage = ' die of unnatural causes.'
elseif maxage == 0 then
maxage = ' die at a very young age.'
elseif maxage == 1 then
maxage = ' live about '..tostring(maxage)..' year.'
else
maxage = ' live about '..tostring(maxage)..' years.'
end
--' is expected to '..
local blurb = { text=pronoun..maxage,
pen=dfhack.pen.parse{fg=COLOR_DARKGREY,bg=0}}
table.insert(tbltxt,blurb)
table.insert(tbltxt,NEWLINE)
table.insert(tbltxt,NEWLINE)
end
else
local pronoun = pronouns[unit.sex] or 'It'
local blurb = { text= pronoun..' is dead.',
pen=dfhack.pen.parse{fg=COLOR_MAGENTA,bg=0}}
table.insert(tbltxt,blurb)
table.insert(tbltxt,NEWLINE)
table.insert(tbltxt,NEWLINE)
end
end
function Grazer(tbltxt,unit)
local caste = df.global.world.raws.creatures.all[unit.race].caste[unit.caste]
if caste.flags.GRAZER then
-- It satisfies X units of hunger when grazing.
-- Grazing satisfies X units of hunger.
--local pronoun = pronouns[unit.sex] or 'It'
local grazer = tostring(caste.misc.grazer)
local blurb = { text='Grazing satisfies '..grazer..' units of hunger.',
--text=pronoun..' satisfies '..grazer..' units of hunger when grazing.',
pen=dfhack.pen.parse{fg=COLOR_LIGHTGREEN,bg=0} }
table.insert(tbltxt,blurb)
table.insert(tbltxt,NEWLINE)
table.insert(tbltxt,NEWLINE)
end
end
function Milkable(tbltxt,unit)
local caste = df.global.world.raws.creatures.all[unit.race].caste[unit.caste]
if caste.flags.MILKABLE then
--
local pronoun = pronouns[unit.sex] or 'It'
local milk = dfhack.matinfo.decode( caste.extracts.milkable_mat , caste.extracts.milkable_matidx )
if milk then
local days,seconds = math.modf ( caste.misc.milkable / 1200 )
if seconds > 0 then
days = tostring(days)..' to '..tostring(days + 1)
else
days = tostring(days)
end
local blurb = { text=pronoun..' produces '..milk:toString()..' every '..days..' days.',
--text=pronoun..' satisfies '..grazer..' units of hunger when grazing.',
pen=dfhack.pen.parse{fg=COLOR_LIGHTCYAN,bg=0} }
table.insert(tbltxt,blurb)
table.insert(tbltxt,NEWLINE)
table.insert(tbltxt,NEWLINE)
end
end
end
function BodySize(tbltxt,unit)
if not dfhack.units.isDead(unit) then
local g = df.global
local caste = df.global.world.raws.creatures.all[unit.race].caste[unit.caste]
--local misc = creatures[unit.race].caste[unit.caste].misc
local bs1 = caste.body_size_1
local bs2 = caste.body_size_2
local bam = caste.body_appearance_modifiers
local ubam = unit.appearance.body_modifiers
local by = unit.relations.birth_year
local bt = unit.relations.birth_time
local bm = math.floor (bt / 33600)
local bd = math.floor ( (bt % 33600) / 1200 ) + 1
local age_y = g.cur_year - by
local age_t = g.cur_year_tick - bt
local age_d = age_y * 336 + (age_t / 1200) -- float
local bs
local rise, run, init = 0,0,0
for i,v in ipairs(bs2) do
if age_d >= v then -- bs2[0] always 0
init = bs1[i]
run = v
else
rise = bs1[i] - init
run = v - run
break
end
end
if run > 0 then -- if a creature only has one size defined, no growth
-- if unit is older than needed for adult size, rise = 0
bs = (rise/run) * age_d + init
else
bs = init
end
for i,v in ipairs(ubam) do
-- HEIGHT BROADNESS LENGTH
if bam[i].type >=0 and bam[i].type <=2 then
bs = bs * (v/100)
end
end
bs = tostring(math.floor(bs + 0.5))
local pronoun
if unit.name.has_name then
pronoun = str2FirstUpper(unit.name.first_name)
else
pronoun = pronouns[unit.sex] or 'It'
end
local blurb = { text=pronoun..' appears to be about '..bs..' cubic decimeters in size.',
pen=dfhack.pen.parse{fg=COLOR_LIGHTBLUE,bg=0} }
table.insert(tbltxt,blurb)
table.insert(tbltxt,NEWLINE)
table.insert(tbltxt,NEWLINE)
end
end
function Description(tbltxt,unit)
local dsc = df.global.world.raws.creatures.all[unit.race].caste[unit.caste].description
if not isBlank(dsc) then
local blurb = { text=dsc, } -- justify, add line wrap
--pen=dfhack.pen.parse{fg=COLOR_WHITE,bg=0} }
table.insert(tbltxt,blurb)
table.insert(tbltxt,NEWLINE)
table.insert(tbltxt,NEWLINE)
end
end
function Name(tbltxt,unit)
--local ident = dfhack.units.getIdentity(unit)
local name = dfhack.TranslateName( dfhack.units.getVisibleName(unit) )
local prof = dfhack.units.getProfessionName(unit)
local color = dfhack.units.getProfessionColor(unit)
-- is name blank? is prof blank?
local blurb
if isBlank(name) then
if isBlank(prof) then
blurb = { text='I am a mystery',
pen=dfhack.pen.parse{fg=color,bg=0} }
elseif unit.civ_id == df.global.ui.civ_id then
blurb = { text='Stray '..prof,
pen=dfhack.pen.parse{fg=color,bg=0} }
else
blurb = { text=prof,
pen=dfhack.pen.parse{fg=color,bg=0} }
end
else
if isBlank(prof) then
blurb = { text=name,
pen=dfhack.pen.parse{fg=color,bg=0} }
else
blurb = { text=name..', '..prof,
pen=dfhack.pen.parse{fg=color,bg=0} }
end
end
table.insert(tbltxt,blurb)
table.insert(tbltxt,NEWLINE)
table.insert(tbltxt,NEWLINE)
end
MoreUnitInfo = defclass(MoreUnitInfo, gui.FramedScreen)
MoreUnitInfo.ATTRS={
frame_style = gui.GREY_LINE_FRAME,
frame_title = 'I need a name',
frame_inset = 1,
}
function MoreUnitInfo:init(args)
self.unit = args.unit or getUnit_byVS(true)
local infotext={}
if self.unit then
Name(infotext,self.unit)
Description(infotext,self.unit)
Age(infotext,self.unit)
Grazer(infotext,self.unit)
Milkable(infotext,self.unit)
BodySize(infotext,self.unit)
else
table.insert(infotext,{ text="No unit is selected in the UI or context not supported.",
pen=dfhack.pen.parse{fg=COLOR_LIGHTRED,bg=0} })
table.insert(infotext,NEWLINE)
end
local mainPage=widgets.Panel{
subviews={widgets.Label{text=infotext , frame={l=1,t=1,r=1,yalign=0} }}}
local pages=widgets.Pages{subviews={mainPage},view_id="pages"}
self:addviews{ pages }
end
function MoreUnitInfo:onInput(keys)
if keys.LEAVESCREEN or keys.SELECT then
self:dismiss()
end
end
function MoreUnitInfo:onGetSelectedUnit()
return self.unit
end
MoreUnitInfo.focus_path = 'morunitinfo' -- -> dfhack/lua/moreunitinfo
-- only show if MoreUnitInfo isn't the current focus and unit reference found
if ( dfhack.isWorldLoaded() and 'dfhack/lua/'..MoreUnitInfo.focus_path ~= dfhack.gui.getCurFocus() ) then
local unit = getUnit_byVS()
if unit then
local mui = MoreUnitInfo{unit}
mui:show()
end
end
* I know, it is a bit of a mess