prelimary voice support

still need to fill voice.lua with more voices
This commit is contained in:
Lilian Jónsdóttir 2021-08-23 18:36:33 -07:00
parent a98c428fe8
commit dd80dc3d1c
8 changed files with 433 additions and 62 deletions

View file

@ -9,9 +9,7 @@ local function log(...) if config.debug then common.log(...) end end
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
if actor.object.id == friend.object.id or actor.object.baseObject.id == friend.object.baseObject.id then return true end
end
return false
end
@ -80,11 +78,13 @@ local function alertGuards(aggressor, cell)
if not npc.disabled and npc.object.isGuard and npc.mobile 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.object.name,
table.choice(common.dialogues[config.language].join_combat),
if config.combatDialogue == common.dialogueMode.text then
local response = common.playGuardText(npc.object.name, table.choice(common.dialogues.text[config.language].join_combat),
aggressor)
log(response)
elseif config.combatDialogue == common.dialogueMode.voice then
local response = common.playGuardVoice(npc.mobile, "join_combat")
log("Playing sound file: %s", response)
end
npc.mobile:startCombat(aggressor)

View file

@ -5,17 +5,20 @@ local this = {}
this.modName = "More Attentive Guards" -- or something
this.author = "Celediel"
this.version = "1.1.6"
this.modInfo = "Guards with some actual spatial awareness!\n\n" ..
"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.modInfo = "Guards with some actual spatial awareness!\n\nGuards 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 = {
text = require("celediel.MoreAttentiveGuards.dialogue.text"),
voice = require("celediel.MoreAttentiveGuards.dialogue.voice")
}
this.dialogueMode = { none = 0, text = 1, voice = 2 }
this.configString = string.gsub(this.modName, "%s+", "")
-- }}}
-- {{{ NPC stuff or whatever
this.basicIdles = {60, 20, 20, 20, 0, 0, 0, 0}
this.basicIdles = { 60, 20, 20, 20, 0, 0, 0, 0 }
-- }}}
@ -38,9 +41,9 @@ this.generateWanderRange = function(cell)
return (cell.isInterior and not cell.behavesAsExterior) and 200 or 2000
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
this.playGuardText = 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
@ -55,6 +58,41 @@ this.guardDialogue = function(npc, str, target)
return output
end
-- Plays a random sound of specified type and returns the path of the sound file that was played
this.playGuardVoice = function(mobile, type)
local distanceCap = 2500 -- sounds further away than this are too quiet to be heard
local ref = mobile.reference
local sex = ref.baseObject.female and "f" or "m"
local race = ref.baseObject.race.id:lower()
local directory, soundPath, sound
this.log("before: ref:%s sex:%s race:%s soundPath:%s type:%s", ref.id, sex, race, soundPath, type)
-- make sure the race/sex/type combo exists in the voice data
if this.dialogues.voice[race] and this.dialogues.voice[race][sex] and this.dialogues.voice[race][sex][type] then
directory = string.format("vo\\%s\\%s\\", this.dialogues.voice[race].dir, sex)
sound = table.choice(this.dialogues.voice[race][sex][type])
-- sound will be nil if the race/sex/type combo is an empty table
if sound then soundPath = directory .. sound.file .. ".mp3" end
end
this.log("after: ref:%s sex:%s race:%s soundPath:%s type:%s", ref.id, sex, race, soundPath, type)
local distanceFromPlayer = math.clamp(mobile.position:distance(tes3.mobilePlayer.position), 0, distanceCap) or 0
local volume = 1 - (distanceFromPlayer / distanceCap)
-- LuaFormatter off
if soundPath then
tes3.say({
soundPath = soundPath,
subtitle = sound.subtitle,
volume = volume,
reference = mobile
})
end
-- LuaFormatter on
return soundPath
end
-- }}}
return this

View file

@ -9,13 +9,13 @@ this.default = {
debug = false,
-- sneak
sneakEnable = true,
sneakDialogue = true,
sneakDialogue = common.dialogueMode.voice,
sneakDialogueTimer = 5,
sneakDialogueChance = 67,
-- combat
combatEnable = true,
combatDistance = 850,
combatDialogue = true,
combatDialogue = common.dialogueMode.voice,
ignored = {
["mer_tgw_guar"] = true,
["mer_tgw_guar_w"] = true

View file

@ -0,0 +1,318 @@
--[[
{file = "filename without MP3", subtitle = "what it says"},
]]
-- LuaFormatter off
local voices = {
argonian = {
f = {
["sneaking"] = {
{file = "Hlo_AF000a", subtitle = "What?"},
{file = "Idl_AF007", subtitle = "What was that?"},
{file = "Idl_AF001", subtitle = "Sniff."},
},
["stop_sneaking"] = {
{file = "Srv_AF003", subtitle = "You should leave."},
{file = "Srv_AF012", subtitle = "Leave! Before I eat it!"},
{file = "Srv_AF010", subtitle = "It should go away and die!"},
{file = "Hlo_AF000c", subtitle = "Humph."},
{file = "Hlo_AF000b", subtitle = "Humph."},
{file = "Thf_AF003", subtitle = "Hiss."},
},
["stop_following"] = {
{file = "Hlo_AF019", subtitle = "You hardly seem worth the trouble, criminal."},
{file = "Hlo_AF000b", subtitle = "Humph."},
{file = "Hlo_AF000c", subtitle = "Humph."},
{file = "Hlo_AF000d", subtitle = "I won't waste my time on the likes of you."},
{file = "Hlo_AF000e", subtitle = "Get out of here!"},
{file = "Thf_AF003", subtitle = "Hiss."},
},
["join_combat"] = {
{file = "Hlo_AF017", subtitle = "Your life is mine!"},
{file = "Hlo_AF014", subtitle = "Kill it!"},
{file = "Hlo_AF014", subtitle = "Rip it apart!"},
{file = "Hlo_AF012", subtitle = "Bleed!"},
{file = "Atk_AF010", subtitle = "Hahahaha."},
}
},
m = {
["sneaking"] = {
{file = "Flw_AM001", subtitle = "Where are you going?"},
{file = "Thf_AM005", subtitle = "I see you!"},
{file = "Hlo_AM106", subtitle = "You make a name for yourself, criminal."},
{file = "Hlo_AM107", subtitle = "Your crimes are known to us."},
{file = "Hlo_AM056", subtitle = "Sniff. This scent is new."},
{file = "Hlo_AM040", subtitle = "Is there nothing for you to do?"},
{file = "Hlo_AM027", subtitle = "Must you make a pest of yourself?"},
},
["stop_sneaking"] = {
{file = "Srv_AM012", subtitle = "Leave! Before I eat it!"},
{file = "Srv_AM009", subtitle = "It should go away and die!"},
{file = "Hlo_AM046", subtitle = "Crime doesn't suit you, friend."},
{file = "Hlo_AM022", subtitle = "Be gone!"},
},
["stop_following"] = {
{file = "Hlo_AM019", subtitle = "You hardly seem worth the trouble, criminal."},
{file = "Hlo_AM018", subtitle = "Get away, criminal."},
{file = "Hlo_AM022", subtitle = "Be gone!"},
},
["join_combat"] = {
{file = "bAtk_AM002", subtitle = "Your head will be my new trophy!"},
{file = "bAtk_AM005", subtitle = "Your cursed bloodline ends here!"},
{file = "Atk_AM010", subtitle = "Bash!"},
{file = "Atk_AM011", subtitle = "Kill!"},
{file = "Atk_AM012", subtitle = "It will die!"},
{file = "Atk_AM013", subtitle = "Suffer!"},
{file = "Atk_AM014", subtitle = "Die!"},
}
},
dir = "a"
},
breton = {
f = {
["sneaking"] = {
},
["stop_sneaking"] = {
},
["stop_following"] = {
},
["join_combat"] = {
}
},
m = {
["sneaking"] = {
},
["stop_sneaking"] = {
},
["stop_following"] = {
},
["join_combat"] = {
}
},
dir = "b"
},
["dark elf"] = {
f = {
["sneaking"] = {
},
["stop_sneaking"] = {
},
["stop_following"] = {
},
["join_combat"] = {
}
},
m = {
["sneaking"] = {
{file = "Flw_DM001", subtitle = "Where are you going?"},
{file = "Idl_DM007", subtitle = "What was that?"},
{file = "Hlo_DM165", subtitle = "There are better ways than theft to earn a coin, outlander."},
},
["stop_sneaking"] = {
{file = "Hlo_DM021", subtitle = "Bothersome creature."},
{file = "Hlo_DM001", subtitle = "Go away."},
{file = "Hlo_DM000b", subtitle ="Humph."},
{file = "Hlo_DM000c", subtitle = "Hmmph."},
},
["stop_following"] = {
{file = "Hlo_DM111", subtitle = "Move along, outlander."},
{file = "Hlo_DM035", subtitle = "Keep moving, scum."},
{file = "Hlo_DM021", subtitle = "Bothersome creature."},
{file = "Hlo_DM000b", subtitle ="Humph."},
{file = "Hlo_DM000c", subtitle = "Hmmph."},
},
["join_combat"] = {
}
},
dir = "d"
},
["high elf"] = {
f = {
["sneaking"] = {
},
["stop_sneaking"] = {
},
["stop_following"] = {
},
["join_combat"] = {
}
},
m = {
["sneaking"] = {
},
["stop_sneaking"] = {
},
["stop_following"] = {
},
["join_combat"] = {
}
},
dir = "h"
},
imperial = {
f = {
["sneaking"] = {
},
["stop_sneaking"] = {
},
["stop_following"] = {
},
["join_combat"] = {
}
},
m = {
["sneaking"] = {
{file = "Hlo_IM007", subtitle = "Are you here to start trouble, or are you just stupid?"},
{file = "Flw_IM001", subtitle = "Where are you going?"},
{file = "Hlo_IM057", subtitle = "Stay out of trouble and you won't get hurt."},
},
["stop_sneaking"] = {
{file = "bIdl_IM028", subtitle = "Just as well..."},
{file = "Hlo_IM000e", subtitle = "Get out of here."},
{file = "Srv_IM027", subtitle = "You are a nuisance to me. Please leave."}
},
["stop_following"] = {
{file = "Hlo_IM000e", subtitle = "Get out of here."},
{file = "Srv_IM027", subtitle = "You are a nuisance to me. Please leave."},
{file = "Hlo_IM006", subtitle = "What a pathetic excuse for a criminal!"}
},
["join_combat"] = {
{file = "Atk_IM009", subtitle = "Die, scoundrel!"},
{file = "CrAtk_IM005", subtitle = "Die!"},
{file = "Atk_IM010", subtitle = "You're hardly a match for me!"},
{file = "Atk_IM007", subtitle = "Let's see what you're made of!"},
{file = "Hlo_IM004", subtitle = "Since you're already on death's door, may I open it for you?"},
{file = "Hlo_IM018", subtitle = "You're a disgrace to the Empire."},
{file = "Hlo_IM000d", subtitle = "You're about to find more trouble than you can possibly imagine."}
}
},
dir = "i"
},
khajiit = {
f = {
["sneaking"] = {
},
["stop_sneaking"] = {
},
["stop_following"] = {
},
["join_combat"] = {
}
},
m = {
["sneaking"] = {
},
["stop_sneaking"] = {
},
["stop_following"] = {
},
["join_combat"] = {
}
},
dir = "k"
},
nord = {
f = {
["sneaking"] = {
},
["stop_sneaking"] = {
},
["stop_following"] = {
},
["join_combat"] = {
}
},
m = {
["sneaking"] = {
},
["stop_sneaking"] = {
},
["stop_following"] = {
},
["join_combat"] = {
}
},
dir = "n"
},
orc = {
f = {
["sneaking"] = {
},
["stop_sneaking"] = {
},
["stop_following"] = {
},
["join_combat"] = {
}
},
m = {
["sneaking"] = {
},
["stop_sneaking"] = {
},
["stop_following"] = {
},
["join_combat"] = {
}
},
dir = "o"
},
redguard = {
f = {
["sneaking"] = {
},
["stop_sneaking"] = {
},
["stop_following"] = {
},
["join_combat"] = {
}
},
m = {
["sneaking"] = {
},
["stop_sneaking"] = {
},
["stop_following"] = {
},
["join_combat"] = {
}
},
dir = "r"
},
["wood elf"] = {
f = {
["sneaking"] = {
},
["stop_sneaking"] = {
},
["stop_following"] = {
},
["join_combat"] = {
}
},
m = {
["sneaking"] = {
},
["stop_sneaking"] = {
},
["stop_following"] = {
},
["join_combat"] = {
}
},
dir = "w"
}
}
-- LuaFormatter on
-- TR voices
voices["t_els_cathay"] = voices.khajiit
voices["t_els_cathay-raht"] = voices.khajiit
voices["t_els_ohmes"] = voices.khajiit
voices["t_els_ohmes-raht"] = voices.khajiit
voices["t_els_suthay"] = voices.khajiit
voices["t_sky_reachman"] = voices.breton -- todo: combine Nord + Breton
voices["t_pya_seaelf"] = voices["high elf"] -- todo: something better
return voices

View file

@ -2,7 +2,7 @@ local sneak = require("celediel.MoreAttentiveGuards.sneak")
local combat = require("celediel.MoreAttentiveGuards.combat")
local common = require("celediel.MoreAttentiveGuards.common")
-- in order for this to work, functions in returned table must follow pattern: onEventName
-- in order for this to work, functions in returned table must follow naming pattern: onEventName
local function registerFunctionEvents(t)
for name, func in pairs(t) do
if type(func) == "function" then event.register(name:gsub("on(%u)", string.lower), func) end

View file

@ -3,15 +3,12 @@ local common = require("celediel.MoreAttentiveGuards.common")
-- {{{ helper functions
local function createTableVar(id) return mwse.mcm.createTableVariable({id = id, table = config}) end
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
for name, _ in pairs(common.dialogues.text) do
options[#options + 1] = { label = name:gsub("^%l", string.upper), value = name }
end
return options
end
@ -37,7 +34,8 @@ local mainCategory = page:createCategory(common.modName)
local generalCategory = mainCategory:createCategory("Common settings")
generalCategory:createDropdown({
label = "Language",
label = "Text Language",
description = "If dialogue mode is set to text, this language will be used.",
options = createLanguageOptions(),
variable = createTableVar("language")
})
@ -60,10 +58,15 @@ sneakCategory:createYesNoButton({
variable = createTableVar("sneakEnable")
})
sneakCategory:createYesNoButton({
sneakCategory:createDropdown({
label = "Sneak dialogue",
description = "Guards sometimes say things to you when you sneak.",
variable = createTableVar("sneakDialogue")
variable = createTableVar("sneakDialogue"),
options = {
{ label = "Text", value = common.dialogueMode.text },
{ label = "Voice", value = common.dialogueMode.voice },
{ label = "None", value = common.dialogueMode.none }
}
})
sneakCategory:createSlider({
@ -108,10 +111,15 @@ combatCategory:createSlider({
variable = createTableVar("combatDistance")
})
combatCategory:createYesNoButton({
label = "Enable combat dialogue",
combatCategory:createDropdown({
label = "Combat dialogue",
description = "Guards have things to say when they come to the rescue of a player who is attacked unprovoked.",
variable = createTableVar("combatDialogue")
variable = createTableVar("combatDialogue"),
options = {
{ label = "Text", value = common.dialogueMode.text },
{ label = "Voice", value = common.dialogueMode.voice },
{ label = "None", value = common.dialogueMode.none }
}
})
-- }}}
@ -121,9 +129,9 @@ template:createExclusionsPage({
description = "Guards will not respond to these NPCs or creatures attacking the player.",
showAllBlocked = false,
filters = {
{label = "Plugins", type = "Plugin"},
{label = "NPCs", type = "Object", objectType = tes3.objectType.npc},
{label = "Creatures", type = "Object", objectType = tes3.objectType.creature}
{ label = "Plugins", type = "Plugin" },
{ label = "NPCs", type = "Object", objectType = tes3.objectType.npc },
{ label = "Creatures", type = "Object", objectType = tes3.objectType.creature }
},
variable = createTableVar("ignored")
})

View file

@ -50,16 +50,21 @@ end
-- {{{ timer functions
local function startDialogue()
local function startDialogue(chance)
if not follower then return end
local dialogue = table.choice(common.dialogues[config.language].sneaking)
local roll = math.random(0, 100)
local roll = type(chance) == "number" and chance or math.random(0, 100)
log("Dialogue roll = %s > %s", config.sneakDialogueChance, roll)
log("Dialogue roll = %s > %s == %s", config.sneakDialogueChance, roll, config.sneakDialogueChance > roll)
if config.sneakDialogueChance > roll then
local response = common.guardDialogue(follower.object.name, dialogue, tes3.mobilePlayer)
log(response)
if config.sneakDialogue == common.dialogueMode.text then
local response = common.playGuardText(follower.object.name, table.choice(common.dialogues.text[config.language].sneaking),
tes3.mobilePlayer)
log(response)
elseif config.sneakDialogue == common.dialogueMode.voice then
local response = common.playGuardVoice(follower, "sneaking")
log("Playing sound file: %s", response)
end
end
end
@ -73,9 +78,8 @@ local function stopFollowing(onTimer)
local wanderRange = common.generateWanderRange(tes3.getPlayerCell())
local idles = common.generateIdles()
log("%s has probably reached their original destination, resuming %s range wander...", follower.object.name,
wanderRange)
tes3.setAIWander({reference = follower, range = wanderRange, reset = true, idles = idles})
log("%s has probably reached their original destination, resuming %s range wander...", follower.object.name, wanderRange)
tes3.setAIWander({ reference = follower, range = wanderRange, reset = true, idles = idles })
follower = nil
interop.setGuardFollower(follower)
@ -91,23 +95,27 @@ local function stopFollowing(onTimer)
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)
"(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 and config.sneakDialogue then
local response = common.guardDialogue(follower.object.name,
table.choice(common.dialogues[config.language].stop_following),
tes3.mobilePlayer)
log(response)
if onTimer then
if config.sneakDialogue == common.dialogueMode.text then
local response = common.playGuardText(follower.object.name, table.choice(common.dialogues[config.language].stop_following),
tes3.mobilePlayer)
log(response)
elseif config.sneakDialogue == common.dialogueMode.voice then
local response = common.playGuardVoice(follower, "stop_following")
log("Playing sound file: %s", response)
end
end
if dialogueTimer and dialogueTimer.state == timer.active then dialogueTimer:cancel() end
tes3.setAITravel({reference = follower, destination = ogPosition})
tes3.setAITravel({ reference = follower, destination = ogPosition })
ogPosition = nil
timer.start({duration = duration, iterations = 1, callback = startWander})
timer.start({ duration = duration, iterations = 1, callback = startWander })
end
timer.delayOneFrame(startTravel)
@ -121,17 +129,13 @@ local function startFollowing()
if followTime <= 0 then return end
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})
tes3.setAIFollow({ reference = follower, target = tes3.mobilePlayer })
followTimer = timer.start({duration = followTime, callback = stopFollowing})
followTimer = timer.start({ duration = followTime, callback = stopFollowing })
if config.sneakDialogue then
startDialogue()
dialogueTimer = timer.start({
duration = config.sneakDialogueTimer,
iterations = -1,
callback = startDialogue
})
if config.sneakDialogue ~= common.dialogueMode.none then
startDialogue(0) -- always say something the first time
dialogueTimer = timer.start({ duration = config.sneakDialogueTimer, iterations = -1, callback = startDialogue })
end
isFollowing = true
@ -143,11 +147,14 @@ local function startFollowing()
end
local function abortFollow()
if config.sneakDialogue then
local response = common.guardDialogue(follower.object.name,
table.choice(common.dialogues[config.language].stop_sneaking),
-- send a dialogue to let player know guard doesn't care any more
if config.sneakDialogue == common.dialogueMode.text then
local response = common.playGuardText(follower.object.name, table.choice(common.dialogues.text[config.language].stop_sneaking),
tes3.mobilePlayer)
log(response)
elseif config.sneakDialogue == common.dialogueMode.voice then
local response = common.playGuardVoice(follower, "stop_sneaking")
log("Playing sound file: %s", response)
end
stopFollowing(false)
end