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 4 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
122 changes: 108 additions & 14 deletions bodyswap.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
--@ module = true

local dialogs = require 'gui.dialogs'
local utils = require 'utils'
local validArgs = utils.invert({
'unit',
Expand All @@ -12,16 +12,16 @@ if args.help then
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 +33,58 @@ function clearNemesisFromLinkedSites(nem)
end
end

function createNemesis(unit)
-- shamelessly copypasted from makeown.lua
Crystalwarrior marked this conversation as resolved.
Show resolved Hide resolved
local function get_translation(race_id)
local race_name = df.global.world.raws.creatures.all[race_id].creature_id
local backup = nil
for _,translation in ipairs(df.global.world.raws.language.translations) do
if translation.name == race_name then
return translation
end
if translation.name == 'GEN_DIVINE' then
backup = translation
end
end
-- Use a divine name if no normal name is found
if backup then
return backup
end
-- Use the first language in the list if no divine language is found, this is normally the DWARF language.
return df.global.world.raws.language.translations[0]
end

local function pick_first_name(race_id)
local translation = get_translation(race_id)
return translation.words[math.random(0, #translation.words-1)].value
end

local LANGUAGE_IDX = 0
local word_table = df.global.world.raws.language.word_table[LANGUAGE_IDX][35]

local function name_nemesis(nemesis)
local figure = nemesis.figure
if figure.name.has_name then return end

figure.name.first_name = pick_first_name(figure.race)
figure.name.words.FrontCompound = word_table.words.FrontCompound[math.random(0, #word_table.words.FrontCompound-1)]
figure.name.words.RearCompound = word_table.words.RearCompound[math.random(0, #word_table.words.RearCompound-1)]

figure.name.language = LANGUAGE_IDX
figure.name.parts_of_speech.FrontCompound = df.part_of_speech.Noun
figure.name.parts_of_speech.RearCompound = df.part_of_speech.Verb3rdPerson
figure.name.type = df.language_name_type.Figure
figure.name.has_name = true
end

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 +97,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 +109,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,15 +134,23 @@ 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

function swapAdvUnit(newUnit)
-- shamelessly copy pasted from flashstep.lua
local function reveal_tile(pos)
local block = dfhack.maps.getTileBlock(pos)
local des = block.designation[pos.x%16][pos.y%16]
Crystalwarrior marked this conversation as resolved.
Show resolved Hide resolved
des.hidden = false
des.pile = true -- reveal the tile on the map
end

local function swapAdvUnit(newUnit)
if not newUnit then
qerror('Target unit not specified!')
end
Expand All @@ -116,16 +166,59 @@ function swapAdvUnit(newUnit)
qerror("Failed to obtain target nemesis!")
end

name_nemesis(newNem)

setOldAdvNemFlags(oldNem)
setNewAdvNemFlags(newNem)
configureAdvParty(newNem)
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_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))

dfhack.gui.revealInDwarfmodeMap(pos, true)
end

dfhack.gui.revealInDwarfmodeMap(xyz2pos(dfhack.units.getPosition(newUnit)), true)
-- 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


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!")
Expand All @@ -137,7 +230,8 @@ if not dfhack_flags.module then
if args.unit then
qerror("Invalid unit id: " .. args.unit)
else
qerror("Target unit not specified!")
swapAdvUnitPrompt()
return
end
end
swapAdvUnit(unit)
Expand Down
7 changes: 5 additions & 2 deletions docs/bodyswap.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,15 @@ Usage

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.

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.