Created
December 9, 2025 19:59
-
-
Save ArthurHub/073225f48e41dd033bef9125a908b6ba to your computer and use it in GitHub Desktop.
Bullet Time VATS Fix for FRIK
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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