Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bodyswap Improvements and Additions #1164

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 131 additions & 27 deletions bodyswap.lua
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -92,14 +83,33 @@ 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)
party.party_extra_members:resize(0)
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!')
Expand All @@ -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!")
Expand All @@ -122,22 +135,113 @@ 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
myk002 marked this conversation as resolved.
Show resolved Hide resolved
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use dfhack.units.isKilled(unit) instead of accessing the flag directly

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use dfhack.units.isKilled

local slayerName = ""
if slayer.name.has_name then
slayerName = ", " .. dfhack.TranslateName(slayer.name) .. ","
end
Comment on lines +201 to +204
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use dfhack.units.getReadableName(slayer) to get the name of the histfig

qerror("The unit's slayer" .. slayerName .. " is dead!")
end

swapAdvUnit(slayer)
end

if not dfhack_flags.module then
if df.global.gamemode ~= df.game_mode.ADVENTURE 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)
Expand Down
1 change: 1 addition & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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``
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needs to be moved into changelog for current version

- `adv-fix-sleepers`: renamed to `fix/sleepers`
- `adv-rumors`: merged into `advtools`

Expand Down
18 changes: 15 additions & 3 deletions docs/bodyswap.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,27 @@ Usage
::

bodyswap [--unit <id>]
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
myk002 marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you need to mention that bodyswap linger must be run immediately after you have died, and that it will put you into the body of your killer.

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``
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
``bodyswap unit 42``
``bodyswap --unit 42``

Takes control of unit with id 42.
``bodyswap linger``
Takes control of your killer when you die
42 changes: 0 additions & 42 deletions linger.lua
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please also remove docs/linger.rst

This file was deleted.