diff --git a/bodyswap.lua b/bodyswap.lua index e64f43f783..fce5a96513 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,13 +25,13 @@ 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.PetOwner ~= -1 then return true @@ -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 dfhack.units.isKilled(unit) 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 dfhack.units.isKilled(slayer) then + local slayerName = "" + if slayer.name.has_name then + slayerName = ", " .. dfhack.units.getReadableName(slayer) .. "," + 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 6c8f0dd35e..2baab22be6 100644 --- a/changelog.txt +++ b/changelog.txt @@ -42,6 +42,7 @@ Template for new versions: - `hide-tutorials`: new ``reset`` command that will re-enable popups in the current game ## Removed +- `linger`: merged into `bodyswap` as ``bodyswap linger`` # 51.02-r1 diff --git a/docs/bodyswap.rst b/docs/bodyswap.rst index 772c882de0..18d9bcf20a 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 immediately after you have died, it will put you in the body of your killer. +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. + 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/docs/linger.rst b/docs/linger.rst deleted file mode 100644 index 19d1bb6d9b..0000000000 --- a/docs/linger.rst +++ /dev/null @@ -1,22 +0,0 @@ -linger -====== - -.. dfhack-tool:: - :summary: Take control of your adventurer's killer. - :tags: unavailable - -Run this script after being presented with the "You are deceased." message to -abandon your dead adventurer and take control of your adventurer's killer. - -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. - -Usage ------ - -:: - - linger diff --git a/linger.lua b/linger.lua deleted file mode 100644 index 1e618827fc..0000000000 --- a/linger.lua +++ /dev/null @@ -1,38 +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 - qerror("Your slayer, " .. dfhack.df2console(dfhack.units.getReadableName(slayer)) .. " is dead!") -end - -bodyswap.swapAdvUnit(slayer)