diff --git a/bodyswap.lua b/bodyswap.lua index cbc014b58..f4b0e6676 100644 --- a/bodyswap.lua +++ b/bodyswap.lua @@ -1,27 +1,19 @@ --@ module = true +local dialogs = require('gui.dialogs') +local utils = require('utils') +local argparse = require('argparse') +local makeown = reqscript('makeown') -local utils = require 'utils' -local validArgs = utils.invert({ - 'unit', - 'help' -}) -local args = utils.processArgs({ ... }, validArgs) - -if args.help then - print(dfhack.script_help()) - return -end - -function setNewAdvNemFlags(nem) +local function setNewAdvNemFlags(nem) nem.flags.ACTIVE_ADVENTURER = true nem.flags.ADVENTURER = true end -function setOldAdvNemFlags(nem) +local function setOldAdvNemFlags(nem) nem.flags.ACTIVE_ADVENTURER = false end -function clearNemesisFromLinkedSites(nem) +local function clearNemesisFromLinkedSites(nem) -- omitting this step results in duplication of the unit entry in df.global.world.units.active when the site to which the historical figure is linked is reloaded with said figure present as a member of the player party -- this can be observed as part of the normal recruitment process when the player adds a site-linked historical figure to their party if not nem.figure then @@ -33,15 +25,15 @@ function clearNemesisFromLinkedSites(nem) end end -function createNemesis(unit) +local function createNemesis(unit) local nemesis = unit:create_nemesis(1, 1) nemesis.figure.flags.never_cull = true return nemesis end -function isPet(nemesis) +local function isPet(nemesis) if nemesis.unit then - if nemesis.unit.relationship_ids.Pet ~= -1 then + if nemesis.unit.relationship_ids.PetOwner ~= -1 then return true end elseif nemesis.figure then -- in case the unit is offloaded @@ -54,7 +46,7 @@ function isPet(nemesis) return false end -function processNemesisParty(nemesis, targetUnitID, alreadyProcessed) +local function processNemesisParty(nemesis, targetUnitID, alreadyProcessed) -- configures the target and any leaders/companions to behave as cohesive adventure mode party members local alreadyProcessed = alreadyProcessed or {} alreadyProcessed[tostring(nemesis.id)] = true @@ -66,8 +58,7 @@ function processNemesisParty(nemesis, targetUnitID, alreadyProcessed) elseif isPet(nemesis) then -- pets belonging to the target or to their companions df.global.adventure.interactions.party_pets:insert('#', nemesis.figure.id) else - df.global.adventure.interactions.party_core_members:insert('#', nemesis.figure.id) -- placing all non-pet companions into the core party list to enable tactical mode swapping - nemesis.flags.ADVENTURER = true + df.global.adventure.interactions.party_extra_members:insert('#', nemesis.figure.id) -- placing all non-pet companions into the extra party list if nemUnit then -- check in case the companion is offloaded nemUnit.relationship_ids.GroupLeader = targetUnitID end @@ -92,7 +83,7 @@ function processNemesisParty(nemesis, targetUnitID, alreadyProcessed) end end -function configureAdvParty(targetNemesis) +local function configureAdvParty(targetNemesis) local party = df.global.adventure.interactions party.party_core_members:resize(0) party.party_pets:resize(0) @@ -100,6 +91,25 @@ function configureAdvParty(targetNemesis) processNemesisParty(targetNemesis, targetNemesis.unit_id) end +-- shamelessly copy pasted from flashstep.lua +local function reveal_tile(pos) + local des = dfhack.maps.getTileFlags(pos) + des.hidden = false + des.pile = true -- reveal the tile on the map +end + +local function reveal_around(pos) + reveal_tile(xyz2pos(pos.x-1, pos.y-1, pos.z)) + reveal_tile(xyz2pos(pos.x, pos.y-1, pos.z)) + reveal_tile(xyz2pos(pos.x+1, pos.y-1, pos.z)) + reveal_tile(xyz2pos(pos.x-1, pos.y, pos.z)) + reveal_tile(pos) + reveal_tile(xyz2pos(pos.x+1, pos.y, pos.z)) + reveal_tile(xyz2pos(pos.x-1, pos.y+1, pos.z)) + reveal_tile(xyz2pos(pos.x, pos.y+1, pos.z)) + reveal_tile(xyz2pos(pos.x+1, pos.y+1, pos.z)) +end + function swapAdvUnit(newUnit) if not newUnit then qerror('Target unit not specified!') @@ -111,6 +121,9 @@ function swapAdvUnit(newUnit) return end + -- Make sure the unit we're swapping into isn't nameless + makeown.name_unit(newUnit) + local newNem = dfhack.units.getNemesis(newUnit) or createNemesis(newUnit) if not newNem then qerror("Failed to obtain target nemesis!") @@ -122,8 +135,77 @@ function swapAdvUnit(newUnit) df.global.adventure.player_id = newNem.id df.global.world.units.adv_unit = newUnit oldUnit.idle_area:assign(oldUnit.pos) + local pos = xyz2pos(dfhack.units.getPosition(newUnit)) + -- reveal the tiles around the bodyswapped unit + reveal_around(pos) + -- Focus on the revealed pos + dfhack.gui.revealInDwarfmodeMap(pos, true) +end + +-- shamelessly copy pasted from gui/sitemap.lua +local function get_unit_choices() + local choices = {} + for _, unit in ipairs(df.global.world.units.active) do + if not dfhack.units.isActive(unit) or + dfhack.units.isHidden(unit) + then + goto continue + end + local name = dfhack.units.getReadableName(unit) + table.insert(choices, { + text=name, + unit=unit, + search_key=dfhack.toSearchNormalized(name), + }) + ::continue:: + end + return choices +end + +local function swapAdvUnitPrompt() + local choices = get_unit_choices() + dialogs.showListPrompt('bodyswap', "Select a unit to bodyswap to:", COLOR_WHITE, + choices, function(id, choice) + swapAdvUnit(choice.unit) + end, nil, nil, true) +end + +function getHistoricalSlayer(unit) + local histFig = unit.hist_figure_id ~= -1 and df.historical_figure.find(unit.hist_figure_id) + if not histFig then + return + end - dfhack.gui.revealInDwarfmodeMap(xyz2pos(dfhack.units.getPosition(newUnit)), true) + local deathEvents = df.global.world.history.events_death + for i = #deathEvents - 1, 0, -1 do + local event = deathEvents[i] --as:df.history_event_hist_figure_diedst + if event.victim_hf == unit.hist_figure_id then + return df.historical_figure.find(event.slayer_hf) + end + end +end + +function lingerAdvUnit(unit) + if not unit.flags2.killed then + qerror("Target unit hasn't died yet!") + end + + local slayerHistFig = getHistoricalSlayer(unit) + local slayer = slayerHistFig and df.unit.find(slayerHistFig.unit_id) + if not slayer then + slayer = df.unit.find(unit.relationship_ids.LastAttacker) + end + if not slayer then + qerror("Slayer not found!") + elseif slayer.flags2.killed then + local slayerName = "" + if slayer.name.has_name then + slayerName = ", " .. dfhack.TranslateName(slayer.name) .. "," + end + qerror("The unit's slayer" .. slayerName .. " is dead!") + end + + swapAdvUnit(slayer) end if not dfhack_flags.module then @@ -131,13 +213,35 @@ if not dfhack_flags.module then qerror("This script can only be used in adventure mode!") end - local unit = args.unit and df.unit.find(tonumber(args.unit)) or dfhack.gui.getSelectedUnit() + local options = { + help = false, + unit = -1, + } + + local args = { ... } + local positionals = argparse.processArgsGetopt(args, { + {'h', 'help', handler = function() options.help = true end}, + {'u', 'unit', handler = function(arg) options.unit = argparse.nonnegativeInt(arg, 'unit') end, hasArg = true}, + }) + + if positionals[1] == 'help' or options.help then + print(dfhack.script_help()) + return + end + + if positionals[1] == 'linger' then + lingerAdvUnit(dfhack.world.getAdventurer()) + return + end + + local unit = options.unit == -1 and dfhack.gui.getSelectedUnit(true) or df.unit.find(options.unit) if not unit then print("Enter the following if you require assistance: help bodyswap") - if args.unit then - qerror("Invalid unit id: " .. args.unit) + if options.unit ~= -1 then + qerror("Invalid unit id: " .. options.unit) else - qerror("Target unit not specified!") + swapAdvUnitPrompt() + return end end swapAdvUnit(unit) diff --git a/changelog.txt b/changelog.txt index 91ffd5ae8..ea0ee26f0 100644 --- a/changelog.txt +++ b/changelog.txt @@ -99,6 +99,7 @@ Template for new versions: - `max-wave`: merged into `pop-control` - `devel/find-offsets`, `devel/find-twbt`, `devel/prepare-save`: remove development scripts that are no longer useful - `fix/item-occupancy`, `fix/tile-occupancy`: merged into `fix/occupancy` +- `linger`: merged into `bodyswap` as ``bodyswap linger`` - `adv-fix-sleepers`: renamed to `fix/sleepers` - `adv-rumors`: merged into `advtools` diff --git a/docs/bodyswap.rst b/docs/bodyswap.rst index 772c882de..72ad36aba 100644 --- a/docs/bodyswap.rst +++ b/docs/bodyswap.rst @@ -14,15 +14,27 @@ Usage :: bodyswap [--unit ] + bodyswap linger If no specific unit id is specified, the target unit is the one selected in the user interface, such as by opening the unit's status screen or viewing its -description. +description. Otherwise, a valid list of units to bodyswap into will be shown. +If bodyswapping into an entity that has no historical figure, a new historical figure is created for it. +If said unit has no name, a new name is randomly generated for it, based on the unit's race. +If no valid language is found for that race, it will use the DIVINE language. + +If you run bodyswap linger, the killer is identified by examining the historical event generated +when the adventurer died. If this is unsuccessful, the killer is assumed to be the last unit to have +attacked the adventurer prior to their death. + +This will fail if the unit in question is no longer present on the local map or is also dead. Examples -------- ``bodyswap`` - Takes control of the selected unit. -``bodyswap --unit 42`` + Takes control of the selected unit, or brings up a list of swappable units if no unit is selected. +``bodyswap unit 42`` Takes control of unit with id 42. +``bodyswap linger`` + Takes control of your killer when you die diff --git a/linger.lua b/linger.lua deleted file mode 100644 index 2207c48d1..000000000 --- a/linger.lua +++ /dev/null @@ -1,42 +0,0 @@ -local bodyswap = reqscript('bodyswap') - -if df.global.gamemode ~= df.game_mode.ADVENTURE then - qerror("This script can only be used in adventure mode!") -end - -local adventurer = df.nemesis_record.find(df.global.adventure.player_id).unit -if not adventurer.flags2.killed then - qerror("Your adventurer hasn't died yet!") -end - -function getHistoricalSlayer(unit) - local histFig = unit.hist_figure_id ~= -1 and df.historical_figure.find(unit.hist_figure_id) - if not histFig then - return - end - - local deathEvents = df.global.world.history.events_death - for i = #deathEvents - 1, 0, -1 do - local event = deathEvents[i] --as:df.history_event_hist_figure_diedst - if event.victim_hf == unit.hist_figure_id then - return df.historical_figure.find(event.slayer_hf) - end - end -end - -local slayerHistFig = getHistoricalSlayer(adventurer) -local slayer = slayerHistFig and df.unit.find(slayerHistFig.unit_id) -if not slayer then - slayer = df.unit.find(adventurer.relationship_ids.LastAttacker) -end -if not slayer then - qerror("Killer not found!") -elseif slayer.flags2.killed then - local slayerName = "" - if slayer.name.has_name then - slayerName = ", " .. dfhack.TranslateName(slayer.name) .. "," - end - qerror("Your slayer" .. slayerName .. " is dead!") -end - -bodyswap.swapAdvUnit(slayer)