From f41adf2846f9b75229437c99d0f983732e7283c4 Mon Sep 17 00:00:00 2001 From: Petter Jennison Date: Mon, 23 Oct 2023 09:46:00 +0100 Subject: [PATCH 01/15] script for a gui pregnancy tool --- gui/pregnancy.lua | 328 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 328 insertions(+) create mode 100644 gui/pregnancy.lua diff --git a/gui/pregnancy.lua b/gui/pregnancy.lua new file mode 100644 index 0000000000..f7a765a5fa --- /dev/null +++ b/gui/pregnancy.lua @@ -0,0 +1,328 @@ +local gui = require('gui') +local widgets = require('gui.widgets') + +PregnancyGui = defclass(PregnancyGui, widgets.Window) +PregnancyGui.ATTRS { + frame_title='My Window', + frame={w=50, h=45}, + resizable=true, -- if resizing makes sense for your dialog + resize_min={w=50, h=20}, -- try to allow users to shrink your windows +} + +function PregnancyGui:init() + self.mother = false + self.father = false + self.father_historical = false + self.msg = {} + -- self.success = false + 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="Select Mother", + key='CUSTOM_SHIFT_M', + on_activate=self:callback('selectmother'), + }, + }, + }, + widgets.ResizingPanel{ + frame={t=6}, + frame_style=gui.FRAME_INTERIOR, + autoarrange_subviews=true, + subviews={ + widgets.WrappedLabel{ + text_to_wrap=self:callback('getFatherLabel') + }, + widgets.HotkeyLabel{ + frame={l=0}, + label="Select Father", + 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.ResizingPanel{ + frame={t=12}, + frame_style=gui.FRAME_INTERIOR, + autoarrange_subviews=1, + subviews={ + widgets.HotkeyLabel{ + frame={l=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.TooltipLabel{ + text_to_wrap=self.msg, + show_tooltip=true + }, + + widgets.ToggleHotkeyLabel{ + view_id='Force', + label='Force', + options={{label='On', value=true, pen=COLOR_GREEN}, + {label='Off', value=false, pen=COLOR_RED}}, + initial_option=false + }, + }, + }, + widgets.ResizingPanel{ + frame={t=22}, + 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, + }, + }, + }, + } +end + +function PregnancyGui:selectmother() + local unit = dfhack.gui.getSelectedUnit() + if unit then + if unit.sex==0 and dfhack.units.isAdult(unit) then + self.mother = unit + self:updateLayout() + end + end +end + +function PregnancyGui:selectfather() + local unit = dfhack.gui.getSelectedUnit() + if unit and dfhack.units.isAdult(unit) then + self.father = unit + self.father_historical = false + self:updateLayout() + end +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 +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 a adult female') + end +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) + ) + end + else return ('No father selected') + end +end + +function PregnancyGui:findName(unit) + if unit.name.has_name then + return dfhack.TranslateName(unit.name) + elseif unit.name.nickname ~= "" then + return unit.name.nickname + else return ('Unnamed %s. (Unit id:%s)'):format( + string.upper(df.global.world.raws.creatures.all[unit.race].name[0]), + unit.id + ) + end +end + +function PregnancyGui:findSpouse(unit) + local historical_spouse, spouse_loc, spouse, spouseid + local culled = false + + --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 + 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' + 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 + end + end + if culled then + return {'Unknown','culled'} + else + return {dfhack.TranslateName(historical_spouse.name),spouse_loc,historical_spouse} + end +end + +function PregnancyGui:CreatePregnancy() + local genes,father_id,father_caste,father_name + local bypass = true + local force = self.subviews.Force:getOptionValue() + + local count = #self.msg + for i=0, count do self.msg[i]=nil end --empty self.msg + + 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) + 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) + 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 aborted'):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 + -- self.success = false + if bypass or force then + --TODO add GUI to select the number of months for pregnancy timer + self.mother.pregnancy_timer=math.random(1, 13000) + self.mother.pregnancy_caste=father_caste + self.mother.pregnancy_spouse=father_id + self.mother.pregnancy_genes=genes + -- self.success = true + 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() +end + +PregnancyScreen = defclass(PregnancyScreen, gui.ZScreen) +PregnancyScreen.ATTRS { + focus_path='PregnancyScreen', +} + +function PregnancyScreen:init() + self:addviews{PregnancyGui{}} +end + +function PregnancyScreen:onDismiss() + view = nil +end + +view = view and view:raise() or PregnancyScreen{}:show() \ No newline at end of file From 15e675214e1376319ed3469c0b0d0791d0a94f18 Mon Sep 17 00:00:00 2001 From: Petter Jennison Date: Sat, 28 Oct 2023 15:20:33 +0100 Subject: [PATCH 02/15] Implementing myk002's suggestions --- gui/pregnancy.lua | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/gui/pregnancy.lua b/gui/pregnancy.lua index f7a765a5fa..20c64ac563 100644 --- a/gui/pregnancy.lua +++ b/gui/pregnancy.lua @@ -3,7 +3,7 @@ local widgets = require('gui.widgets') PregnancyGui = defclass(PregnancyGui, widgets.Window) PregnancyGui.ATTRS { - frame_title='My Window', + frame_title='Pregnancy manager', frame={w=50, h=45}, resizable=true, -- if resizing makes sense for your dialog resize_min={w=50, h=20}, -- try to allow users to shrink your windows @@ -14,7 +14,6 @@ function PregnancyGui:init() self.father = false self.father_historical = false self.msg = {} - -- self.success = false self:addviews{ widgets.ResizingPanel{ frame={t=0}, @@ -178,10 +177,9 @@ function PregnancyGui:getFatherLabel() end function PregnancyGui:findName(unit) - if unit.name.has_name then - return dfhack.TranslateName(unit.name) - elseif unit.name.nickname ~= "" then - return unit.name.nickname + 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 @@ -262,7 +260,7 @@ function PregnancyGui:CreatePregnancy() 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 aborted'):format( + table.insert(self.msg, ('SUCCESS:%sMother:%s%sFather:%s%sPrevious pregnancy with %s replaced'):format( NEWLINE, self:findName(self.mother), NEWLINE, @@ -314,7 +312,7 @@ end PregnancyScreen = defclass(PregnancyScreen, gui.ZScreen) PregnancyScreen.ATTRS { - focus_path='PregnancyScreen', + focus_path='pregnancy', } function PregnancyScreen:init() From d0d7106945d0f58bbe22f96c2b069ebbf7e7888f Mon Sep 17 00:00:00 2001 From: Petter Jennison Date: Sat, 28 Oct 2023 19:05:05 +0100 Subject: [PATCH 03/15] added Rangeslider widget for pregnancy term --- gui/pregnancy.lua | 81 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 67 insertions(+), 14 deletions(-) diff --git a/gui/pregnancy.lua b/gui/pregnancy.lua index 20c64ac563..6590f5e85a 100644 --- a/gui/pregnancy.lua +++ b/gui/pregnancy.lua @@ -4,7 +4,7 @@ local widgets = require('gui.widgets') PregnancyGui = defclass(PregnancyGui, widgets.Window) PregnancyGui.ATTRS { frame_title='Pregnancy manager', - frame={w=50, h=45}, + 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 } @@ -14,6 +14,18 @@ function PregnancyGui:init() 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 + self:addviews{ widgets.ResizingPanel{ frame={t=0}, @@ -32,7 +44,7 @@ function PregnancyGui:init() }, }, widgets.ResizingPanel{ - frame={t=6}, + frame={t=5}, frame_style=gui.FRAME_INTERIOR, autoarrange_subviews=true, subviews={ @@ -54,34 +66,69 @@ function PregnancyGui:init() }, }, }, - widgets.ResizingPanel{ - frame={t=12}, + widgets.Panel{ + frame={t=12,h=14}, frame_style=gui.FRAME_INTERIOR, - autoarrange_subviews=1, subviews={ widgets.HotkeyLabel{ - frame={l=0}, + 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.TooltipLabel{ - text_to_wrap=self.msg, - show_tooltip=true - }, - widgets.ToggleHotkeyLabel{ + frame={l=1, t=1}, view_id='Force', label='Force', 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=self.msg + }, }, }, widgets.ResizingPanel{ - frame={t=22}, + frame={t=26}, frame_style=gui.FRAME_INTERIOR, autoarrange_subviews=true, subviews={ @@ -237,13 +284,19 @@ function PregnancyGui:findSpouse(unit) end function PregnancyGui:CreatePregnancy() - local genes,father_id,father_caste,father_name + local genes,father_id,father_caste,father_name local bypass = true local force = self.subviews.Force:getOptionValue() local count = #self.msg for i=0, count do self.msg[i]=nil end --empty self.msg + 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 + end + if self.father then genes=self.father.appearance.genes:new() father_id=self.father.hist_figure_id @@ -293,7 +346,7 @@ function PregnancyGui:CreatePregnancy() -- self.success = false if bypass or force then --TODO add GUI to select the number of months for pregnancy timer - self.mother.pregnancy_timer=math.random(1, 13000) + 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 From 523c58558064d3291be2e93369be2632c4349040 Mon Sep 17 00:00:00 2001 From: Petter Jennison Date: Sat, 28 Oct 2023 19:08:04 +0100 Subject: [PATCH 04/15] Apply suggestions from code review Co-authored-by: Myk --- gui/pregnancy.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gui/pregnancy.lua b/gui/pregnancy.lua index 6590f5e85a..b3fc94eebb 100644 --- a/gui/pregnancy.lua +++ b/gui/pregnancy.lua @@ -147,7 +147,7 @@ function PregnancyGui:init() end function PregnancyGui:selectmother() - local unit = dfhack.gui.getSelectedUnit() + local unit = dfhack.gui.getSelectedUnit(true) if unit then if unit.sex==0 and dfhack.units.isAdult(unit) then self.mother = unit @@ -157,7 +157,7 @@ function PregnancyGui:selectmother() end function PregnancyGui:selectfather() - local unit = dfhack.gui.getSelectedUnit() + local unit = dfhack.gui.getSelectedUnit(true) if unit and dfhack.units.isAdult(unit) then self.father = unit self.father_historical = false @@ -175,7 +175,7 @@ function PregnancyGui:spouseFather() self.father_historical = father self.father = false end - self:updateLayout() + self:updateLayout() end end @@ -196,7 +196,7 @@ function PregnancyGui:getMotherLabel() NEWLINE ) end - else return ('No mother selected - Must be a adult female') + else return ('No mother selected - Must be an adult female') end end From fb43a1e2629f746677bc9fa326004ed53497694c Mon Sep 17 00:00:00 2001 From: Petter Jennison Date: Sat, 28 Oct 2023 19:44:37 +0100 Subject: [PATCH 05/15] More myk002 suggestions/corrections --- gui/pregnancy.lua | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/gui/pregnancy.lua b/gui/pregnancy.lua index b3fc94eebb..7c6739861f 100644 --- a/gui/pregnancy.lua +++ b/gui/pregnancy.lua @@ -10,7 +10,10 @@ PregnancyGui.ATTRS { } function PregnancyGui:init() - self.mother = false + 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 = {} @@ -37,7 +40,7 @@ function PregnancyGui:init() }, widgets.HotkeyLabel{ frame={l=0}, - label="Select Mother", + label="Set mother to selected unit", key='CUSTOM_SHIFT_M', on_activate=self:callback('selectmother'), }, @@ -53,13 +56,13 @@ function PregnancyGui:init() }, widgets.HotkeyLabel{ frame={l=0}, - label="Select Father", + 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", + 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 @@ -80,7 +83,7 @@ function PregnancyGui:init() widgets.ToggleHotkeyLabel{ frame={l=1, t=1}, view_id='Force', - label='Force', + label='Replace existing pregnancy', options={{label='On', value=true, pen=COLOR_GREEN}, {label='Off', value=false, pen=COLOR_RED}}, initial_option=false @@ -149,7 +152,7 @@ end function PregnancyGui:selectmother() local unit = dfhack.gui.getSelectedUnit(true) if unit then - if unit.sex==0 and dfhack.units.isAdult(unit) then + if unit.sex==df.pronoun_type.she and dfhack.units.isAdult(unit) then self.mother = unit self:updateLayout() end @@ -175,7 +178,7 @@ function PregnancyGui:spouseFather() self.father_historical = father self.father = false end - self:updateLayout() + self:updateLayout() end end @@ -343,14 +346,12 @@ function PregnancyGui:CreatePregnancy() )) end end - -- self.success = false + if bypass or force then - --TODO add GUI to select the number of months for pregnancy timer 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 - -- self.success = true if not force then table.insert(self.msg, ('SUCCESS:%sMother:%s%sFather:%s'):format( NEWLINE, From 04d2b664e07518ad36e5ac1acb0331e72c2dce0a Mon Sep 17 00:00:00 2001 From: Petter Jennison Date: Thu, 2 Nov 2023 09:17:52 +0000 Subject: [PATCH 06/15] myk002 newline+msg changes pregnancy.lua added Newline and self.msg changes to pregnancy.lua --- gui/pregnancy.lua | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/gui/pregnancy.lua b/gui/pregnancy.lua index 7c6739861f..e759dfa118 100644 --- a/gui/pregnancy.lua +++ b/gui/pregnancy.lua @@ -126,7 +126,7 @@ function PregnancyGui:init() }, widgets.WrappedLabel{ frame={t=8},--, h=5}, - text_to_wrap=self.msg + text_to_wrap=function() return self.msg end }, }, }, @@ -291,8 +291,7 @@ function PregnancyGui:CreatePregnancy() local bypass = true local force = self.subviews.Force:getOptionValue() - local count = #self.msg - for i=0, count do self.msg[i]=nil end --empty self.msg + self.msg = {} 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')) @@ -377,4 +376,4 @@ function PregnancyScreen:onDismiss() view = nil end -view = view and view:raise() or PregnancyScreen{}:show() \ No newline at end of file +view = view and view:raise() or PregnancyScreen{}:show() From a72a99974d6dbe575511c69091cdfd083e400249 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 11 Aug 2024 13:38:05 -0700 Subject: [PATCH 07/15] recode instruments output for printing to console --- instruments.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/instruments.lua b/instruments.lua index 4b634c163e..fe9c232472 100644 --- a/instruments.lua +++ b/instruments.lua @@ -61,13 +61,13 @@ local function print_list() local building_tag = instrument.flags.PLACED_AS_BUILDING and " (building, " or " (handheld, " local reaction = getAssemblyReaction(instrument.id) - dfhack.print(instrument.name .. building_tag) + dfhack.print(dfhack.df2console(instrument.name .. building_tag)) if #instrument.pieces == 0 then - print(describeReaction(reaction) .. ")") + print(dfhack.df2console(describeReaction(reaction) .. ")")) else - print(df.job_skill[reaction.skill] .. "/assemble)") + print(dfhack.df2console(df.job_skill[reaction.skill] .. "/assemble)")) for _, str in pairs(instruments[instrument.name]) do - print(" " .. str) + print(dfhack.df2console(" " .. str)) end end print() From e155986c1a94778045b2c20360b08c3c3e6c703e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 12 Aug 2024 10:21:25 -0700 Subject: [PATCH 08/15] guard debug output with a DEBUG check --- timestream.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/timestream.lua b/timestream.lua index 89ee941f6d..bf69f2a476 100644 --- a/timestream.lua +++ b/timestream.lua @@ -100,7 +100,9 @@ end local function check_new_unit(unit_id) local unit = df.unit.find(unit_id) if not unit then return end - print('registering new unit', unit.id, dfhack.units.getReadableName(unit)) + if DEBUG >= 3 then + print('registering new unit', unit.id, dfhack.units.getReadableName(unit)) + end register_birthday(unit) end From 48f8ee6ef41ca74a9266098ab307aa0e0ab8cb25 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 12 Aug 2024 10:27:27 -0700 Subject: [PATCH 09/15] make it much harder to accidentally offend the elves --- caravan.lua | 1 + changelog.txt | 3 + internal/caravan/common.lua | 116 ++++++++------ internal/caravan/movegoods.lua | 233 ++++++++++++++++----------- internal/caravan/trade.lua | 277 +++++++++++++++++++++++++++++---- internal/confirm/specs.lua | 53 ++++--- 6 files changed, 499 insertions(+), 184 deletions(-) diff --git a/caravan.lua b/caravan.lua index 7c434c3cbb..2d3a0cfb79 100644 --- a/caravan.lua +++ b/caravan.lua @@ -18,6 +18,7 @@ end OVERLAY_WIDGETS = { trade=trade.TradeOverlay, tradebanner=trade.TradeBannerOverlay, + tradeethics=trade.TradeEthicsWarningOverlay, tradeagreement=tradeagreement.TradeAgreementOverlay, movegoods=movegoods.MoveGoodsOverlay, movegoods_hider=movegoods.MoveGoodsHiderOverlay, diff --git a/changelog.txt b/changelog.txt index 059011337e..ff92542951 100644 --- a/changelog.txt +++ b/changelog.txt @@ -30,6 +30,9 @@ Template for new versions: - `embark-anyone`: allows you to embark as any civilisation, including dead, and non-dwarven ones ## 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 +- `caravan`: If you have managed to select an item that is ethically unacceptable to the merchant, an "Ethics warning" badge will now appear next to the "Trade" button. Clicking on the badge will show you which items that you have selected are problematic. The dialog has a button that you can click to deselect the problematic items in the trade list. +- `confirm`: If you have ethically unacceptable items selected for trade, the "Are you sure you want to trade" confirmation will warn you about them ## Fixes - `timestream`: ensure child growth events (e.g. becoming an adult) are not skipped over diff --git a/internal/caravan/common.lua b/internal/caravan/common.lua index 056ab1e2d2..cbf95eb809 100644 --- a/internal/caravan/common.lua +++ b/internal/caravan/common.lua @@ -111,10 +111,10 @@ function get_slider_widgets(self, suffix) key_back='CUSTOM_SHIFT_C', key='CUSTOM_SHIFT_V', options={ - {label='XXTatteredXX', value=3}, - {label='XFrayedX', value=2}, - {label='xWornx', value=1}, - {label='Pristine', value=0}, + {label='XXTatteredXX', value=3, pen=COLOR_BROWN}, + {label='XFrayedX', value=2, pen=COLOR_LIGHTRED}, + {label='xWornx', value=1, pen=COLOR_YELLOW}, + {label='Pristine', value=0, pen=COLOR_GREEN}, }, initial_option=3, on_change=function(val) @@ -132,10 +132,10 @@ function get_slider_widgets(self, suffix) key_back='CUSTOM_SHIFT_E', key='CUSTOM_SHIFT_R', options={ - {label='XXTatteredXX', value=3}, - {label='XFrayedX', value=2}, - {label='xWornx', value=1}, - {label='Pristine', value=0}, + {label='XXTatteredXX', value=3, pen=COLOR_BROWN}, + {label='XFrayedX', value=2, pen=COLOR_LIGHTRED}, + {label='xWornx', value=1, pen=COLOR_YELLOW}, + {label='Pristine', value=0, pen=COLOR_GREEN}, }, initial_option=0, on_change=function(val) @@ -160,7 +160,7 @@ function get_slider_widgets(self, suffix) }, }, widgets.Panel{ - frame={t=5, l=0, r=0, h=4}, + frame={t=6, l=0, r=0, h=4}, subviews={ widgets.CycleHotkeyLabel{ view_id='min_quality'..suffix, @@ -170,13 +170,13 @@ function get_slider_widgets(self, suffix) key_back='CUSTOM_SHIFT_Z', key='CUSTOM_SHIFT_X', options={ - {label='Ordinary', value=0}, - {label='-Well Crafted-', value=1}, - {label='+Finely Crafted+', value=2}, - {label='*Superior*', value=3}, - {label=CH_EXCEPTIONAL..'Exceptional'..CH_EXCEPTIONAL, value=4}, - {label=CH_MONEY..'Masterful'..CH_MONEY, value=5}, - {label='Artifact', value=6}, + {label='Ordinary', value=0, pen=COLOR_GRAY}, + {label='-Well Crafted-', value=1, pen=COLOR_LIGHTBLUE}, + {label='+Fine Crafted+', value=2, pen=COLOR_BLUE}, + {label='*Superior*', value=3, pen=COLOR_YELLOW}, + {label=CH_EXCEPTIONAL..'Exceptional'..CH_EXCEPTIONAL, value=4, pen=COLOR_BROWN}, + {label=CH_MONEY..'Masterful'..CH_MONEY, value=5, pen=COLOR_MAGENTA}, + {label='Artifact', value=6, pen=COLOR_GREEN}, }, initial_option=0, on_change=function(val) @@ -194,13 +194,13 @@ function get_slider_widgets(self, suffix) key_back='CUSTOM_SHIFT_Q', key='CUSTOM_SHIFT_W', options={ - {label='Ordinary', value=0}, - {label='-Well Crafted-', value=1}, - {label='+Finely Crafted+', value=2}, - {label='*Superior*', value=3}, - {label=CH_EXCEPTIONAL..'Exceptional'..CH_EXCEPTIONAL, value=4}, - {label=CH_MONEY..'Masterful'..CH_MONEY, value=5}, - {label='Artifact', value=6}, + {label='Ordinary', value=0, pen=COLOR_GRAY}, + {label='-Well Crafted-', value=1, pen=COLOR_LIGHTBLUE}, + {label='+Fine Crafted+', value=2, pen=COLOR_BLUE}, + {label='*Superior*', value=3, pen=COLOR_YELLOW}, + {label=CH_EXCEPTIONAL..'Exceptional'..CH_EXCEPTIONAL, value=4, pen=COLOR_BROWN}, + {label=CH_MONEY..'Masterful'..CH_MONEY, value=5, pen=COLOR_MAGENTA}, + {label='Artifact', value=6, pen=COLOR_GREEN}, }, initial_option=6, on_change=function(val) @@ -225,7 +225,7 @@ function get_slider_widgets(self, suffix) }, }, widgets.Panel{ - frame={t=10, l=0, r=0, h=4}, + frame={t=12, l=0, r=0, h=4}, subviews={ widgets.CycleHotkeyLabel{ view_id='min_value'..suffix, @@ -235,14 +235,14 @@ function get_slider_widgets(self, suffix) key_back='CUSTOM_SHIFT_B', key='CUSTOM_SHIFT_N', options={ - {label='1'..CH_MONEY, value={index=1, value=1}}, - {label='20'..CH_MONEY, value={index=2, value=20}}, - {label='50'..CH_MONEY, value={index=3, value=50}}, - {label='100'..CH_MONEY, value={index=4, value=100}}, - {label='500'..CH_MONEY, value={index=5, value=500}}, - {label='1000'..CH_MONEY, value={index=6, value=1000}}, + {label='1'..CH_MONEY, value={index=1, value=1}, pen=COLOR_BROWN}, + {label='20'..CH_MONEY, value={index=2, value=20}, pen=COLOR_BROWN}, + {label='50'..CH_MONEY, value={index=3, value=50}, pen=COLOR_BROWN}, + {label='100'..CH_MONEY, value={index=4, value=100}, pen=COLOR_BROWN}, + {label='500'..CH_MONEY, value={index=5, value=500}, pen=COLOR_BROWN}, + {label='1000'..CH_MONEY, value={index=6, value=1000}, pen=COLOR_BROWN}, -- max "min" value is less than max "max" value since the range of inf - inf is not useful - {label='5000'..CH_MONEY, value={index=7, value=5000}}, + {label='5000'..CH_MONEY, value={index=7, value=5000}, pen=COLOR_BROWN}, }, initial_option=1, on_change=function(val) @@ -260,13 +260,13 @@ function get_slider_widgets(self, suffix) key_back='CUSTOM_SHIFT_T', key='CUSTOM_SHIFT_Y', options={ - {label='1'..CH_MONEY, value={index=1, value=1}}, - {label='20'..CH_MONEY, value={index=2, value=20}}, - {label='50'..CH_MONEY, value={index=3, value=50}}, - {label='100'..CH_MONEY, value={index=4, value=100}}, - {label='500'..CH_MONEY, value={index=5, value=500}}, - {label='1000'..CH_MONEY, value={index=6, value=1000}}, - {label='Max', value={index=7, value=math.huge}}, + {label='1'..CH_MONEY, value={index=1, value=1}, pen=COLOR_BROWN}, + {label='20'..CH_MONEY, value={index=2, value=20}, pen=COLOR_BROWN}, + {label='50'..CH_MONEY, value={index=3, value=50}, pen=COLOR_BROWN}, + {label='100'..CH_MONEY, value={index=4, value=100}, pen=COLOR_BROWN}, + {label='500'..CH_MONEY, value={index=5, value=500}, pen=COLOR_BROWN}, + {label='1000'..CH_MONEY, value={index=6, value=1000}, pen=COLOR_BROWN}, + {label='Max', value={index=7, value=math.huge}, pen=COLOR_GREEN}, }, initial_option=7, on_change=function(val) @@ -479,10 +479,22 @@ function get_advanced_filter_widgets(self, context) } end -function get_info_widgets(self, export_agreements, context) +function get_info_widgets(self, export_agreements, strict_ethical_bins_default, context) return { + widgets.CycleHotkeyLabel{ + view_id='provenance', + frame={t=0, l=0, w=34}, + key='CUSTOM_SHIFT_P', + label='Item origins:', + options={ + {label='All', value='all', pen=COLOR_GREEN}, + {label='Foreign-made only', value='foreign', pen=COLOR_YELLOW}, + {label='Fort-made only', value='local', pen=COLOR_BLUE}, + }, + on_change=function() self:refresh_list() end, + }, widgets.Panel{ - frame={t=0, l=0, r=0, h=2}, + frame={t=2, l=0, r=0, h=2}, subviews={ widgets.Label{ frame={t=0, l=0}, @@ -506,7 +518,7 @@ function get_info_widgets(self, export_agreements, context) key='CUSTOM_SHIFT_A', options={ {label='Yes', value=true, pen=COLOR_GREEN}, - {label='No', value=false} + {label='No', value=false}, }, initial_option=false, on_change=function() self:refresh_list() end, @@ -515,7 +527,7 @@ function get_info_widgets(self, export_agreements, context) }, }, widgets.Panel{ - frame={t=3, l=0, r=0, h=3}, + frame={t=5, l=0, r=0, h=4}, subviews={ widgets.Label{ frame={t=0, l=0}, @@ -530,7 +542,7 @@ function get_info_widgets(self, export_agreements, context) key='CUSTOM_SHIFT_G', options={ {label='Show only ethically acceptable items', value='only', pen=COLOR_GREEN}, - {label='Ignore ethical restrictions', value='show'}, + {label='Ignore ethical restrictions', value='show', pen=COLOR_YELLOW}, {label='Show only ethically unacceptable items', value='hide', pen=COLOR_RED}, }, initial_option='only', @@ -538,10 +550,26 @@ function get_info_widgets(self, export_agreements, context) visible=self.animal_ethics or self.wood_ethics, on_change=function() self:refresh_list() end, }, + widgets.ToggleHotkeyLabel{ + view_id='strict_ethical_bins', + frame={t=3, l=0}, + key='CUSTOM_SHIFT_U', + options={ + {label='Include mixed bins', value=false, pen=COLOR_GREEN}, + {label='Exclude mixed bins', value=true, pen=COLOR_YELLOW}, + }, + initial_option=strict_ethical_bins_default, + option_gap=0, + visible=function() + if not self.animal_ethics and not self.wood_ethics then return false end + return self.subviews.ethical:getOptionValue() ~= 'show' + end, + on_change=function() self:refresh_list() end, + }, }, }, widgets.Panel{ - frame={t=7, l=0, r=0, h=5}, + frame={t=10, l=0, r=0, h=5}, subviews={ widgets.Label{ frame={t=0, l=0}, diff --git a/internal/caravan/movegoods.lua b/internal/caravan/movegoods.lua index feb5c73255..c935c5fb1d 100644 --- a/internal/caravan/movegoods.lua +++ b/internal/caravan/movegoods.lua @@ -14,10 +14,10 @@ local widgets = require('gui.widgets') MoveGoods = defclass(MoveGoods, widgets.Window) MoveGoods.ATTRS { frame_title='Move goods to/from depot', - frame={w=85, h=46}, + frame={w=86, h=46}, resizable=true, - resize_min={h=35}, - frame_inset={l=1, t=1, b=1, r=0}, + resize_min={h=40}, + frame_inset={l=0, t=1, b=1, r=0}, pending_item_ids=DEFAULT_NIL, depot=DEFAULT_NIL, } @@ -155,7 +155,7 @@ function MoveGoods:init() self:addviews{ widgets.CycleHotkeyLabel{ view_id='sort', - frame={l=0, t=0, w=21}, + frame={l=1, t=0, w=21}, label='Sort by:', key='CUSTOM_SHIFT_S', options={ @@ -173,32 +173,38 @@ function MoveGoods:init() }, widgets.EditField{ view_id='search', - frame={l=26, t=0}, + frame={l=27, t=0, r=1}, label_text='Search: ', on_char=function(ch) return ch:match('[%l -]') end, }, widgets.Panel{ - frame={t=2, l=0, w=38, h=14}, - subviews=common.get_slider_widgets(self), - }, - widgets.ToggleHotkeyLabel{ - view_id='hide_forbidden', - frame={t=2, l=40, w=28}, - label='Hide forbidden items:', - key='CUSTOM_SHIFT_F', - options={ - {label='Yes', value=true, pen=COLOR_GREEN}, - {label='No', value=false} + frame={t=2, l=0, r=0, h=18}, + frame_style=gui.FRAME_INTERIOR, + subviews={ + widgets.Panel{ + frame={t=0, l=0, w=38}, + subviews=common.get_slider_widgets(self), + }, + widgets.ToggleHotkeyLabel{ + view_id='hide_forbidden', + frame={t=0, l=40, w=28}, + label='Hide forbidden items:', + key='CUSTOM_SHIFT_F', + options={ + {label='Yes', value=true, pen=COLOR_GREEN}, + {label='No', value=false} + }, + initial_option=false, + on_change=function() self:refresh_list() end, + }, + widgets.Panel{ + frame={t=1, l=40, r=0}, + subviews=common.get_info_widgets(self, get_export_agreements(), false, self.predicate_context), + }, }, - initial_option=false, - on_change=function() self:refresh_list() end, - }, - widgets.Panel{ - frame={t=4, l=40, r=1, h=12}, - subviews=common.get_info_widgets(self, get_export_agreements(), self.predicate_context), }, widgets.Panel{ - frame={t=17, l=0, r=0, b=6}, + frame={t=21, l=0, r=0, b=7}, subviews={ widgets.CycleHotkeyLabel{ view_id='sort_dist', @@ -255,48 +261,60 @@ function MoveGoods:init() }, } }, - widgets.Label{ - frame={l=0, b=4, h=1, r=0}, - text={ - 'Total value of items marked for trade:', - {gap=1, - text=function() return common.obfuscate_value(self.value_pending) end}, - }, - }, - widgets.HotkeyLabel{ - frame={l=0, b=2}, - label='Select all/none', - key='CUSTOM_CTRL_A', - on_activate=self:callback('toggle_visible'), - auto_width=true, + widgets.Divider{ + frame={b=6, h=1}, + frame_style=gui.FRAME_INTERIOR, + frame_style_l=false, + frame_style_r=false, }, - widgets.ToggleHotkeyLabel{ - view_id='group_items', - frame={l=25, b=2, w=24}, - label='Group items:', - key='CUSTOM_CTRL_G', - options={ - {label='Yes', value=true, pen=COLOR_GREEN}, - {label='No', value=false} - }, - initial_option=true, - on_change=function() self:refresh_list() end, - }, - widgets.ToggleHotkeyLabel{ - view_id='inside_containers', - frame={l=51, b=2, w=30}, - label='Inside containers:', - key='CUSTOM_CTRL_I', - options={ - {label='Yes', value=true, pen=COLOR_GREEN}, - {label='No', value=false} + widgets.Panel{ + frame={l=1, r=1, b=0, h=5}, + subviews={ + widgets.Label{ + frame={l=0, t=0}, + text={ + 'Total value of items marked for trade:', + {gap=1, + text=function() return common.obfuscate_value(self.value_pending) end, + pen=COLOR_GREEN}, + }, + }, + widgets.Label{ + frame={l=0, t=2}, + text='Click to mark/unmark for trade. Shift click to mark/unmark a range of items.', + }, + widgets.HotkeyLabel{ + frame={l=0, b=0}, + label='Select all/none', + key='CUSTOM_CTRL_A', + on_activate=self:callback('toggle_visible'), + auto_width=true, + }, + widgets.ToggleHotkeyLabel{ + view_id='group_items', + frame={l=25, b=0, w=24}, + label='Group items:', + key='CUSTOM_CTRL_G', + options={ + {label='Yes', value=true, pen=COLOR_GREEN}, + {label='No', value=false} + }, + initial_option=true, + on_change=function() self:refresh_list() end, + }, + widgets.ToggleHotkeyLabel{ + view_id='inside_containers', + frame={l=51, b=0, w=30}, + label='Inside containers:', + key='CUSTOM_CTRL_I', + options={ + {label='Yes', value=true, pen=COLOR_GREEN}, + {label='No', value=false} + }, + initial_option=false, + on_change=function() self:refresh_list() end, + }, }, - initial_option=false, - on_change=function() self:refresh_list() end, - }, - widgets.WrappedLabel{ - frame={b=0, l=0, r=0}, - text_to_wrap='Click to mark/unmark for trade. Shift click to mark/unmark a range of items.', }, } @@ -390,41 +408,50 @@ local function make_choice_text(at_depot, dist, value, quantity, desc) } end +local function is_ethical_item(item, animal_ethics, wood_ethics) + return (not animal_ethics or not item:isAnimalProduct()) and + (not wood_ethics or not common.has_wood(item)) +end + local function is_ethical_product(item, animal_ethics, wood_ethics) if not animal_ethics and not wood_ethics then return true end - if item.flags.container then - local contained_items = dfhack.items.getContainedItems(item) - if df.item_binst:is_instance(item) then - -- ignore the safety of the bin itself (unless the bin is empty) - -- so items inside can still be traded - local has_items = false - for _, contained_item in ipairs(contained_items) do - has_items = true - if (not animal_ethics or not contained_item:isAnimalProduct()) and - (not wood_ethics or not common.has_wood(contained_item)) - then - -- bin passes if at least one contained item is safe - return true + + -- if item is not a container or is an empty container, then the ethics is not mixed + -- and the ethicality of the item speaks for itself + local has_ethical = is_ethical_item(item, animal_ethics, wood_ethics) + local is_mixed = false + if not item.flags.container then + return has_ethical, is_mixed + end + local contained_items = dfhack.items.getContainedItems(item) + if #contained_items == 0 then + return has_ethical, is_mixed + end + + if df.item_binst:is_instance(item) then + for _, contained_item in ipairs(contained_items) do + if is_ethical_item(contained_item, animal_ethics, wood_ethics) then + if not has_ethical then + has_ethical, is_mixed = true, true + break end + elseif has_ethical then + is_mixed = true + break end - if has_items then - -- no contained items are safe - return false - end - else - -- for other types of containers, any contamination makes it untradeable - for _, contained_item in ipairs(contained_items) do - if (animal_ethics and contained_item:isAnimalProduct()) or - (wood_ethics and common.has_wood(contained_item)) - then - return false - end + end + elseif has_ethical then + -- for other types of containers, any contamination makes it unethical since contained + -- items cannot be individually selected in the barter screen + for _, contained_item in ipairs(contained_items) do + if not is_ethical_item(contained_item, animal_ethics, wood_ethics) then + has_ethical = false + break end end end - return (not animal_ethics or not item:isAnimalProduct()) and - (not wood_ethics or not common.has_wood(item)) + return has_ethical, is_mixed end local function make_container_search_key(item, desc) @@ -492,7 +519,7 @@ function MoveGoods:cache_choices() group.data.has_risky = group.data.has_risky or is_risky group.data.has_requested = group.data.has_requested or is_requested else - local is_ethical = is_ethical_product(item, self.animal_ethics, self.wood_ethics) + local has_ethical, is_ethical_mixed = is_ethical_product(item, self.animal_ethics, self.wood_ethics) local data = { desc=desc, per_item_value=value, @@ -507,10 +534,12 @@ function MoveGoods:cache_choices() selected=is_pending and 1 or 0, num_at_depot=item.flags.in_building and 1 or 0, has_forbidden=is_forbidden, + has_foreign=item.flags.foreign, has_banned=is_banned, has_risky=is_risky, has_requested=is_requested, - ethical=is_ethical, + has_ethical=has_ethical, + ethical_mixed=is_ethical_mixed, dirty=false, } local search_key @@ -555,9 +584,11 @@ function MoveGoods:get_choices() local raw_choices = self:cache_choices() local choices = {} local include_forbidden = not self.subviews.hide_forbidden:getOptionValue() + local provenance = self.subviews.provenance:getOptionValue() local banned = self.subviews.banned:getOptionValue() local only_agreement = self.subviews.only_agreement:getOptionValue() local ethical = self.subviews.ethical:getOptionValue() + local strict_ethical_bins = self.subviews.strict_ethical_bins:getOptionValue() local min_condition = self.subviews.min_condition:getOptionValue() local max_condition = self.subviews.max_condition:getOptionValue() local min_quality = self.subviews.min_quality:getOptionValue() @@ -567,8 +598,9 @@ function MoveGoods:get_choices() for _,choice in ipairs(raw_choices) do local data = choice.data if ethical ~= 'show' then - if ethical == 'hide' and data.ethical then goto continue end - if ethical == 'only' and not data.ethical then goto continue end + if strict_ethical_bins and data.ethical_mixed then goto continue end + if ethical == 'hide' and data.has_ethical then goto continue end + if ethical == 'only' and not data.has_ethical then goto continue end end if not include_forbidden then if choice.item_id then @@ -579,6 +611,13 @@ function MoveGoods:get_choices() goto continue end end + if provenance ~= 'all' then + if (provenance == 'local' and data.has_foreign) or + (provenance == 'foreign' and not data.has_foreign) + then + goto continue + end + end if min_condition < data.wear then goto continue end if max_condition > data.wear then goto continue end if min_quality > data.quality then goto continue end @@ -717,6 +756,10 @@ function MoveGoodsModal:onDismiss() if dfhack.items.getHolderBuilding(item) == depot then item.flags.in_building = true else + -- TODO: if there is just one (ethical, if filtered) item inside of a bin, mark the item for + -- trade instead of the bin + -- TODO: give containers that have some items inside of them marked for trade a ":" marker in the UI + -- TODO: correlate items inside containers marked for trade across the cached choices so no choices are lost dfhack.items.markForTrade(item, depot) end elseif not item_data.pending and pending[item_id] then @@ -724,7 +767,7 @@ function MoveGoodsModal:onDismiss() if spec_ref then dfhack.job.removeJob(spec_ref.data.job) end - elseif not item_data.pending and item.flags.in_building then + elseif not item_data.pending and item.flags.in_building and dfhack.items.getHolderBuilding(item) == depot then item.flags.in_building = false end end diff --git a/internal/caravan/trade.lua b/internal/caravan/trade.lua index 2e6c22fec1..05e3adf3b1 100644 --- a/internal/caravan/trade.lua +++ b/internal/caravan/trade.lua @@ -34,9 +34,9 @@ local trade = df.global.game.main_interface.trade Trade = defclass(Trade, widgets.Window) Trade.ATTRS { frame_title='Select trade goods', - frame={w=84, h=47}, + frame={w=86, h=47}, resizable=true, - resize_min={w=48, h=27}, + resize_min={w=48, h=40}, } local TOGGLE_MAP = { @@ -139,7 +139,7 @@ end local STATUS_COL_WIDTH = 7 local VALUE_COL_WIDTH = 6 -local FILTER_HEIGHT = 15 +local FILTER_HEIGHT = 18 function Trade:init() self.cur_page = 1 @@ -174,8 +174,8 @@ function Trade:init() label='Bins:', key='CUSTOM_SHIFT_B', options={ - {label='trade bin with contents', value=true}, - {label='trade contents only', value=false}, + {label='Trade bin with contents', value=true, pen=COLOR_YELLOW}, + {label='Trade contents only', value=false, pen=COLOR_GREEN}, }, initial_option=false, on_change=function() self:refresh_list() end, @@ -215,23 +215,24 @@ function Trade:init() }, widgets.Panel{ frame={t=7, l=0, r=0, h=FILTER_HEIGHT}, + frame_style=gui.FRAME_INTERIOR, visible=function() return self.subviews.filters:getOptionValue() end, on_layout=function() local panel_frame = self.subviews.list_panel.frame if self.subviews.filters:getOptionValue() then - panel_frame.t = 7 + FILTER_HEIGHT + panel_frame.t = 7 + FILTER_HEIGHT + 1 else panel_frame.t = 7 end end, subviews={ widgets.Panel{ - frame={t=0, l=0, w=38, h=FILTER_HEIGHT}, + frame={t=0, l=0, w=38}, visible=function() return self.cur_page == 1 end, subviews=common.get_slider_widgets(self, '1'), }, widgets.Panel{ - frame={t=0, l=0, w=38, h=FILTER_HEIGHT}, + frame={t=0, l=0, w=38}, visible=function() return self.cur_page == 2 end, subviews=common.get_slider_widgets(self, '2'), }, @@ -241,15 +242,15 @@ function Trade:init() subviews=common.get_advanced_filter_widgets(self, self.predicate_contexts[1]), }, widgets.Panel{ - frame={t=2, l=40, r=0, h=FILTER_HEIGHT-2}, + frame={t=1, l=40, r=0}, visible=function() return self.cur_page == 2 end, - subviews=common.get_info_widgets(self, {trade.mer.buy_prices}, self.predicate_contexts[2]), + subviews=common.get_info_widgets(self, {trade.mer.buy_prices}, true, self.predicate_contexts[2]), }, }, }, widgets.Panel{ view_id='list_panel', - frame={t=7, l=0, r=0, b=4}, + frame={t=7, l=0, r=0, b=5}, subviews={ widgets.CycleHotkeyLabel{ view_id='sort_status', @@ -295,17 +296,23 @@ function Trade:init() }, } }, + widgets.Divider{ + frame={b=4, h=1}, + frame_style=gui.FRAME_INTERIOR, + frame_style_l=false, + frame_style_r=false, + }, + widgets.Label{ + frame={b=2, l=0, r=0}, + text='Click to mark/unmark for trade. Shift click to mark/unmark a range of items.', + }, widgets.HotkeyLabel{ - frame={l=0, b=2}, + frame={l=0, b=0}, label='Select all/none', key='CUSTOM_CTRL_A', on_activate=self:callback('toggle_visible'), auto_width=true, }, - widgets.WrappedLabel{ - frame={b=0, l=0, r=0}, - text_to_wrap='Click to mark/unmark for trade. Shift click to mark/unmark a range of items.', - }, } -- replace the FilteredList's built-in EditField with our own @@ -384,10 +391,12 @@ function Trade:cache_choices(list_idx, trade_bins) item_idx=item_idx, quality=item.flags.artifact and 6 or item:getQuality(), wear=wear_level, + has_foreign=item.flags.foreign, has_banned=is_banned, has_risky=is_risky, has_requested=is_requested, - ethical=is_ethical, + has_ethical=is_ethical, + ethical_mixed=false, } if parent_data then data.update_container_fn = function(from, to) @@ -396,7 +405,8 @@ function Trade:cache_choices(list_idx, trade_bins) parent_data.has_banned = parent_data.has_banned or is_banned parent_data.has_risky = parent_data.has_risky or is_risky parent_data.has_requested = parent_data.has_requested or is_requested - parent_data.ethical = parent_data.ethical and is_ethical + parent_data.ethical_mixed = parent_data.ethical_mixed or (parent_data.has_ethical ~= is_ethical) + parent_data.has_ethical = parent_data.has_ethical or is_ethical end local is_container = df.item_binst:is_instance(item) local search_key @@ -427,9 +437,11 @@ end function Trade:get_choices() local raw_choices = self:cache_choices(self.cur_page-1, self.subviews.trade_bins:getOptionValue()) + local provenance = self.subviews.provenance:getOptionValue() local banned = self.cur_page == 1 and 'ignore' or self.subviews.banned:getOptionValue() local only_agreement = self.cur_page == 2 and self.subviews.only_agreement:getOptionValue() or false local ethical = self.cur_page == 1 and 'show' or self.subviews.ethical:getOptionValue() + local strict_ethical_bins = self.subviews.strict_ethical_bins:getOptionValue() local min_condition = self.subviews['min_condition'..self.cur_page]:getOptionValue() local max_condition = self.subviews['max_condition'..self.cur_page]:getOptionValue() local min_quality = self.subviews['min_quality'..self.cur_page]:getOptionValue() @@ -440,8 +452,16 @@ function Trade:get_choices() for _,choice in ipairs(raw_choices) do local data = choice.data if ethical ~= 'show' then - if ethical == 'hide' and data.ethical then goto continue end - if ethical == 'only' and not data.ethical then goto continue end + if strict_ethical_bins and data.ethical_mixed then goto continue end + if ethical == 'hide' and data.has_ethical then goto continue end + if ethical == 'only' and not data.has_ethical then goto continue end + end + if provenance ~= 'all' then + if (provenance == 'local' and data.has_foreign) or + (provenance == 'foreign' and not data.has_foreign) + then + goto continue + end end if min_condition < data.wear then goto continue end if max_condition > data.wear then goto continue end @@ -516,7 +536,7 @@ end -- TradeScreen -- -view = view or nil +trade_view = trade_view or nil TradeScreen = defclass(TradeScreen, gui.ZScreen) TradeScreen.ATTRS { @@ -540,7 +560,7 @@ end function TradeScreen:onRenderFrame() if not df.global.game.main_interface.trade.open then - if view then view:dismiss() end + if trade_view then trade_view:dismiss() end elseif self.reset_pending and (dfhack.gui.matchFocusString('dfhack/lua/caravan/trade') or dfhack.gui.matchFocusString('dwarfmode/Trade/Default')) @@ -551,7 +571,7 @@ function TradeScreen:onRenderFrame() end function TradeScreen:onDismiss() - view = nil + trade_view = nil end -- ------------------- @@ -822,7 +842,7 @@ function TradeBannerOverlay:init() label='DFHack trade UI', key='CUSTOM_CTRL_T', enabled=function() return trade.stillunloading == 0 and trade.havetalker == 1 end, - on_activate=function() view = view and view:raise() or TradeScreen{}:show() end, + on_activate=function() trade_view = trade_view and trade_view:raise() or TradeScreen{}:show() end, }, } end @@ -831,8 +851,213 @@ function TradeBannerOverlay:onInput(keys) if TradeBannerOverlay.super.onInput(self, keys) then return true end if keys._MOUSE_R or keys.LEAVESCREEN then - if view then - view:dismiss() + if trade_view then + trade_view:dismiss() + end + end +end + +-- ------------------- +-- Ethics +-- + +Ethics = defclass(Ethics, widgets.Window) +Ethics.ATTRS { + frame_title='Ethical transgressions', + frame={w=45, h=30}, + resizable=true, +} + +function Ethics:init() + self.choices = {} + self.animal_ethics = common.is_animal_lover_caravan(trade.mer) + self.wood_ethics = common.is_tree_lover_caravan(trade.mer) + + self:addviews{ + widgets.Label{ + frame={l=0, t=0}, + text={ + 'You have ', + {text=self:callback('get_transgression_count'), pen=self:callback('get_transgression_color')}, + ' item', + {text=function() return self:get_transgression_count() == 1 and '' or 's' end}, + ' selected for trade', NEWLINE, + 'that would offend the merchants:', + }, + }, + widgets.List{ + view_id='list', + frame={l=0, r=0, t=3, b=2}, + }, + widgets.HotkeyLabel{ + frame={l=0, b=0}, + key='CUSTOM_CTRL_A', + label='Deselect items in trade list', + auto_width=true, + on_activate=self:callback('deselect_transgressions'), + }, + } + + self:rescan() +end + +function Ethics:get_transgression_count() + return #self.choices +end + +function Ethics:get_transgression_color() + return next(self.choices) and COLOR_LIGHTRED or COLOR_LIGHTGREEN +end + +-- also used by confirm +function for_selected_item(list_idx, fn) + local goodflags = trade.goodflag[list_idx] + local in_selected_container = false + for item_idx, item in ipairs(trade.good[list_idx]) do + local goodflag = goodflags[item_idx] + if goodflag == GOODFLAG.UNCONTAINED_SELECTED or goodflag == GOODFLAG.CONTAINER_COLLAPSED_SELECTED then + in_selected_container = true + elseif goodflag == GOODFLAG.UNCONTAINED_UNSELECTED or goodflag == GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED then + in_selected_container = false + end + if in_selected_container or TARGET_REVMAP[goodflag] then + if fn(item_idx, item) then + return + end + end + end +end + +local function for_ethics_violation(fn, animal_ethics, wood_ethics) + if not animal_ethics and not wood_ethics then return end + for_selected_item(1, function(item_idx, item) + if not is_ethical_product(item, animal_ethics, wood_ethics) then + if fn(item_idx, item) then return true end + end + end) +end + +function Ethics:rescan() + local choices = {} + for_ethics_violation(function(item_idx, item) + local choice = { + text=dfhack.items.getReadableDescription(item), + data={item_idx=item_idx}, + } + table.insert(choices, choice) + end, self.animal_ethics, self.wood_ethics) + + self.subviews.list:setChoices(choices) + self.choices = choices +end + +function Ethics:deselect_transgressions() + local goodflags = trade.goodflag[1] + for _,choice in ipairs(self.choices) do + local goodflag = goodflags[choice.data.item_idx] + if TARGET_REVMAP[goodflag] then + goodflags[choice.data.item_idx] = TOGGLE_MAP[goodflag] + end + end + self:rescan() +end + +-- ------------------- +-- EthicsScreen +-- + +ethics_view = ethics_view or nil + +EthicsScreen = defclass(EthicsScreen, gui.ZScreen) +EthicsScreen.ATTRS { + focus_path='caravan/trade/ethics', +} + +function EthicsScreen:init() + self.ethics_window = Ethics{} + self:addviews{self.ethics_window} +end + +function EthicsScreen:onInput(keys) + if self.reset_pending then return false end + local handled = EthicsScreen.super.onInput(self, keys) + if keys._MOUSE_L and not self.ethics_window:getMouseFramePos() then + -- check for modified selection + self.reset_pending = true + end + return handled +end + +function EthicsScreen:onRenderFrame() + if not df.global.game.main_interface.trade.open then + if ethics_view then ethics_view:dismiss() end + elseif self.reset_pending and + (dfhack.gui.matchFocusString('dfhack/lua/caravan/trade') or + dfhack.gui.matchFocusString('dwarfmode/Trade/Default')) + then + self.reset_pending = nil + self.ethics_window:rescan() + end +end + +function EthicsScreen:onDismiss() + ethics_view = nil +end + +-- -------------------------- +-- TradeEthicsWarningOverlay +-- + +-- also called by confirm +function has_ethics_violation() + local violated = false + for_ethics_violation(function() + violated = true + return true + end, common.is_animal_lover_caravan(trade.mer), common.is_tree_lover_caravan(trade.mer)) + return violated +end + +TradeEthicsWarningOverlay = defclass(TradeEthicsWarningOverlay, overlay.OverlayWidget) +TradeEthicsWarningOverlay.ATTRS{ + desc='Adds warning to the trade screen when you are about to offend the elves.', + default_pos={x=-54,y=-5}, + default_enabled=true, + viewscreens='dwarfmode/Trade/Default', + frame={w=9, h=2}, + visible=has_ethics_violation, +} + +function TradeEthicsWarningOverlay:init() + self:addviews{ + widgets.BannerPanel{ + frame={l=0, w=9}, + subviews={ + widgets.Label{ + frame={l=1, r=1}, + text={ + 'Ethics', NEWLINE, + 'warning', + }, + on_click=function() ethics_view = ethics_view and ethics_view:raise() or EthicsScreen{}:show() end, + text_pen=COLOR_LIGHTRED, + auto_width=false, + }, + }, + }, + } +end + +function TradeEthicsWarningOverlay:preUpdateLayout(rect) + self.frame.w = (rect.width - 95) // 2 +end + +function TradeEthicsWarningOverlay:onInput(keys) + if TradeEthicsWarningOverlay.super.onInput(self, keys) then return true end + + if keys._MOUSE_R or keys.LEAVESCREEN then + if ethics_view then + ethics_view:dismiss() end end end diff --git a/internal/confirm/specs.lua b/internal/confirm/specs.lua index d3d12d94e5..cbb33cd90e 100644 --- a/internal/confirm/specs.lua +++ b/internal/confirm/specs.lua @@ -1,11 +1,12 @@ --@module = true --- if adding a new spec, just run `confirm` to load it and make it live +-- if adding a new spec, run `confirm` to load it and make it live -- -- remember to reload the overlay when adding/changing specs that have -- intercept_frames defined local json = require('json') +local trade_internal = reqscript('internal/caravan/trade') local CONFIG_FILE = 'dfhack-config/confirm.json' @@ -55,16 +56,21 @@ local mi = df.global.game.main_interface local plotinfo = df.global.plotinfo local function trade_goods_any_selected(which) - for _, sel in ipairs(mi.trade.goodflag[which]) do - if sel == 1 then return true end - end + local any_selected = false + trade_internal.for_selected_item(which, function() + any_selected = true + return true + end) + return any_selected end local function trade_goods_all_selected(which) - for _, sel in ipairs(mi.trade.goodflag[which]) do - if sel ~= 1 then return false end - end - return true + local num_selected = 0 + trade_internal.for_selected_item(which, function(idx) + print(idx) + num_selected = num_selected + 1 + end) + return #mi.trade.goodflag[which] == num_selected end local function trade_agreement_items_any_selected() @@ -102,7 +108,7 @@ ConfirmSpec{ title='Cancel trade', message='Are you sure you want leave this screen? Selected items will not be saved.', intercept_keys={'LEAVESCREEN', '_MOUSE_R'}, - context='dwarfmode/Trade', + context='dwarfmode/Trade/Default', predicate=function() return trade_goods_any_selected(0) or trade_goods_any_selected(1) end, } @@ -112,7 +118,7 @@ ConfirmSpec{ message='Are you sure you want mark all fortress goods at the depot? Your current fortress goods selections will be lost.', intercept_keys='_MOUSE_L', intercept_frame={r=47, b=7, w=12, h=3}, - context='dwarfmode/Trade', + context='dwarfmode/Trade/Default', predicate=function() return trade_goods_any_selected(1) and not trade_goods_all_selected(1) end, pausable=true, } @@ -123,7 +129,7 @@ ConfirmSpec{ message='Are you sure you want unmark all fortress goods at the depot? Your current fortress goods selections will be lost.', intercept_keys='_MOUSE_L', intercept_frame={r=30, b=7, w=14, h=3}, - context='dwarfmode/Trade', + context='dwarfmode/Trade/Default', predicate=function() return trade_goods_any_selected(1) and not trade_goods_all_selected(1) end, pausable=true, } @@ -134,7 +140,7 @@ ConfirmSpec{ message='Are you sure you want mark all merchant goods at the depot? Your current merchant goods selections will be lost.', intercept_keys='_MOUSE_L', intercept_frame={l=0, r=72, b=7, w=12, h=3}, - context='dwarfmode/Trade', + context='dwarfmode/Trade/Default', predicate=function() return trade_goods_any_selected(0) and not trade_goods_all_selected(0) end, pausable=true, } @@ -145,19 +151,28 @@ ConfirmSpec{ message='Are you sure you want mark all merchant goods at the depot? Your current merchant goods selections will be lost.', intercept_keys='_MOUSE_L', intercept_frame={l=0, r=40, b=7, w=14, h=3}, - context='dwarfmode/Trade', + context='dwarfmode/Trade/Default', predicate=function() return trade_goods_any_selected(0) and not trade_goods_all_selected(0) end, pausable=true, } +local function get_ethics_message(msg) + local lines = {msg} + if trade_internal.has_ethics_violation() then + table.insert(lines, '') + table.insert(lines, 'You have items selected that will offend the merchants. Proceeding with this trade will anger them. You can click on the Ethics warning badge to see which items the merchants will find offensive.') + end + return table.concat(lines, NEWLINE) +end + ConfirmSpec{ id='trade-confirm-trade', title='Confirm trade', - message="Are you sure you want to trade the selected goods?", + message=curry(get_ethics_message, 'Are you sure you want to trade the selected goods?'), intercept_keys='_MOUSE_L', intercept_frame={l=0, r=23, b=4, w=11, h=3}, - context='dwarfmode/Trade', - predicate=function() return trade_goods_any_selected(0) and trade_goods_any_selected(1) end, + context='dwarfmode/Trade/Default', + predicate=function() return trade_goods_any_selected(1) end, pausable=true, } @@ -167,7 +182,7 @@ ConfirmSpec{ message='Are you sure you want seize marked merchant goods? This will make the merchant unwilling to trade further and will damage relations with the merchant\'s civilization.', intercept_keys='_MOUSE_L', intercept_frame={l=0, r=73, b=4, w=11, h=3}, - context='dwarfmode/Trade', + context='dwarfmode/Trade/Default', predicate=function() return mi.trade.mer.mood > 0 and trade_goods_any_selected(0) end, pausable=true, } @@ -175,10 +190,10 @@ ConfirmSpec{ ConfirmSpec{ id='trade-offer', title='Offer fortress goods', - message='Are you sure you want to offer these goods? You will receive no payment.', + message=curry(get_ethics_message, 'Are you sure you want to offer these goods? You will receive no payment.'), intercept_keys='_MOUSE_L', intercept_frame={l=40, r=5, b=4, w=19, h=3}, - context='dwarfmode/Trade', + context='dwarfmode/Trade/Default', predicate=function() return trade_goods_any_selected(1) end, pausable=true, } From 3f1272851bf3e02657290c2bdbe0b03d2982ca2f Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 12 Aug 2024 11:39:40 -0700 Subject: [PATCH 10/15] update docs for caravan overlays --- docs/caravan.rst | 105 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 77 insertions(+), 28 deletions(-) diff --git a/docs/caravan.rst b/docs/caravan.rst index e29f149844..1e365cd159 100644 --- a/docs/caravan.rst +++ b/docs/caravan.rst @@ -49,31 +49,14 @@ Overlays -------- Additional functionality is provided on the various trade-related screens via -`overlay` widgets. - -Trade screen -```````````` - -- ``Shift+Click checkbox``: Select all items inside a bin without selecting the - bin itself -- ``Ctrl+Click checkbox``: Collapse or expand a single bin (as is possible in - the "Move goods to/from depot" screen) -- ``Ctrl+c``: Collapses all bins. The hotkey hint can also be clicked as though - it were a button. -- ``Ctrl+x``: Collapses everything (all item categories and anything - collapsible within each category). The hotkey hint can also be clicked as - though it were a button. - -There is also a reminder of the fast scroll functionality provided by the -vanilla game when you hold shift while scrolling (this works everywhere). - -You can turn the overlay on and off in `gui/control-panel`, or you can -reposition it to your liking with `gui/overlay`. The overlay is named -``caravan.tradeScreenExtension``. +`overlay` widgets. You can turn the overlays on and off in `gui/control-panel`, +or you can reposition them to your liking with `gui/overlay`. Bring item to depot ``````````````````` +**caravan.movegoods** + When the trade depot is selected, a button appears to bring up the DFHack enhanced move trade goods screen. You'll get a searchable, sortable list of all your tradeable items, with hotkeys to quickly select or deselect all visible @@ -82,27 +65,93 @@ items. There are filter sliders for selecting items of various condition levels and quality. For example, you can quickly trade all your tattered, frayed, and worn clothing by setting the condition slider to include from tattered to worn, then -hitting Ctrl-V to select all. +hitting ``Ctrl-a`` to select all. Click on an item and shift-click on a second item to toggle all items between the two that you clicked on. If the one that you shift-clicked on was selected, the range of items will be deselected. If the one you shift-clicked on was not selected, then the range of items will be selected. -Trade agreement -``````````````` +If any current merchants have ethical concerns, the list of goods that you can +bring to the depot is automatically filtered (by default) to only show +ethically acceptible items. Be aware that, again, by default, if you have items +in bins, and there are unethical items mixed into the bins, then the bins will +still be brought to the depot so you can trade the ethical items within those +bins. Please use the DFHack enhanced trade screen for the actual barter to +ensure the unethical items are not actually selected for sale. + +**caravan.movegoods_hider** + +This overlay simply hides the vanilla "Move trade goods" button, so if you +routinely prefer to use the enhanced DFHack "Move goods" dialog, you won't +accidentally click the vanilla button. + +**caravan.assigntrade** + +This overlay provides a button on the vanilla "Move trade goods" screen to +launch the DFHack enhanced dialog. + +Trade screen +```````````` + +**caravan.trade** + +This overlay enables some convenent gestures and keyboard shortcuts for working +with bins: + +- ``Shift-Click checkbox``: Select all items inside a bin without selecting the + bin itself +- ``Ctrl-Click checkbox``: Collapse or expand a single bin +- ``Ctrl-Shift-Click checkbox``: Select all items within the bin and collapse it +- ``Ctrl-c``: Collapse all bins +- ``Ctrl-x``: Collapse everything (all item categories and anything + collapsible within each category) + +There is also a reminder of the fast scroll functionality provided by the +vanilla game when you hold shift while scrolling (this works everywhere). + +**caravan.tradebanner** + +This overlay provides a button you can click to bring up the DFHack enhanced +trade dialog, which you can use to quickly search, filter, and select caravan +and fort goods for trade. -A small panel is shown with some useful shortcuts: +For example, to select all steel items for purchase, search for ``steel`` and +hit ``Ctrl-a`` (or click the "Select all" button) to select them all. -* ``Ctrl-A`` for selecting all/none in the currently shown category. -* ``Ctrl-M`` for selecting items with specific base material price (only +By default, the DFHack trade dialog will automatically filter out items that +the merchants you are trading with find ethically offensive. + +You can also bring up the DFHack trade dialog with the keyboard shortcut +``Ctrl-t``. + +**caravan.tradeethics** + +This overlay shows an "Ethics warning" badge next to the ``Trade`` button when +you have any items selected for sale that would offend the merchants that you +are trading with. Clicking on the badge will show a list of problematic items, +and you can click the button on the dialog to deselect all the problematic +items in your trade list. + +Trade agreements +```````````````` + +**caravan.tradeagreement** + +This adds a small panel with some useful shortcuts: + +* ``Ctrl-a`` for selecting all/none in the currently shown category. +* ``Ctrl-m`` for selecting items with specific base material price (only enabled for item categories where this matters, like gems and leather). Display furniture ````````````````` +**caravan.displayitemselector** + A button is added to the screen when you are viewing display furniture -(pedestals and display cases) where you can launch an item assignment GUI. +(pedestals and display cases) where you can launch a the extended DFhack item +assignment GUI. The dialog allows you to sort by name, value, or where the item is currently assigned for display. From 45330380ae920d1871171a83c5913a418f350ac9 Mon Sep 17 00:00:00 2001 From: Elias <39592174+foxxelias@users.noreply.github.com> Date: Tue, 13 Aug 2024 00:53:48 +0300 Subject: [PATCH 11/15] fixed wrong logical operator sequence --- empty-bin.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/empty-bin.lua b/empty-bin.lua index f94a140816..8e38c13d80 100644 --- a/empty-bin.lua +++ b/empty-bin.lua @@ -18,7 +18,7 @@ local function emptyContainer(container) print('Emptying ' .. dfhack.items.getReadableDescription(container)) local pos = xyz2pos(dfhack.items.getPosition(container)) for _, item in ipairs(items) do - local skip_liquid = item:getType() == df.item_type.LIQUID_MISC or item:getType() == df.item_type.DRINK and not options.liquids + local skip_liquid = not options.liquids and (item:getType() == df.item_type.LIQUID_MISC or item:getType() == df.item_type.DRINK) if skip_liquid then print(' ' .. dfhack.items.getReadableDescription(item) .. ' was skipped because the --liquids flag was not provided') else From a53e7c77381f6bb8ab138adf8c4b777df106cf4d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 12 Aug 2024 23:47:54 -0700 Subject: [PATCH 12/15] spruce up gui/pregnancy --- gui/pregnancy.lua | 620 +++++++++++++++++++++++----------------------- 1 file changed, 313 insertions(+), 307 deletions(-) diff --git a/gui/pregnancy.lua b/gui/pregnancy.lua index e759dfa118..026a8c4493 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() From 97392d8d750b6e3f0fe38be4ef64866b1211d32c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 13 Aug 2024 00:15:08 -0700 Subject: [PATCH 13/15] gui/pregnancy docs --- changelog.txt | 1 + docs/gui/pregnancy.rst | 32 ++++++++++++++++++++++++++++++++ gui/pregnancy.lua | 14 ++++++++++++-- 3 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 docs/gui/pregnancy.rst diff --git a/changelog.txt b/changelog.txt index ff92542951..4ebea52671 100644 --- a/changelog.txt +++ b/changelog.txt @@ -28,6 +28,7 @@ Template for new versions: ## New Tools - `embark-anyone`: allows you to embark as any civilisation, including dead, and non-dwarven ones +- `gui/pregnancy`: view and generate pregnancies with specified parents ## 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 new file mode 100644 index 0000000000..9ed48161d4 --- /dev/null +++ b/docs/gui/pregnancy.rst @@ -0,0 +1,32 @@ +gui/pregnancy +============= + +.. dfhack-tool:: + :summary: Generate pregnancies with pairings of your choice. + :tags: adventure fort armok animals units + +This tool provides an interface for producing pregnancies with specific mothers +and fathers. + +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 +automatically selected as the other parent. You can click on other units on the +map and choose them as alternate mothers or fathers as desired. + +If a unit is selected as a mother or father, or is listed as a spouse, you can +zoom the map to their location by clicking on their name in the `gui/pregnancy` +UI. + +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. + +Usage +----- + +:: + + gui/pregnancy diff --git a/gui/pregnancy.lua b/gui/pregnancy.lua index 026a8c4493..c43f00b3d4 100644 --- a/gui/pregnancy.lua +++ b/gui/pregnancy.lua @@ -198,8 +198,18 @@ function Pregnancy:init() key_back='CUSTOM_SHIFT_Z', key='CUSTOM_Z', options={ - {label='Default', value='default'}, - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, + {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', }, From c3f1b416a5cd3cdf2920025a27f59d5860d41c97 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 13 Aug 2024 07:10:43 -0700 Subject: [PATCH 14/15] fix up histfig spouse processing --- gui/pregnancy.lua | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/gui/pregnancy.lua b/gui/pregnancy.lua index c43f00b3d4..dac365f502 100644 --- a/gui/pregnancy.lua +++ b/gui/pregnancy.lua @@ -263,7 +263,9 @@ 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 + if not mother or not mother.pregnancy_genes or mother.pregnancy_timer <= 0 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 @@ -291,10 +293,13 @@ function Pregnancy:get_spouse_hf(who) 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 + local hf = df.historical_figure.find(unit.hist_figure_id) + if not hf then return end + + for _, link in ipairs(hf.histfig_links) do + if link._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) + return df.historical_figure.find(link.target_hf) end end end From 8f28742229dc8f2e24ed76091e0c7f790fd335ba Mon Sep 17 00:00:00 2001 From: Elias <39592174+foxxelias@users.noreply.github.com> Date: Tue, 13 Aug 2024 21:07:36 +0300 Subject: [PATCH 15/15] Update changelog.txt --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index ff92542951..50b3369c45 100644 --- a/changelog.txt +++ b/changelog.txt @@ -36,6 +36,7 @@ Template for new versions: ## Fixes - `timestream`: ensure child growth events (e.g. becoming an adult) are not skipped over +- `empty-bin`: ``--liquids`` option correctly emptying containers filled with LIQUID_MISC ## Misc Improvements - `gui/sitemap`: show whether a unit is friendly, hostile, or wildlife