Last active
March 9, 2022 00:59
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- 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