From 8029b0ca21e43064f51e9f4a4696735512487e84 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 23 Aug 2024 02:26:35 -0700 Subject: [PATCH] set or clear spouse relationships also add confirmation dialogs for disruptive actions, such as choosing a partner from a different race, which clears the selected unit --- changelog.txt | 2 +- docs/gui/pregnancy.rst | 20 +- gui/pregnancy.lua | 583 +++++++++++++++++++++++++++-------------- 3 files changed, 402 insertions(+), 203 deletions(-) diff --git a/changelog.txt b/changelog.txt index 2c84dd719..c469dbed9 100644 --- a/changelog.txt +++ b/changelog.txt @@ -29,7 +29,7 @@ Template for new versions: ## New Tools - `embark-anyone`: allows you to embark as any civilisation, including dead, and non-dwarven ones - `idle-crafting`: Allow dwarfs to automatically satisfy their need to craft objects. -- `gui/pregnancy`: view and generate pregnancies with specified parents +- `gui/pregnancy`: view and generate pregnancies or arrange marriages with specified partners ## New Features - `caravan`: DFHack dialogs for trade screens (both ``Bring goods to depot`` and the ``Trade`` barter screen) can now filter by item origins (foreign vs. fort-made) and can filter bins by whether they have a mix of ethically acceptable and unacceptable items in them diff --git a/docs/gui/pregnancy.rst b/docs/gui/pregnancy.rst index 9ed48161d..334832a52 100644 --- a/docs/gui/pregnancy.rst +++ b/docs/gui/pregnancy.rst @@ -6,7 +6,7 @@ gui/pregnancy :tags: adventure fort armok animals units This tool provides an interface for producing pregnancies with specific mothers -and fathers. +and fathers. It can also assign or unassign spouses. If a unit is selected when you run `gui/pregnancy`, they will be pre-selected as a parent. If the unit has a spouse of a different gender, they will be @@ -21,8 +21,14 @@ A unit must be on the map to participate in a pregnancy. For example, you cannot designate a father that is not on-site, even if they are the selected mother's spouse. -Children cannot be selected as a parent, and, due to game limitations, -cross-species pregnancies are not supported. +You can make the selected parents spouses by clicking on the "Set selected + as spouse" button, but the button is only enabled if you first +dissolve existing spouse relationships for both partners. If either new spouse +has existing lovers, you'll get a confirmation dialog, and if you choose to +proceed, the lover relationships will be removed. + +Children and units that are insane cannot be selected as a parent, and, due to +game limitations, cross-species pregnancies are not supported. Usage ----- @@ -30,3 +36,11 @@ Usage :: gui/pregnancy + +Technical notes +--------------- + +The reason for the requirement that a father must be on the map to contribute +to a pregnancy is that the genes used for the pregnancy are associated with the +physical unit. They are not stored with the "historical figure" that represents +the father when he is off-map. diff --git a/gui/pregnancy.lua b/gui/pregnancy.lua index dac365f50..a5d15a67a 100644 --- a/gui/pregnancy.lua +++ b/gui/pregnancy.lua @@ -1,3 +1,4 @@ +local dlg = require('gui.dialogs') local gui = require('gui') local widgets = require('gui.widgets') @@ -7,7 +8,99 @@ local function zoom_to(unit) end local function is_viable_partner(unit, required_pronoun) - return unit and unit.sex == required_pronoun and dfhack.units.isAdult(unit) + return unit and unit.sex == required_pronoun and dfhack.units.isAdult(unit) and dfhack.units.isSane(unit) +end + +-- clears other_hf's link to hf (assumes there's only one reverse link) +local function clear_rev_hf_link(link_type, hfid, other_hfid) + local other_hf = df.historical_figure.find(other_hfid) + if not other_hf then return end + for i, link in ipairs(other_hf.histfig_links) do + if link._type == link_type and link.target_hf == hfid then + other_hf.histfig_links:erase(i) + link:delete() + break + end + end +end + +local function clear_hf_links(link_type, hfid) + local hf = df.historical_figure.find(hfid) + if not hf then return end + for i = #hf.histfig_links-1,0,-1 do + local link = hf.histfig_links[i] + if link._type == link_type then + clear_rev_hf_link(link_type, hfid, link.target_hf) + hf.histfig_links:erase(i) + link:delete() + end + end +end + +local function has_hf_links(link_type, hfid) + local hf = df.historical_figure.find(hfid) + if not hf then return false end + for i = #hf.histfig_links-1,0,-1 do + local link = hf.histfig_links[i] + if link._type == link_type then return true end + end + return false +end + +local function clear_spouse(unit, noconfirm) + if not unit then return end + local function do_clear_spouse() + clear_hf_links(df.histfig_hf_link_spousest, unit.hist_figure_id) + local spouse = df.unit.find(unit.relationship_ids.Spouse) + if spouse then + spouse.relationship_ids.Spouse = -1 + end + unit.relationship_ids.Spouse = -1 + end + if noconfirm then + do_clear_spouse() + else + dlg.showYesNoPrompt('Clear spouse', + ('Really clear spouse for %s?'):format(dfhack.units.getReadableName(unit)), + COLOR_YELLOW, do_clear_spouse) + end +end + +-- adds a link to hf pointing to other_hf +local function add_hf_link(link_type, hfid, other_hfid) + local hf = df.historical_figure.find(hfid) + if not hf then return end + local link = link_type:new() + link.target_hf = other_hfid + link.link_strength = 100 + hf.histfig_links:insert('#', link) +end + +local function set_spouse(unit1, unit2) + local function do_set_spouse() + clear_spouse(unit1, true) + clear_spouse(unit2, true) + unit1.relationship_ids.Spouse = unit2.id + unit2.relationship_ids.Spouse = unit1.id + add_hf_link(df.histfig_hf_link_spousest, unit1.hist_figure_id, unit2.hist_figure_id) + add_hf_link(df.histfig_hf_link_spousest, unit2.hist_figure_id, unit1.hist_figure_id) + dfhack.gui.showAutoAnnouncement(df.announcement_type.MARRIAGE, xyz2pos(dfhack.units.getPosition(unit1)), + ('%s and %s have married!'):format(dfhack.TranslateName(unit1.name), dfhack.TranslateName(unit2.name)), + COLOR_LIGHTMAGENTA) + end + local unit1_has_lovers = has_hf_links(df.histfig_hf_link_loverst, unit1.hist_figure_id) + local unit2_has_lovers = has_hf_links(df.histfig_hf_link_loverst, unit2.hist_figure_id) + if unit1_has_lovers or unit2_has_lovers then + dlg.showYesNoPrompt('Clear lovers', + 'New partners have existing lovers. Spurn them?', + COLOR_YELLOW, function() + clear_hf_links(df.histfig_hf_link_loverst, unit1.hist_figure_id) + clear_hf_links(df.histfig_hf_link_loverst, unit2.hist_figure_id) + do_set_spouse() + end) + else + do_set_spouse() + end end ---------------------- @@ -16,8 +109,9 @@ end Pregnancy = defclass(Pregnancy, widgets.Window) Pregnancy.ATTRS { - frame_title='Pregnancy manager', - frame={w=50, h=28, r=2, t=18}, + frame_title='Pregnancy and family manager', + frame={w=50, h=29, r=2, t=18}, + frame_inset={t=1, l=1, r=1}, resizable=true, } @@ -27,203 +121,272 @@ function Pregnancy:init() self.dirty = 0 self:addviews{ - 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)', + widgets.Panel{ + frame={t=0}, + subviews={ + 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.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=5, l=2}, + label="Dissolve spouse relationship", + key='CUSTOM_X', + auto_width=true, + on_activate=function() clear_spouse(self:get_mother()) self.dirty = 2 end, + visible=function() + local mother = self:get_mother() + return mother and mother.relationship_ids.Spouse ~= -1 + end, + }, + widgets.HotkeyLabel{ + frame={t=5, l=2}, + label="Set selected father as spouse", + key='CUSTOM_X', + auto_width=true, + on_activate=function() set_spouse(self:get_mother(), self:get_father()) self.dirty = 2 end, + visible=function() + local mother = self:get_mother() + return not mother or mother.relationship_ids.Spouse == -1 + end, + enabled=function() + if self.mother_id == -1 then return false end + local father = self:get_father() + return father and father.relationship_ids.Spouse == -1 + end, + }, + widgets.HotkeyLabel{ + frame={t=7, l=0}, + label="Choose selected unit to be the mother", + 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, + }, }, - text_pen=COLOR_BLUE, - auto_width=true, - visible=function() return not self:get_spouse_unit('mother') and self:get_spouse_hf('mother') 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={t=9, 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.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)', + widgets.Panel{ + frame={t=11}, + subviews={ + widgets.Label{ + frame={t=0, l=0}, + text='Father:', + }, + widgets.Label{ + frame={t=0, l=8}, + text={ + 'None ', + {text='(optionally select an adult male)', pen=COLOR_GRAY}, + }, + visible=function() return not self:get_father() end, + }, + widgets.Label{ + frame={t=0, 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=2, l=0}, + text='Spouse:', + }, + widgets.Label{ + frame={t=2, 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=2, 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=2, 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=3, 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=4, l=2}, + label="Dissolve spouse relationship", + key='CUSTOM_SHIFT_X', + auto_width=true, + on_activate=function() clear_spouse(self:get_father()) self.dirty = 2 end, + visible=function() + local father = self:get_father() + return father and father.relationship_ids.Spouse ~= -1 + end, + }, + widgets.HotkeyLabel{ + frame={t=4, l=2}, + label="Set selected mother as spouse", + key='CUSTOM_SHIFT_X', + auto_width=true, + on_activate=function() set_spouse(self:get_mother(), self:get_father()) self.dirty = 2 end, + visible=function() + local father = self:get_father() + return not father or father.relationship_ids.Spouse == -1 + end, + enabled=function() + if self.father_id == -1 then return false end + local mother = self:get_mother() + return mother and mother.relationship_ids.Spouse == -1 + end, + }, + widgets.HotkeyLabel{ + frame={t=6, l=0}, + label="Choose selected unit to be the father", + 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, + }, }, - 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={t=19, 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', pen=COLOR_BROWN}, - {label='0', value=0, pen=COLOR_BROWN}, - {label='1', value=1, pen=COLOR_BROWN}, - {label='2', value=2, pen=COLOR_BROWN}, - {label='3', value=3, pen=COLOR_BROWN}, - {label='4', value=4, pen=COLOR_BROWN}, - {label='5', value=5, pen=COLOR_BROWN}, - {label='6', value=6, pen=COLOR_BROWN}, - {label='7', value=7, pen=COLOR_BROWN}, - {label='8', value=8, pen=COLOR_BROWN}, - {label='9', value=9, pen=COLOR_BROWN}, - {label='10', value=10, pen=COLOR_BROWN}, - }, - initial_option='default', - }, widgets.Panel{ - frame={t=21, w=23, h=3}, - frame_style=gui.FRAME_INTERIOR, + frame={t=21}, subviews={ - widgets.HotkeyLabel{ - key='CUSTOM_SHIFT_P', - label="Generate pregnancy", - on_activate=self:callback('commit'), - enabled=function() return self:get_mother() end, + widgets.CycleHotkeyLabel{ + view_id='term', + frame={t=0, l=0, w=40}, + label='Pregnancy term (in months):', + key_back='CUSTOM_SHIFT_Z', + key='CUSTOM_Z', + options={ + {label='Default', value='default', pen=COLOR_BROWN}, + {label='0', value=0, pen=COLOR_BROWN}, + {label='1', value=1, pen=COLOR_BROWN}, + {label='2', value=2, pen=COLOR_BROWN}, + {label='3', value=3, pen=COLOR_BROWN}, + {label='4', value=4, pen=COLOR_BROWN}, + {label='5', value=5, pen=COLOR_BROWN}, + {label='6', value=6, pen=COLOR_BROWN}, + {label='7', value=7, pen=COLOR_BROWN}, + {label='8', value=8, pen=COLOR_BROWN}, + {label='9', value=9, pen=COLOR_BROWN}, + {label='10', value=10, pen=COLOR_BROWN}, + }, + initial_option='default', + }, + widgets.Panel{ + frame={t=2, w=23, h=3}, + frame_style=gui.FRAME_INTERIOR, + subviews={ + widgets.HotkeyLabel{ + key='CUSTOM_SHIFT_P', + label="Generate pregnancy", + on_activate=self:callback('commit'), + enabled=function() return self:get_mother() end, + }, + } }, - } + }, }, } @@ -317,33 +480,55 @@ 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 + local father = self:get_father() + local function do_set_mother() + if self.father_id ~= -1 then + if not father or father.race ~= unit.race then + self.father_id = -1 + end + end + self.mother_id = unit.id + if self.father_id == -1 then + self:set_father(self:get_spouse_unit('mother')) end + self.dirty = 2 end - if self.father_id == -1 then - self:set_father(self:get_spouse_unit('mother')) + if father and father.race ~= unit.race then + dlg.showYesNoPrompt('Race mismatch', + 'Are you sure you want to select this unit as the mother?\n' .. + 'The unit\'s race does not match the selected father.\n' .. + 'The choice for father will be reset.', + COLOR_YELLOW, do_set_mother) + else + do_set_mother() end - self.dirty = 2 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 + local mother = self:get_mother() + local function do_set_father() + if self.mother_id ~= -1 then + if not mother or mother.race ~= unit.race then + self.mother_id = -1 + end + end + self.father_id = unit.id + if self.mother_id == -1 then + self:set_mother(self:get_spouse_unit('father')) end + self.dirty = 2 end - if self.mother_id == -1 then - self:set_mother(self:get_spouse_unit('father')) + if mother and mother.race ~= unit.race then + dlg.showYesNoPrompt('Race mismatch', + 'Are you sure you want to select this unit as the father?\n' .. + 'The unit\'s race does not match the selected mother.\n' .. + 'The choice for mother will be reset.', + COLOR_YELLOW, do_set_father) + else + do_set_father() end - self.dirty = 2 end local function get_term_ticks(months)