untyped global function BurnMeter_Init global function BurnMeter_SetBoostLimit global function BurnMeter_SetBoostRewardCount global function BurnMeter_GetLimitedRewardCount global function ForceSetGlobalBurncardOverride global function GetSelectedBurncardRefFromWeaponOrPlayer global function RunBurnCardUseFunc global function UseBurnCardWeapon global function UseBurnCardWeaponInCriticalSection global function BurnMeter_GiveRewardDirect global function GetBurnCardWeaponSkin global function InitBurnMeterPersistentData const float PHASE_REWIND_LENGTH = 2.0 // taken from wraith portal in apex, assuming it's the same as tf2's // 0.1 can be too fast for ttf2 scripts const float PHASE_REWIND_PATH_SNAPSHOT_INTERVAL = 0.2 // mainly controlled by this const, the higher the rewind path will be longer, assuming this one is like vanilla's const int PHASE_REWIND_MAX_SNAPSHOTS = int( PHASE_REWIND_LENGTH / PHASE_REWIND_PATH_SNAPSHOT_INTERVAL ) - 1 const int PHASE_REWIND_DATA_MAX_POSITIONS = int( PHASE_REWIND_LENGTH * 10 ) + 1 // hardcoded but maybe good const float AMPED_WEAPONS_LENGTH = 30.0 const int MAPHACK_PULSE_COUNT = 4 const float MAPHACK_PULSE_DELAY = 2.0 const float MAPHACK_PULSE_LENGTH = 0.5 struct { string forcedGlobalBurncardOverride = "" table boostRewardCount table boostLimits } file void function BurnMeter_Init() { // turret precaches // do we have to cache these on client? release builds sure don't PrecacheModel( Dev_GetAISettingAssetByKeyField_Global( "npc_turret_sentry_burn_card_ap", "DefaultModelName" ) ) PrecacheModel( Dev_GetAISettingAssetByKeyField_Global( "npc_turret_sentry_burn_card_at", "DefaultModelName" ) ) // setup burncard use funcs BurnReward_GetByRef( "burnmeter_amped_weapons" ).rewardAvailableCallback = PlayerUsesAmpedWeaponsBurncard BurnReward_GetByRef( "burnmeter_smart_pistol" ).rewardAvailableCallback = PlayerUsesSmartPistolBurncard BurnReward_GetByRef( "burnmeter_emergency_battery" ).rewardAvailableCallback = Burnmeter_EmergencyBattery BurnReward_GetByRef( "burnmeter_radar_jammer" ).rewardAvailableCallback = PlayerUsesRadarJammerBurncard BurnReward_GetByRef( "burnmeter_maphack" ).rewardAvailableCallback = PlayerUsesMaphackBurncard BurnReward_GetByRef( "burnmeter_phase_rewind" ).rewardAvailableCallback = PlayerUsesPhaseRewindBurncard // these ones aren't so important, they're either for fd ( unsupported rn ) or unused BurnReward_GetByRef( "burnmeter_harvester_shield" ).rewardAvailableCallback = PlayerUsesHarvesterShieldBurncard BurnReward_GetByRef( "burnmeter_amped_weapons_permanent" ).rewardAvailableCallback = PlayerUsesPermanentAmpedWeaponsBurncard BurnReward_GetByRef( "burnmeter_instant_battery" ).rewardAvailableCallback = Burnmeter_AmpedBattery BurnReward_GetByRef( "burnmeter_rodeo_grenade" ).rewardAvailableCallback = PlayerUsesRodeoGrenadeBurncard BurnReward_GetByRef( "burnmeter_nuke_titan" ).rewardAvailableCallback = PlayerUsesNukeTitanBurncard // unused in vanilla, fun though BurnMeter_SetBoostRewardCount( "burnmeter_ticks", 2 ) BurnMeter_SetBoostLimit( "burnmeter_ticks", 6 ) BurnMeter_SetBoostLimit( "burnmeter_ap_turret_weapon", 3 ) BurnMeter_SetBoostLimit( "burnmeter_at_turret_weapon", 3 ) BurnMeter_SetBoostLimit( "burnmeter_holopilot_nova", 3 ) // setup player callbacks AddCallback_GameStateEnter( eGameState.Playing, InitBurncardsForIntroPlayers ) AddCallback_OnClientConnected( InitBurncardsForLateJoiner ) AddCallback_OnPlayerRespawned( StartPhaseRewindLifetime ) AddCallback_OnPlayerRespawned( InitialisePermenantAmpedWeaponsForPlayer ) AddCallback_OnTitanBecomesPilot( RemoveAmpedWeaponsForTitanPilot ) // necessary signals RegisterSignal( "StopAmpedWeapons" ) } void function BurnMeter_SetBoostLimit( string burnRef, int limit ) { file.boostLimits[burnRef] <- limit } void function BurnMeter_SetBoostRewardCount( string burnRef, int rewardCount ) { file.boostRewardCount[burnRef] <- rewardCount } int function BurnMeter_GetLimitedRewardCount( entity player, string burnRef = "" ) { // added burnRef as a parameter, used for dice roll // means we dont call two lots of BurnReward_GetRandom() whilst also being able to give multiple items per dice roll (ticks) if (burnRef == "") { EarnObject earnObject = PlayerEarnMeter_GetReward( player ) burnRef = earnObject.ref } int limit = -1 int rewardCount = 1 if ( burnRef in file.boostLimits ) limit = file.boostLimits[burnRef] if ( burnRef in file.boostRewardCount ) rewardCount = file.boostRewardCount[burnRef] if ( limit < 0 ) return rewardCount int current = PlayerInventory_CountBurnRef( player, burnRef ) int delta = limit - current if ( delta <= 0 ) return 0 return int( min( delta, rewardCount ) ) } void function ForceSetGlobalBurncardOverride( string ref ) { file.forcedGlobalBurncardOverride = ref } string function GetSelectedBurncardRefFromWeaponOrPlayer( entity weapon, entity player ) { // determine the burncard we're using // in actual gameplay, this will always be the player's selected burncard // however, if we want to manually give burncards and such, we want to make sure they'll still work // so some extra work goes into this string ref = GetSelectedBurnCardRef( player ) if ( file.forcedGlobalBurncardOverride.len() > 0 ) ref = file.forcedGlobalBurncardOverride if ( IsValid( weapon ) ) { // determine via weapon mods, this assumes weapon mod names are the same as burn refs, which works in practice but is a bit weird // this does crash with the burnmeter_doublexp mod, but who cares, it doesn't get hit normally if ( weapon.GetWeaponClassName() == "mp_ability_burncardweapon" ) { foreach ( string mod in weapon.GetMods() ) if ( mod.find( "burnmeter_" ) == 0 ) return mod } // determine via weapon name in the case of stuff like holopilot etc else { // unfortunately, we have to hardcode this, we don't have a way of getting refs directly from weapons other than the burncard weapon // this should be modular at some point, wish we could just iterate over burncards and find ones with the current weapon, but this isn't possible switch ( weapon.GetWeaponClassName() ) { case "mp_ability_holopilot_nova": return "burnmeter_holopilot_nova" case "mp_weapon_arc_trap": return "burnmeter_arc_trap" case "mp_weapon_frag_drone": return "burnmeter_ticks" case "mp_weapon_hard_cover": return "burnmeter_hard_cover" case "mp_ability_turretweapon": // turret has 2 burncards, antititan and antipilot if( weapon.HasMod( "burnmeter_at_turret_weapon" ) || weapon.HasMod( "burnmeter_at_turret_weapon_inf" ) ) return "burnmeter_at_turret_weapon" else return "burnmeter_ap_turret_weapon" // note: cloak and stim both have burn_card_weapon_mod mods, but they aren't used and don't call burncard code at all, likely for tf1 infinite stim/cloak burncards? default: print( "tried to use unknown burncard weapon " + weapon.GetWeaponClassName() ) return "burnmeter_amped_weapons" } } } return ref } void function InitPlayerBurncards( entity player ) { string ref = GetSelectedBurncardRefFromWeaponOrPlayer( null, player ) BurnReward reward = BurnReward_GetByRef( ref ) player.SetPlayerNetInt( TOP_INVENTORY_ITEM_BURN_CARD_ID, reward.id ) if ( IsAlive( player ) ) thread PhaseRewindLifetime( player ) } void function InitBurncardsForIntroPlayers() { // gotta do this, since sh_burnmeter uses this netint foreach ( entity player in GetPlayerArray() ) InitPlayerBurncards( player ) } void function InitBurncardsForLateJoiner( entity player ) { // gotta do this, since sh_burnmeter uses this netint if ( GetGameState() > eGameState.Prematch ) InitPlayerBurncards( player ) } void function StartPhaseRewindLifetime( entity player ) { thread PhaseRewindLifetime( player ) } void function PhaseRewindLifetime( entity player ) { player.EndSignal( "OnDestroy" ) player.EndSignal( "OnDeath" ) OnThreadEnd( function() : ( player ) { player.p.burnCardPhaseRewindStruct.phaseRetreatSavedPositions.clear() }) while ( true ) { PhaseRewindData rewindData rewindData.origin = player.GetOrigin() rewindData.angles = < 0, player.EyeAngles().y, 0 > rewindData.velocity = player.GetVelocity() rewindData.wasInContextAction = player.ContextAction_IsActive() rewindData.wasCrouched = player.IsCrouched() if ( player.p.burnCardPhaseRewindStruct.phaseRetreatSavedPositions.len() >= PHASE_REWIND_DATA_MAX_POSITIONS ) { // shift all snapshots left for ( int i = 0; i < PHASE_REWIND_DATA_MAX_POSITIONS - 1; i++ ) player.p.burnCardPhaseRewindStruct.phaseRetreatSavedPositions[ i ] = player.p.burnCardPhaseRewindStruct.phaseRetreatSavedPositions[ i + 1 ] player.p.burnCardPhaseRewindStruct.phaseRetreatSavedPositions[ PHASE_REWIND_DATA_MAX_POSITIONS - 1 ] = rewindData } else player.p.burnCardPhaseRewindStruct.phaseRetreatSavedPositions.append( rewindData ) wait PHASE_REWIND_PATH_SNAPSHOT_INTERVAL } } void function RunBurnCardUseFunc( entity player, string itemRef ) { print( itemRef ) void functionref( entity ) ornull func = BurnReward_GetByRef( itemRef ).rewardAvailableCallback if ( func != null ) ( expect void functionref( entity ) ( func ) )( player ) else print( "tried to call usefunc for burncard " + itemRef + ", but func did not exist!" ) } void function UseBurnCardWeapon( entity weapon, entity player ) { string ref = GetSelectedBurncardRefFromWeaponOrPlayer( weapon, player ) Remote_CallFunction_Replay( player, "ServerCallback_RewardUsed", BurnReward_GetByRef( ref ).id ) RunBurnCardUseFunc( player, ref ) if ( PlayerEarnMeter_IsRewardAvailable( player ) ) PlayerEarnMeter_SetRewardUsed( player ) PlayerInventory_PopInventoryItem( player ) } void function UseBurnCardWeaponInCriticalSection( entity weapon, entity ownerPlayer ) { // ignoring critical section stuff, assuming it was necessary in tf1 where burncards were part of inventory, but not here UseBurnCardWeapon( weapon, ownerPlayer ) } void function BurnMeter_GiveRewardDirect( entity player, string itemRef ) { PlayerInventory_PushInventoryItemByBurnRef( player, itemRef ) Remote_CallFunction_Replay( player, "ServerCallback_RewardReadyMessage", player.s.respawnTime ) } int function GetBurnCardWeaponSkin( entity weapon ) { return GetBoostSkin( GetSelectedBurncardRefFromWeaponOrPlayer( weapon, weapon.GetOwner() ) ) } // stub void function InitBurnMeterPersistentData( entity player ) {} // burncard use funcs void function PlayerUsesAmpedWeaponsBurncard( entity player ) { thread PlayerUsesAmpedWeaponsBurncardThreaded( player ) } void function PlayerUsesAmpedWeaponsBurncardThreaded( entity player ) { array weapons = player.GetMainWeapons() //weapons.extend( player.GetOffhandWeapons() ) // idk? unsure of vanilla behaviour here foreach ( entity weapon in weapons ) { if( weapon.GetWeaponPrimaryClipCountMax() > 0 ) weapon.SetWeaponPrimaryClipCount( weapon.GetWeaponPrimaryClipCountMax() ) // kind of a fix to get ammo to full, cba to give new weapon weapon.RemoveMod( "silencer" ) // both this and the burnmod will override firing fx, if a second one overrides this we crash foreach ( string mod in GetWeaponBurnMods( weapon.GetWeaponClassName() ) ) { // catch incompatibilities just in case try { weapon.AddMod( mod ) } catch( ex ) { weapons.removebyvalue( weapon ) } } // needed to display amped weapon time left weapon.SetScriptFlags0( weapon.GetScriptFlags0() | WEAPONFLAG_AMPED ) weapon.SetScriptTime0( Time() + AMPED_WEAPONS_LENGTH ) } wait AMPED_WEAPONS_LENGTH // note: weapons may have been destroyed or picked up by other people by this point, so need to verify this foreach ( entity weapon in weapons ) { if ( !IsValid( weapon ) ) continue foreach ( string mod in GetWeaponBurnMods( weapon.GetWeaponClassName() ) ) weapon.RemoveMod( mod ) weapon.SetScriptFlags0( weapon.GetScriptFlags0() & ~WEAPONFLAG_AMPED ) } } void function RemoveAmpedWeaponsForTitanPilot( entity player, entity titan ) { if ( !( "hasPermenantAmpedWeapons" in player.s ) || !player.s.hasPermenantAmpedWeapons ) foreach ( entity weapon in player.GetMainWeapons() ) foreach ( string mod in GetWeaponBurnMods( weapon.GetWeaponClassName() ) ) weapon.RemoveMod( mod ) } void function PlayerUsesSmartPistolBurncard( entity player ) { // take secondary weapon array sidearms = player.GetMainWeapons() if ( sidearms.len() > 1 ) player.TakeWeaponNow( sidearms[ 1 ].GetWeaponClassName() ) // take secondary weapon player.GiveWeapon( "mp_weapon_smart_pistol" ) player.SetActiveWeaponByName( "mp_weapon_smart_pistol" ) // do we need to track the player losing smart pistol, then give their old weapon back? idk not implementing for now, check later } void function PlayerUsesRadarJammerBurncard( entity player ) { foreach ( entity otherPlayer in GetPlayerArray() ) { MessageToPlayer( otherPlayer, eEventNotifications.BurnMeter_RadarJammerUsed, player ) if ( otherPlayer.GetTeam() != player.GetTeam() ) StatusEffect_AddTimed( otherPlayer, eStatusEffect.minimap_jammed, 1.0, RADAR_JAM_TIME, RADAR_JAM_TIME ) } } void function PlayerUsesMaphackBurncard( entity player ) { thread PlayerUsesMaphackBurncardThreaded( player ) } void function PlayerUsesMaphackBurncardThreaded( entity player ) { player.EndSignal( "OnDestroy" ) // If the user disconnects, we need to clean up hanging sonar effects, so hold relevant data here. int playerTeam = player.GetTeam() bool cleanup = false array entities, affectedEntities OnThreadEnd( function() : ( cleanup, playerTeam, affectedEntities ) { if ( !cleanup ) // Map hack ended when sonar wasn't active, no cleanup needed return foreach ( entity ent in affectedEntities ) { if ( IsValid( ent ) ) SonarEnd( ent, playerTeam ) } DecrementSonarPerTeam( playerTeam ) } ) // todo: potentially look into ScanMinimap in _passives for doing this better? boost is pretty likely based off it pretty heavily for ( int i = 0; i < MAPHACK_PULSE_COUNT; i++ ) { EmitSoundOnEntityOnlyToPlayer( player, player, "Burn_Card_Map_Hack_Radar_Pulse_V1_1P" ) entities = GetPlayerArray() entities.extend( GetNPCArray() ) entities.extend( GetPlayerDecoyArray() ) IncrementSonarPerTeam( playerTeam ) if ( IsAlive( player ) ) // only owner can see the pulse Remote_CallFunction_Replay( player, "ServerCallback_SonarPulseFromPosition", player.GetOrigin().x, player.GetOrigin().y, player.GetOrigin().z, SONAR_GRENADE_RADIUS ) foreach ( entity ent in entities ) { if ( !IsValid( ent ) ) // Not sure why we can get invalid entities at this point continue if ( ent.IsPlayer() ) { // Map Hack also gives radar on enemies for longer than the sonar duration. if ( ent.GetTeam() == playerTeam ) thread ScanMinimap( ent, false, MAPHACK_PULSE_DELAY - 0.2 ) } if ( ent.GetTeam() != playerTeam ) { StatusEffect_AddTimed( ent, eStatusEffect.maphack_detected, 1.0, MAPHACK_PULSE_DELAY, 0.0 ) affectedEntities.append( ent ) SonarStart( ent, player.GetOrigin(), playerTeam, player ) } } cleanup = true wait MAPHACK_PULSE_LENGTH DecrementSonarPerTeam( playerTeam ) // JFS - loop through entities that were explicitly given sonar in case they switched teams during the wait foreach ( entity ent in affectedEntities ) { if ( IsValid( ent ) ) SonarEnd( ent, playerTeam ) } cleanup = false affectedEntities.clear() wait MAPHACK_PULSE_DELAY - MAPHACK_PULSE_LENGTH } } void function PlayerUsesPhaseRewindBurncard( entity player ) { thread PlayerUsesPhaseRewindBurncardThreaded( player ) } void function PlayerUsesPhaseRewindBurncardThreaded( entity player ) { player.EndSignal( "OnDestroy" ) player.EndSignal( "OnDeath" ) entity mover = CreateScriptMover( player.GetOrigin(), player.GetAngles() ) player.SetParent( mover, "REF" ) OnThreadEnd( function() : ( player, mover ) { CancelPhaseShift( player ) player.DeployWeapon() player.SetPredictionEnabled( true ) player.ClearParent() ViewConeFree( player ) mover.Destroy() }) array positions = clone player.p.burnCardPhaseRewindStruct.phaseRetreatSavedPositions array tempArray int segment = positions.len() / PHASE_REWIND_MAX_SNAPSHOTS for( int i = 0; i < PHASE_REWIND_MAX_SNAPSHOTS; i++ ) { if( positions.len() <= segment * i ) break tempArray.append( positions[ segment * i ] ) } positions = tempArray vector startOrigin = player.GetOrigin() ViewConeZero( player ) player.HolsterWeapon() player.SetPredictionEnabled( false ) PhaseShift( player, 0.0, positions.len() * PHASE_REWIND_PATH_SNAPSHOT_INTERVAL * 1.5 ) EmitSoundOnEntityOnlyToPlayer( player, player, "pilot_phaserewind_1p" ) EmitSoundOnEntityExceptToPlayer( player, player, "pilot_phaserewind_3p" ) for ( int i = positions.len() - 1; i > -1; i-- ) { // should looks better? float moveTime = PHASE_REWIND_PATH_SNAPSHOT_INTERVAL + 0.1 player.SetVelocity( -positions[ i ].velocity ) mover.NonPhysicsMoveTo( positions[ i ].origin, moveTime, 0, 0 ) mover.NonPhysicsRotateTo( positions[ i ].angles, moveTime, 0, 0 ) wait PHASE_REWIND_PATH_SNAPSHOT_INTERVAL } player.SetVelocity( <0, 0, 0> ) if( positions[0].wasCrouched ) player.SetOrigin( player.GetOrigin() + < 0, 0, 20 > ) // fix position after rewind PutPhaseRewindedPlayerInSafeSpot( player, 1 ) } void function PutPhaseRewindedPlayerInSafeSpot( entity player, int severity ) { vector baseOrigin = player.GetOrigin() if ( PutEntityInSafeSpot( player, player, null, < baseOrigin.x, baseOrigin.y + severity, baseOrigin.z >, baseOrigin ) ) return if ( PutEntityInSafeSpot( player, player, null, < baseOrigin.x, baseOrigin.y - severity, baseOrigin.z >, baseOrigin ) ) return if ( PutEntityInSafeSpot( player, player, null, < baseOrigin.x + severity, baseOrigin.y, baseOrigin.z >, baseOrigin ) ) return if ( PutEntityInSafeSpot( player, player, null, < baseOrigin.x - severity, baseOrigin.y, baseOrigin.z >, baseOrigin ) ) return if ( PutEntityInSafeSpot( player, player, null, < baseOrigin.x, baseOrigin.y, baseOrigin.z + severity >, baseOrigin ) ) return if ( PutEntityInSafeSpot( player, player, null, < baseOrigin.x, baseOrigin.y, baseOrigin.z - severity >, baseOrigin ) ) return return PutPhaseRewindedPlayerInSafeSpot( player, severity + 5 ) } void function PlayerUsesNukeTitanBurncard( entity player ) { thread PlayerUsesNukeBurncardThreaded( player ) } void function PlayerUsesNukeBurncardThreaded( entity player ) { Point spawnpoint = GetTitanReplacementPoint( player, false ) entity titan = CreateOgre( TEAM_UNASSIGNED, spawnpoint.origin, spawnpoint.angles ) DispatchSpawn( titan ) titan.kv.script_hotdrop = "4" thread NPCTitanHotdrops( titan, false, "at_hotdrop_drop_2knee_turbo" ) Remote_CallFunction_Replay( player, "ServerCallback_ReplacementTitanSpawnpoint", spawnpoint.origin.x, spawnpoint.origin.y, spawnpoint.origin.z, Time() + GetHotDropImpactTime( titan, "at_hotdrop_drop_2knee_turbo" ) + 1.6 ) DoomTitan( titan ) titan.SetBossPlayer(player) // Do this so that if we crush something we get awarded the kill. entity soul = titan.GetTitanSoul() soul.soul.nukeAttacker = player // Use this to get credit for the explosion kills. NPC_SetNuclearPayload( titan ) titan.WaitSignal( "ClearDisableTitanfall" ) titan.ClearBossPlayer() // Stop being the boss so we don't get an award for this titan blowing up. thread TitanEjectPlayer( titan, true ) } void function InitialisePermenantAmpedWeaponsForPlayer( entity player ) { player.s.hasPermenantAmpedWeapons <- false } void function PlayerUsesPermanentAmpedWeaponsBurncard( entity player ) { player.s.hasPermenantAmpedWeapons = true array weapons = player.GetMainWeapons() //weapons.extend( player.GetOffhandWeapons() ) // idk? unsure of vanilla behaviour here foreach ( entity weapon in weapons ) { weapon.RemoveMod( "silencer" ) // both this and the burnmod will override firing fx, if a second one overrides this we crash foreach ( string mod in GetWeaponBurnMods( weapon.GetWeaponClassName() ) ) { // catch incompatibilities just in case try { weapon.AddMod( mod ) } catch( ex ) { weapons.removebyvalue( weapon ) } } weapon.SetScriptFlags0( weapon.GetScriptFlags0() | WEAPONFLAG_AMPED ) } } void function PlayerUsesHarvesterShieldBurncard( entity player ) { player.SetPlayerNetInt( "numHarvesterShieldBoost", player.GetPlayerNetInt( "numHarvesterShieldBoost" ) + 1 ) } void function PlayerUsesRodeoGrenadeBurncard( entity player ) { player.SetPlayerNetInt( "numSuperRodeoGrenades", player.GetPlayerNetInt( "numSuperRodeoGrenades" ) + 1 ) } // unused burncard that's mentioned in a few areas and has a validiation function in sh_burnmeter ( BurnMeter_SummonReaperCanUse ), thought it'd be neat to add it void function PlayerUsesReaperfallBurncard( entity player ) { Point spawnpoint = GetTitanReplacementPoint( player, false ) entity reaper = CreateSuperSpectre( player.GetTeam(), spawnpoint.origin, spawnpoint.angles ) DispatchSpawn( reaper ) thread SuperSpectre_WarpFall( reaper ) }