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.

This mod and script examples also work perfectly with the original 32-bit Skyrim.

SkyrimDTWWMeter1

resources

intro

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

The mod includes two scripts, the first allows the player to enable and disable the meter and the second polls OnUpdate Event every X seconds to determine if should display the meter or remove it. 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 main script polls infrequently since a speedy refresh isn’t necessary and to reduce overhead for a player with a heavy-scripted mod setup. Since checking the player’s condition is very fast, we could probably get away with more frequent updates. In beast form, the script polls every 3-7 seconds to update the meter else it checks every 30 seconds. If player is not a werewolf, I decided to set the next update event to 600 seconds to cover when player becomes a lycanthrope later in game. Of course, the player may disable to turn the main script off completely.

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 start/stop DTWereWolfWatch
  • DTWereWolfWatch script: polls the game every X seconds and updates as necessary

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
; Author: Dracotorre
; Version: 1.0
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
GlobalVariable property DTWW_Enabled auto
Quest property DTWerewolfWatch_quest auto
Message property DTWW_DisableSoonMeterMessage auto ; not used here
Event OnEffectStart(Actor akTarget, Actor akCaster)
;Debug.Notification("DTWW - Toggle")
actor playerActorRef = game.GetPlayer()
if akCaster == playerActorRef
ToggleWWMeter()
endif
EndEvent
Function ToggleWWMeter()
if DTWW_Enabled.GetValue() as Bool
DTWW_Enabled.SetValue(false as Float)
;Debug.Notification("Werewolf Meter will disable....")
;DTWW_DisableSoonMeterMessage.Show()
(DTWerewolfWatch_quest as DTWerewolfWatch).UnRegister()
else
DTWW_Enabled.SetValue(true as Float)
(DTWerewolfWatch_quest as DTWerewolfWatch).Register()
endif
endFunction

DTWerewolfWatch

The complete source file is 300+ lines long. Please refer to the included source file available within the BSA-file. Download at Nexus and use BSA-Manager to extract.

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: boolean flag
  • 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
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
Spell property DTWW_WerwolfMeterSpell auto ; keep magicka bar from regenerating
Spell property DTWW_WolfMeterToggleSpell auto
Spell Property BeastForm auto
Keyword property ActorTypeCreature auto
Quest property PlayerWerewolfQuest auto
Message property DTWW_DisableMeterMessage auto
Message property DTWW_EnableMeterMessage auto
Message property DTWW_DisableSoonMeterMessage auto
GlobalVariable Property DLC1WerewolfMaxPerks Auto

The OnUpdate Event is called every X seconds using RegisterForSingleUpdate (or RegisterForUpdate). I try to keep this function short and easy to read by calling other functions that do the work. Same for the Register and UnRegister functions, brief and easy to read.

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 = DTWW_Enabled.GetValue() as Bool
if 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 == false
UnregisterForUpdate()
DisableMeter(playerActorRef)
return
endif
ProcessCheckMeter(playerActorRef)
endEvent

Instead of showing the entire ProcessCheckMater 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, 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 spell fails to silently add to player.

partial DTWerewolfWatch.psc - ProcessCheckMeter part 1
Function ProcessCheckMeter(actor playerActorRef)
float updateSecs = 90.0
float playerLastKnownShiftTime = DTWW_PlayerLastKnownShiftBackTime.GetValue()
if playerActorRef.HasSpell(BeastForm) || playerActorRef.HasKeyword(ActorTypeCreature)
updateSecs = 30.0
else
;Debug.Notification("DTWW not a werewolf")
updateSecs = 600.0
endif
if playerActorRef.HasKeyword(ActorTypeCreature) && PlayerWerewolfQuest.IsRunning() && PlayerWerewolfQuest.GetStage() < 100
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)
updateSecs = 3.0
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)
showTimeMeter = playerActorRef.AddSpell(DTWW_WerwolfMeterSpell, false)
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 reset.

partial DTWerewolfWatch.psc - ProcessCheckMeter part 3
elseif playerLastKnownShiftTime != 0 as Float
; restore
updateSecs = 120.0
if RestoreMagickaAndGlobals(playerActorRef) == false
;Debug.Notification("DTWW - failed restore...try again")
updateSecs = 0.67
endif
elseif playerActorRef.HasSpell(DTWW_WerwolfMeterSpell)
; if failed to remove before, try again
;Debug.Notification("DTWW - Has meterSpell - removing")
if RestoreMagickaAndGlobals(playerActorRef) == false
updateSecs = 1.2
endif
endif
if updateSecs >= 0.5
RegisterForSingleUpdate(updateSecs)
else
RegisterForSingleUpdate(2.0)
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
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 Twitter @dracotorre 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.