diff --git a/NEWS.rst b/NEWS.rst index e966eda6a..ee8c8a4bf 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -82,6 +82,8 @@ New scripts - `pref-adjust`: Adjust all preferences of all dwarves in play - `rejuvenate`: make any "old" dwarf 20 years old - `starvingdead`: make undead weaken after one month on the map, and crumble after six +- `emigration`: stressed dwarves may leave your fortress if they see a chance +- `gui/family-affairs`: investigate and alter romantic relationships New tweaks ---------- diff --git a/docs/images/family-affairs.png b/docs/images/family-affairs.png new file mode 100644 index 000000000..faf59390e Binary files /dev/null and b/docs/images/family-affairs.png differ diff --git a/scripts/emigration.lua b/scripts/emigration.lua new file mode 100644 index 000000000..d523177bf --- /dev/null +++ b/scripts/emigration.lua @@ -0,0 +1,130 @@ +--Allow stressed dwarves to emigrate from the fortress +-- For 34.11 by IndigoFenix; update and cleanup by PeridexisErrant +-- old version: http://dffd.bay12games.com/file.php?id=8404 +--[[=begin + +emigration +========== +Allows dwarves to emigrate from the fortress when stressed, +in proportion to how badly stressed they are and adjusted +for who they would have to leave with - a dwarven merchant +being more attractive than leaving alone (or with an elf). +The check is made monthly. + +A happy dwarf (ie with negative stress) will never emigrate. + +Usage: ``emigration enable|disable`` + +=end]] + +local args = {...} +if args[1] == "enable" then + enabled = true +elseif args[1] == "disable" then + enabled = false +end + +function desireToStay(unit,method,civ_id) + -- on a percentage scale + value = 100 - unit.status.current_soul.personality.stress_level / 5000 + if method == 'merchant' or method == 'diplomat' then + if civ_id ~= unit.civ_id then value = value*2 end end + if method == 'wild' then + value = value*5 end + return value +end + +function desert(u,method,civ) + u.relations.following = nil + local line = dfhack.TranslateName(dfhack.units.getVisibleName(u)) .. " has " + if method == 'merchant' then + line = line.."joined the merchants" + u.flags1.merchant = true + u.civ_id = civ + elseif method == 'diplomat' then + line = line.."followed the diplomat" + u.flags1.diplomat = true + u.civ_id = civ + else + line = line.."abandoned the settlement in search of a better life." + u.civ_id = -1 + u.flags1.forest = true + u.animal.leave_countdown = 2 + end + print(line) + dfhack.gui.showAnnouncement(line, COLOR_WHITE) +end + +function canLeave(unit) + for _, skill in pairs(unit.status.current_soul.skills) do + if skill.rating > 14 then return false end + end + if unit.flags1.caged + or u.race ~= df.global.ui.race_id + or u.civ_id ~= df.global.ui.civ_id + or dfhack.units.isDead(u) + or dfhack.units.isOpposedToLife(u) + or u.flags1.merchant + or u.flags1.diplomat + or unit.flags1.chained + or dfhack.units.getNoblePositions(unit) ~= nil + or unit.military.squad_id ~= -1 + or dfhack.units.isCitizen(unit) + or dfhack.units.isSane(unit) + or unit.profession ~= 103 + or not dfhack.units.isDead(unit) + then return false end + return true +end + +function checkForDeserters(method,civ_id) + local allUnits = df.global.world.units.active + for i=#allUnits-1,0,-1 do -- search list in reverse + local u = allUnits[i] + if canLeave(u) and math.random(100) < desireToStay(u,method,civ_id) then + desert(u,method,civ_id) + end + end +end + +function checkmigrationnow() + local merchant_civ_ids = {} + local diplomat_civ_ids = {} + local allUnits = df.global.world.units.active + for i=0, #allUnits-1 do + local unit = allUnits[i] + if dfhack.units.isSane(unit) + and not dfhack.units.isDead(unit) + and not dfhack.units.isOpposedToLife(unit) + and not unit.flags1.tame + then + if unit.flags1.merchant then table.insert(merchant_civ_ids, unit.civ_id) end + if unit.flags1.diplomat then table.insert(diplomat_civ_ids, unit.civ_id) end + end + end + + for _, civ_id in pairs(merchant_civ_ids) do checkForDeserters('merchant', civ_id) end + for _, civ_id in pairs(diplomat_civ_ids) do checkForDeserters('diplomat', civ_id) end + checkForDeserters('wild', -1) +end + +local function event_loop() + checkmigrationnow() + dfhack.timeout(1, 'months', event_loop) +end + +dfhack.onStateChange.loadEmigration = function(code) + if code==SC_MAP_LOADED then + if enabled then + print("Emigration enabled.") + event_loop() + else + print("Emigration disabled.") + end + end +end + +if dfhack.isMapLoaded() then + dfhack.onStateChange.loadEmigration(SC_MAP_LOADED) +end + diff --git a/scripts/gui/family-affairs.lua b/scripts/gui/family-affairs.lua new file mode 100644 index 000000000..51525f36e --- /dev/null +++ b/scripts/gui/family-affairs.lua @@ -0,0 +1,296 @@ +-- gui/family-affairs +-- derived from v1.2 @ http://www.bay12forums.com/smf/index.php?topic=147779 +local help = [[=begin + +gui/family-affairs +================== +A user-friendly interface to view romantic relationships, +with the ability to add, remove, or otherwise change them at +your whim - fantastic for depressed dwarves with a dead spouse +(or matchmaking players...). + +The target/s must be alive, sane, and in fortress mode. + +.. image:: /docs/images/family-affairs.png + :align: center + +``gui/family-affairs [unitID]`` + shows GUI for the selected unit, or the specified unit ID + +``gui/family-affairs divorce [unitID]`` + removes all spouse and lover information from the unit + and it's partner, bypassing almost all checks. + +``gui/family-affairs [unitID] [unitID]`` + divorces the two specificed units and their partners, + then arranges for the two units to marry, bypassing + almost all checks. Use with caution. + +=end]] + +local dlg = require ('gui.dialogs') + +function ErrorPopup (msg,color) + if not tostring(msg) then msg = "Error" end + if not color then color = COLOR_LIGHTRED end + dlg.showMessage("Dwarven Family Affairs", msg, color, nil) +end + +function AnnounceAndGamelog (text,l) + if not l then l = true end + dfhack.gui.showAnnouncement(text, _G["COLOR_LIGHTMAGENTA"]) + if l then + local log = io.open('gamelog.txt', 'a') + log:write(text.."\n") + log:close() + end +end + +function ListPrompt (msg, choicelist, bool, yes_func) +dlg.showListPrompt( + "Dwarven Family Affairs", + msg, + COLOR_WHITE, + choicelist, + --called if choice is yes + yes_func, + --called on cancel + function() end, + 15, + bool + ) +end + +function GetMarriageSummary (source) + local familystate = "" + + if source.relations.spouse_id ~= -1 then + if dfhack.units.isSane(df.unit.find(source.relations.spouse_id)) then + familystate = dfhack.TranslateName(source.name).." has a spouse ("..dfhack.TranslateName(df.unit.find(source.relations.spouse_id).name)..")" + end + if dfhack.units.isSane(df.unit.find(source.relations.spouse_id)) == false then + familystate = dfhack.TranslateName(source.name).."'s spouse is dead or not sane, would you like to choose a new one?" + end + end + + if source.relations.spouse_id == -1 and source.relations.lover_id ~= -1 then + if dfhack.units.isSane(df.unit.find(source.relations.lover_id)) then + familystate = dfhack.TranslateName(source.name).." already has a lover ("..dfhack.TranslateName(df.unit.find(source.relations.spouse_id).name)..")" + end + if dfhack.units.isSane(df.unit.find(source.relations.lover_id)) == false then + familystate = dfhack.TranslateName(source.name).."'s lover is dead or not sane, would you like that love forgotten?" + end + end + + if source.relations.spouse_id == -1 and source.relations.lover_id == -1 then + familystate = dfhack.TranslateName(source.name).." is not involved in romantic relationships with anyone" + end + + if source.relations.pregnancy_timer > 0 then + familystate = familystate.."\nShe is pregnant." + local father = df.historical_figure.find(source.relations.pregnancy_spouse) + if father then + familystate = familystate.." The father is "..dfhack.TranslateName(father.name).."." + end + end + + return familystate +end + +function GetSpouseData (source) + local spouse = df.unit.find(source.relations.spouse_id) + local spouse_hf + if spouse then + spouse_hf = df.historical_figure.find (spouse.hist_figure_id) + end + return spouse,spouse_hf +end + +function GetLoverData (source) + local lover = df.unit.find(source.relations.spouse_id) + local lover_hf + if lover then + lover_hf = df.historical_figure.find (lover.hist_figure_id) + end + return lover,lover_hf +end + +function EraseHFLinksLoverSpouse (hf) + for i = #hf.histfig_links-1,0,-1 do + if hf.histfig_links[i]._type == df.histfig_hf_link_spousest or hf.histfig_links[i]._type == df.histfig_hf_link_loverst then + local todelete = hf.histfig_links[i] + hf.histfig_links:erase(i) + todelete:delete() + end + end +end + +function Divorce (source) + local source_hf = df.historical_figure.find(source.hist_figure_id) + local spouse,spouse_hf = GetSpouseData (source) + local lover,lover_hf = GetLoverData (source) + + source.relations.spouse_id = -1 + source.relations.lover_id = -1 + + if source_hf then + EraseHFLinksLoverSpouse (source_hf) + end + if spouse then + spouse.relations.spouse_id = -1 + spouse.relations.lover_id = -1 + end + if lover then + spouse.relations.spouse_id = -1 + spouse.relations.lover_id = -1 + end + if spouse_hf then + EraseHFLinksLoverSpouse (spouse_hf) + end + if lover_hf then + EraseHFLinksLoverSpouse (lover_hf) + end + + local partner = spouse or lover + if not partner then + AnnounceAndGamelog(dfhack.TranslateName(source.name).." is now single") + else + AnnounceAndGamelog(dfhack.TranslateName(source.name).." and "..dfhack.TranslateName(partner.name).." are now single") + end +end + +function Marriage (source,target) + local source_hf = df.historical_figure.find(source.hist_figure_id) + local target_hf = df.historical_figure.find(target.hist_figure_id) + source.relations.spouse_id = target.id + target.relations.spouse_id = source.id + + local new_link = df.histfig_hf_link_spousest:new() -- adding hf link to source + new_link.target_hf = target_hf.id + new_link.link_strength = 100 + source_hf.histfig_links:insert('#',new_link) + + new_link = df.histfig_hf_link_spousest:new() -- adding hf link to target + new_link.target_hf = source_hf.id + new_link.link_strength = 100 + target_hf.histfig_links:insert('#',new_link) +end + +function ChooseNewSpouse (source) + + if not source then + qerror("no unit") return + end + if (source.profession == 103 or source.profession == 104) then + ErrorPopup("target is too young") return + end + if not (source.relations.spouse_id == -1 and source.relations.lover_id == -1) then + ErrorPopup("target already has a spouse or a lover") + qerror("source already has a spouse or a lover") + return + end + + local choicelist = {} + targetlist = {} + + for k,v in pairs (df.global.world.units.active) do + if dfhack.units.isCitizen(v) + and v.race == source.race + and v.sex ~= source.sex + and v.relations.spouse_id == -1 + and v.relations.lover_id == -1 + and not (v.profession == 103 or v.profession == 104) + then + table.insert(choicelist,dfhack.TranslateName(v.name)..', '..dfhack.units.getProfessionName(v)) + table.insert(targetlist,v) + end + end + + if #choicelist > 0 then + ListPrompt( + "Assign new spouse for "..dfhack.TranslateName(source.name), + choicelist, + true, + function(a,b) + local target = targetlist[a] + Marriage (source,target) + AnnounceAndGamelog(dfhack.TranslateName(source.name).." and "..dfhack.TranslateName(target.name).." have married!") + end) + else + ErrorPopup("No suitable candidates") + end +end + +function MainDialog (source) + + local familystate = GetMarriageSummary(source) + + familystate = familystate.."\nSelect action:" + local choicelist = {} + local on_select = {} + + local child = (source.profession == 103 or source.profession == 104) + local is_single = source.relations.spouse_id == -1 and source.relations.lover_id == -1 + local ready_for_marriage = single and not child + + if not child then + table.insert(choicelist,"Remove romantic relationships (if any)") + table.insert(on_select, Divorce) + if ready_for_marriage then + table.insert(choicelist,"Assign a new spouse") + table.insert(on_select,ChooseNewSpouse) + end + if not ready_for_marriage then + table.insert(choicelist,"[Assign a new spouse]") + table.insert(on_select,function () ErrorPopup ("Existing relationships must be removed if you wish to assign a new spouse.") end) + end + else + table.insert(choicelist,"Leave this child alone") + table.insert(on_select,nil) + end + + ListPrompt(familystate, choicelist, false, + function(a,b) if on_select[a] then on_select[a](source) end end) +end + + +local args = {...} + +if args[1] == "help" or args[1] == "?" then print(helpstr) return end + +if not df.global.gamemode == 0 then + print (helpstr) qerror ("invalid gamemode") return +end + +if args[1] == "divorce" and tonumber(args[2]) then + local unit = df.unit.find(args[2]) + if unit then Divorce (unit) return end +end + +if tonumber(args[1]) and tonumber(args[2]) then + local unit1 = df.unit.find(args[1]) + local unit2 = df.unit.find(args[2]) + if unit1 and unit2 then + Divorce (unit1) + Divorce (unit2) + Marriage (unit1,unit2) + return + end +end + +local selected = dfhack.gui.getSelectedUnit(true) +if tonumber(args[1]) then + selected = df.unit.find(tonumber(args[1])) or selected +end + +if selected then + if dfhack.units.isCitizen(selected) and dfhack.units.isSane(selected) then + MainDialog(selected) + else + qerror("You must select sane fortress citizen.") + return + end +else + print (helpstr) + qerror("select a sane fortress dwarf") +end