Skip to content

Instantly share code, notes, and snippets.

@celediel
Last active March 9, 2022 00:59
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save celediel/67d6bcd20b1eadbd7fbc8040cad22081 to your computer and use it in GitHub Desktop.
Save celediel/67d6bcd20b1eadbd7fbc8040cad22081 to your computer and use it in GitHub Desktop.
My fork of DragonDoor - MWSE Lua mod that makes enemies pursue through doors
-- DragonDoor! Originally written by Archimag https://www.nexusmods.com/morrowind/mods/47169
-- Fork by Celediel https://gist.github.com/celediel/67d6bcd20b1eadbd7fbc8040cad22081
-- {{{ variables and such
local defaultConfig = {
message = false,
help = true,
helpMessage = true,
helpRespondDistance = 3000,
saveOverrideKey = {keyCode = 56}
}
local config = mwse.loadConfig("DragonDoor!", defaultConfig)
-- used for keeping track of what hunters are doing
local status = {following = 1, notFollowing = 2, lostSight = 3, inDifferentCell = 4}
-- global
local hunters = {}
local helpers = {}
local chaseTimer, dTimer, attackCell
local matrix = tes3matrix33.new()
-- onActivate
local list, count
-- DoorKick
-- I still don't know what this does
local DAR = {}
local DAT, lastFrameTime
local summon = {
["atronach_flame_summon"] = true,
["atronach_frost_summon"] = true,
["atronach_storm_summon"] = true,
["golden saint_summon"] = true,
["daedroth_summon"] = true,
["dremora_summon"] = true,
["scamp_summon"] = true,
["winged twilight_summon"] = true,
["clannfear_summon"] = true,
["hunger_summon"] = true,
["Bonewalker_Greater_summ"] = true,
["ancestor_ghost_summon"] = true,
["skeleton_summon"] = true,
["bonelord_summon"] = true,
["4nm_daedraspider_s"] = true,
["4nm_dremora_mage_s"] = true,
["4nm_skaafin_s"] = true,
["4nm_xivkyn_s"] = true,
["4nm_mazken_s"] = true,
["4nm_ogrim_s"] = true,
["4nm_skeleton_mage_s"] = true,
["4nm_lich_elder_s"] = true,
["BM_bear_black_summon"] = true,
["BM_wolf_grey_summon"] = true,
["BM_wolf_bone_summon"] = true,
["bonewalker_summon"] = true,
["centurion_sphere_summon"] = true,
["fabricant_summon"] = true
}
-- }}}
-- {{{ misc functions
local function log(...)
tes3.messageBox(...)
mwse.log("[DragonDoor!] %s", string.format(...))
end
local function doCleanup()
hunters = {}
helpers = {}
chaseTimer = nil
dTimer = nil
if config.message then log("Cleanup time!") end
end
-- }}}
-- {{{ check functions
local function isFriendlyActor(actor)
for friend in tes3.iterate(tes3.mobilePlayer.friendlyActors) do
if actor.object.id == friend.object.id or actor.object.baseObject.id == friend.object.baseObject.id then
return true
end
end
return false
end
-- fucking formatter makes this shitty looking
local function actorCheck(actor)
-- make sure we're not already hunting
return hunters[actor.reference] == nil and --
-- ignore craven npcs
((actor.actorType == tes3.actorType.npc and actor.flee < 70) or --
-- and biped creatures that aren't summons
(actor.actorType == tes3.actorType.creature and actor.object.biped and not summon[actor.object.baseObject.id])) and --
-- low health enemies don't follow
actor.health.normalized > 0.5 and --
-- ignore followers
not isFriendlyActor(actor) --
end
-- }}}
-- {{{ helper functions
local function callForHelp(actor)
if actor.health.current > 0 and actor.inCombat and actor.fatigue.current > 0 and actor.paralyze < 1 and
actor.silence < 1 then
helpers[actor.reference]:cancel()
if config.helpMessage then log("%s calls for help!", actor.object.name) end
for ref in tes3.iterate(actor.reference.cell.actors) do
if hunters[ref] == nil and actor.reference ~= ref and actor.reference.position:distance(ref.position) <
config.helpRespondDistance and ref.mobile and not ref.mobile.isDead and ref.mobile.fight >= 90 and
not ref.mobile.inCombat then
ref.mobile:startCombat(tes3.mobilePlayer)
if config.helpMessage then log("%s heard and runs into battle!", ref.object.name) end
end
end
end
end
-- }}}
-- {{{ event functions
local function onCombatStarted(e)
if e.target == tes3.mobilePlayer then
-- if combat is started in the same frame that a cell change occurs, NPCs won't be fully loaded,
-- thus their vampire status may not be updated properly. Here's a dumb fix for that:
timer.delayOneFrame(function()
if actorCheck(e.actor) then
hunters[e.actor.reference] = {
startingPosition = e.actor.position:copy(),
startingCell = e.actor.reference.cell,
status = status.following
}
if config.message then
log("%s joined the battle! Enemies = %s", e.actor.object.name, table.size(hunters))
end
end
-- todo: functionize this check
if config.callForHelp and e.actor.reference.cell.isInterior and helpers[e.actor.reference] == nil and
not summon[e.actor.object.baseObject.id] then
helpers[e.actor.reference] = timer.start {
duration = 3,
iterations = 20,
callback = function() callForHelp(e.actor) end
}
end
end)
end
end
local function DoorKick(e)
if DAR[e.reference] then e.mobile.impulseVelocity = DAR[e.reference] end
if tes3.worldController.lastFrameTime ~= lastFrameTime then
lastFrameTime = tes3.worldController.lastFrameTime
DAT = DAT + 1
if DAT == 15 then
event.unregister("calcMoveSpeed", DoorKick)
DAT = nil
DAR = {}
end
end
end
local function onActivate(e)
if not e.target.object.objectType == tes3.objectType.door then return end
if e.activator == tes3.player then
list = "You are hunted by: "
count = 0
for ref, data in pairs(hunters) do
if data.status == status.following then
if attackCell[ref.cell] and ref.mobile.inCombat then
if ref.mobile.health.normalized <= 0.5 or ref.mobile.fatigue.current < 0 then
data.status = status.notFollowing
if config.message then
log("%s no longer wants to chase you", ref.object.name)
end
elseif tes3.player.position:distance(ref.position) > 5000 then
data.status = status.lostSight
if config.message then
log("%s lost sight of you. Distance = %d", ref.object.name,
tes3.player.position:distance(ref.position))
end
else
data.tim = 1 +
math.floor(tes3.player.position:distance(ref.position) /
(200 + ref.mobile.speed.current * 3))
list = list .. ref.object.name .. ", "
count = count + 1
end
else
data.status = status.lostSight
if config.message then log("%s lost sight of you!", ref.object.name) end
end
elseif data.status == status.lostSight then
if attackCell[ref.cell] and ref.mobile.inCombat then
data.status = status.following
data.tim = 1 +
math.floor(tes3.player.position:distance(ref.position) /
(200 + ref.mobile.speed.current * 3))
if config.message then log("%s see you again", ref.object.name) end
list = list .. ref.object.name .. ", "
count = count + 1
end
elseif data.status == status.inDifferentCell then
list = list .. ref.object.name .. ", "
count = count + 1
end
end
if config.message and count ~= 0 then log("%s Total = %s/%s", list, count, table.size(hunters)) end
if dTimer then dTimer:cancel() end
dTimer = timer.start {duration = 0.3, callback = function() dTimer = nil end}
else
for ref in tes3.iterate(e.target.cell.actors) do
if ref.mobile and not ref.mobile.isDead and e.target.position:distance(ref.position) < 400 then
matrix:fromEulerXYZ(ref.orientation.x, ref.orientation.y, ref.orientation.z)
DAR[ref] = matrix:transpose().y * -2000
end
end
if table.size(DAR) > 0 and DAT == nil then
DAT = 0
lastFrameTime = tes3.worldController.lastFrameTime
event.register("calcMoveSpeed", DoorKick)
end
if DAT == nil then
DAT = 0
event.register("calcMoveSpeed", DoorKick)
end
if config.message then
log("%s open the door %s Total = %d", e.activator.object.name, e.target.object.id, table.size(DAR))
end
end
end
local function onCellChanged(e)
attackCell = {}
for _, cell in pairs(tes3.getActiveCells()) do attackCell[cell] = true end
if e.previousCell and table.size(hunters) > 0 then
if dTimer and (e.cell.isInterior or e.previousCell.isInterior) then
for ref, data in pairs(hunters) do
if data.status == status.following then
if ref.cell ~= tes3.player.cell and (ref.cell.isInterior or tes3.player.cell.isInterior) then
data.followCell = tes3.player.cell
data.followPosition = tes3.player.position:copy()
data.status = status.inDifferentCell
elseif attackCell[ref.cell] then
if config.message then log("%s see you!", ref.object.name) end
end
elseif data.status == status.notFollowing then
if not attackCell[ref.cell] and not attackCell[data.startingCell] then
tes3.positionCell {cell = data.startingCell, position = data.startingPosition, reference = ref}
hunters[ref] = nil
if config.message then
log("%s is beaten and returned to their place", ref.object.name)
end
end
elseif data.status == status.lostSight then
if attackCell[ref.cell] then
data.status = status.following
if config.message then log("%s see you again!", ref.object.name) end
elseif not attackCell[data.startingCell] then
tes3.positionCell {cell = data.startingCell, position = data.startingPosition, reference = ref}
hunters[ref] = nil
if config.message then log("%s returned to their place", ref.object.name) end
end
elseif data.status == status.inDifferentCell and attackCell[ref.cell] then
data.status = status.following
if config.message then log("%s see you again!!", ref.object.name) end
end
end
if table.size(hunters) ~= 0 and chaseTimer == nil then
chaseTimer = timer.start({
duration = 1,
iterations = -1,
callback = function()
local fin = true
local night = tes3.worldController.hour.value >= 20 or tes3.worldController.hour.value < 6
local inside
for ref, data in pairs(hunters) do
inside = data.followCell.isInterior and not data.followCell.behavesAsExterior
if data.status == status.inDifferentCell then
fin = nil
data.tim = data.tim - 1
if data.tim <= 0 then
if (ref.object.baseObject.head.vampiric or
tes3.isAffectedBy({reference = ref, effect = tes3.effect.vampirism})) and
not night and not inside then
-- go back to starting location
if config.message then
log("%s is a vampire and didn't want to follow you outside!",
ref.object.name)
end
tes3.positionCell {
cell = data.startingCell,
position = data.startingPosition,
reference = ref
}
hunters[ref] = nil
else
-- follow player
tes3.positionCell {
cell = data.followCell,
position = data.followPosition,
reference = ref
}
if attackCell[data.followCell] then
data.status = status.following
ref.mobile:startCombat(tes3.mobilePlayer)
ref.mobile.actionData.aiBehaviorState = 3
if config.message then
log("%s opened the door and found you! Distance = %d", ref.object.name,
tes3.player.position:distance(data.followPosition))
end
else
data.status = status.lostSight
if config.message then
log("%s opened the door and lost sight of you!", ref.object.name)
end
end
end
end
end
end
if fin then
chaseTimer:cancel()
chaseTimer = nil
if config.message then log("The chase is over") end
end
end
})
end
elseif not (e.cell.isInterior or e.previousCell.isInterior) then
for ref, data in pairs(hunters) do
if not ref.cell.isInterior and tes3.player.position:distance(ref.position) > 8000 then
tes3.positionCell {cell = data.startingCell, position = data.startingPosition, reference = ref}
if config.message then
log("%s returned to their place (EXTERIOR)", ref.object.name)
end
hunters[ref] = nil
end
end
else
for ref, data in pairs(hunters) do
if not attackCell[data.startingCell] then
tes3.positionCell {cell = data.startingCell, position = data.startingPosition, reference = ref}
if config.message then
log("%s returned to their place (TELEPORT)", ref.object.name)
end
hunters[ref] = nil
end
end
end
end
end
local function onCombatStopped(e) if e.actor == tes3.mobilePlayer then doCleanup() end end
local function onSave(e)
if table.size(hunters) > 0 and not tes3.worldController.inputController:isKeyDown(config.saveOverrideKey.keyCode) then
log("You cannot save the game when %s enemies hunt you!", table.size(hunters))
return false
end
end
local function onDeath(e) if hunters[e.reference] then hunters[e.reference] = nil end end
local function onLoaded(e) doCleanup() end
-- }}}
-- {{{ MCM
-- todo: put in own file
local function registerModConfig()
local template = mwse.mcm.createTemplate("DragonDoor!")
template:saveOnClose("DragonDoor!", config)
template:register()
local page = template:createPage()
page:createYesNoButton({
label = "Show messages",
variable = mwse.mcm.createTableVariable({id = "message", table = config})
})
page:createYesNoButton({
label = "Show messages when NPCs call for help",
variable = mwse.mcm.createTableVariable({id = "helpMessage", table = config})
})
page:createYesNoButton({
label = "NPCs will call for help in battle",
variable = mwse.mcm.createTableVariable({id = "callForHelp", table = config})
})
page:createSlider({
label = "The distance from which the enemies hear a call for help",
min = 1000,
max = 5000,
step = 10,
jump = 500,
variable = mwse.mcm.createTableVariable({id = "helpRespondDistance", table = config})
})
page:createKeyBinder({
allowCombinations = false,
variable = mwse.mcm:createTableVariable({id = "saveOverrideKey", table = config}),
label = "Emergency save button. Hold this button while saving to remove the ban on saving when pursuing."
})
end
-- }}}
-- {{{ init and event registering
local function onInitialized()
event.register("combatStarted", onCombatStarted)
event.register("cellChanged", onCellChanged)
event.register("activate", onActivate)
event.register("loaded", onLoaded)
event.register("combatStopped", onCombatStopped)
event.register("death", onDeath)
event.register("save", onSave)
end
event.register("initialized", onInitialized)
event.register("modConfigReady", registerModConfig)
-- }}}
-- vim: fdm:marker
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment