diff --git a/gui/pregnancy.lua b/gui/pregnancy.lua index e759dfa11..026a8c449 100644 --- a/gui/pregnancy.lua +++ b/gui/pregnancy.lua @@ -1,375 +1,381 @@ local gui = require('gui') local widgets = require('gui.widgets') -PregnancyGui = defclass(PregnancyGui, widgets.Window) -PregnancyGui.ATTRS { +local function zoom_to(unit) + if not unit then return end + dfhack.gui.revealInDwarfmodeMap(xyz2pos(dfhack.units.getPosition(unit)), true, true) +end + +local function is_viable_partner(unit, required_pronoun) + return unit and unit.sex == required_pronoun and dfhack.units.isAdult(unit) +end + +---------------------- +-- Pregnancy +-- + +Pregnancy = defclass(Pregnancy, widgets.Window) +Pregnancy.ATTRS { frame_title='Pregnancy manager', - frame={w=64, h=35}, - resizable=true, -- if resizing makes sense for your dialog - resize_min={w=50, h=20}, -- try to allow users to shrink your windows + frame={w=50, h=28, r=2, t=18}, + resizable=true, } -function PregnancyGui:init() - if dfhack.gui.getSelectedUnit(true).sex == df.pronoun_type.she then - self.mother = dfhack.gui.getSelectedUnit(true) - else self.mother = false - end - self.father = false - self.father_historical = false - self.msg = {} - - local term_options = {} - local term_index = {} - local months - for months=0,10 do - -- table.insert(term_options,{label=('%s months'):format(months),value=months}) --I tried this to add labels, probably doing something wrong, it broke the range widget - table.insert(term_options,months) --this works though - end - for k,v in ipairs(term_options) do - term_index[v] = k - end +function Pregnancy:init() + self.cache = {} + self.mother_id, self.father_id = -1, -1 + self.dirty = 0 self:addviews{ - widgets.ResizingPanel{ - frame={t=0}, - frame_style=gui.FRAME_INTERIOR, - autoarrange_subviews=true, - subviews={ - widgets.WrappedLabel{ - text_to_wrap=self:callback('getMotherLabel') - }, - widgets.HotkeyLabel{ - frame={l=0}, - label="Set mother to selected unit", - key='CUSTOM_SHIFT_M', - on_activate=self:callback('selectmother'), - }, + widgets.Label{ + frame={t=0, l=0}, + text='Mother:', + }, + widgets.Label{ + frame={t=0, l=8}, + text='None (please select an adult female)', + text_pen=COLOR_YELLOW, + visible=function() return not self:get_mother() end, + }, + widgets.Label{ + frame={t=0, l=8}, + text={{text=self:callback('get_name', 'mother')}}, + text_pen=COLOR_LIGHTMAGENTA, + auto_width=true, + on_click=function() zoom_to(self:get_mother()) end, + visible=self:callback('get_mother'), + }, + widgets.Label{ + frame={t=1, l=0}, + text={{text=self:callback('get_pregnancy_desc')}}, + }, + widgets.Label{ + frame={t=3, l=0}, + text='Spouse:', + }, + widgets.Label{ + frame={t=3, l=8}, + text='None', + visible=function() return not self:get_spouse_unit('mother') and not self:get_spouse_hf('mother') end, + }, + widgets.Label{ + frame={t=3, l=8}, + text={{text=self:callback('get_spouse_name', 'mother')}}, + text_pen=COLOR_BLUE, + auto_width=true, + on_click=function() zoom_to(self:get_spouse_unit('mother')) end, + visible=self:callback('get_spouse_unit', 'mother'), + }, + widgets.Label{ + frame={t=3, l=8}, + text={ + {text=self:callback('get_spouse_hf_name', 'mother')}, + ' (off-site)', }, + text_pen=COLOR_BLUE, + auto_width=true, + visible=function() return not self:get_spouse_unit('mother') and self:get_spouse_hf('mother') end, }, - widgets.ResizingPanel{ - frame={t=5}, - frame_style=gui.FRAME_INTERIOR, - autoarrange_subviews=true, - subviews={ - widgets.WrappedLabel{ - text_to_wrap=self:callback('getFatherLabel') - }, - widgets.HotkeyLabel{ - frame={l=0}, - label="Set father to selected unit", - key='CUSTOM_SHIFT_F', - on_activate=self:callback('selectfather'), - }, - widgets.HotkeyLabel{ - frame={l=5}, - label="Set mother's spouse as the father", - key='CUSTOM_F', - on_activate=self:callback('spouseFather'), - disabled=function() return not self.mother or self.mother.relationship_ids.Spouse == -1 end - }, + widgets.HotkeyLabel{ + frame={t=4, l=2}, + label="Set mother's spouse as the father", + key='CUSTOM_F', + auto_width=true, + on_activate=function() self:set_father(self:get_spouse_unit('mother')) end, + enabled=function() + local spouse = self:get_spouse_unit('mother') + return spouse and spouse.id ~= self.father_id and is_viable_partner(spouse, df.pronoun_type.he) + end, + }, + widgets.HotkeyLabel{ + frame={t=6, l=0}, + label="Set mother to selected unit", + key='CUSTOM_SHIFT_M', + auto_width=true, + on_activate=self:callback('set_mother'), + enabled=function() + local unit = dfhack.gui.getSelectedUnit(true) + return unit and unit.id ~= self.mother_id and is_viable_partner(unit, df.pronoun_type.she) + end, + }, + widgets.Divider{ + frame={t=8, h=1}, + frame_style=gui.FRAME_THIN, + frame_style_l=false, + frame_style_r=false, + }, + widgets.Label{ + frame={t=10, l=0}, + text='Father:', + }, + widgets.Label{ + frame={t=10, l=8}, + text={ + 'None ', + {text='(optionally select an adult male)', pen=COLOR_GRAY}, }, + visible=function() return not self:get_father() end, }, - widgets.Panel{ - frame={t=12,h=14}, - frame_style=gui.FRAME_INTERIOR, - subviews={ - widgets.HotkeyLabel{ - frame={l=0, t=0}, - key='CUSTOM_SHIFT_P', - label="Create pregnancy", - on_activate=self:callback('CreatePregnancy'), - enabled=function() return self.mother or self.father and self.father_historical end - }, - widgets.ToggleHotkeyLabel{ - frame={l=1, t=1}, - view_id='Force', - label='Replace existing pregnancy', - options={{label='On', value=true, pen=COLOR_GREEN}, - {label='Off', value=false, pen=COLOR_RED}}, - initial_option=false - }, - widgets.TooltipLabel{ - frame={l=0, t=3}, - text_to_wrap='Pregnancy term range (months):', - show_tooltip=true, - text_pen=COLOR_WHITE - }, - widgets.CycleHotkeyLabel{ - view_id='min_term', - frame={l=0, t=6, w=SLIDER_LABEL_WIDTH}, - label='Min pregnancy term:', - key_back='CUSTOM_SHIFT_Z', - key='CUSTOM_SHIFT_X', - options=term_options, - initial_option=7 - }, - widgets.CycleHotkeyLabel{ - view_id='max_term', - frame={l=30, t=6, w=SLIDER_LABEL_WIDTH}, - label='Max pregnancy term:', - key_back='CUSTOM_SHIFT_Q', - key='CUSTOM_SHIFT_W', - options=term_options, - initial_option=9 - }, - widgets.RangeSlider{ - frame={l=0, t=4}, - num_stops=#term_options, - get_left_idx_fn=function() - return term_index[self.subviews.min_term:getOptionLabel()] - end, - get_right_idx_fn=function() - return term_index[self.subviews.max_term:getOptionLabel()] - end, - on_left_change=function(idx) self.subviews.min_term:setOption(idx, true) end, - on_right_change=function(idx) self.subviews.max_term:setOption(idx, true) end, - }, - widgets.WrappedLabel{ - frame={t=8},--, h=5}, - text_to_wrap=function() return self.msg end - }, + widgets.Label{ + frame={t=10, l=8}, + text={{text=self:callback('get_name', 'father')}}, + text_pen=function() + local spouse = self:get_spouse_unit('mother') + if spouse and self.father_id == spouse.id then + return COLOR_BLUE + end + return COLOR_CYAN + end, + auto_width=true, + on_click=function() zoom_to(self:get_father()) end, + visible=self:callback('get_father'), + }, + widgets.Label{ + frame={t=12, l=0}, + text='Spouse:', + }, + widgets.Label{ + frame={t=12, l=8}, + text='None', + visible=function() return not self:get_spouse_unit('father') and not self:get_spouse_hf('father') end, + }, + widgets.Label{ + frame={t=12, l=8}, + text={{text=self:callback('get_spouse_name', 'father')}}, + text_pen=function() + local spouse = self:get_spouse_unit('father') + if spouse and self.mother_id == spouse.id then + return COLOR_LIGHTMAGENTA + end + return COLOR_CYAN + end, + auto_width=true, + on_click=function() zoom_to(self:get_spouse_unit('father')) end, + visible=self:callback('get_spouse_unit', 'father'), + }, + widgets.Label{ + frame={t=12, l=8}, + text={ + {text=self:callback('get_spouse_hf_name', 'father')}, + ' (off-site)', + }, + text_pen=COLOR_CYAN, + auto_width=true, + visible=function() return not self:get_spouse_unit('father') and self:get_spouse_hf('father') end, + }, + widgets.HotkeyLabel{ + frame={t=13, l=2}, + label="Set father's spouse as the mother", + key='CUSTOM_M', + auto_width=true, + on_activate=function() self:set_mother(self:get_spouse_unit('father')) end, + enabled=function() + local spouse = self:get_spouse_unit('father') + return spouse and spouse.id ~= self.mother_id and is_viable_partner(spouse, df.pronoun_type.she) + end, + }, + widgets.HotkeyLabel{ + frame={t=15, l=0}, + label="Set father to selected unit", + key='CUSTOM_SHIFT_F', + auto_width=true, + on_activate=self:callback('set_father'), + enabled=function() + local unit = dfhack.gui.getSelectedUnit(true) + return unit and unit.id ~= self.father_id and is_viable_partner(unit, df.pronoun_type.he) + end, + }, + widgets.Divider{ + frame={t=17, h=1}, + frame_style=gui.FRAME_THIN, + frame_style_l=false, + frame_style_r=false, + }, + widgets.CycleHotkeyLabel{ + view_id='term', + frame={t=19, l=0, w=40}, + label='Pregnancy term (in months):', + key_back='CUSTOM_SHIFT_Z', + key='CUSTOM_Z', + options={ + {label='Default', value='default'}, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, }, + initial_option='default', }, - widgets.ResizingPanel{ - frame={t=26}, + widgets.Panel{ + frame={t=21, w=23, h=3}, frame_style=gui.FRAME_INTERIOR, - autoarrange_subviews=true, subviews={ widgets.HotkeyLabel{ - frame={l=1, b=0}, - key='LEAVESCREEN', - label="Return to game", - on_activate=function() - repeat until not self:onInput{LEAVESCREEN=true} - view:dismiss() - end, + key='CUSTOM_SHIFT_P', + label="Generate pregnancy", + on_activate=self:callback('commit'), + enabled=function() return self:get_mother() end, }, - }, + } }, } -end -function PregnancyGui:selectmother() local unit = dfhack.gui.getSelectedUnit(true) - if unit then - if unit.sex==df.pronoun_type.she and dfhack.units.isAdult(unit) then - self.mother = unit - self:updateLayout() - end - end + self:set_mother(unit) + self:set_father(unit) end -function PregnancyGui:selectfather() - local unit = dfhack.gui.getSelectedUnit(true) - if unit and dfhack.units.isAdult(unit) then - self.father = unit - self.father_historical = false +function Pregnancy:get_mother() + self.cache.mother = self.cache.mother or df.unit.find(self.mother_id) + return self.cache.mother +end + +function Pregnancy:get_father() + self.cache.father = self.cache.father or df.unit.find(self.father_id) + return self.cache.father +end + +function Pregnancy:render(dc) + if self.dirty > 0 then + -- needs multiple iterations of updateLayout because of multiple + -- layers of indirection in the text generation self:updateLayout() + self.dirty = self.dirty - 1 end + Pregnancy.super.render(self, dc) + self.cache = {} end -function PregnancyGui:spouseFather() - local father = self:findSpouse(self.mother)[3] - if father then - if df.unit.find(father.unit_id) then - self.father = df.unit.find(father.unit_id) - self.father_historical = false - else - self.father_historical = father - self.father = false - end - self:updateLayout() - end +function Pregnancy:get_name(who) + local unit = self['get_'..who](self) + return unit and dfhack.units.getReadableName(unit) or '' end -function PregnancyGui:getMotherLabel() - if self.mother then - local motherName = dfhack.TranslateName(self.mother.name) - if self.mother.relationship_ids.Spouse > -1 then - local spouseInfo = self:findSpouse(self.mother) - return ('Selected mother: %s.%sShe is married to %s (%s).'):format( - self:findName(self.mother), - NEWLINE, - spouseInfo[1], - spouseInfo[2] - ) - else - return ('Selected mother: %s.%sShe is unmarried.'):format( - self:findName(self.mother), - NEWLINE - ) - end - else return ('No mother selected - Must be an adult female') +local TICKS_PER_DAY = 1200 +local TICKS_PER_MONTH = 28 * TICKS_PER_DAY + +function Pregnancy:get_pregnancy_desc() + local mother = self:get_mother() + if not mother or not mother.pregnancy_genes then return 'Not currently pregnant' end + local term_str = 'today' + if mother.pregnancy_timer > TICKS_PER_MONTH then + local num_months = (mother.pregnancy_timer + TICKS_PER_MONTH//2) // TICKS_PER_MONTH + term_str = ('in %d month%s'):format(num_months, num_months == 1 and '' or 's') + elseif mother.pregnancy_timer > TICKS_PER_DAY then + local num_days = (mother.pregnancy_timer + TICKS_PER_DAY//2) // TICKS_PER_DAY + term_str = ('in %d day%s'):format(num_days, num_days == 1 and '' or 's') end + return ('Currently pregnant: coming to term %s'):format(term_str) end -function PregnancyGui:getFatherLabel() - if self.father or self.father_historical then - if self.father_historical or self.father.relationship_ids.Spouse > -1 then - local father = self.father or self.father_historical - local spouseInfo = self:findSpouse(father) - return ('Selected father: %s.%s%s is married to %s (%s).'):format( - self:findName(father), - NEWLINE, - df.pronoun_type[father.sex]:gsub("^%l", string.upper), - spouseInfo[1], - spouseInfo[2] - ) - else - return ('Selected father: %s.%s%s is unmarried.'):format( - self:findName(self.father), - NEWLINE, - df.pronoun_type[self.father.sex]:gsub("^%l", string.upper) - ) +function Pregnancy:get_spouse_unit(who) + local unit = self['get_'..who](self) + if not unit then return end + return df.unit.find(unit.relationship_ids.Spouse) +end + +function Pregnancy:get_spouse_hf(who) + local unit = self['get_'..who](self) + if not unit or unit.relationship_ids.Spouse == -1 then + return + end + local spouse = df.unit.find(unit.relationship_ids.Spouse) + if spouse then + return df.historical_figure.find(spouse.hist_figure_id) + end + + for _, relation in ipairs(unit.histfig_links) do + if relation._type == df.histfig_hf_link_spousest then + -- may be nil due to hf culling, but then we just treat it as not having a spouse + return df.historical_figure.find(relation.target_hf) end - else return ('No father selected') end end -function PregnancyGui:findName(unit) - local name = dfhack.TranslateName(unit.name) - if name ~= "" then - return name - else return ('Unnamed %s. (Unit id:%s)'):format( - string.upper(df.global.world.raws.creatures.all[unit.race].name[0]), - unit.id - ) - end +function Pregnancy:get_spouse_name(who) + local spouse = self:get_spouse_unit(who) + return spouse and dfhack.units.getReadableName(spouse) or '' end -function PregnancyGui:findSpouse(unit) - local historical_spouse, spouse_loc, spouse, spouseid - local culled = false +function Pregnancy:get_spouse_hf_name(who) + local spouse_hf = self:get_spouse_hf(who) + return spouse_hf and dfhack.units.getReadableName(spouse_hf) or '' +end - --setting variables for if mother or father are local, followed by finding the father's spouse if he is not local - if self.father == unit or self.mother == unit then - spouseid = unit.relationship_ids.Spouse - spouse = df.unit.find(spouseid) - elseif self.father_historical == unit then - for index, relation in pairs(unit.histfig_links) do - if relation._type == df.histfig_hf_link_spousest then - historical_spouse=df.historical_figure.find(relation.target_hf) - if not historical_spouse then culled = true --there was an id, but there wasn't a histfig with that id (due culling) - elseif df.global.plotinfo.site_id==historical_spouse.info.whereabouts.site then - spouse_loc = 'local' - else spouse_loc = 'offsite' - end - end +function Pregnancy:set_mother(unit) + unit = unit or dfhack.gui.getSelectedUnit(true) + if not is_viable_partner(unit, df.pronoun_type.she) then return end + self.mother_id = unit.id + if self.father_id ~= -1 then + local father = self:get_father() + if not father or father.race ~= unit.race then + self.father_id = -1 end - return {dfhack.TranslateName(historical_spouse.name),spouse_loc,historical_spouse} end - - --if the spouse is local this should identify them: - if spouse then - historical_spouse = df.historical_figure.find(spouse.hist_figure_id) or false - spouse_loc = 'local' + if self.father_id == -1 then + self:set_father(self:get_spouse_unit('mother')) end + self.dirty = 2 +end - --if spouse is not local (offsite): - if spouseid > -1 and not spouse then --spouse exists but isnt on the map, so search historical units: - local historical_unit = df.historical_figure.find(unit.hist_figure_id) - for index, relation in pairs(historical_unit.histfig_links) do - if relation._type == df.histfig_hf_link_spousest then - historical_spouse=df.historical_figure.find(relation.target_hf) - if not historical_spouse then culled = true --there was an id, but there wasn't a histfig with that id (due culling) - elseif df.global.plotinfo.site_id==historical_spouse.info.whereabouts.site then--i dont think this should ever be true - spouse_loc = 'local' - else spouse_loc = 'offsite' - end - end +function Pregnancy:set_father(unit) + unit = unit or dfhack.gui.getSelectedUnit(true) + if not is_viable_partner(unit, df.pronoun_type.he) then return end + self.father_id = unit.id + if self.mother_id ~= -1 then + local mother = self:get_mother() + if not mother or mother.race ~= unit.race then + self.mother_id = -1 end end - if culled then - return {'Unknown','culled'} - else - return {dfhack.TranslateName(historical_spouse.name),spouse_loc,historical_spouse} + if self.mother_id == -1 then + self:set_mother(self:get_spouse_unit('father')) end + self.dirty = 2 end -function PregnancyGui:CreatePregnancy() - local genes,father_id,father_caste,father_name - local bypass = true - local force = self.subviews.Force:getOptionValue() +local function get_term_ticks(months) + local ticks = months * TICKS_PER_MONTH + -- subtract off a random amount between 0 and half a month + ticks = math.max(1, ticks - math.random(0, TICKS_PER_MONTH//2)) + return ticks +end - self.msg = {} +function Pregnancy:commit() + local mother = self:get_mother() + local father = self:get_father() or mother - if self.subviews.min_term:getOptionLabel() > self.subviews.max_term:getOptionLabel() then - table.insert(self.msg,('Min term has to be less then max term')) - self:updateLayout() - return + local term_months = self.subviews.term:getOptionValue() + if term_months == 'default' then + local caste_flags = mother.enemy.caste_flags + if caste_flags.CAN_SPEAK or caste_flags.CAN_LEARN then + term_months = 9 + else + term_months = 6 + end end - if self.father then - genes=self.father.appearance.genes:new() - father_id=self.father.hist_figure_id - father_caste=self.father.caste - father_name=self:findName(self.father) + if mother.pregnancy_genes then + mother.pregnancy_genes:assign(father.appearance.genes) else - genes=self.mother.appearance.genes:new()--i dont think historical figures have genes - father_id=self.father_historical.id - father_caste=self.father_historical.caste - father_name=self:findName(self.father_historical) + mother.pregnancy_genes = father.appearance.genes:new() end - if self.mother.pregnancy_timer > 0 then - local og_father = df.historical_figure.find(self.mother.pregnancy_spouse) - bypass = false - if force and og_father then - table.insert(self.msg, ('SUCCESS:%sMother:%s%sFather:%s%sPrevious pregnancy with %s replaced'):format( - NEWLINE, - self:findName(self.mother), - NEWLINE, - father_name, - NEWLINE, - dfhack.TranslateName(og_father.name) - )) - elseif force then - table.insert(self.msg, ('SUCCESS:%sMother:%s%sFather:%s%sPrevious pregnancy aborted'):format( - NEWLINE, - self:findName(self.mother), - NEWLINE, - father_name, - NEWLINE - )) - elseif og_father then - table.insert(self.msg, ('FAILED:%s%s already pregnant with %s%s'):format( - NEWLINE, - self:findName(self.mother), - dfhack.TranslateName(og_father.name), - force - )) - else - table.insert(self.msg, ('FAILED:%s%s is already pregnant, no father is recorded'):format( - NEWLINE, - self:findName(self.mother) - )) - end - end + mother.pregnancy_timer = get_term_ticks(term_months) + mother.pregnancy_caste = father.caste + mother.pregnancy_spouse = father.hist_figure_id ~= mother.hist_figure_id and father.hist_figure_id or -1 - if bypass or force then - self.mother.pregnancy_timer=math.random(self.subviews.min_term:getOptionLabel()*33600+1, self.subviews.max_term:getOptionLabel()*33600+1) - self.mother.pregnancy_caste=father_caste - self.mother.pregnancy_spouse=father_id - self.mother.pregnancy_genes=genes - if not force then - table.insert(self.msg, ('SUCCESS:%sMother:%s%sFather:%s'):format( - NEWLINE, - self:findName(self.mother), - NEWLINE, - father_name - )) - end - end - self:updateLayout() + self.dirty = 2 end +---------------------- +-- PregnancyScreen +-- + PregnancyScreen = defclass(PregnancyScreen, gui.ZScreen) PregnancyScreen.ATTRS { focus_path='pregnancy', } function PregnancyScreen:init() - self:addviews{PregnancyGui{}} + self:addviews{Pregnancy{}} end function PregnancyScreen:onDismiss()