untyped

global function BurnMeter_Init
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
const float PHASE_REWIND_PATH_SNAPSHOT_INTERVAL = 0.1
const int PHASE_REWIND_MAX_SNAPSHOTS = int( PHASE_REWIND_LENGTH / PHASE_REWIND_PATH_SNAPSHOT_INTERVAL )

const float AMPED_WEAPONS_LENGTH = 30.0

const int MAPHACK_PULSE_COUNT = 4
const float MAPHACK_PULSE_DELAY = 2.0

struct {
	string forcedGlobalBurncardOverride = ""
} 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

	// setup player callbacks
	AddCallback_GameStateEnter( eGameState.Playing, InitBurncardsForIntroPlayers )
	AddCallback_OnClientConnected( InitBurncardsForLateJoiner )

	AddCallback_OnPlayerRespawned( StartPhaseRewindLifetime )
	AddCallback_OnTitanBecomesPilot( RemoveAmpedWeaponsForTitanPilot )

	// necessary signals
	RegisterSignal( "StopAmpedWeapons" )
}

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 = player.GetAngles()
		rewindData.velocity = player.GetVelocity()
		rewindData.wasInContextAction = player.ContextAction_IsActive()
		rewindData.wasCrouched = player.IsCrouched()

		if ( player.p.burnCardPhaseRewindStruct.phaseRetreatSavedPositions.len() >= PHASE_REWIND_MAX_SNAPSHOTS )
		{
			// shift all snapshots left
			for ( int i = 0; i < PHASE_REWIND_MAX_SNAPSHOTS - 1; i++ )
				player.p.burnCardPhaseRewindStruct.phaseRetreatSavedPositions[ i ] = player.p.burnCardPhaseRewindStruct.phaseRetreatSavedPositions[ i + 1 ]

			player.p.burnCardPhaseRewindStruct.phaseRetreatSavedPositions[ PHASE_REWIND_MAX_SNAPSHOTS - 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 )

	// dont remove in RunBurnCardUseFunc because it can be called in non-burn_card_weapon_mod contexts
	// TODO: currently not sure how burncards can be stacked ( max clipcount for all burncards is 1, so can't just set that )
	// if this gets figured out, add a conditional check here to prevent removes if they've got burncards left
	if ( PlayerEarnMeter_IsRewardAvailable( player ) )
		PlayerEarnMeter_SetRewardUsed( player )

	player.TakeWeapon( BurnReward_GetByRef( ref ).weaponName )
}

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 )
{
	BurnReward burncard = BurnReward_GetByRef( itemRef )
	
	array<string> mods = [ "burn_card_weapon_mod" ]
	if ( burncard.extraWeaponMod != "" )
		mods.append( burncard.extraWeaponMod )
	
	// ensure inventory slot isn't full to avoid crash
	entity preexistingWeapon = player.GetOffhandWeapon( OFFHAND_INVENTORY )
	if ( IsValid( preexistingWeapon ) )
		player.TakeWeaponNow( preexistingWeapon.GetWeaponClassName() )
	
	player.GiveOffhandWeapon( burncard.weaponName, OFFHAND_INVENTORY, mods )
	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<entity> 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 )
			}
		}

		// 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 )
{
	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<entity> 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" )
	player.EndSignal( "OnDeath" )

	// 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" )
		array<entity> aliveplayers = GetPlayerArray()
		foreach ( entity otherPlayer in GetPlayerArray() )
		{
			Remote_CallFunction_Replay( otherPlayer, "ServerCallback_SonarPulseFromPosition", player.GetOrigin().x, player.GetOrigin().y, player.GetOrigin().z, SONAR_GRENADE_RADIUS )

			if ( otherPlayer.GetTeam() != player.GetTeam() && aliveplayers.find(otherPlayer) != -1 && aliveplayers.find(player) != -1 )
			{
				StatusEffect_AddTimed( otherPlayer, eStatusEffect.maphack_detected, 1.0, MAPHACK_PULSE_DELAY / 2, 0.0 )
				SonarStart( otherPlayer, player.GetOrigin(), player.GetTeam(), player )
				IncrementSonarPerTeam( player.GetTeam() )
			}
		}
		wait MAPHACK_PULSE_DELAY
		foreach ( entity otherPlayer in GetPlayerArray() ) {
			if ( otherPlayer.GetTeam() != player.GetTeam() && aliveplayers.find(otherPlayer) != -1 && aliveplayers.find(player) != -1 ) {
				SonarEnd (otherPlayer, player.GetTeam() )
				DecrementSonarPerTeam( player.GetTeam() )
			}
		}
	}
}

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<PhaseRewindData> positions = clone player.p.burnCardPhaseRewindStruct.phaseRetreatSavedPositions

	ViewConeZero( player )
	player.HolsterWeapon()
	player.SetPredictionEnabled( false )
	PhaseShift( player, 0.0, positions.len() * PHASE_REWIND_PATH_SNAPSHOT_INTERVAL * 1.5 )

	for ( int i = positions.len() - 1; i > -1; i-- )
	{
		mover.NonPhysicsMoveTo( positions[ i ].origin, PHASE_REWIND_PATH_SNAPSHOT_INTERVAL, 0, 0 )
		mover.NonPhysicsRotateTo( positions[ i ].angles, PHASE_REWIND_PATH_SNAPSHOT_INTERVAL, 0, 0 )
		wait PHASE_REWIND_PATH_SNAPSHOT_INTERVAL
	}

	// this isn't vanilla but it's cool lol, should prolly remove it tho
	player.SetVelocity( -positions[ positions.len() - 1 ].velocity )
}

void function PlayerUsesNukeTitanBurncard( entity player )
{
	thread PlayerUsesNukeBurncardThreaded( player )
}

void function PlayerUsesNukeBurncardThreaded( entity player )
{
	// if this is given manually ( i.e. not the equipped burnreward in inventory ), this will run at bad times
	// so do this check here, yes, this will cause people to lose their cards and get nothing, but better than free titan regens
	if ( !BurnMeterPlayer_CanUseReward( player, BurnReward_GetByRef( "burnmeter_nuke_titan" ) ) )
		return

	float ownedFrac = PlayerEarnMeter_GetOwnedFrac( player )

	// use player's titan loadout, but with warpfall so faster and no dome
	TitanLoadoutDef titanLoadout = GetTitanLoadoutForPlayer( player )
	titanLoadout.passive3 = "pas_warpfall"

	thread CreateTitanForPlayerAndHotdrop( player, GetTitanReplacementPoint( player, false ) )

	entity titan = player.GetPetTitan()
	SetTeam( titan, TEAM_UNASSIGNED ) // make it so you can kill yourself lol
	DoomTitan( titan )
	NPC_SetNuclearPayload( titan )
	// this should get run after the vanilla set_usable's event, so titan is never embarkable
	// embarking a titan in this state WILL kill the server so uhh, pretty bad
	AddAnimEvent( titan, "set_usable", void function( entity titan ) { titan.UnsetUsable() } )

	titan.WaitSignal( "TitanHotDropComplete" )
	AutoTitan_SelfDestruct( titan )

	while ( PlayerEarnMeter_GetMode( player ) == eEarnMeterMode.PET )
		WaitFrame()

	// restore original earnmeter values, no way to set earned that's exposed unfortunately
	PlayerEarnMeter_SetOwnedFrac( player, ownedFrac )
}

void function PlayerUsesPermanentAmpedWeaponsBurncard( entity player )
{
	array<entity> 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 )
}