updated 2020 January for Werewolf Time Meter version 2

This article introduces some Papyrus scripting techniques and reviews my scripts for “Werewolf Time Meter” mod for Skyrim Special Edition. In a nutshell, the “Werewolf Time Meter” borrows the blue magicka bar as a countdown meter during beast form transformation. If you have limited experience writing scripts or programs, please see introduction and tutorials on Papyrus Reference.

SkyrimDTWWMeter1

resources

intro

Instead of focusing on learning Papyrus, I go over technique and the workings of the two scripts making up “Werewolf Time Meter.” See “Creation Kit Papyrus Reference” for brief examples and tutorials.

The mod includes three script files (actually four): a spell-effect script allows the player to enable and disable the meter and the other controls, a main controller to display the meter, and a player-alias script to handle events. In the plugin there exists a spell effect to disable (damage) magicka regeneration preventing the time meter bar growing until beast form ends. This means upon detecting the player’s character has become a beast, the spell effect must be added. Remove if exists when not in beast form.

The player-alias OnRaceChangeEvent alerts the main script to check if beast or not to start or stop updating the magicka bar. During transformation the main script polls infrequently to calculate time remaining and update the meter. Since checking the player’s condition is very fast, we could probably get away with more frequent updates but only needs to be fast enough to update bar. 3-7 seconds seems acceptable.

Since the player cannot normally use power spells in beast form, we could probably forget the case of enable/disable toggle during beast form. However, mods may exist allowing it so we should at least consider the possibility. We should also consider odd scenarios and try to make the script as robust as possible.

name convention for quicker mod recognition

A convention I use for naming objects in a mod is to prepend with the mod-name initials making it easier to tell what belongs to the mod. I use “DTWW” for properties. My script names also follow similar naming style.

  • DTWerewolfWatchToggle script: called by player action to enable/disable DTWereWolfWatch
  • DTWereWolfWatch script: handles updating and restoring the magicka bar
  • DTPlayerAliasScript: handles events, OnPlayerGameLoad, OnRaceChange, and OnHit

CK - Papyrus basics

The Creation Kit (CK) Papyrus compiler turns a source (psc-file) into a game-ready script file (pex). These are located in your \Data\scripts\ and \Data\scripts\source\ folders. If you’re concerned about private information keep in mind that the compiled pex-file includes your PC’s name and your login name. You may use Notepad++ with syntax-coloring and to launch the compiler. See my previous post, “Setup for Script Work with Creation Kit and Notepad++” on how to get started. It’s also possible to decompile a pex-file into a source using “Champollion” by li1lnx.

  • The semi-colon (;) character at the start marks a comment line.
  • Boolean comparison denoted similar to other languages: == (is equal to), != (not equal to), <, >, <=, >=
  • A GlobalVariable is a Float type, but may be cast to Bool or Int.
  • Use Property to connect objects to the plugin or other scripts.
  • An Event occurs on a game condition which may be quest initialization, update at a specified time, when magic effect happens, or button activation.
  • You may use Debug.Notification to help validate conditions during play testing. I comment these out when finished testing.

scripts

program / script writing basics

Writing clean, easy to read scripts helps reduce bugs and allows for re-use.

  • Try to break up large functions into smaller, bite-size morsels. Make reading easier.
  • Do you need to calculate the same thing more than once? Put that into a function to return the result.
  • A function should have a specific goal: display a message, add spell to player, reset our globals.

The following are examples of short, helpful functions found in my script. You may copy/paste/edit into your scripts to your heart’s desire.

find maximum magicka
; including buffs - unfortunately returns base magicka if current magicka = 0
float Function GetMaxMagickaActorValue(Actor starget)
float currentVal = starget.GetActorValue("Magicka")
if currentVal <= 0.0
return starget.GetBaseActorValue("Magicka")
endif
return ( currentVal / starget.GetActorValuePercentage("Magicka"))
EndFunction
find hour of day frome game-time
; game-time is a float based on number days passed
; find current time using Utility.GetCurrentGameTime()
; find werewolf-shiftback time from PlayerWerewolfShiftBackTime unless MTSE "lunar"
float Function GetHourFromGameTime(float gameTime)
gameTime -= Math.Floor(gameTime)
gameTime *= 24.0
return gameTime
endFunction

There’s another way to find hour by forcing the time to an integer (dayNum) and subtracting from time as shown below:

GetLunarTransformEndTime
; default assume MTSE by Brevi for Special Edition
float Function GetLunarTransformEndTime(float currentTime)
int dayNum = currentTime as Int
float endHour = 0.20833333 ;5am for MTSE
if DLC1WerewolfMaxPerks.GetValue() < 32
endHour = 0.250 ; 6am for MTE
endif
float fractionDay = currentTime - dayNum as Float
if fractionDay > 0.1667
dayNum += 1
endif
return dayNum as Float + endHour
endFunction

Below is how to adjust the magicka bar (up or down) with a new value for the actor. There’s no need to check maximum since game allows over-adding to fill.

UpdateMagickaMeterWithValue
function UpdateMagickaMeterWithValue(float newVal, Actor playActor)
float magickaCurrent = playActor.GetActorValue("Magicka")
if newVal < 0
newVal = 0
endif
float diffVal = newVal - magickaCurrent
if diffVal < 0
diffVal = diffVal * -1
playActor.DamageActorValue("Magicka", diffVal)
else
playActor.RestoreActorValue("Magicka", diffVal)
endif
endFunction

DTWerewolfWatchToggle

The toggle is called by the spell effect, DTWerewolfSpellEffect, thus it extends ActiveMagicEffect. The “watch” script extends a quest, and this script must access that other script to tell it to enable or disable. It’s done using the Register and UnRegister functions found in that script as noted below in the ToggleWWMeter() function. OnEffectStart event happens whenever the spell effect is activated. Even though here the spell is only attached to the player, it’s a good habit to check if the Actor is the player in case of mistakes or later we decide to expand the spell.

Call a function in another script by using the property reference such as, (DTWerewolfWatch_quest as DTWerewolfWatch).Register() where DTWerewolfWatch is the name of the quest in this cast. Note the parenthesis around the cast.

  • DTWerewolfWatch_quest: connects to script below

Below is the entire script.

DTWerewolfWatchToggle.psc
scriptName DTWerewolfWatchToggle extends ActiveMagicEffect
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Werewolf Time Meter toggle
; Author: Dracotorre
; Version: 2.0
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
GlobalVariable property DTWW_Enabled auto
Quest property DTWerewolfWatch_quest auto
Message property DTWW_DisableSoonMeterMessage auto ; not used / deprecated
Event OnEffectStart(Actor akTarget, Actor akCaster)
;Debug.Notification("DTWW - Toggle")
actor playerActorRef = Game.GetPlayer()
if (akCaster == playerActorRef)
ToggleWWMeter()
endif
EndEvent
Function ToggleWWMeter()
int toggleVal = DTWW_Enabled.GetValueInt()
if (toggleVal >= 2)
DTWW_Enabled.SetValueInt(0)
(DTWerewolfWatch_quest as DTWerewolfWatch).UnRegister()
elseIf (toggleVal == 1)
DTWW_Enabled.SetValueInt(2)
(DTWerewolfWatch_quest as DTWerewolfWatch).Register()
else
DTWW_Enabled.SetValueInt(1)
(DTWerewolfWatch_quest as DTWerewolfWatch).Register()
endif
endFunction

DTWWPlayerAliasScript

This script catches events to tell DTWerewolfWatch script if something should happen. Let’s look at the event for transforming to and from beast.

partial DTWWPlayerAliasScript.psc OnRaceSwitchComplete
; from comment: It is possible for this event to be hit when loading a save with a player character of a different race.
Event OnRaceSwitchComplete()
Actor playerRef = self.GetActorReference()
if (DTWW_Initialized.GetValueInt() <= 0)
(DTWerewolfWatchP as DTWerewolfWatch).InitializeMeterWatch(playerRef)
endIf
if ((DTWerewolfWatchP as DTWerewolfWatch).PlayerIsWerewolfBeast(playerRef))
; mark as creature to check later to avoid issue with other race switches
IsCreature = true
else
; check if was a creature and setting to unequip
if (IsCreature && (DTWerewolfWatchP as DTWerewolfWatch).DTWW_Enabled.GetValueInt() >= 2)
playerRef.UnequipAll()
endIf
IsCreature = false
endIf
; wait a bit before updating meter
Utility.Wait(5.25)
(DTWerewolfWatchP as DTWerewolfWatch).ProcessCheckMeter(playerRef)
endEvent

The OnRaceSwitchComplete happens when changing to/from werewolf beast or vampire lord, but it also may happen when player loads a save for another character or during transformed beast. Like the comment warns, we should keep track if creature or not if we plan to do something. Above shows optional UnequipAll to undress the character at end of transformation, so must keep track. Otherwise the function simply tells DTWerewolfWatch.ProcessCheckMeter that a race change happened to let it decide if needs to start or end the meter display.

DTWerewolfWatch

The complete source file is 300+ lines long. Please refer to the source file available on GitHub.

If enabled, this script polls every X seconds using OnUpdate event. If the player disables, check if anything needs to be restored and cancel the next OnUpdate.

Near the top, I list all the properties together of objects found in the plugin files. Remember all the properties starting with “DTWW_” have been added in the DTWereWolfTimeMeter.esp plugin, and the others are part of the default game. Save key information to global variables to be used later. Here it’s good to know what time the player shifted to beast form (DTWW_PlayerShiftedToWereWolfTime), last known end-transformation time (DTWW_PlayerLastKnownShiftBackTime), and of course, it this darn thing is even running (DTWW_Enabled). Message objects edited in the CK notify the player of what’s happening.

If you’d like to see how these properties appear in Creation Kit, open my ESP-file, DTWerewolfTimeMeter.esp, and filter for “DTW” objects. Also see the quest, DTWW_WerewolfWatch, under the “scripts” tab.

  • DTWW_Enabled: 0, 1, 2 for disabled, enabled, enabled-with-unequip
  • DTWW_WerwolfMeterSpell: silently added with effect to damage magicka regeneration at 1200%
  • DTWW_PlayerLastKnownShiftBackTime: to check if bloodlust has been extended
  • DTWW_PlayerShiftedToWerwolfTime: to calculate time remaining and percentage for bar
  • DTWW_WolfMeterToggleSpell: if player has this toggle spell then has been initialized
  • DTWW_DamageMagickaRate: the effect that stops magicka regen - check if player has it to add or remove the spell
partial DTWerewolfWatch.psc - properties
GlobalVariable property PlayerWerewolfShiftBackTime auto
GlobalVariable property DTWW_Enabled auto
GlobalVariable property DTWW_PlayerLastKnownShiftBackTime auto
GlobalVariable property DTWW_PlayerShiftedToWerwolfTime auto
GlobalVariable property DTWW_PlayerOrigMagicka auto
GlobalVariable property DTWW_PlayerHasAtronochStone auto
GlobalVariable property DTWW_PlayerHasAtronochPerk auto
GlobalVariable property DTWW_Initialized auto
Spell property DTWW_WerwolfMeterSpell auto
Spell property DTWW_WolfMeterToggleSpell auto
Spell Property BeastForm auto
Spell property DoomAtronochAbility auto
Perk property Atronoch auto
Keyword property ActorTypeCreature auto
Quest property PlayerWerewolfQuest auto
Message property DTWW_DisableMeterMessage auto
Message property DTWW_EnableMeterMessage auto
Message property DTWW_EnableMeterUneqMessage auto
Message property DTWW_DisableSoonMeterMessage auto
Message property DTWW_MinRemainMessage auto
MagicEffect property DTWW_DamageMagickaRate auto
GlobalVariable Property DLC1WerewolfMaxPerks Auto

The OnUpdate Event is only called on Register, when playerAlias calls, or during meter display every few seconds to keep it updated.

I like to make sure initialization happens when the player isn’t busy, such as waiting until after the game introduction. Check Game.IsFightingControlsEnabled() and if false then wait again to try later. Before running the main function, ProcessCheckMeter, I added a precautionary check in case the meter has been disabled somehow.

partial DTWerewolfWatch.psc - OnUpdate()
Event OnUpdate()
actor playerActorRef = game.GetPlayer()
bool isEnabled = true
bool lastEnabled = false
if (DTWW_Enabled.GetValueInt() >= 1)
lastEnabled = true
endIf
if (DTWW_Initialized.GetValueInt() > 0 || playerActorRef.HasSpell(DTWW_WolfMeterToggleSpell))
isEnabled = lastEnabled
elseIf !Game.IsFightingControlsEnabled()
; player busy - wait to init later
UnregisterForUpdate()
RegisterForSingleUpdate(33.0)
return
else
isEnabled = InitializeMeterWatch(playerActorRef)
endIf
if (!isEnabled)
UnregisterForUpdate()
DisableMeter(playerActorRef)
return
endIf
ProcessCheckMeter(playerActorRef)
endEvent

Instead of showing the entire ProcessCheckMeter function, I cover the decision-making. Note the missing segments marked by ; Do stuff here.

The key here is to find out if the player is in beast form or not by checking the PlayerWerewolfQuest to see if IsRunning and the stage status. Looking at this quest in the CK, note that state 100 is the end so we only need to worry if PlayerWerewoflQuest.GetStage() < 100, beacuse otherwise the player is about to transform back.

Key decisions:

  • Is the player in beast form? If so, check to add spell, DTWW_WerwolfMeterSpell, if needed. Update meter.
  • Has the player shifted back? Remove DTWW_WerwolfMeterSpell and restore magicka.

Other conditions of note include checking if the player has extended bloodlust by feeding, or if extended bloodlust by MTSE lunar transformation.

Compatibility issues:

“Moonlight Tales” by Brevi doesn’t use PlayerWerewolfShiftBackTime for all-night lunar transformation. Instead, this value is set to 999 days in the future and an event is set. We don’t have direct access to that information, so instead it’s calculated using the function seen in the examples above, GetLunarTransformEndTime.

The boolean, showTimeMeter variable checks in case the damage-magicka-regen still needs adding.

partial DTWerewolfWatch.psc - ProcessCheckMeter part 1
bool Function PlayerIsWerewolfBeast(Actor playerActorRef)
if (playerActorRef == None)
playerActorRef = Game.GetPlayer()
endIf
if (playerActorRef.HasKeyword(ActorTypeCreature) && PlayerWerewolfQuest.IsRunning() && PlayerWerewolfQuest.GetStage() < 100)
return true
endIf
return false
endFunction
Function ProcessCheckMeter(actor playerActorRef)
if (DTWW_Enabled.GetValue() > 0.0 && playerActorRef != None)
float updateSecs = 90.0 ; to update
float playerLastKnownShiftTime = DTWW_PlayerLastKnownShiftBackTime.GetValue()
bool isBeast = false
bool retryRemove = false
if (PlayerIsWerewolfBeast(playerActorRef))
isBeast = true
float playerShiftTime = PlayerWerewolfShiftBackTime.GetValue()
float playerBecameWerwolfTime = DTWW_PlayerShiftedToWerwolfTime.GetValue()
float currenTime = Utility.GetCurrentGameTime()
float lunarShiftLimit = currenTime + 100.0 ;mtse sets 999 + days passed
bool showTimeMeter = playerActorRef.HasSpell(DTWW_WerwolfMeterSpell)
if (playerShiftTime > lunarShiftLimit && playerLastKnownShiftTime > 0.0)
; did we enter MTSE all-night while in beast form?
float shiftHour = GetHourFromGameTime(playerLastKnownShiftTime)
if (playerLastKnownShiftTime < (currenTime + 0.1667) && (shiftHour < 5.0 || shiftHour > 6.0))
; all-night activation! Update end-transform time
playerShiftTime = GetLunarTransformEndTime(currenTime)
else
playerShiftTime = playerLastKnownShiftTime
endIf
endIf
if (playerShiftTime != playerLastKnownShiftTime)
if playerLastKnownShiftTime == 0
if playerShiftTime > lunarShiftLimit
; lunar transformation adjustment
playerShiftTime = GetLunarTransformEndTime(currenTime)
;Debug.Notification("DTWW - set Lunar transform time: " + playerShiftTime)
endIf
playerLastKnownShiftTime = playerShiftTime
playerBecameWerwolfTime = currenTime
float magickaCurrent = playerActorRef.GetActorValue("Magicka")
DTWW_PlayerOrigMagicka.SetValue(magickaCurrent)
DTWW_PlayerShiftedToWerwolfTime.SetValue(playerBecameWerwolfTime)
if playerActorRef.HasPerk(Atronoch)
DTWW_PlayerHasAtronochPerk.SetValue(1.0)
else
DTWW_PlayerHasAtronochPerk.SetValue(0.0)
endIf
if playerActorRef.HasSpell(DoomAtronochAbility)
DTWW_PlayerHasAtronochStone.SetValue(1.0)
else
DTWW_PlayerHasAtronochStone.SetValue(0.0)
endIf
if (playerActorRef.HasSpell(DTWW_WerwolfMeterSpell) || playerActorRef.HasMagicEffect(DTWW_DamageMagickaRate))
showTimeMeter = true
else
showTimeMeter = playerActorRef.AddSpell(DTWW_WerwolfMeterSpell, false)
endIf
endIf
DTWW_PlayerLastKnownShiftBackTime.SetValue(playerShiftTime)

Above at the end, we must check if shift-time has been updated and if it’s the first we’ve noticed (if playerLastKnownShiftTime == 0). Add spell and begin the meter if needed. If for some reason the spell fails to add, we can try again as shown below. The trimmed section below calculate the meter percentage and decides and updates using the function above in the examples.

partial DTWerewolfWatch.psc - ProcessCheckMeter part 2
elseIf (showTimeMeter == false)
; try adding again
showTimeMeter = playerActorRef.AddSpell(DTWW_WerwolfMeterSpell, false)
endIf
; calculate time remain in hours for meter and update
;
; Do stuff here - See original source code
; ...

If player has transformed back and have yet to restore (playerLastKnownShiftTime != 0), need to stop the meter and restore magicka level.

partial DTWerewolfWatch.psc - ProcessCheckMeter part 3
elseIf (playerLastKnownShiftTime != 0.0)
; restore
if (RestoreMagickaAndGlobals(playerActorRef) == false)
;Debug.Notification("DTWW - failed restore...try again")
retryRemove = true
updateSecs = 1.5
endIf
elseIf playerActorRef.HasSpell(DTWW_WerwolfMeterSpell)
; if failed to remove before, try again
;Debug.Notification("DTWW - Has meterSpell - removing")
if (RestoreMagickaAndGlobals(playerActorRef) == false)
retryRemove = true
updateSecs = 1.5
endIf
else
MeterDisplayed = false
endIf
if (isBeast || retryRemove)
if (updateSecs > 0.333)
RegisterForSingleUpdate(updateSecs)
else
RegisterForSingleUpdate(2.0)
endIf
endIf
endFunction

At the end of beast form and shifted back, we must remove the damage-magicka-regen spell, restore the magicka bar, and reset our global variables. I check to make sure the spell is removed as precaution and return success/failure so caller may decided if need to retry.

partial DTWerewolfWatch.psc - RestoreMagickaAndGlobals
bool Function RestoreMagickaAndGlobals(Actor playerActorRef)
bool spellRemoved = true
MeterDisplayed = false
if playerActorRef.HasSpell(DTWW_WerwolfMeterSpell)
spellRemoved = playerActorRef.RemoveSpell(DTWW_WerwolfMeterSpell)
endIf
if spellRemoved
DTWW_PlayerLastKnownShiftBackTime.SetValue(0 as Float)
float origMagicka = DTWW_PlayerOrigMagicka.GetValue()
if origMagicka <= 5
origMagicka = GetMaxMagickaActorValue(playerActorRef)
endIf
if origMagicka >= playerActorRef.GetBaseActorValue("Magicka")
; ensure refills completely
origMagicka = origMagicka + 100
endIf
DTWW_PlayerShiftedToWerwolfTime.SetValue(0)
UpdateMagickaMeterWithValue(origMagicka, playerActorRef)
DTWW_PlayerOrigMagicka.SetValue(0)
endIf
return spellRemoved
EndFunction

That’s how my werewolf transformation-time meter works, and a bit about writing Papyrus scripts for TES V: Skyrim.

Questions? Contact me on Nexusmods or via gmail.


Skyrim, Skyrim Special Edition, Creation Kit, and The Elder Scrolls are trademarks of Bethesda Softworks LLC. All other trademarks belong to their respective owners.