diff options
Diffstat (limited to 'Northstar.CustomServers/scripts/vscripts/_script_movers.gnut')
-rw-r--r-- | Northstar.CustomServers/scripts/vscripts/_script_movers.gnut | 1783 |
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 |