Skip to content

Instantly share code, notes, and snippets.

@ArthurHub
Created December 9, 2025 19:59
Show Gist options
  • Select an option

  • Save ArthurHub/073225f48e41dd033bef9125a908b6ba to your computer and use it in GitHub Desktop.

Select an option

Save ArthurHub/073225f48e41dd033bef9125a908b6ba to your computer and use it in GitHub Desktop.
Bullet Time VATS Fix for FRIK
Scriptname Cobal:BTVxManagerScript extends Quest
{Bullet Time VATS manager. keeps track of player input to bypass VATS and initiate my own version. With thanks to Frazaman and others for fo4vr tools }
;==== Propeties ===========================================================================================================
Actor Property PlayerRef Auto
ActorValue Property ActionPoints Auto
ActorValue Property BTVxPlayerInVATS Auto
Holotape Property BTVxSettingsHoloTape Auto Const
Message Property BTVxButtonDetectStartMSG Auto
Message Property BTVxButtonResultMSG Auto
MagicEffect Property SlowTimeEffect Auto Const
SPELL Property BTVxSlowTime00 Auto Const
SPELL Property BTVxSlowTime01 Auto Const
SPELL Property BTVxSlowTime02 Auto Const
SPELL Property BTVxSlowTime03 Auto Const
SPELL Property BTVxSlowTime04 Auto Const
Perk Property Blitz01 Auto
Perk Property Blitz02 Auto
Perk Property QuickHands02 Auto
Perk Property VATSsprintAPDrainPerk1 Auto ;controls AP use for sprint and teleport while in VATS
Perk Property BTVxMysteriousStranger01 Auto
Perk Property BTVxConcentratedFire01 Auto
Perk Property BTVxBlitz01 Auto
Perk Property BTVxGunFu01 Auto
Perk Property BTVxGunFu02 Auto
Perk Property BTVxGunFu03 Auto
Perk Property BTVxPenetrator01 Auto
Perk Property BTVxCriticalBanker01 Auto
Perk Property BTVxIronFist05 Auto
GlobalVariable Property BTVxAPattackCost auto
GlobalVariable Property BTVxAPscopeAttackCost auto
GlobalVariable Property BTVxAPjumpCost auto
GlobalVariable Property BTVxAPreloadCost auto
GlobalVariable Property BTVxAPCostUserMult auto
GlobalVariable Property BTVxSlowTimeUserSetting auto
GlobalVariable Property BTVxUserSettingTeleport auto ;set via holotape. cant reliably retrieve the ini (but can reliably set it) so we ask the user what kind of movement they are using
GlobalVariable Property BTVxUseVATSteleport auto ;set via holotape. Does the user want to teleport in vats or not
GlobalVariable Property BTVxCTRLbuttonId auto
GlobalVariable Property BTVxCTRLdeviceId auto
GlobalVariable Property BTVxButtonDetectMode auto
GlobalVariable Property BTVxIModUserSetting auto
GlobalVariable Property BTVxUninstall auto
ImageSpaceModifier Property BTVxImodGreen Auto Const
ImageSpaceModifier Property BTVxImodClarity Auto Const
ImageSpaceModifier Property BTVxImodFocus Auto Const
ImageSpaceModifier Property BTVxImodBookends Auto Const
ImageSpaceModifier Property BTVxImodEmpty Auto Const
Sound Property VatsStartSound Auto
Sound Property VatsStopSound Auto
;==== Variables ===========================================================================================================
CustomEvent StartingVATS
CustomEvent StoppingVATS
InputEnableLayer BTVxVATSInputLayer
InputEnableLayer BTVbuttonDetectInputLayer
Bool bInScope
Bool bSwitchMoveStyle
Bool bUseDirectMove
Bool bUseBookends
Float APCostJump
Float APCostReload
Float APCostUserMult
Float BlitzMult
Int VATSbuttonId
Int VATSdeviceId
ImageSpaceModifier VATSiMod
Spell SlowTimeSpell
;==== Setup ===========================================================================================================
Event OnInit()
RegisterForMenuOpenCloseEvent("VATSMenu")
RegisterForMenuOpenCloseEvent("ScopeMenu") ;we need to know this at all times. You can enter vats while scoped, and stay scoped when AP loss forces VATS to close. We never unregister for the menu events
RegisterForMenuOpenCloseEvent("PipboyMenu") ;Once this closes we read user settings.
RegisterForRemoteEvent(PlayerRef, "OnPlayerLoadGame")
PapyrusVR.RegisterForVRButtonEvents(Self) ;The button events, thanks to fo4vr tools.
PlayerRef.additem(BTVxSettingsHoloTape) ;holotape can be crafted on chembench if needed.
SetupVATS()
SetUserSettings()
Endevent
Event Actor.OnPlayerLoadGame(Actor akSender)
PapyrusVR.RegisterForVRButtonEvents(Self)
SetupVATS()
SetUserSettings()
StopVATS() ;In case a save happens during VATS
if (GetState() != "NotinVATS") ;Overkill, but just in case and since this only runs once every load
gotostate("NotinVATS")
endIf
EndEvent
Function SetUserSettings() ;User setting for awarenes is handled in awareness script.
VATSbuttonId = BTVxCTRLbuttonId.getvalueint()
VATSdeviceId = BTVxCTRLdeviceId.getvalueint()
If BTVxIModUserSetting.getvalueint() == 0
VATSiMod = BTVxImodGreen
bUseBookends = false
ElseIf BTVxIModUserSetting.getvalueint() == 1
VATSiMod = BTVxImodClarity
bUseBookends = false
ElseIf BTVxIModUserSetting.getvalueint() == 2 ;redundant, decided against having another option. Removed it from the terminal object.
VATSiMod = BTVxImodFocus
bUseBookends = false
ElseIf BTVxIModUserSetting.getvalueint() == 3 ;bookends. The beginning and end of VATs are marked with a short flash of VATS green. In between, while vats is active, there is no imod.
VATSiMod = BTVxImodBookends
bUseBookends = true
ElseIf BTVxIModUserSetting.getvalueint() == 4 ;no imod effects, empty imod
VATSiMod = BTVxImodEmpty
bUseBookends = false
EndIf
If BTVxSlowTimeUserSetting.getvalue() == 0
SlowTimeSpell = BTVxSlowTime00
ElseIf BTVxSlowTimeUserSetting.getvalue() == 1
SlowTimeSpell = BTVxSlowTime01
ElseIf BTVxSlowTimeUserSetting.getvalue() == 2
SlowTimeSpell = BTVxSlowTime02
ElseIf BTVxSlowTimeUserSetting.getvalue() == 3
SlowTimeSpell = BTVxSlowTime03
ElseIf BTVxSlowTimeUserSetting.getvalue() == 4
SlowTimeSpell = BTVxSlowTime04
EndIf
If BTVxUserSettingTeleport.getvalueint() == 0 && BTVxUseVATSteleport.getvalueint() == 0 ;Move/VatsMove
bSwitchMoveStyle = false
bUseDirectMove = true
Elseif BTVxUserSettingTeleport.getvalueint() == 1 && BTVxUseVATSteleport.getvalueint() == 1 ;teleport/VATSteleport
bSwitchMoveStyle = false
bUseDirectMove = false
Elseif BTVxUserSettingTeleport.getvalueint() == 0 && BTVxUseVATSteleport.getvalueint() == 1 ;Move/VatsTeleport Default
bSwitchMoveStyle = true
bUseDirectMove = false
Elseif BTVxUserSettingTeleport.getvalueint() == 1 && BTVxUseVATSteleport.getvalueint() == 0 ;Teleport/VatsMove I dont think anyone would use teleport in the regular game and direct move in vats only. For the sake of completeness.
bSwitchMoveStyle = true
bUseDirectMove = true
Endif
APCostUserMult = BTVxAPCostUserMult.getvalue()
if playerRef.HasPerk(Blitz01) == false
BlitzMult = 1.0
ElseIf playerRef.HasPerk(Blitz01) == true && playerRef.HasPerk(Blitz02) == false
BlitzMult = 0.5
ElseIf playerRef.HasPerk(Blitz02) == true
BlitzMult = 0.2
Endif
If playerRef.HasPerk(QuickHands02) == true
APCostReload = 0.0
ElseIf playerRef.HasPerk(QuickHands02) == false
APCostReload = (BTVxAPreloadCost.getvalue() * APCostUserMult)
EndIf
APCostJump = ((BTVxAPjumpCost.getvalue() * BlitzMult) * APCostUserMult)
If BTVxUninstall.getvalue() == 1
Uninstall()
Endif
EndFunction
Function SetupVATS()
If PlayerRef.HasPerk(VATSsprintAPDrainPerk1) == true ;This perk controls AP drain for sprint and teleport. It should only be on the player while VATS is active.
playerRef.removeperk(VATSsprintAPDrainPerk1)
EndIf
If PlayerRef.HasPerk(BTVxMysteriousStranger01) == false
playerRef.addperk(BTVxMysteriousStranger01)
EndIf
If PlayerRef.HasPerk(BTVxConcentratedFire01) == false
playerRef.addperk(BTVxConcentratedFire01)
EndIf
If PlayerRef.HasPerk(BTVxBlitz01) == false
playerRef.addperk(BTVxBlitz01)
EndIf
If PlayerRef.HasPerk(BTVxGunFu01) == false
playerRef.addperk(BTVxGunFu01)
EndIf
If PlayerRef.HasPerk(BTVxGunFu02) == false
playerRef.addperk(BTVxGunFu02)
EndIf
If PlayerRef.HasPerk(BTVxGunFu03) == false
playerRef.addperk(BTVxGunFu03)
EndIf
If PlayerRef.HasPerk(BTVxPenetrator01) == false
playerRef.addperk(BTVxPenetrator01)
EndIf
If PlayerRef.HasPerk(BTVxCriticalBanker01) == false
playerRef.addperk(BTVxCriticalBanker01)
EndIf
If PlayerRef.HasPerk(BTVxIronFist05) == false
playerRef.addperk(BTVxIronFist05)
EndIf
bInScope = false
gotostate("NotinVATS")
EndFunction
Function Uninstall()
UnRegisterForMenuOpenCloseEvent("VATSMenu")
UnRegisterForMenuOpenCloseEvent("ScopeMenu") ;we need to know this at all times. You can enter vats while scoped, and stay scoped when AP loss forces VATS to close. We never unregister for the menu events
UnRegisterForMenuOpenCloseEvent("PipboyMenu") ;Once this closes we read user settings.
UnRegisterForRemoteEvent(PlayerRef, "OnPlayerLoadGame")
PapyrusVR.UnRegisterForVRButtonEvents(Self)
If PlayerRef.HasPerk(VATSsprintAPDrainPerk1) == true
playerRef.removeperk(VATSsprintAPDrainPerk1)
EndIf
If PlayerRef.HasPerk(BTVxMysteriousStranger01) == true
playerRef.removeperk(BTVxMysteriousStranger01)
EndIf
If PlayerRef.HasPerk(BTVxConcentratedFire01) == true
playerRef.removeperk(BTVxConcentratedFire01)
EndIf
If PlayerRef.HasPerk(BTVxBlitz01) == true
playerRef.removeperk(BTVxBlitz01)
EndIf
If PlayerRef.HasPerk(BTVxGunFu01) == true
playerRef.removeperk(BTVxGunFu01)
EndIf
If PlayerRef.HasPerk(BTVxGunFu02) == true
playerRef.removeperk(BTVxGunFu02)
EndIf
If PlayerRef.HasPerk(BTVxGunFu03) == true
playerRef.removeperk(BTVxGunFu03)
EndIf
If PlayerRef.HasPerk(BTVxPenetrator01) == true
playerRef.removeperk(BTVxPenetrator01)
EndIf
If PlayerRef.HasPerk(BTVxCriticalBanker01) == true
playerRef.removeperk(BTVxCriticalBanker01)
EndIf
If PlayerRef.HasPerk(BTVxIronFist05) == true
playerRef.removeperk(BTVxIronFist05)
EndIf
self.Stop()
EndFunction
;==== "Empty state" ===========================================================================================================
Event OnMenuOpenCloseEvent(string asMenuName, bool abOpening)
if (asMenuName== "VATSMenu")
if(abOpening)
UI.CloseMenu("VATSMenu")
Endif
endif
if (asMenuName== "ScopeMenu")
if(abOpening)
bInScope = true
Else
bInScope = false
Endif
endif
if (asMenuName == "PipboyMenu") && (abOpening == false)
If (BTVxButtonDetectMode.getvalueint() == 0)
SetUserSettings()
ElseIf (BTVxButtonDetectMode.getvalueint() == 1)
BTVbuttonDetectInputLayer = InputEnableLayer.Create()
BTVbuttonDetectInputLayer.DisablePlayerControls(true, true, true, true, true, true, true, true, true, true, true)
BTVxButtonDetectStartMSG.show(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
utility.wait(0.5)
GoToState("DetectButtonState")
Endif
endif
Endevent
Event OnAnimationEvent(ObjectReference akSource, string asEventName)
if (asEventName == "WeaponFire") || (asEventName == "WeaponSwing")
DamageAP(BTVxAPattackCost.getvalue())
Elseif (asEventName == "JumpUp")
DamageAP(APCostJump)
Elseif (asEventName == "reloadComplete")
DamageAP(APCostReload)
; Else
; Debug.Notification(asEventName)
endIf
endEvent
Function DamageAP(Float Damage) ;This function is called by the player alias script when it detects one of the following; removeitem for throwables, use of a consumable, equiping a weapon, equiping gear with an AP legendary effect
If bInScope == false
PlayerRef.DamageValue(ActionPoints, Damage)
Else
PlayerRef.DamageValue(ActionPoints, BTVxAPscopeAttackCost.GetValue())
EndIf
EndFunction
Function StopVATS()
playerRef.DispelSpell(SlowTimeSpell)
SendCustomEvent("StoppingVATS")
BTVxVATSInputLayer.Reset()
BTVxVATSInputLayer.Delete()
BTVxVATSInputLayer = none
If bSwitchMoveStyle == True
Utility.SetINIBool("bUsingDirectMovement:VR", !bUseDirectMove)
Endif
playerRef.removeperk(VATSsprintAPDrainPerk1)
VATSiMod.Remove()
If bUseBookends == true
VATSiMod.apply()
Endif
VatsStopSound.play(playerRef)
UnregisterForAnimationEvent(playerRef as objectReference, "WeaponFire") ;Useful events: WeaponFire(gun fire), WeaponSwing(Hit with melee or unarmed, not bash), FootBack, FootFront, FootLeft, FootRight (all footevents when walking), WalkSlow, WalkMedium, WalkNormal (all walkevents when moving with melee or unarmed)
UnregisterForAnimationEvent(playerRef as objectReference, "WeaponSwing")
UnregisterForAnimationEvent(playerRef as objectReference, "jumpUp")
UnregisterForAnimationEvent(playerRef as objectReference, "reloadComplete")
PlayerRef.setValue(BTVxPlayerInVATS, 0)
GoToState("NotinVATS")
; Debug.Notification("VATS Released")
EndFunction
Function OnVRButtonEvent(int buttonEvent, int buttonId, int deviceId)
EndFunction
;==== NOTINVATS ===========================================================================================================
State NotinVATS
Function OnVRButtonEvent(int buttonEvent, int buttonId, int deviceId) ;ButtonEvent: 0=Touch event, 1=Touch released, 2=Button down, 3=Button up. | ButtonID: The button | DeviceID: Quest/pico/other standard controllers 1=right 2=left | VATS is buttonEvent=2, buttonId=1, int deviceId=1
if (!Utility.IsInMenuMode() && !UI.IsMenuOpen("PipboyMenu"))
If (deviceId == VATSdeviceId) && (buttonEvent == 2) && (buttonId == VATSbuttonId) ;Button pressed ;Debug.Notification("Got event from deviceID:" + deviceId + ". with ButtonEvent: " + buttonEvent + ". for buttonID: " + buttonId)
BTVxVATSInputLayer = InputEnableLayer.Create()
;(move, fight, Camsw, Look, Sneak, menu, activ, journ, VATS, favo, run)
BTVxVATSInputLayer.DisablePlayerControls(false, false, false, false, false, false, false, false, true, false, false) ;we close off only vats, the button also has other functions. The input layer comes into existence before the game calls VATS and thus vats is blocked.
elseif (deviceId == VATSdeviceId) && (buttonEvent == 3) && (buttonId == VATSbuttonId) ;Button released
if (!Utility.IsInMenuMode()) && (playerRef.Getvalue(ActionPoints) > 2) ;we check again in case one of the buttons other functions is used. Like transfering stuff to a workbench for example. in that case we need to get rid of the input layer again.
GoToState("InVats")
Else
BTVxVATSInputLayer.Reset()
BTVxVATSInputLayer.Delete()
BTVxVATSInputLayer = none
EndIf
EndIf
endIf
EndFunction
EndSTate
;==== INVATS ===========================================================================================================
State InVats
event onbeginstate (string asOldState)
SlowTimeSpell.Cast(playerRef as objectReference)
SendCustomEvent("StartingVATS")
If bSwitchMoveStyle == True
Utility.SetINIBool("bUsingDirectMovement:VR", bUseDirectMove)
Endif
RegisterForAnimationEvent(playerRef as objectReference, "WeaponFire")
RegisterForAnimationEvent(playerRef as objectReference, "WeaponSwing")
RegisterForAnimationEvent(playerRef as objectReference, "jumpUp")
RegisterForAnimationEvent(playerRef as objectReference, "reloadComplete")
playerRef.addperk(VATSsprintAPDrainPerk1) ;This perk controls AP use for sprinting and teleport. The multiplier equalizes the ap cost with direct move cost.
VATSiMod.Remove()
VATSiMod.apply()
VatsStartSound.play(playerRef)
PlayerRef.setValue(BTVxPlayerInVATS, 1)
DamageAP(1.0) ;makes the ap counter show up on the hud... or "Gun Up Display". ALso kicks you out of vats if you somehow got in with a too low value.
;Debug.Notification("State InVATS has begun")
endevent
Function OnVRButtonEvent(int buttonEvent, int buttonId, int deviceId)
If (deviceId == VATSdeviceId) && (buttonEvent == 2) && (buttonId == VATSbuttonId)
;We do nothing until button release because we are in modded vats
elseif (deviceId == VATSdeviceId) && (buttonEvent == 3) && (buttonId == VATSbuttonId) && !UI.IsMenuOpen("PipboyMenu") ;This is where we stop pressing the button.
StopVATS()
EndIf
EndFunction
EndSTate
;==== Button Detection ===========================================================================================================
State DetectButtonState
Function OnVRButtonEvent(int buttonEvent, int buttonId, int deviceId)
If (buttonEvent == 2) && (BTVxButtonDetectMode.getvalueint() == 1)
BTVxButtonDetectMode.setvalueint(0)
VATSbuttonId = buttonId ;redundant because we're going to call SetUserSettings, doing it anyway.
VATSdeviceId = deviceId
BTVxCTRLbuttonId.setvalueint(buttonId)
BTVxCTRLdeviceId.setvalueint(deviceId)
SetUserSettings()
BTVxButtonResultMSG.show(VATSdeviceId, VATSbuttonId, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
BTVbuttonDetectInputLayer.Reset()
BTVbuttonDetectInputLayer.Delete()
BTVbuttonDetectInputLayer = none
GoToState("NotinVATS")
endif
EndFunction
EndState
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment