initial commit

This commit is contained in:
Lilian Jónsdóttir 2020-07-25 21:37:32 -07:00
parent 2fc3aaf79b
commit c30f84e616
9 changed files with 628 additions and 0 deletions

88
.gitignore vendored Normal file
View file

@ -0,0 +1,88 @@
# Created by https://www.toptal.com/developers/gitignore/api/lua,visualstudiocode,windows
# Edit at https://www.toptal.com/developers/gitignore?templates=lua,visualstudiocode,windows
### Lua ###
# Compiled Lua sources
luac.out
# luarocks build files
*.src.rock
*.zip
*.tar.gz
# Object files
*.o
*.os
*.ko
*.obj
*.elf
# Precompiled Headers
*.gch
*.pch
# Libraries
*.lib
*.a
*.la
*.lo
*.def
*.exp
# Shared objects (inc. Windows DLLs)
*.dll
*.so
*.so.*
*.dylib
# Executables
*.exe
*.out
*.app
*.i*86
*.x86_64
*.hex
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
### Windows ###
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
[Bb]uild
# End of https://www.toptal.com/developers/gitignore/api/lua,visualstudiocode,windows

View file

@ -0,0 +1,68 @@
local common = require("celediel.MoreAttentiveGuards.common")
local config = require("celediel.MoreAttentiveGuards.config").getConfig()
local this = {}
-- {{{ helper functions
local function log(...) if config.debug then common.log(...) end end
local function alertGuards(aggressor, cell)
-- a wanted player gets no help
if tes3.mobilePlayer.bounty > 0 then
log("Player is wanted, ignoring combat.")
return
end
log("Checking for guards in cell %s to bring justice to %s", cell.name or cell.id, aggressor.object.name)
local playerPos = tes3.mobilePlayer.position
for npc in cell:iterateReferences(tes3.objectType.npc) do
local distance = playerPos:distance(npc.position)
if npc.object.isGuard and distance <= config.combatDistance then
log("Alerting %s, %s units away, to the combat!", npc.object.name, distance)
if config.combatDialogue then
local response = common.guardDialogue(npc, table.choice(common.dialogues[config.language].join_combat),
aggressor)
log(response)
end
npc.mobile:startCombat(aggressor)
end
end
end
-- }}}
-- {{{ returned event functions
this.onCombatStart = function(e)
if not config.combatEnable then return end
-- if player initiates combat or combat is not against player, do nothing
if e.actor == tes3.mobilePlayer or e.target ~= tes3.mobilePlayer then return end
-- inCombat is true after player has taken combat actions
-- or after combat has gone on awhile, but hopefully the guards will already be attacking by then
-- should be fine in cities, but will prevent players from provoking NPCs
-- in the wilderness and leading them into town
if tes3.mobilePlayer.inCombat then
log("Player is in combat, not sure who started it, so not helping.")
return
end
local cell = tes3.getPlayerCell()
if cell.isInterior and not cell.behavesAsExterior then
alertGuards(e.actor, cell)
else
for _, extCell in pairs(tes3.getActiveCells()) do alertGuards(e.actor, extCell) end
end
end
-- }}}
return this
-- vim:fdm=marker

View file

@ -0,0 +1,58 @@
local this = {}
-- {{{ mod info and such
this.modName = "More Attentive Guards" -- or something
this.author = "Celediel"
this.version = "0.0.1"
this.modInfo = [[Guards with some actual spacial awareness!
Guards who catch you sneaking will follow you for a bit of
time, and will also come to the player's rescue if attacked unprovoked.]]
this.dialogues = require("celediel.MoreAttentiveGuards.dialogues")
this.configString = string.gsub(this.modName, "%s+", "")
-- }}}
-- {{{ NPC stuff or whatever
this.basicIdles = {60, 20, 20, 20, 0, 0, 0, 0}
-- }}}
-- {{{ functions
this.log = function(...) mwse.log("[%s] %s", this.modName, string.format(...)) end
-- https://en.uesp.net/wiki/Tes3Mod:AIWander told me some things about idles
this.generateIdles = function()
local idles = {}
-- idles[1] = 0 -- ? idle 1 is not used?
for i = 1, 4 do idles[i] = math.random(0, 60) end
idles[5] = 0 -- ? Idle6: Rubbing hands together and showing wares
for i = 6, 8 do idles[i] = math.random(0, 60) end
return idles
end
this.guardDialogue = function(npc, str, target)
-- target of the dialogue, either an NPC/Creature, or the player's class or race
-- this is what %s is replaced with in the dialogue string; npc/creature for combat, player for sneak
local targetOrPlayer
if target == tes3.mobilePlayer then
targetOrPlayer = math.random() >= 0.5 and target.object.class.name or target.object.race.name
else
targetOrPlayer = target.object.name
end
local name = npc.object.name
local message = string.format(str, targetOrPlayer)
local output = string.format("%s: %s", name, message)
tes3.messageBox(output)
return output
end
-- }}}
return this
-- vim:fdm=marker

View file

@ -0,0 +1,26 @@
local common = require("celediel.MoreAttentiveGuards.common")
local this = {}
local currentConfig
this.default = {
-- common
language = "english",
debug = false,
-- sneak
sneakEnable = true,
sneakDialogue = true,
sneakDialogueTimer = 5,
sneakDialogueChance = 67,
-- combat
combatEnable = true,
combatDistance = 850,
combatDialogue = true
}
function this.getConfig()
currentConfig = currentConfig or mwse.loadConfig(common.configString, this.default)
return currentConfig
end
return this

View file

@ -0,0 +1,35 @@
return {
["english"] = {
-- guards might say this every so often to players who are sneaking
-- %s is replaced with race or class
["sneaking"] = {
"Why are you sneaking around like a s'wit?",
"What are you up to, %s...?",
"What are you up to...?",
"I'm watching you, %s...",
"I'm watching you...",
"I've got my eye on you, %s...",
"I've got my eye on you..."
},
-- guards say this when players stop sneaking while being followed
-- %s is replaced with race or class
["stop_sneaking"] = {
"That's what I thought.",
"That's better, %s.",
},
-- guards say this when they're satisfied that the player is not doing anything illegal
-- %s is replaced with race or class
["stop_following"] = {
"I don't have time for this...",
"Sneaking around for no reason, are we...?",
"Sneaking around for no reason, are we...? Alright then, %s.",
},
-- guards say this when coming to player's rescue when they're attacked unprovoked
-- %s is replaced with the name of the npc or creature attacking the player
["join_combat"] = {
"Not today %s!",
"You n'wah!",
"Stop right there, criminal scum!"
}
}
}

View file

@ -0,0 +1,15 @@
local sneak = require("celediel.MoreAttentiveGuards.sneak")
local combat = require("celediel.MoreAttentiveGuards.combat")
local common = require("celediel.MoreAttentiveGuards.common")
local eventPattern = "on(%u)"
local function onInitialized()
-- in order for this to work, all functions in returned table must be named onEventName
for name, func in pairs(sneak) do event.register(name:gsub(eventPattern, string.lower), func) end
for name, func in pairs(combat) do event.register(name:gsub(eventPattern, string.lower), func) end
common.log("%s initialized", common.modName)
end
event.register("initialized", onInitialized)
event.register("modConfigReady", function() mwse.mcm.register(require("celediel.MoreAttentiveGuards.mcm")) end)

View file

@ -0,0 +1,109 @@
local config = require("celediel.MoreAttentiveGuards.config").getConfig()
local common = require("celediel.MoreAttentiveGuards.common")
local function createTableVar(id) return mwse.mcm.createTableVariable({id = id, table = config}) end
local function createLanguageOptions()
local options = {}
-- I guess I don't know how ipairs works
local i = 1
for name, _ in pairs(common.dialogues) do
options[i] = {label = name:gsub("^%l", string.upper), value = name}
i = i + 1 -- wtf lua
end
return options
end
local template = mwse.mcm.createTemplate(common.modName)
template:saveOnClose(common.configString, config)
local page = template:createSideBarPage({
label = "Sidebar page",
description = string.format("%s v%s by %s\n\n%s\n\n", common.modName, common.version, common.author, common.modInfo)
})
local mainCategory = page:createCategory(common.modName)
-- {{{ general settings
local generalCategory = mainCategory:createCategory("Common settings")
generalCategory:createDropdown({label = "Language", options = createLanguageOptions(), variable = createTableVar("language")})
generalCategory:createYesNoButton({
label = "Debug mode",
description = "Print debug messages to the log.",
variable = createTableVar("debug")
})
-- }}}
-- {{{ sneak settings
local sneakCategory = mainCategory:createCategory("Sneak Settings")
sneakCategory:createYesNoButton({
label = "Enable sneak module",
description = "Guards who catch you sneaking will follow you for a bit of time.",
variable = createTableVar("sneakEnable")
})
sneakCategory:createYesNoButton({
label = "Sneak dialogue",
description = "Guards sometimes say things to you when you sneak.",
variable = createTableVar("sneakDialogue")
})
sneakCategory:createSlider({
label = "Sneak dialogue chance",
description = "Percent chance a guard will say something each time the dialogue timer fires.",
min = 0,
max = 100,
step = 1,
jump = 5,
variable = createTableVar("sneakDialogueChance")
})
sneakCategory:createSlider({
label = "Sneak dialogue timer",
description = "Roll for dialogue every x seconds while following.",
min = 0,
max = 60,
step = 1,
jump = 5,
variable = createTableVar("sneakDialogueTimer")
})
-- }}}
-- {{{ combat settings
local combatCategory = mainCategory:createCategory("Combat Settings")
combatCategory:createYesNoButton({
label = "Enable combat module",
description = "Guards will come to the rescue of a player who is attacked unprovoked.",
variable = createTableVar("combatEnable")
})
combatCategory:createSlider({
label = "Guard alert range",
description = "How far away guards are alerted to combat against the player",
min = 1,
max = 20000,
step = 10,
jump = 50,
variable = createTableVar("combatDistance")
})
combatCategory:createYesNoButton({
label = "Enable combat dialogue",
description = "Guards have things to say when they come to the rescue of a player who is attacked unprovoked.",
variable = createTableVar("combatDialogue")
})
-- }}}
return template
-- vim:fdm=marker

View file

@ -0,0 +1,164 @@
local common = require("celediel.MoreAttentiveGuards.common")
local config = require("celediel.MoreAttentiveGuards.config").getConfig()
local this = {}
-- {{{ variables and such
-- only one guard following at a time makes sense
-- other guards are like "oh they've got this, I don't need to help"
local follower
local followTimer
local dialogueTimer
local isFollowing = false
local ogPosition
-- }}}
-- {{{ helper functions
local function log(...) if config.debug then common.log(...) end end
local function calculateFollowTime()
-- Modified formula from RubberMan's "Inquisitive Guards"
-- https://www.nexusmods.com/morrowind/mods/46538
local sneak = tes3.mobilePlayer.sneak.value and tes3.mobilePlayer.sneak.value or tes3.mobilePlayer.sneak.base
local max = tes3.hasCodePatchFeature(110) and (sneak <= 100 and 101 or 0) or 101
local value = (max - sneak) / 3
-- round to nearest integer
return math.fmod(value, 1) >= 0.5 and math.ceil(value) or math.floor(value)
end
-- }}}
-- {{{ timer functions
local function startDialogue()
if not follower then return end
local dialogue = table.choice(common.dialogues[config.language].sneaking)
local roll = math.random(0, 100)
log("Dialogue roll = %s > %s", config.sneakDialogueChance, roll)
if config.sneakDialogueChance > roll then
local response = common.guardDialogue(follower, dialogue, tes3.mobilePlayer)
log(response)
end
end
-- * NPC travels back to where they were before following player, then wanders
-- * tries to guess how long it'll take NPC to get back to their original position
local function stopFollowing(onTimer)
if not follower or not isFollowing then return end
isFollowing = false
local function startWander()
log("%s has probably reached their original destination, resuming wander...", follower.object.name)
tes3.setAIWander({reference = follower, range = 2000, reset = false, idles = common.generateIdles()})
follower = nil
end
local function startTravel()
-- I couldn't think of a better way to "know" when they've reach their destination
-- so I set a timer based on the distance from the original position. It's okay-ish.
local distance = ogPosition:distance(follower.position)
local duration = math.ceil(distance / 95)
-- duration of 0 is bad for timers, so
duration = duration > 0 and duration or 1
log("%s has decided that %s isn't doing anything suspicious, heading back to %s... " ..
"(which is %s distance units away... it'll probably take %s seconds to get there)",
follower.object.name, tes3.player.object.name, ogPosition, distance, duration)
-- send a dialogue to let player know guard doesn't care any more
if onTimer then
local response = common.guardDialogue(follower,
table.choice(common.dialogues[config.language].stop_following),
tes3.mobilePlayer)
log(response)
end
if dialogueTimer and dialogueTimer.state == timer.active then dialogueTimer:cancel() end
tes3.setAITravel({reference = follower, destination = ogPosition})
ogPosition = nil
timer.start({duration = duration, iterations = 1, callback = startWander})
end
timer.delayOneFrame(startTravel)
end
local function startFollowing()
if not follower or isFollowing then return end
local function startFollow()
local followTime = calculateFollowTime()
log("%s starting to follow %s for %s time units", follower.object.name, tes3.player.object.name, followTime)
tes3.setAIFollow({reference = follower, target = tes3.mobilePlayer})
followTimer = timer.start({duration = followTime, callback = stopFollowing})
if config.sneakDialogue then
startDialogue()
dialogueTimer = timer.start({
duration = config.sneakDialogueTimer,
iterations = -1,
callback = startDialogue
})
end
isFollowing = true
end
ogPosition = follower.position:copy()
timer.delayOneFrame(startFollow)
end
local function abortFollow()
local response = common.guardDialogue(follower, table.choice(common.dialogues[config.language].stop_sneaking),
tes3.mobilePlayer)
log(response)
stopFollowing(false)
end
-- }}}
-- {{{ returned event functions
this.onDetectSneak = function(e)
if not config.sneakEnable then return end
if e.target ~= tes3.mobilePlayer or not tes3.mobilePlayer.isSneaking or not e.detector.object.isGuard then return end
if not isFollowing then
log("%s is checking for %s %ssuccessfully", e.detector.object and e.detector.object.name or "no one",
e.target.object and e.target.object.name or "no one", e.isDetected and "" or "un")
end
if e.isDetected and not follower and not isFollowing then
follower = e.detector
-- follow for a time
startFollowing()
-- else -- uncomment this for extreme debug messages
-- log("Not following because detection = %s or follower = %s or isFollowing = %s", e.isDetected, follower,
-- isFollowing)
end
end
this.onCalcMoveSpeed = function(e)
if e.mobile ~= tes3.mobilePlayer or tes3.mobilePlayer.isSneaking then return end
if follower and followTimer and followTimer.state == timer.active then
log("Player not sneaking, aborting follow")
followTimer:cancel()
abortFollow()
end
end
-- }}}
return this
-- vim: fdm=marker

65
README.md Normal file
View file

@ -0,0 +1,65 @@
# More Attentive Guards for Morrowind #
## About ##
An MWSE-Lua mod that replicates the features of
[Inquisitive Guards](https://www.nexusmods.com/morrowind/mods/46538) by RubberMan
and
[Protective Guards](http://download.fliggerty.com/download-110-20) by Fliggerty
* Guards that catch the player sneaking about in town will follow them for a length of time dependent on their sneak skill.
* Guards will come to the aid of an unwanted player who is attacked unprovoked
* MCM Menu for configuration of many options
### Dialogue ###
* Guards will say things:
* While the player is sneaking, on a chance based timer
* When the player stops sneaking while being followed
* When the guard gets bored of following the player
* When coming to the aid of an unwanted player who is attacked unprovoked
Open dialogues.lua to edit the dialogues. I'm no writer so they're probably bad, but the framework is there for the dialogues to be easily edited or translated.
To add a new language add something like this to dialogues.lua:
```
["your language"] = {
-- guards might say this every so often to players who are sneaking
-- %s is replaced with race or class
["sneaking"] = {
"dialogue goes here",
},
-- guards say this when players stop sneaking while being followed
-- %s is replaced with race or class
["stop_sneaking"] = {
"dialogue goes here",
},
-- guards say this when they're satisfied that the player is not doing anything illegal
-- %s is replaced with race or class
["stop_following"] = {
"dialogue goes here",
},
-- guards say this when coming to player's rescue when they're attacked unprovoked
-- %s is replaced with the name of the npc or creature attacking the player
["join_combat"] = {
"dialogue goes here",
}
}
```
The MCM dropdown will automatically be populated with the configured language, and once selected, those dialogues will be used.
## Requirements ##
MWSE 2.1 nightly @ [github](https://github.com/MWSE/MWSE)
## Credits ##
* MWSE Team for MWSE with Lua support
* Lua is way different than MWScript, but I did glance at the scripts from
both Inquisitive Guards and Protective Guards for inspiration
## License ##
MIT License. See LICENSE file.