aboutsummaryrefslogtreecommitdiff
path: root/Northstar.CustomServers/scripts/vscripts/_script_movers.gnut
diff options
context:
space:
mode:
Diffstat (limited to 'Northstar.CustomServers/scripts/vscripts/_script_movers.gnut')
-rw-r--r--Northstar.CustomServers/scripts/vscripts/_script_movers.gnut1783
1 files changed, 1783 insertions, 0 deletions
diff --git a/Northstar.CustomServers/scripts/vscripts/_script_movers.gnut b/Northstar.CustomServers/scripts/vscripts/_script_movers.gnut
new file mode 100644
index 00000000..ca7b839b
--- /dev/null
+++ b/Northstar.CustomServers/scripts/vscripts/_script_movers.gnut
@@ -0,0 +1,1783 @@
+untyped
+
+global function ScriptMovers_Init
+global function ScriptedSwitchDeactivate
+global function ScriptToyChangeStatusLights
+global function SetSwitchUseFunc
+global function ScriptedRotatorRotate
+global function CodeCallback_PreBuildAINFile
+
+const FX_BARREL_EXPLOSION = $"P_spectre_suicide"
+const FX_GENERATOR_EXPLOSION = $"xo_exp_death"
+const FX_CONSOLE_EXPLOSION = $"xo_exp_death"
+const FX_PANEL_EXPLOSION = $"P_drone_exp_md"
+const FX_BARREL_FIRE_SMOKE = $"P_fire_small_FULL"
+const FX_GENERATOR_FIRE_SMOKE = $"P_fire_med_FULL"
+const FX_CONSOLE_FIRE_SMOKE = $"P_fire_med_FULL"
+const FX_PANEL_FIRE_SMOKE = $"P_fire_small_FULL"
+
+const SOUND_BARREL_EXPLODE = "corporate_spectre_death_explode"
+const SOUND_GENERATOR_EXPLODE = "Goblin_Dropship_Explode"
+const SOUND_PANEL_EXPLODE = "AngelCity_Scr_DroneExplodes"
+const SOUND_CONSOLE_EXPLODE = "corporate_spectre_death_explode"
+
+const FAN_PUSH_RAMP_TIME = 0.8
+const FAN_DEFAULT_PUSH_ACCEL = 25000 // units/sec^2
+const FAN_PUSH_ANTI_GRAVITY = 1000 // units/sec^2
+const FAN_PUSH_DECAY_SIDE_VELOCITY = false
+const FAN_DEBUG = false
+
+struct
+{
+ table switchCallbacks
+ //array<entity> entsInWindTunnel
+} file
+
+void function ScriptMovers_Init()
+{
+ AddSpawnCallbackEditorClass( "prop_dynamic", "script_door", ScriptedDoorInit )
+ AddSpawnCallbackEditorClass( "prop_dynamic", "script_switch", ScriptedSwitchInit )
+ AddSpawnCallbackEditorClass( "script_mover_lightweight", "script_rotator", ScriptedRotatorThink )
+ AddSpawnCallbackEditorClass( "script_mover_lightweight", "script_seesaw", SeeSawThink )
+ AddSpawnCallbackEditorClass( "prop_dynamic", "shootable_clasp", ClaspInit )
+
+ AddSpawnCallback_ScriptName( "FanPusher", FanPusherThink )
+
+ PrecacheParticleSystem( FX_BARREL_EXPLOSION )
+ PrecacheParticleSystem( FX_GENERATOR_EXPLOSION )
+ PrecacheParticleSystem( FX_PANEL_EXPLOSION )
+ PrecacheParticleSystem( FX_BARREL_FIRE_SMOKE )
+ PrecacheParticleSystem( FX_GENERATOR_FIRE_SMOKE )
+ PrecacheParticleSystem( FX_PANEL_FIRE_SMOKE )
+ PrecacheParticleSystem( FX_CONSOLE_EXPLOSION )
+ PrecacheParticleSystem( FX_CONSOLE_FIRE_SMOKE )
+
+ AddSpawnCallback( "script_mover", MoverInit )
+ AddSpawnCallback( "script_mover_lightweight", MoverInit )
+
+ RegisterSignal( "OpenDoor" )
+ RegisterSignal( "CloseDoor" )
+ RegisterSignal( "OnDeactivate")
+ RegisterSignal( "OnActivate")
+ RegisterSignal( "StopRotating" )
+}
+
+void function FlagControlsDoor( entity door, string flag )
+{
+ EndSignal( door, "OnDestroy" )
+ while ( true )
+ {
+ WaitSignal( level, flag )
+ if ( Flag( flag ) )
+ Signal( door, "OpenDoor" )
+ else
+ Signal( door, "CloseDoor" )
+ }
+}
+
+void function TriggerControlsDoor( entity door, entity trigger )
+{
+ EndSignal( door, "OpenDoor" )
+ EndSignal( door, "OnDestroy" )
+ EndSignal( trigger, "OnDestroy" )
+
+ WaitSignal( trigger, "OnTrigger" )
+
+ Signal( door, "OpenDoor" )
+}
+
+void function MotionActivatedDoor( entity door )
+{
+ bool doorOpen = false
+ if ( door.HasKey( "startOpen" ) )
+ doorOpen = (door.kv.startOpen == "1")
+
+ EndSignal( door, "OnDestroy" )
+
+ while ( true )
+ {
+ if ( doorOpen )
+ {
+ while ( ArrayEntityWithinDistance( GetPlayerArray(), door.GetOrigin(), 300 ) )
+ wait 0.2
+ Signal( door, "CloseDoor" )
+ }
+ else
+ {
+ while ( !ArrayEntityWithinDistance( GetPlayerArray(), door.GetOrigin(), 200 ) )
+ wait 0.2
+ Signal( door, "OpenDoor" )
+ }
+ doorOpen = !doorOpen
+ wait 1
+ }
+}
+
+void function CodeCallback_PreBuildAINFile()
+{
+ array<entity> doors = GetEntArrayByClass_Expensive( "prop_dynamic" )
+
+ foreach ( entity door in doors )
+ {
+ if ( GetEditorClass( door ) != "script_door" )
+ continue
+
+ door.SetBoneFollowersSolid( false )
+ array<entity> linkedEnts = door.GetLinkEntArray()
+ foreach ( entity ent in linkedEnts )
+ {
+ if ( ent.GetClassName() == "func_brush" )
+ {
+ ent.NotSolid()
+ break
+ }
+ }
+ }
+}
+
+
+void function ScriptedDoorInit( entity door )
+{
+ #if DEV
+ array validModels = [
+ $"models/door/door_imc_interior_03_128_animated.mdl",
+ $"models/door/pod_door_Hangar_IMC_01_animated.mdl",
+ $"models/door/door_512x512x16_elevatorstyle01_animated.mdl",
+ $"models/door/door_256x256x8_elevatorstyle01_animated.mdl",
+ $"models/door/door_128x104x8_rolldownstyle01_animated.mdl",
+ $"models/door/door_256x256x8_rolldownstyle01_animated.mdl",
+ $"models/door/door_256_02_beacon_metal_door_animated.mdl",
+ $"models/door/door_beacon_core_animated.mdl",
+ $"models/door/door_128x104x8_elevatorstyle01_animated.mdl"
+ $"models/door/door_marvin_animated.mdl"
+ ]
+ Assert( validModels.contains( door.GetModelName() ), "Door model at " + door.GetOrigin() + " is invalid: " + door.GetModelName() )
+ #endif
+
+ EndSignal( door, "OnDestroy" )
+ door.SetBlocksLOS( true )
+
+ bool doorOpen = false
+ if ( door.HasKey( "startOpen" ) )
+ doorOpen = (door.kv.startOpen == "1")
+ bool initializing = true
+
+ if ( door.HasKey( "script_flag" ) )
+ {
+ string flag = expect string( door.kv.script_flag )
+ FlagInit( flag )
+ if ( doorOpen )
+ FlagSet( flag )
+ thread FlagControlsDoor( door, flag )
+ }
+
+ string flagToggle
+ if ( door.HasKey( "scr_flagToggle" ) )
+ {
+ flagToggle = expect string( door.kv.scr_flagToggle )
+ FlagInit( flagToggle )
+ }
+
+ if ( door.HasKey( "motionActivated" ) && door.kv.motionActivated == "1" )
+ thread MotionActivatedDoor( door )
+
+ // The door can link to a func_brush that is the collision of the door. The collision will be enabled/disabled based on the door state
+ entity clipBrush
+ array<entity> linkedEnts = door.GetLinkEntArray()
+ foreach ( entity ent in linkedEnts )
+ {
+ if ( ent.GetClassName() == "func_brush" )
+ {
+ clipBrush = ent
+ break
+ }
+ }
+ if ( IsValid( clipBrush ) )
+ {
+ clipBrush.Hide()
+ clipBrush.NotSolid()
+ WaitFrame()
+ }
+
+ // A trigger_multiple can link to the door, causing the door to open. It will only open the door once.
+ entity trigger
+ array<entity> linkParents = door.GetLinkParentArray()
+ foreach ( entity ent in linkParents )
+ {
+ if ( ent.GetClassName() == "trigger_multiple" )
+ {
+ trigger = ent
+ break
+ }
+ }
+ if ( IsValid( trigger ) )
+ thread TriggerControlsDoor( door, trigger )
+
+ while ( IsValid( door ) )
+ {
+ // UPDATE THE DOOR STATE
+ if ( doorOpen )
+ door.Anim_Play("open")
+ else if ( !initializing )
+ door.Anim_Play("close")
+
+ if ( IsValid( clipBrush ) )
+ {
+ ToggleNPCPathsForEntity( clipBrush, doorOpen )
+ if ( doorOpen )
+ {
+ clipBrush.Hide()
+ clipBrush.NotSolid()
+ }
+ else
+ {
+ clipBrush.Show()
+ clipBrush.Solid()
+ }
+ }
+ else
+ {
+ // door must be setup with expensive bone_follower collision for this to work
+ ToggleNPCPathsForEntity( door, doorOpen )
+ }
+
+ if ( flagToggle != "" )
+ {
+ if ( doorOpen )
+ FlagSet( flagToggle )
+ else
+ FlagClear( flagToggle )
+ }
+ initializing = false
+
+ // WAIT FOR STATE CHANGE
+ if ( doorOpen )
+ WaitSignal( door, "CloseDoor" )
+ else
+ WaitSignal( door, "OpenDoor" )
+
+ CreateShakeRumbleOnly( door.GetOrigin(), 15, 150, 1 )
+
+ doorOpen = !doorOpen
+ }
+}
+
+
+void function ScriptedSwitchInit( entity button )
+{
+ #if DEV
+ array< asset > validModels = [
+ $"models/domestic/light_switch_touchscreen.mdl",
+ $"models/props/pressure_plates/pressure_plate_titan_industrial_01.mdl",
+ $"models/domestic/elevator_switch_01.mdl",
+ $"models/beacon/crane_room_monitor_console.mdl",
+ $"models/props/global_access_panel_button/global_access_panel_button_wall.mdl",
+ $"models/props/global_access_panel_button/global_access_panel_button_console.mdl"
+ ]
+ Assert( validModels.contains( button.GetModelName() ) )
+ #endif
+
+ bool usesSkins
+ int activeSkinID
+ int inactiveSkinID
+
+ switch( button.GetModelName() )
+ {
+ case $"models/props/global_access_panel_button/global_access_panel_button_wall.mdl":
+ case $"models/props/global_access_panel_button/global_access_panel_button_console.mdl":
+ usesSkins = true
+ activeSkinID = 0
+ inactiveSkinID = 1
+ break
+ case $"models/beacon/crane_room_monitor_console.mdl":
+ usesSkins = true
+ activeSkinID = 1
+ inactiveSkinID = 2
+ break
+ default:
+ usesSkins = false
+ break
+ }
+
+ int contextId = 0
+ button.Highlight_SetFunctions( contextId, 0, true, HIGHLIGHT_OUTLINE_INTERACT_BUTTON, 1, 0, false )
+ button.Highlight_SetParam( contextId, 0, HIGHLIGHT_COLOR_INTERACT )
+ button.Highlight_SetCurrentContext( contextId )
+
+ EndSignal( button, "OnDestroy" )
+ EndSignal( button, "OnDeactivate" )
+
+ OnThreadEnd(
+ function() : ( button )
+ {
+ // If we haven't destroyed the button, it must be inactive
+ if ( IsValid( button ) )
+ {
+ //ScriptToyChangeStatusLights( button, $"runway_light_red" )
+ Entity_StopFXArray( button )
+ button.UnsetUsable()
+ button.Highlight_HideInside( 1.0 )
+ button.Highlight_HideOutline( 1.0 )
+ }
+ }
+ )
+
+ bool buttonActivated = false
+ bool buttonIsSingleUse = false
+ bool initialized = false
+ bool buttonIsUsable = false
+ float multiUseDelay = 0.2
+
+ bool isPressurePlate = button.GetModelName() == $"models/props/pressure_plates/pressure_plate_titan_industrial_01.mdl"
+
+ if ( isPressurePlate )
+ button.kv.solid = 0 //hack until we can figure out why collision on this model kills titans when embarked
+
+ if ( button.HasKey( "singleUse" ) )
+ buttonIsSingleUse = (button.kv.singleUse == "1" )
+
+ if ( button.HasKey( "usable" ) )
+ buttonIsUsable = (button.kv.usable == "1" )
+
+ if ( button.HasKey( "multiUseDelay" ) )
+ {
+ multiUseDelay = float(button.kv.multiUseDelay)
+ if ( multiUseDelay > 0.0 )
+ Assert( !buttonIsSingleUse, "script_switch at " + button.GetOrigin() + "has multiUseDelay set and is single use" )
+ }
+
+ string flagToggle
+ if ( button.HasKey( "scr_flagToggle" ) )
+ {
+ flagToggle = expect string( button.kv.scr_flagToggle )
+ FlagInit( flagToggle )
+ }
+
+ string flagRequired
+ if ( button.HasKey( "scr_flagRequired" ) )
+ {
+ flagRequired = expect string( button.kv.scr_flagRequired )
+ FlagInit( flagRequired )
+ }
+
+ string hintString_hold = "#HOLD_TO_USE_GENERIC"
+ if ( button.HasKey( "hintString_hold" ) && button.kv.hintString_hold != "" )
+ hintString_hold = string( button.kv.hintString_hold )
+ string hintString_press = "#PRESS_TO_USE_GENERIC"
+ if ( button.HasKey( "hintString_press" ) && button.kv.hintString_press != "" )
+ hintString_press = string( button.kv.hintString_press )
+
+ entity trigger = GetLinkedTrigger( button )
+
+ //need a trigger for pressure plate unless it's just for show
+ if ( ( isPressurePlate ) && ( buttonIsUsable ) )
+ Assert( IsValid( trigger ), "script_switch pressure plate at " + button.GetOrigin() + " needs to link to a trigger_multiple" )
+
+ if ( isPressurePlate && buttonIsUsable )
+ {
+ Assert( IsValid( trigger ), "pressure plate switch at " + button.GetOrigin() + " requires a triggerTarget to activate" )
+ Assert( trigger.GetClassName() == "trigger_multiple", "pressure plate switch at " + button.GetOrigin() + " requires a trigger_multiple to activate" )
+ Assert( trigger.kv.spawnflags == "3", "Trigger for pressure plate at " + button.GetOrigin() + " needs spawnflags set to 3" )
+ }
+
+ if ( !isPressurePlate )
+ {
+ button.SetUsable()
+ button.SetUsableByGroup( "pilot" )
+ button.SetUsePrompts( hintString_hold, hintString_press )
+ button.Highlight_ShowInside( 1.0 )
+ button.Highlight_ShowOutline( 1.0 )
+ }
+
+ var player //hack: have to use "var" when waiting on a usable signal or trigger
+ bool buttonUsedOnce = false
+
+ while ( IsValid( button ) )
+ {
+ //-------------------------
+ // UPDATE EFFECTS
+ //-------------------------
+
+ if ( buttonActivated )
+ {
+ //ScriptToyChangeStatusLights( button, $"runway_light_red" )
+ Entity_StopFXArray( button )
+ if ( usesSkins )
+ button.SetSkin( inactiveSkinID )
+ }
+ else
+ {
+ ScriptToyChangeStatusLights( button, $"runway_light_green" )
+ if ( usesSkins )
+ button.SetSkin( activeSkinID )
+ }
+
+ if ( !buttonIsUsable )
+ break //exit loop if we just want the pretty lights, but no player usability
+
+ if ( buttonIsSingleUse && buttonUsedOnce )
+ break //exit loop if this is a single use button
+
+ if ( isPressurePlate )
+ {
+ //-------------------------
+ // WAIT FOR STATE CHANGE (PRESSURE PLATE)
+ //-------------------------
+
+ if ( buttonActivated )
+ waitthread PressurePlateWaitSignal( trigger, "OnEndTouchAll" )
+ else
+ waitthread PressurePlateWaitSignal( trigger, "OnTrigger" )
+ }
+ else
+ {
+ if ( flagRequired != "" && !Flag( flagRequired ) )
+ {
+ if ( !isPressurePlate )
+ {
+ if ( button.HasKey( "disabledHintString" ) )
+ button.SetUsePrompts( button.kv.disabledHintString, button.kv.disabledHintString )
+ else
+ button.UnsetUsable()
+ }
+
+ //ScriptToyChangeStatusLights( button, $"runway_light_red" )
+ Entity_StopFXArray( button )
+ if ( usesSkins )
+ button.SetSkin( inactiveSkinID )
+ FlagWait( flagRequired )
+ ScriptToyChangeStatusLights( button, $"runway_light_green" )
+ if ( usesSkins )
+ button.SetSkin( activeSkinID )
+ button.SetUsePrompts( hintString_hold , hintString_press )
+ button.SetUsable()
+ }
+
+ //-------------------------
+ // WAIT FOR STATE CHANGE (SIMPLE PUSH BUTTON)
+ //-------------------------
+
+ if ( buttonActivated )
+ {
+ wait multiUseDelay
+ }
+ else
+ {
+ player = button.WaitSignal( "OnPlayerUse" ).player
+ if ( !IsValid( player ) )
+ continue
+ if ( !player.IsPlayer() )
+ continue
+ }
+ }
+
+ //--------------------------------------
+ // Player activated, switch button state
+ //--------------------------------------
+
+ buttonUsedOnce = true
+ buttonActivated = !buttonActivated
+
+ EmitSoundOnEntity( button, "Switch_Activate" )
+
+ button.Signal( "OnActivate" )
+
+ if ( !isPressurePlate && buttonActivated )
+ {
+ button.UnsetUsable() //make the button unusable right after clicking so player doesn't double hit it
+ button.Highlight_HideInside( 1.0 )
+ button.Highlight_HideOutline( 1.0 )
+
+ // Run callbacks
+ if ( button in file.switchCallbacks )
+ {
+ foreach( table callbackTable in file.switchCallbacks[ button ] )
+ {
+ if ( callbackTable.useEnt == null )
+ callbackTable.useFunc( button, player )
+ else
+ callbackTable.useFunc( button, player, callbackTable.useEnt )
+ }
+ }
+ }
+
+ //------------
+ // SET FLAGS
+ //------------
+
+ // Button activated (green)
+ if ( flagToggle != "" && buttonActivated )
+ {
+ FlagSet( flagToggle )
+ }
+
+ if ( buttonActivated )
+ SpawnSpawnersLinkedToButton( button, expect entity( player ) )
+
+ //else if ( buttonActivated && isPressurePlate )
+ // wait 1.5 //wait a bit before re-enabling the usability
+
+ // Button deactivated (red)
+ if ( flagToggle != "" && !buttonActivated )
+ FlagClear( flagToggle )
+
+ if ( !buttonActivated )
+ {
+ button.SetUsable()
+ button.Highlight_ShowInside( 1.0 )
+ button.Highlight_ShowOutline( 1.0 )
+ }
+ }
+
+ if ( ( isPressurePlate ) && ( IsValid( trigger ) ) )
+ trigger.Destroy()
+ else
+ {
+ button.UnsetUsable()
+ button.Highlight_HideInside( 1.0 )
+ button.Highlight_HideOutline( 1.0 )
+ }
+}
+
+void function SpawnSpawnersLinkedToButton( entity button, entity activator )
+{
+ foreach ( entity linkedEnt in button.GetLinkEntArray() )
+ {
+ if ( IsStalkerRack( linkedEnt ) )
+ {
+ thread SpawnFromStalkerRack( linkedEnt, activator )
+ }
+ else if ( IsSpawner( linkedEnt ) )
+ {
+ entity spawned = linkedEnt.SpawnEntity()
+ DispatchSpawn( spawned )
+ }
+ else
+ {
+ Signal( linkedEnt, "OpenDoor" )
+ }
+ }
+}
+
+void function ScriptedSwitchDeactivate( entity button )
+{
+ Assert( IsValid( button ) )
+ button.Signal( "OnDeactivate" )
+}
+
+void function ScriptToyChangeStatusLights( entity button, asset fxName )
+{
+ //--------------------------
+ // Kill any previous effects
+ //--------------------------
+ Entity_StopFXArray( button )
+
+ //--------------------------
+ // Start new effects at tags
+ //--------------------------
+ array<entity> newFxLights
+ array<string> fxTags
+ entity newFx
+ int index = 0
+ string tagName
+
+ while (true)
+ {
+ tagName = "light" + index
+ local id = button.LookupAttachment( tagName )
+ if ( id == 0 )
+ break
+
+ newFx = PlayLoopFXOnEntity( fxName, button, tagName )
+ newFxLights.append( newFx )
+
+ index++
+ }
+
+ button.e.fxArray = newFxLights
+}
+
+void function PressurePlateWaitSignal( entity trigger, string waitSignal )
+{
+ //waitSignal is either "OnTrigger" or "OnEndTouchAll"
+
+ trigger.EndSignal( "OnDestroy" )
+ var result //hack. Result info from triggers
+
+ while ( IsValid( trigger ) )
+ {
+ result = trigger.WaitSignal( waitSignal )
+
+ if ( !IsValid( result.activator ) )
+ continue
+ if ( !result.activator.IsTitan() )
+ continue
+ if ( ( result.activator.IsPlayer() ) || ( IsPetTitan( result.activator ) ) )
+ break
+ }
+}
+
+void function ScriptedRotatorThink( entity rotator )
+{
+ rotator.Hide()
+
+ if ( rotator.HasKey( "use_local_rotation" ) && rotator.kv.use_local_rotation == "1" )
+ rotator.NonPhysicsSetRotateModeLocal( true )
+
+ EndSignal( rotator, "OnDestroy" )
+
+ vector baseAngles = rotator.GetAngles()
+
+ // Linked entities get parented
+ array<entity> linkedEnts = rotator.GetLinkEntArray()
+ foreach ( entity ent in linkedEnts )
+ {
+ ent.SetParent( rotator, "", true )
+ }
+
+ if ( rotator.HasKey( "player_collides" ) && rotator.kv.player_collides == "1" )
+ rotator.SetPusher( true )
+
+ if ( rotator.HasKey( "change_navmesh" ) && rotator.kv.change_navmesh == "1" )
+ rotator.ChangeNPCPathsOnMove( true )
+
+ // script will custom rotate this one
+ if ( rotator.HasKey( "scripted_rotator" ) && expect string( rotator.kv.scripted_rotator ) == "true" )
+ return
+
+ if ( rotator.HasKey( "script_flag" ) )
+ {
+ string flag = expect string( rotator.kv.script_flag )
+ FlagInit( flag )
+ while ( true )
+ {
+ bool returnToBaseAngle = false
+ if ( rotator.HasKey( "flag_clear_resets" ) )
+ returnToBaseAngle = rotator.kv.flag_clear_resets == "1"
+
+ FlagWait( flag )
+ thread ScriptedRotatorRotate( baseAngles, rotator )
+ FlagWaitClear( flag )
+ Signal( rotator, "StopRotating" )
+
+ // Return when the flag is cleared
+ if ( returnToBaseAngle )
+ {
+ if ( !IsValid( rotator ) )
+ return
+
+ float rotateTime = 1.0
+ if ( rotator.HasKey( "rotate_to_time" ) )
+ rotateTime = float( rotator.kv.rotate_to_time )
+ float easeTime = 0.0
+ if ( rotator.HasKey( "rotate_to_ease" ) && rotator.kv.rotate_to_ease == "1" )
+ easeTime = rotateTime * 0.33
+
+ rotator.NonPhysicsRotateTo( baseAngles, rotateTime, easeTime, easeTime )
+ }
+ }
+ }
+ else
+ {
+ thread ScriptedRotatorRotate( baseAngles, rotator )
+ }
+}
+
+vector function GetRotationVector( entity rotator )
+{
+ string axis
+ if ( rotator.HasKey( "rotation_axis" ) )
+ axis = expect string( rotator.kv.rotation_axis )
+
+ vector angles = rotator.GetAngles()
+ switch ( axis )
+ {
+ case "pitch":
+ return AnglesToRight( angles )
+
+ case "yaw":
+ return AnglesToUp( angles )
+
+ case "roll":
+ default:
+ return AnglesToForward( angles )
+
+ }
+
+ unreachable
+}
+
+void function ScriptedRotatorRotate( vector baseAngles, entity rotator )
+{
+ Signal( rotator, "StopRotating" )
+ EndSignal( rotator, "OnDestroy" )
+ EndSignal( rotator, "StopRotating" )
+
+ OnThreadEnd(
+ function() : ( rotator )
+ {
+ if ( IsValid( rotator ) )
+ rotator.NonPhysicsRotate( Vector( 0, 0, 0), 0 )
+ }
+ )
+
+ if ( rotator.HasKey( "start_delay" ) )
+ {
+ float delay = float( rotator.kv.start_delay )
+ if ( delay > 0 )
+ wait delay
+ }
+
+ if ( rotator.kv.rotate_forever_speed != "0" )
+ {
+ // Rotate forever
+ float speed = float( rotator.kv.rotate_forever_speed )
+ Assert( speed != 0.0 )
+
+ vector rotateVec = GetRotationVector( rotator )
+ rotator.NonPhysicsRotate( rotateVec, speed )
+ WaitForever()
+ }
+ else
+ {
+ // Rotate specified amount
+ Assert( rotator.HasKey( "rotate_to_degrees" ) )
+ Assert( rotator.HasKey( "rotate_to_time" ) )
+
+ string soundEffect = ""
+ if ( rotator.HasKey( "script_sound" ) )
+ soundEffect = string( rotator.kv.script_sound )
+
+ float rotateTime = float( rotator.kv.rotate_to_time )
+ if ( rotateTime > 0.0 )
+ {
+ vector rotateAngles = AnglesCompose( baseAngles, Vector( 0.0, 0.0, float( rotator.kv.rotate_to_degrees ) ) )
+
+ float easeTime = 0.0
+ if ( rotator.HasKey( "rotate_to_ease" ) && rotator.kv.rotate_to_ease == "1" )
+ easeTime = rotateTime * 0.33
+
+ while ( true )
+ {
+ // Rotate to the goal angle
+ rotator.NonPhysicsRotateTo( rotateAngles, rotateTime, easeTime, easeTime )
+ if ( soundEffect != "" )
+ EmitSoundOnEntity( rotator, soundEffect )
+ wait rotateTime
+
+ // Rotate back to base angle if specified
+ if ( !rotator.HasKey( "rotate_to_return_delay" ) || rotator.kv.rotate_to_return_delay == "-1" )
+ return
+ Assert( float( rotator.kv.rotate_to_return_delay ) >= 0.0 )
+ wait float( rotator.kv.rotate_to_return_delay )
+ rotator.NonPhysicsRotateTo( baseAngles, rotateTime, easeTime, easeTime )
+ if ( soundEffect != "" )
+ EmitSoundOnEntity( rotator, soundEffect )
+ wait rotateTime
+
+ // Wait a delay and repeat the rotation if specified
+ if ( !rotator.HasKey( "rotate_to_loop_time" ) || rotator.kv.rotate_to_loop_time == "-1" )
+ return
+ Assert( float( rotator.kv.rotate_to_loop_time ) >= 0.0 )
+ wait float( rotator.kv.rotate_to_loop_time )
+ }
+ }
+ }
+}
+
+void function MoverInit( entity mover )
+{
+ if ( !mover.HasKey( "leveledplaced" ) || mover.kv.leveledplaced != "1" )
+ return
+
+ // Linked entities get parented
+ if ( mover.HasKey( "parent_linked_ents" ) && mover.kv.parent_linked_ents == "1" )
+ {
+ array<entity> linkedEnts = mover.GetLinkEntArray()
+ foreach( entity ent in linkedEnts )
+ {
+ if ( GetEditorClass( ent ) != "script_mover_path" )
+ ent.SetParent( mover, "", true )
+ }
+ }
+
+ if ( mover.HasKey( "player_collides" ) && mover.kv.player_collides == "1" )
+ mover.SetPusher( true )
+
+ if ( mover.HasKey( "change_navmesh" ) && mover.kv.change_navmesh == "1" )
+ mover.ChangeNPCPathsOnMove( true )
+
+ thread MoverThink( mover )
+}
+
+void function MoverThink( entity mover )
+{
+ EndSignal( mover, "OnDestroy" )
+
+ if ( mover.GetModelName() == $"models/dev/editor_ref.mdl" )
+ mover.Hide()
+
+ array<entity> pathNodes = GetNextMoverPathNodes( mover )
+
+ // Go down the path gathering the nodes and init any flags
+ foreach( entity node in pathNodes )
+ InitMoverNodeFlagsAndErrorCheck( node )
+
+ Assert( mover.HasKey( "path_speed") && float( mover.kv.path_speed ) >= 0.0, "script_mover doesnt have a valid path speed" )
+ float pathSpeed = float( mover.kv.path_speed )
+ bool easeIn
+ bool easeOut
+
+ string startFlag = ""
+ if ( mover.HasKey( "script_flag" ) && mover.kv.script_flag != "" )
+ {
+ startFlag = mover.GetValueForKey( "script_flag" )
+ FlagInit( startFlag )
+ }
+
+ if ( mover.HasKey( "dangerous_area_radius" ) )
+ mover.AllowNPCGroundEnt( false )
+
+ if ( pathNodes.len() == 0 )
+ return
+
+ if ( startFlag != "" )
+ FlagWait( startFlag )
+
+ if ( mover.HasKey( "start_delay" ) && float( mover.kv.start_delay ) > 0.0 )
+ wait float( mover.kv.start_delay )
+
+ entity pathNode = pathNodes.getrandom()
+ entity lastNode
+
+ easeOut = pathNode.HasKey( "ease_from_node" ) && pathNode.GetValueForKey( "ease_from_node" ) == "1"
+
+ bool isMoving = false
+
+ while( IsValid( pathNode ) )
+ {
+ bool teleport = false
+ if ( pathNode.HasKey( "teleport_to_node" ) )
+ teleport = pathNode.GetValueForKey( "teleport_to_node" ) == "1"
+
+ bool perfectRotation = false
+ if ( IsValid( lastNode ) && lastNode.HasKey( "perfect_circular_rotation" ) )
+ perfectRotation = lastNode.GetValueForKey( "perfect_circular_rotation" ) == "1"
+
+ float rotationTime = 0.0
+ if ( IsValid( lastNode ) && lastNode.HasKey( "circular_rotation_time" ) )
+ rotationTime = float( lastNode.GetValueForKey( "circular_rotation_time" ) )
+
+ float dist = Distance( pathNode.GetOrigin(), mover.GetOrigin() )
+
+ if ( !isMoving )
+ MoverPath_StartSound( mover, pathNode )
+
+ if ( dist > 0.0 && !teleport )
+ {
+ easeIn = pathNode.HasKey( "ease_to_node" ) && pathNode.GetValueForKey( "ease_to_node" ) == "1"
+ float moveTime = dist / pathSpeed
+ float easeLeaving = easeOut ? moveTime * 0.5 : 0.0
+ float easeArriving = easeIn ? moveTime * 0.5 : 0.0
+ float angleChange = IsValid( lastNode ) ? MoverPath_GetAngleChange( lastNode, pathNode ) : 0.0
+
+ if ( perfectRotation && angleChange != 0 )
+ {
+ string rotationSoundEvent = ""
+ if ( mover.HasKey( "sound_circular_rotation" ) )
+ rotationSoundEvent = mover.GetValueForKey( "sound_circular_rotation" )
+ if ( rotationSoundEvent != "" )
+ EmitSoundOnEntity( mover, rotationSoundEvent )
+
+ vector turnAnchorPos = MoverPath_GetAngleAnchor( lastNode, pathNode )
+
+ // Create a new mover because as far as I know I can't get all the children of the mover and clearparent and reparent.
+ entity curveMover = CreateScriptMover( turnAnchorPos, lastNode.GetAngles() )
+ curveMover.SetPusher( mover.GetPusher() )
+ mover.SetParent( curveMover, "", true )
+
+ // Find the circumference of the turn so we can calculate the rotation time based on the distance traveled around the bend
+ float c = 2 * PI * Length(turnAnchorPos - lastNode.GetOrigin())
+ float frac = fabs(angleChange) / 360.0
+ moveTime = (c * frac) / pathSpeed
+
+ isMoving = true
+ curveMover.NonPhysicsRotateTo( pathNode.GetAngles(), moveTime, 0.0, 0.0 )
+
+ wait moveTime - 0.01
+
+ mover.ClearParent()
+ curveMover.Destroy()
+
+ if ( rotationSoundEvent != "" )
+ StopSoundOnEntity( mover, rotationSoundEvent )
+ }
+ else
+ {
+ // Linear move/rotate
+ isMoving = true
+ if ( mover.HasKey( "dangerous_area_radius" ) )
+ thread CreateMoverDangrousAreas( mover, mover.GetOrigin(), pathNode.GetOrigin(), float( mover.GetValueForKey( "dangerous_area_radius" ) ), moveTime )
+ mover.NonPhysicsMoveTo( pathNode.GetOrigin(), moveTime, easeLeaving, easeArriving )
+ mover.NonPhysicsRotateTo( pathNode.GetAngles(), moveTime, easeLeaving, easeArriving )
+ wait moveTime - 0.01
+ }
+ }
+ else if ( dist == 0.0 && !teleport && rotationTime > 0.0 )
+ {
+ // Rotation in place
+ string rotationSoundEvent = ""
+ if ( mover.HasKey( "sound_rotation" ) )
+ rotationSoundEvent = mover.GetValueForKey( "sound_rotation" )
+ if ( rotationSoundEvent != "" )
+ EmitSoundOnEntity( mover, rotationSoundEvent )
+
+ isMoving = false
+ MoverPath_StopMoveSound( mover )
+ MoverPath_StopSound( mover, pathNode )
+ float easeIn = easeOut ? rotationTime * 0.5 : 0.0
+ float easeOut = easeIn ? rotationTime * 0.5 : 0.0
+ mover.NonPhysicsRotateTo( pathNode.GetAngles(), rotationTime, easeIn, easeOut )
+ wait rotationTime - 0.01
+
+ if ( rotationSoundEvent != "" )
+ StopSoundOnEntity( mover, rotationSoundEvent )
+ }
+ else
+ {
+ mover.SetOrigin( pathNode.GetOrigin() )
+ mover.SetAngles( pathNode.GetAngles() )
+ }
+
+ easeOut = pathNode.HasKey( "ease_from_node" ) && pathNode.GetValueForKey( "ease_from_node" ) == "1"
+
+ if ( pathNode.HasKey( "scr_flag_set" ) )
+ FlagSet( pathNode.GetValueForKey( "scr_flag_set" ) )
+
+ if ( pathNode.HasKey( "scr_flag_clear" ) )
+ FlagClear( pathNode.GetValueForKey( "scr_flag_clear" ) )
+
+ if ( pathNode.HasKey( "scr_flag_wait" ) )
+ {
+ string flag = pathNode.GetValueForKey( "scr_flag_wait" )
+ if ( !Flag( flag ) )
+ {
+ isMoving = false
+ MoverPath_StopMoveSound( mover )
+ MoverPath_StopSound( mover, pathNode )
+ FlagWait( flag )
+ }
+ }
+
+ if ( pathNode.HasKey( "scr_flag_wait_clear" ) )
+ {
+ string flag = pathNode.GetValueForKey( "scr_flag_wait_clear" )
+ if ( Flag( flag ) )
+ {
+ isMoving = false
+ MoverPath_StopMoveSound( mover )
+ MoverPath_StopSound( mover, pathNode )
+ FlagWaitClear( flag )
+ }
+ }
+
+ if ( pathNode.HasKey( "path_wait" ) )
+ {
+ float time = float( pathNode.GetValueForKey( "path_wait" ) )
+ if ( time > 0.0 )
+ {
+ isMoving = false
+ MoverPath_StopMoveSound( mover )
+ MoverPath_StopSound( mover, pathNode )
+ wait time
+ }
+ }
+
+ pathNodes = GetNextMoverPathNodes( pathNode )
+ if ( pathNodes.len() == 0 )
+ {
+ MoverPath_StopMoveSound( mover )
+ MoverPath_StopSound( mover, pathNode )
+ break
+ }
+
+ // Update speed based on the node
+ if ( pathNode.HasKey( "path_speed" ) )
+ pathSpeed = float( pathNode.GetValueForKey( "path_speed" ) )
+
+ lastNode = pathNode
+ pathNode = pathNodes.getrandom()
+ }
+}
+
+void function CreateMoverDangrousAreas( entity mover, vector start, vector end, float radius, float duration )
+{
+ float d = Distance( start, end )
+ float spacing = radius * 1.5
+ int numDangerousSpots = int( ceil( d / spacing ) )
+ vector direction = Normalize( end - start )
+ vector pos
+
+ for ( int i = 0 ; i < numDangerousSpots ; i++ )
+ {
+ pos = start + ( direction * spacing * i )
+ thread CreateMoverDangrousAreaUntilMoverPasses( mover, start, end, pos, radius, duration )
+ }
+}
+
+void function CreateMoverDangrousAreaUntilMoverPasses( entity mover, vector start, vector end, vector pos, float radius, float maxDuration )
+{
+ // Create entity to link it to (lifetime)
+ entity lifetimeEnt = CreateScriptRef( pos )
+
+ // Create the dangerous area
+ AI_CreateDangerousArea_Static( lifetimeEnt, null, radius, TEAM_INVALID, true, true, pos )
+
+ // Wait for mover to go past the dangerous area, or timeout
+ float endTime = Time() + maxDuration
+ while( Time() <= endTime )
+ {
+ if ( DotProduct( end - start, pos - mover.GetOrigin() ) < 0 )
+ break
+ WaitFrame()
+ }
+
+ lifetimeEnt.Destroy()
+}
+
+void function MoverPath_StopMoveSound( entity mover )
+{
+ // Stops any move sounds playing on the mover
+
+ // Stop playing a sound on the mover if one is specified & it is set to do so
+ if ( mover.HasKey( "sound_move" ) && mover.kv.sound_move != "" )
+ {
+ // "sound_move" sound continues to play after moving unless this is checked"
+ if ( mover.HasKey( "stop_sound_move_on_stop" ) && mover.GetValueForKey( "stop_sound_move_on_stop" ) == "1" )
+ StopSoundOnEntity( mover, string( mover.kv.sound_move ) )
+ }
+}
+
+void function MoverPath_StopSound( entity mover, entity node )
+{
+ // Play sound on the node if one is specified
+ if ( node.HasKey( "sound_stop_move" ) && node.kv.sound_stop_move != "" )
+ EmitSoundAtPosition( TEAM_UNASSIGNED, node.GetOrigin(), string( node.kv.sound_stop_move ) )
+
+ // Play sound on the mover if one is specified
+ if ( mover.HasKey( "sound_stop_move" ) && mover.kv.sound_stop_move != "" )
+ EmitSoundOnEntity( mover, string( mover.kv.sound_stop_move ) )
+}
+
+void function MoverPath_StartSound( entity mover, entity node )
+{
+ // Play sound on the node if one is specified
+ if ( node.HasKey( "sound_start_move" ) && node.kv.sound_start_move != "" )
+ EmitSoundAtPosition( TEAM_UNASSIGNED, node.GetOrigin(), string( node.kv.sound_start_move ) )
+
+ // Play sound on mover if one is specified
+ if ( mover.HasKey( "sound_move" ) && mover.kv.sound_move != "" )
+ EmitSoundOnEntity( mover, string( mover.kv.sound_move ) )
+}
+
+float function MoverPath_GetAngleChange( entity node1, entity node2 )
+{
+ vector vec1 = node1.GetForwardVector()
+ vector vec2 = node2.GetForwardVector()
+ float angle = acos( DotProduct( vec1, vec2 ) ) * 180 / PI
+ return angle
+}
+
+vector function MoverPath_GetAngleAnchor( entity node1, entity node2 )
+{
+ vector node1Origin = node1.GetOrigin()
+ vector node2Origin = node2.GetOrigin()
+ vector node1Angles = node1.GetAngles()
+ vector node2Angles = node2.GetAngles()
+ vector node1SideVec
+ vector node2SideVec
+
+ if ( node1Origin.z != node2Origin.z )
+ {
+ // vertical turn
+ node1SideVec = AnglesToUp( node1Angles )
+ node2SideVec = AnglesToUp( node2Angles )
+ }
+ else
+ {
+ // horizontal turn
+ node1SideVec = AnglesToRight( node1Angles )
+ node2SideVec = AnglesToRight( node2Angles )
+ }
+
+ float angleChange = MoverPath_GetAngleChange( node1, node2 )
+ Assert( angleChange != 0 )
+ if ( angleChange > 0 )
+ {
+ node1SideVec *= -1
+ node2SideVec *= -1
+ }
+ float d = Distance( node1Origin, node2Origin )
+ vector intersect = GetClosestPointToLineSegments( node1Origin, node1Origin + node1SideVec * d, node2Origin, node2Origin + node2SideVec * d )
+
+ //DebugDrawLine( intersect, node1Origin, 255, 255, 0, true, 5.0 )
+ //DebugDrawLine( intersect, node2Origin, 0, 255, 255, true, 5.0 )
+ //DebugDrawLine( node1Origin, node1Origin + node1SideVec * 250, 100, 100, 100, true, 5.0 )
+ //DebugDrawLine( node2Origin, node2Origin + node2SideVec * 250, 100, 100, 100, true, 5.0 )
+ //DebugDrawLine( intersect, intersect - <0,0,128>, 100, 0, 0, true, 5.0 )
+ //DebugDrawText( intersect, angleChange.tostring(), true, 5.0 )
+
+ return intersect
+}
+
+array<entity> function GetNextMoverPathNodes( entity node, bool errorChecking = false )
+{
+ array<entity> nodes
+ array<entity> linkedEnts = node.GetLinkEntArray()
+ foreach( entity ent in linkedEnts )
+ {
+ if ( GetEditorClass( ent ) == "script_mover_path" )
+ {
+ if ( !errorChecking && ent.HasKey( "switchtrack_flag" ) && !Flag( ent.GetValueForKey( "switchtrack_flag" ) ) )
+ continue
+ nodes.append( ent )
+ }
+ }
+ return nodes
+}
+
+void function InitMoverNodeFlagsAndErrorCheck( entity node )
+{
+ if ( node.e.moverPathPrecached )
+ return
+
+ if ( node.HasKey( "path_speed" ) )
+ Assert( float( node.kv.path_speed ) > 0.0, "Node path_speed at " + node.GetOrigin() + " must be greater than 0." )
+
+ if ( node.HasKey( "path_wait" ) )
+ Assert( float( node.kv.path_wait ) >= 0.0, "Node path_wait at " + node.GetOrigin() + " must be greater than 0." )
+
+ if ( node.HasKey( "teleport_to_node" ) && node.kv.teleport_to_node == "1" )
+ {
+ if ( node.HasKey( "ease_to_node" ) )
+ Assert( node.kv.ease_to_node == "0", "Node at " + node.GetOrigin() + " cant have both teleport_to_node and ease_to_node checked." )
+ }
+
+ if ( node.HasKey( "scr_flag_set" ) )
+ FlagInit( node.GetValueForKey( "scr_flag_set" ) )
+ if ( node.HasKey( "scr_flag_clear" ) )
+ FlagInit( node.GetValueForKey( "scr_flag_clear" ) )
+ if ( node.HasKey( "scr_flag_wait" ) )
+ FlagInit( node.GetValueForKey( "scr_flag_wait" ) )
+ if ( node.HasKey( "switchtrack_flag" ) )
+ FlagInit( node.GetValueForKey( "switchtrack_flag" ), true )
+
+ node.e.moverPathPrecached = true
+
+ array<entity> pathNodes = GetNextMoverPathNodes( node, true )
+ foreach( entity node in pathNodes )
+ InitMoverNodeFlagsAndErrorCheck( node )
+}
+
+entity function GetLinkedTrigger( entity ent )
+{
+ array<entity> linkedEnts = ent.GetLinkEntArray()
+ foreach ( entity ent in linkedEnts )
+ {
+ if ( ent.GetClassName() == "trigger_multiple" )
+ return ent
+ }
+ return null
+}
+
+struct SeeSawThinkStruct // struct that is internal to seeSaw think logic
+{
+ float speed
+ bool touching
+ bool wasTouched
+ vector startAngles
+ float oldSpeed
+ bool playerIgnore
+ float maxSpeed = 10
+ float acceleration = 0.425
+}
+
+void function SeeSawThink( entity seeSaw )
+{
+ seeSaw.Hide()
+
+ seeSaw.EndSignal( "OnDestroy" )
+// seeSaw.NonPhysicsSetRotateModeLocal( true )
+ seeSaw.SetPusher( true )
+
+ array<entity> parents = seeSaw.GetLinkParentArray()
+ array<entity> brushes
+ foreach ( ent in parents )
+ {
+ if ( ent.GetClassName() == "func_brush" )
+ brushes.append( ent )
+ }
+
+ float minz = 0
+ float maxz = 0
+ foreach ( brush in brushes )
+ {
+ vector mins = brush.GetBoundingMins()
+ vector maxs = brush.GetBoundingMaxs()
+
+ if ( mins.z < minz )
+ minz = mins.z
+
+ if ( maxs.z > maxz )
+ maxz = maxs.z
+ }
+
+ float height = fabs( minz ) + maxz
+
+ entity trigger = seeSaw.GetLinkEnt()
+ trigger.EndSignal( "OnDestroy" )
+
+ SeeSawThinkStruct e
+ e.startAngles = seeSaw.GetAngles()
+
+ if ( seeSaw.HasKey( "script_start_moving" ) && int( seeSaw.kv.script_start_moving ) > 0 )
+ {
+ e.wasTouched = true
+ e.speed = 8
+ }
+
+ thread SeeSawSpeedThink( seeSaw, e )
+
+ if ( seeSaw.HasKey( "script_player_ignore" ) && int( seeSaw.kv.script_player_ignore ) > 0 )
+ {
+ e.playerIgnore = true
+ }
+ else
+ {
+ thread SeeSawTriggerThink( seeSaw, trigger, height, e )
+ }
+
+
+}
+
+void function SeeSawTriggerThink( entity seeSaw, entity trigger, float height, SeeSawThinkStruct e )
+{
+ for ( ;; )
+ {
+ e.touching = false
+ table results = trigger.WaitSignal( "OnTrigger" )
+ entity player = expect entity( results.activator )
+ if ( !IsAlive( player ) )
+ continue
+
+ while ( trigger.IsTouching( player ) )
+ {
+ PlayerNearSeeSaw( player, seeSaw, height, e )
+ WaitFrame()
+ }
+ }
+}
+
+float ornull function SeeSawPitchLimitOverride()
+{
+// return 60.0
+ return null
+}
+
+void function SeeSawSpeedThink( entity seeSaw, SeeSawThinkStruct e )
+{
+ seeSaw.EndSignal( "OnDestroy" )
+ float pitchLimit = 50 // 70.75
+
+ if ( seeSaw.HasKey( "script_pitch_limit" ) )
+ pitchLimit = float( seeSaw.kv.script_pitch_limit )
+
+ vector angles = seeSaw.GetAngles()
+ vector forward = AnglesToRight( angles ) * -1
+ vector rotateDir = forward // < -1,0,0 >
+
+ for ( ;; )
+ {
+ WaitFrame()
+ SeeSawSpeedThink_internal( seeSaw, e, rotateDir, pitchLimit )
+ }
+}
+
+void function SeeSawSpeedThink_internal( entity seeSaw, SeeSawThinkStruct e, vector rotateDir, float pitchLimit )
+{
+ float ornull pitchLimitOverride = SeeSawPitchLimitOverride()
+ if ( pitchLimitOverride != null )
+ {
+ pitchLimit = expect float( pitchLimitOverride )
+ }
+ vector localAngles = seeSaw.GetLocalAngles()
+
+ if ( ( !e.touching && e.wasTouched ) || e.playerIgnore )
+ {
+ vector startForward = AnglesToForward( e.startAngles )
+ vector startUp = AnglesToUp( e.startAngles )
+ vector seeSawForward = AnglesToForward( seeSaw.GetAngles() )
+
+// if ( ge(106)==seeSaw)
+// {
+// printt( "dot is " + DotProduct( startForward, seeSawForward ) + " speed " + e.speed )
+// }
+ //printt( "Dot " + DotProduct( startForward, seeSawForward ) )
+ //printt( "Dot " +
+ //printt( DotProduct( startUp, seeSawForward ) )
+
+
+ // return to normal
+ //printt( "start angles " + e.startAngles + " current angles " + seeSaw.GetAngles() )
+
+ if ( fabs( DotProduct( startForward, seeSawForward ) ) < 0.75 )
+ {
+ if ( DotProduct( startUp, seeSawForward ) > 0 )
+ {
+ if ( e.speed < e.maxSpeed )
+ e.speed += e.acceleration
+ }
+ else
+ {
+ if ( e.speed > -e.maxSpeed )
+ e.speed -= e.acceleration
+ }
+ }
+
+ //DebugDrawText( seeSaw.GetOrigin(), "" + e.speed, true, 1 )
+ //DebugDrawLine( seeSaw.GetOrigin(), GetPlayerArray()[0].GetOrigin(), 255, 0, 0, true, 0.2 )
+ }
+
+// if ( ge(117) == seeSaw )
+// return
+ if ( e.speed < 0 )
+ {
+ if ( localAngles.x < -pitchLimit )
+ {
+ seeSaw.NonPhysicsRotate( rotateDir, 0 )
+ e.speed = 0
+ return
+ }
+ }
+ else
+ {
+ if ( localAngles.x > pitchLimit )
+ {
+ seeSaw.NonPhysicsRotate( rotateDir, 0 )
+ e.speed = 0
+ return
+ }
+ }
+
+ if ( e.oldSpeed != e.speed || pitchLimitOverride != null )
+ {
+ seeSaw.NonPhysicsRotate( rotateDir, e.speed )
+ e.oldSpeed = e.speed
+ }
+}
+
+void function PlayerNearSeeSaw( entity player, entity seeSaw, float height, SeeSawThinkStruct e )
+{
+ vector playerOrigin = player.GetOrigin()
+ vector seeSawOrigin = seeSaw.GetOrigin()
+ //DebugDrawLine( playerOrigin, seeSawOrigin, 255, 0, 0, true, 0.2 )
+ vector originDif = playerOrigin - seeSawOrigin
+ vector difNormal = Normalize( originDif )
+ vector seeSawAngles = seeSaw.GetAngles()
+ vector seeSawUp = AnglesToUp( seeSawAngles )
+ bool onTop = DotProduct( difNormal, seeSawUp ) > 0
+
+ // may need to do something special for hands holding on
+ if ( !onTop )
+ {
+ e.touching = false
+ return
+ }
+
+ float amountAbove = DotProduct( originDif, seeSawUp )
+ amountAbove -= height
+ amountAbove += 2.5 // player is in the ground?
+
+// if ( ge(117) == seeSaw )
+// {
+// printt( "amountAbove " + amountAbove )
+// }
+
+ if ( amountAbove < 0 || amountAbove > 15 )
+ {
+ e.touching = false
+ return
+ }
+
+
+
+ vector seeSawForward = AnglesToForward( seeSawAngles )
+ float amountForward = DotProduct( originDif, seeSawForward )
+ e.speed += Graph( amountForward, 0, 1000, 0, 2 )
+ float maxSpeed = 10
+ e.speed = min( maxSpeed, e.speed )
+ e.speed = max( -maxSpeed, e.speed )
+ e.touching = true
+ e.wasTouched = true
+}
+
+void function ClaspInit(entity clasp)
+{
+ //printt( "" )
+ //printt( "INIT SHOOTABLE CLASP" )
+ //printt( "" )
+
+ if ( clasp.HasKey( "scr_flag_set" ) )
+ FlagInit( clasp.GetValueForKey( "scr_flag_set" ) )
+
+ AddEntityCallback_OnDamaged( clasp, OnClaspDamaged )
+
+}
+
+void function OnClaspDamaged( entity clasp, var damageInfo)
+{
+ //printt( "CLASP HAS TAKEN DAMAGE" )
+
+ //if the attacker is not valid
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ if ( !IsValid( attacker ) || !attacker.IsPlayer() )
+ return
+
+ if ( clasp.HasKey( "scr_flag_set") )
+ {
+ //printt( clasp )
+ //printt( clasp.GetEncodedEHandle() )
+ //printt( clasp.GetTargetName() )
+ //printt( clasp.GetValueForKey( "scr_flag_set" ) )
+ FlagSet( clasp.GetValueForKey( "scr_flag_set" ) )
+ }
+
+ //printt( "" )
+ //printt( "DESTROYING CLASP" )
+ //printt( "" )
+
+ //Destroy the clasp
+ clasp.Destroy()
+}
+
+function SetSwitchUseFunc( button, func, ent = null )
+{
+ local Table = InitControlPanelUseFuncTable()
+ Table.useFunc <- func
+ Table.useEnt <- ent
+
+ if ( !( button in file.switchCallbacks ) )
+ file.switchCallbacks[ button ] <- []
+ file.switchCallbacks[ button ].append( Table )
+}
+
+
+void function FanPusherThink( entity fanPusher )
+{
+ float fanPushDist = 3000
+
+ if ( fanPusher.HasKey( "height" ) )
+ fanPushDist = float( fanPusher.kv.height )
+
+ if ( fanPusher.HasKey( "script_gravityscale" ) && fanPusher.kv.script_gravityscale != "" )
+ {
+ fanPushDist *= string( fanPusher.kv.script_gravityscale ).tofloat()
+ }
+ else
+ {
+ float gravityScale = expect float( GetPlayerSettingsFieldForClassName( DEFAULT_PILOT_SETTINGS, "gravityscale" ) )
+ fanPushDist *= gravityScale // adjusted for new gravity scale
+ }
+
+ float radius = float( fanPusher.kv.script_radius )
+ vector forward = AnglesToForward( fanPusher.GetAngles() )
+ vector cylinderBottom = fanPusher.GetOrigin()
+ vector cylinderTop = cylinderBottom + ( forward * fanPushDist )
+
+ if ( FAN_DEBUG )
+ DebugDrawCylinder( fanPusher.GetOrigin(), fanPusher.GetAngles(), radius, fanPushDist, 100, 0, 0, true, 120.0 )
+
+ string flag = ""
+ if ( fanPusher.HasKey( "script_flag" ) )
+ {
+ flag = string( fanPusher.kv.script_flag )
+ FlagInit( flag )
+ }
+
+ bool lifterFan = DotProduct( forward, <0,0,1> ) >= 0.98
+ float pushAccel = FAN_DEFAULT_PUSH_ACCEL
+ if ( fanPusher.HasKey( "strength" ) )
+ pushAccel = float( fanPusher.kv.strength )
+
+ array<entity> fanPushables
+ table<entity,float> startTimes
+ table<entity,float> startHeights
+
+ // Play fan sound on entity instead of at position because some occluder bug with miles. Easiest fix for late in game. Next project fan pusher shoudln't be info_target that you can't play sounds on
+ entity fanSoundEntity = CreateScriptMover( fanPusher.GetOrigin() )
+ fanSoundEntity.DisableHibernation()
+
+ // Delay to fix code bug where audio wont play at map load
+ wait 0.2
+
+ FanOnSoundEffects( fanPusher, radius, fanSoundEntity )
+
+ while( true )
+ {
+ if ( flag != "" && !Flag( flag ) )
+ {
+ foreach( entity ent, float time in startTimes )
+ ent.e.inWindTunnel = false
+
+ startTimes.clear()
+ startHeights.clear()
+
+ FanOffSoundEffects( fanPusher, radius, fanSoundEntity )
+ FlagWait( flag )
+ FanOnSoundEffects( fanPusher, radius, fanSoundEntity )
+ }
+
+ fanPushables.clear()
+ fanPushables.extend( GetPlayerArray() )
+ fanPushables.extend( GetNPCArray() )
+ fanPushables.extend( GetProjectileArrayEx( "any", TEAM_ANY, TEAM_ANY, cylinderBottom, fanPushDist ) )
+ fanPushables.extend( GetEntArrayByClass_Expensive( "prop_physics" ) )
+
+ foreach( entity ent in fanPushables )
+ {
+ array<vector> testPosArray = [ ent.GetWorldSpaceCenter(), ent.EyePosition() + <0,0,16>, ent.GetOrigin() - <0,0,16> ]
+ bool isInFanCylinder = false
+ vector testPos = ent.GetWorldSpaceCenter()
+ if ( ent.e.windPushEnabled )
+ {
+ foreach( vector pos in testPosArray )
+ {
+ if ( !PointInCylinder( cylinderBottom, cylinderTop, radius, pos ) )
+ continue
+ isInFanCylinder = true
+ break
+ }
+ }
+
+ if ( isInFanCylinder )
+ {
+ if ( ent.IsPlayer() && ent.IsNoclipping() )
+ continue
+
+ if ( !( ent in startTimes ) )
+ {
+ ent.e.inWindTunnel = true
+ ent.e.windTunnelDirection = forward
+ if ( ent.IsPlayer() )
+ thread PlayerInWindTunnel( ent )
+ else
+ ent.SetOrigin( ent.GetOrigin() + <0,0,48> )
+ startTimes[ ent ] <- Time()
+ }
+ float startTime = startTimes[ ent ]
+ ent.e.windTunnelStartTime = startTime
+
+ float startHeight
+ if ( lifterFan )
+ {
+ if ( !( ent in startHeights ) )
+ startHeights[ ent ] <- ent.GetOrigin().z
+ startHeight = startHeights[ ent ]
+ }
+
+ // Figure out what force should be based on proximity to fan
+ vector pointAlongTunnel = GetClosestPointOnLineSegment( cylinderBottom, cylinderTop, testPos )
+ float distanceFromFanAlongTunnel = Distance( pointAlongTunnel, cylinderBottom )
+
+ float fanStrength = GetFanStrengthWithGeoBlockage( ent, testPos, cylinderBottom, cylinderTop, forward )
+ if ( ent.IsPlayer() )
+ ent.SetPlayerNetFloatOverTime( "FanRumbleStrength", fanStrength, 0.0 )
+
+ if ( lifterFan )
+ fanStrength = GraphCapped( distanceFromFanAlongTunnel, 0.0, fanPushDist, 1.0, 0.0 )
+
+ if ( ent.IsProjectile() )
+ fanStrength *= 2.0
+
+ if ( ent.GetModelName() == $"models/containers/barrel.mdl" )
+ fanStrength = 0.0
+
+ // Ramp up the push over time when first entering
+ float ramp = GraphCapped( Time(), startTime, startTime + FAN_PUSH_RAMP_TIME, 0.0, 1.0 )
+
+ float dt = 0.01666667 // old behavior
+ vector velocity = ent.GetVelocity()
+
+ // Apply push to the velocity
+ velocity += forward * ( dt * pushAccel * fanStrength )
+
+ // Decay other directional movement on the vector
+ if ( FAN_PUSH_DECAY_SIDE_VELOCITY )
+ {
+ vector velInOtherDirs = velocity - forward * DotProduct( velocity, forward )
+ float decayFrac = pow( ramp * fanStrength, dt )
+ vector loseVelInOtherDirs = velInOtherDirs * (1 - decayFrac)
+ velocity -= loseVelInOtherDirs
+ }
+
+ // Add some anti-gravity
+ velocity.z += dt * FAN_PUSH_ANTI_GRAVITY * fanStrength
+ ent.e.windTunnelStrength = fanStrength
+
+ // Apply new force to ent
+ ent.SetVelocity( velocity )
+
+ // Hack for drones. You can't set velocity on them yet so I'm doing this for now to test the gameplay
+ if ( ent.GetClassName() == "npc_drone" )
+ {
+ float zChange = dt * pushAccel * fanStrength * 1
+ ent.SetOrigin( ent.GetOrigin() + < 0, 0, zChange > )
+ }
+ }
+ else
+ {
+ if ( ent in startTimes )
+ {
+ ent.e.inWindTunnel = false
+ delete startTimes[ ent ]
+ }
+
+ if ( lifterFan && ent in startHeights )
+ {
+ delete startHeights[ ent ]
+ }
+
+ if ( ent.IsPlayer() )
+ ent.SetPlayerNetFloatOverTime( "FanRumbleStrength", 0.0, 1.0 )
+ }
+ }
+ WaitFrame()
+ }
+}
+
+void function PlayerInWindTunnel( entity player )
+{
+ //int poseIndex = player.LookupPoseParameterIndex( "windfrac" )
+
+ EndSignal( player, "OnDestroy" )
+ OnThreadEnd(
+ function() : ( player )
+ {
+ if ( FAN_DEBUG )
+ printt( "OUT!" )
+ if ( IsValid( player ) )
+ {
+ player.SetOneHandedWeaponUsageOff()
+ FadeOutSoundOnEntity( player, "Beacon_WindBuffet_Player", 1.0 )
+
+ player.kv.airSpeed = player.GetPlayerSettingsField( "airSpeed" )
+ player.kv.airAcceleration = player.GetPlayerSettingsField( "airAcceleration" )
+ }
+ }
+ )
+
+ if ( FAN_DEBUG )
+ printt( "IN!" )
+
+ bool playingWindBuffet = false
+
+ player.kv.airSpeed = 150
+ player.kv.airAcceleration = 650
+
+ while( player.e.inWindTunnel )
+ {
+ //player.GetViewModelEntity().SetPoseParameter( poseIndex, player.e.windTunnelStrength )
+
+ if ( player.e.windTunnelStrength > 0 )
+ {
+ player.SetOneHandedWeaponUsageOn()
+ if ( !playingWindBuffet )
+ {
+ EmitSoundOnEntityOnlyToPlayerWithFadeIn( player, player, "Beacon_WindBuffet_Player", 1.0 )
+ playingWindBuffet = true
+ }
+ }
+ else
+ {
+ player.SetOneHandedWeaponUsageOff()
+ if ( playingWindBuffet )
+ {
+ FadeOutSoundOnEntity( player, "Beacon_WindBuffet_Player", 1.0 )
+ playingWindBuffet = false
+ }
+ }
+ WaitFrame()
+ }
+}
+
+float function GetFanStrengthWithGeoBlockage( entity ent, vector testPos, vector cylinderBottom, vector cylinderTop, vector fanDirection )
+{
+ vector pointAlongFan = GetClosestPointOnLineSegment( cylinderBottom, cylinderTop, testPos )
+ vector vecFromFanCenter = testPos - pointAlongFan
+ vector traceEnd = cylinderBottom + vecFromFanCenter
+
+ // Trace from the entity towards the fan along the fan axis to see if we are getting blocked
+ TraceResults result = TraceLine( testPos, traceEnd, ent, TRACE_MASK_NPCSOLID, TRACE_COLLISION_GROUP_NONE )
+ if ( FAN_DEBUG )
+ {
+ DebugDrawLine( testPos, result.endPos, 255, 255, 0, true, 0.1 )
+ DebugDrawLine( result.endPos, traceEnd, 255, 255, 255, true, 0.1 )
+ //DebugDrawLine( <0,0,0>, result.endPos, 255, 0, 0, true, 0.1 )
+ }
+
+ float distFromCover = Distance( testPos, result.endPos )
+ float strength = GraphCapped( distFromCover, 256, 1024, 0.0, 1.0 )
+ if ( result.fraction == 1.0 )
+ strength = 1.0
+
+ //if ( FAN_DEBUG )
+ //{
+ // printt( "strength:", strength )
+ // printt( "fraction:", result.fraction )
+ // printt( "dist from fan:", Distance( testPos, cylinderBottom ) )
+ // printt( "distFromCover:", distFromCover )
+ //}
+
+ Assert( strength >= 0.0 && strength <= 1.0 )
+ return strength
+}
+
+void function FanOnSoundEffects( entity fanPusher, float radius, entity fanSoundEntity )
+{
+ // Turn on sound
+ if ( radius >= 350 )
+ {
+ EmitSoundOnEntity( fanSoundEntity, "Beacon_VerticalFanControl_On" )
+ if ( fanPusher.HasKey( "fan_loop_sound" ) )
+ EmitSoundOnEntity( fanSoundEntity, string( fanPusher.kv.fan_loop_sound ) )
+
+ }
+ else
+ {
+ EmitSoundOnEntity( fanSoundEntity, "Beacon_MediumBlueFan_On" )
+ string loopAlias = fanPusher.HasKey( "fan_loop_sound" ) ? string( fanPusher.kv.fan_loop_sound ) : "Beacon_MediumBlueFan_Loop_01"
+ EmitSoundOnEntity( fanSoundEntity, loopAlias )
+ //DebugDrawText( fanPusher.GetOrigin(), loopAlias, true, 90.0 )
+ }
+}
+
+void function FanOffSoundEffects( entity fanPusher, float radius, entity fanSoundEntity )
+{
+ // Turn off sound
+ if ( radius >= 350 )
+ {
+ string alias = "Beacon_VerticalFanControl_Off"
+ if ( fanPusher.HasKey( "shutoff_sound" ) )
+ alias = string( fanPusher.kv.shutoff_sound )
+ EmitSoundOnEntity( fanSoundEntity, alias )
+
+ if ( fanPusher.HasKey( "fan_loop_sound" ) )
+ StopSoundOnEntity( fanSoundEntity, string( fanPusher.kv.fan_loop_sound ) )
+ }
+ else
+ {
+ EmitSoundOnEntity( fanSoundEntity, "Beacon_MediumBlueFan_Off" )
+ string loopAlias = fanPusher.HasKey( "fan_loop_sound" ) ? string( fanPusher.kv.fan_loop_sound ) : "Beacon_MediumBlueFan_Loop_01"
+ StopSoundOnEntity( fanSoundEntity, loopAlias )
+ }
+} \ No newline at end of file