diff options
Diffstat (limited to 'Northstar.CustomServers/mod/scripts')
10 files changed, 2840 insertions, 309 deletions
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/_codecallbacks_common.gnut b/Northstar.CustomServers/mod/scripts/vscripts/_codecallbacks_common.gnut index d2621db3..3704b5cc 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/_codecallbacks_common.gnut +++ b/Northstar.CustomServers/mod/scripts/vscripts/_codecallbacks_common.gnut @@ -46,6 +46,8 @@ global function CodeCallback_OnEntityChangedTeam global function AddEntityCallback_OnDamaged global function RemoveEntityCallback_OnDamaged +global function AddEntityCallback_OnFinalDamaged +global function RemoveEntityCallback_OnFinalDamaged global function AddEntityCallback_OnPostDamaged global function RemoveEntityCallback_OnPostDamaged global function AddEntityCallback_OnKilled @@ -121,6 +123,13 @@ void function CodeCallback_DamageEntity( entity ent, var damageInfo ) printt( " after class damage final callbacks:", DamageInfo_GetDamage( damageInfo ) ) #endif + foreach ( callbackFunc in ent.e.entFinalDamageCallbacks ) + callbackFunc( ent, damageInfo ) + + #if VERBOSE_DAMAGE_PRINTOUTS + printt( " after AddEntityCallback_OnFinalDamaged callbacks:", DamageInfo_GetDamage( damageInfo ) ) + #endif + // make destructible vehicles take more damage from DF_EXPLOSION damage type if ( "isDestructibleVehicle" in ent.s && DamageInfo_GetCustomDamageType( damageInfo ) & DF_EXPLOSION ) { @@ -566,7 +575,23 @@ void function RemoveEntityCallback_OnDamaged( entity ent, void functionref( enti Assert( index != -1, "Requested DamageCallback " + string( callbackFunc ) + " to be removed not found! " ) ent.e.entDamageCallbacks.fastremove( index ) - if ( ent.e.entDamageCallbacks.len() == 0 && ent.e.entPostDamageCallbacks.len() == 0 ) + if ( ent.e.entDamageCallbacks.len() == 0 && ent.e.entFinalDamageCallbacks.len() == 0 && ent.e.entPostDamageCallbacks.len() == 0 ) + ent.SetDamageNotifications( false ) +} + +void function AddEntityCallback_OnFinalDamaged( entity ent, void functionref( entity ent, var damageInfo ) callbackFunc ) +{ + Assert( !ent.e.entFinalDamageCallbacks.contains( callbackFunc ), "Already added " + string( callbackFunc ) + " to entity" ) + + ent.SetDamageNotifications( true ) + ent.e.entFinalDamageCallbacks.append( callbackFunc ) +} + +void function RemoveEntityCallback_OnFinalDamaged( entity ent, void functionref( entity ent, var damageInfo ) callbackFunc ) +{ + ent.e.entFinalDamageCallbacks.fastremovebyvalue( callbackFunc ) + + if ( ent.e.entFinalDamageCallbacks.len() == 0 && ent.e.entPostDamageCallbacks.len() == 0 && ent.e.entDamageCallbacks.len() == 0 ) ent.SetDamageNotifications( false ) } @@ -585,7 +610,7 @@ void function RemoveEntityCallback_OnPostDamaged( entity ent, void functionref( Assert( index != -1, "Requested PostDamageCallback " + string( callbackFunc ) + " to be removed not found! " ) ent.e.entPostDamageCallbacks.fastremove( index ) - if ( ent.e.entPostDamageCallbacks.len() == 0 && ent.e.entDamageCallbacks.len() == 0 ) + if ( ent.e.entPostDamageCallbacks.len() == 0 && ent.e.entDamageCallbacks.len() == 0 && ent.e.entFinalDamageCallbacks.len() == 0 ) ent.SetDamageNotifications( false ) } diff --git a/Northstar.CustomServers/mod/scripts/vscripts/_entitystructs.gnut b/Northstar.CustomServers/mod/scripts/vscripts/_entitystructs.gnut index 378ceae3..9dadea15 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/_entitystructs.gnut +++ b/Northstar.CustomServers/mod/scripts/vscripts/_entitystructs.gnut @@ -232,6 +232,7 @@ global struct ServerEntityStruct SpawnPointData spawnPointData array<void functionref( entity ent, var damageInfo )> entDamageCallbacks + array<void functionref( entity ent, var damageInfo )> entFinalDamageCallbacks array<void functionref( entity ent, var damageInfo )> entPostDamageCallbacks array<void functionref( entity titan, entity attacker )> entSegmentLostCallbacks array<void functionref( entity ent, var damageInfo, float actualShieldDamage )> entPostShieldDamageCallbacks diff --git a/Northstar.CustomServers/mod/scripts/vscripts/_utility.gnut b/Northstar.CustomServers/mod/scripts/vscripts/_utility.gnut index 3546e3b7..4e5c5aa9 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/_utility.gnut +++ b/Northstar.CustomServers/mod/scripts/vscripts/_utility.gnut @@ -4017,7 +4017,7 @@ int function GameTime_TimeLeftMinutes() if ( GetGameState() == eGameState.Prematch ) return int( ( expect float( GetServerVar( "gameStartTime" ) ) - Time()) / 60.0 ) - return floor( GameTime_TimeLimitMinutes() - GameTime_PlayingTime() / 60 ).tointeger() + return floor( GameTime_PlayingTime() / 60 ).tointeger() } int function GameTime_TimeLeftSeconds() @@ -4025,30 +4025,25 @@ int function GameTime_TimeLeftSeconds() if ( GetGameState() == eGameState.Prematch ) return int( expect float( GetServerVar( "gameStartTime" ) ) - Time() ) - return floor( GameTime_TimeLimitSeconds() - GameTime_PlayingTime() ).tointeger() + return GameTime_PlayingTime().tointeger() } +// WARN: this function includes WaitingForPlayers and Prematch duration! int function GameTime_Seconds() { return floor( Time() ).tointeger() } +// WARN: this function includes WaitingForPlayers Prematch duration! int function GameTime_Minutes() { return int( floor( GameTime_Seconds() / 60 ) ) } +// this function only counts the time limit during eGameState.Playing float function GameTime_PlayingTime() { - return GameTime_PlayingTimeSince( Time() ) -} - -float function GameTime_PlayingTimeSince( float sinceTime ) -{ int gameState = GetGameState() - - // temp fix because i have no fucking clue why this crashes - if ( gameState < eGameState.Playing ) return 0 @@ -4057,17 +4052,15 @@ float function GameTime_PlayingTimeSince( float sinceTime ) if ( gameState > eGameState.SuddenDeath ) return (expect float( GetServerVar( "roundEndTime" ) ) - expect float( GetServerVar( "roundStartTime" ) ) ) else - return sinceTime - expect float( GetServerVar( "roundStartTime" ) ) - + return floor( expect float( GetServerVar( "roundEndTime" ) ) - Time() ) } else { if ( gameState > eGameState.SuddenDeath ) return (expect float( GetServerVar( "gameEndTime" ) ) - expect float( GetServerVar( "gameStartTime" ) ) ) else - return sinceTime - expect float( GetServerVar( "gameStartTime" ) ) + return floor( expect float( GetServerVar( "gameEndTime" ) ) - Time() ) } - unreachable } @@ -4411,4 +4404,4 @@ bool function PlayerHasTitan( entity player ) return true return false -}
\ No newline at end of file +} diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_ai_gamemodes.gnut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_ai_gamemodes.gnut index 0fad768c..4ed7ee4a 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_ai_gamemodes.gnut +++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_ai_gamemodes.gnut @@ -1,7 +1,6 @@ global function AiGameModes_Init -global function AiGameModes_SetGruntWeapons -global function AiGameModes_SetSpectreWeapons +global function AiGameModes_SetNPCWeapons global function AiGameModes_SpawnDropShip global function AiGameModes_SpawnDropPod @@ -15,25 +14,20 @@ const INTRO_DROPSHIP_CUTOFF = 2000 struct { - array< string > gruntWeapons = [ "mp_weapon_rspn101" ] - array< string > spectreWeapons = [ "mp_weapon_hemlok_smg" ] + table< string, array<string> > npcWeaponsTable // npcs have their default weapon in aisettings file } file void function AiGameModes_Init() { - } //------------------------------------------------------ -void function AiGameModes_SetGruntWeapons( array< string > weapons ) +void function AiGameModes_SetNPCWeapons( string npcClass, array<string> weapons ) { - file.gruntWeapons = weapons -} - -void function AiGameModes_SetSpectreWeapons( array< string > weapons ) -{ - file.spectreWeapons = weapons + if ( !( npcClass in file.npcWeaponsTable ) ) + file.npcWeaponsTable[ npcClass ] <- [] + file.npcWeaponsTable[ npcClass ] = weapons } //------------------------------------------------------ @@ -59,7 +53,7 @@ void function AiGameModes_SpawnDropShip( vector pos, vector rot, int team, int c foreach ( guy in guys ) { - ReplaceWeapon( guy, file.gruntWeapons[ RandomInt( file.gruntWeapons.len() ) ], [] ) + SetUpNPCWeapons( guy ) guy.EnableNPCFlag( NPC_ALLOW_PATROL | NPC_ALLOW_INVESTIGATE | NPC_ALLOW_HAND_SIGNALS | NPC_ALLOW_FLEE ) } @@ -68,31 +62,23 @@ void function AiGameModes_SpawnDropShip( vector pos, vector rot, int team, int c } -void function AiGameModes_SpawnDropPod( vector pos, vector rot, int team, string content /*( ͡° ͜ʖ ͡°)*/, void functionref( array<entity> guys ) squadHandler = null ) +void function AiGameModes_SpawnDropPod( vector pos, vector rot, int team, string content /*( ͡° ͜ʖ ͡°)*/, void functionref( array<entity> guys ) squadHandler = null, int flags = 0 ) { - string squadName = MakeSquadName( team, UniqueString( "" ) ) - array<entity> guys - entity pod = CreateDropPod( pos, <0,0,0> ) - InitFireteamDropPod( pod ) - + InitFireteamDropPod( pod, flags ) + + waitthread LaunchAnimDropPod( pod, "pod_testpath", pos, rot ) + + string squadName = MakeSquadName( team, UniqueString( "" ) ) + array<entity> guys for ( int i = 0; i < 4 ;i++ ) { entity npc = CreateNPC( content, team, pos,<0,0,0> ) DispatchSpawn( npc ) SetSquad( npc, squadName ) - switch ( content ) - { - case "npc_soldier": - ReplaceWeapon( npc, file.gruntWeapons[ RandomInt( file.gruntWeapons.len() ) ], [] ) - break - - case "npc_spectre": - ReplaceWeapon( npc, file.spectreWeapons[ RandomInt( file.spectreWeapons.len() ) ], [] ) - break - } + SetUpNPCWeapons( npc ) npc.SetParent( pod, "ATTACH", true ) @@ -100,25 +86,26 @@ void function AiGameModes_SpawnDropPod( vector pos, vector rot, int team, string guys.append( npc ) } - // The order here is different so we can show on minimap while were still falling + ActivateFireteamDropPod( pod, guys ) + + // start searching for enemies if ( squadHandler != null ) thread squadHandler( guys ) - - waitthread LaunchAnimDropPod( pod, "pod_testpath", pos, rot ) - - ActivateFireteamDropPod( pod, guys ) } +const float REAPER_WARPFALL_DELAY = 4.7 // same as fd does void function AiGameModes_SpawnReaper( vector pos, vector rot, int team, string aiSettings = "", void functionref( entity reaper ) reaperHandler = null ) { - thread Reaper_Spawnpoint( pos, team, 11.2 ) + float reaperLandTime = REAPER_WARPFALL_DELAY + 1.2 // reaper takes ~1.2s to warpfall + thread HotDrop_Spawnpoint( pos, team, reaperLandTime, false, damagedef_reaper_fall ) - wait 10 - // spawn reapers right before it warpfalls, or round_end clean up will crash the game + wait REAPER_WARPFALL_DELAY entity reaper = CreateSuperSpectre( team, pos, rot ) + reaper.EndSignal( "OnDestroy" ) // reaper highlight Highlight_SetFriendlyHighlight( reaper, "sp_enemy_pilot" ) - reaper.Highlight_SetParam( 1, 0, < 3,3,3 > ) + reaper.Highlight_SetParam( 1, 0, < 1,1,1 > ) + SetDefaultMPEnemyHighlight( reaper ) Highlight_SetEnemyHighlight( reaper, "enemy_titan" ) SetSpawnOption_Titanfall( reaper ) @@ -127,15 +114,18 @@ void function AiGameModes_SpawnReaper( vector pos, vector rot, int team, string if ( aiSettings != "" ) SetSpawnOption_AISettings( reaper, aiSettings ) + HideName( reaper ) // prevent flash a name onto it DispatchSpawn( reaper ) - + + reaper.WaitSignal( "WarpfallComplete" ) + ShowName( reaper ) // show name again after drop if ( reaperHandler != null ) thread reaperHandler( reaper ) } // copied from cl_replacement_titan_hud.gnut -void function Reaper_Spawnpoint( vector origin, int team, float impactTime, bool hasFriendlyWarning = false ) +void function HotDrop_Spawnpoint( vector origin, int team, float impactTime, bool hasFriendlyWarning = false, int damageDef = -1 ) { array<entity> targetEffects = [] vector surfaceNormal = < 0, 0, 1 > @@ -146,32 +136,50 @@ void function Reaper_Spawnpoint( vector origin, int team, float impactTime, bool { entity effectFriendly = StartParticleEffectInWorld_ReturnEntity( index, origin, surfaceNormal ) SetTeam( effectFriendly, team ) - EffectSetControlPointVector( effectFriendly, 1, < 128,188,255 > ) + EffectSetControlPointVector( effectFriendly, 1, FRIENDLY_COLOR_FX ) effectFriendly.kv.VisibilityFlags = ENTITY_VISIBLE_TO_FRIENDLY + effectFriendly.DisableHibernation() // prevent it from fading out targetEffects.append( effectFriendly ) } entity effectEnemy = StartParticleEffectInWorld_ReturnEntity( index, origin, surfaceNormal ) SetTeam( effectEnemy, team ) - EffectSetControlPointVector( effectEnemy, 1, < 255,99,0 > ) + EffectSetControlPointVector( effectEnemy, 1, ENEMY_COLOR_FX ) effectEnemy.kv.VisibilityFlags = ENTITY_VISIBLE_TO_ENEMY + effectEnemy.DisableHibernation() // prevent it from fading out targetEffects.append( effectEnemy ) - + + // so enemy npcs will mostly avoid them + entity damageAreaInfo + if ( damageDef > -1 ) + { + damageAreaInfo = CreateEntity( "info_target" ) + DispatchSpawn( damageAreaInfo ) + AI_CreateDangerousArea_DamageDef( damageDef, damageAreaInfo, team, true, true ) + } + wait impactTime + // clean up foreach( entity targetEffect in targetEffects ) { if ( IsValid( targetEffect ) ) EffectStop( targetEffect ) } + if ( IsValid( damageAreaInfo ) ) + damageAreaInfo.Destroy() } // including aisettings stuff specifically for at bounty titans +const float TITANFALL_WARNING_DURATION = 5.0 void function AiGameModes_SpawnTitan( vector pos, vector rot, int team, string setFile, string aiSettings = "", void functionref( entity titan ) titanHandler = null ) { entity titan = CreateNPCTitan( setFile, TEAM_BOTH, pos, rot ) SetSpawnOption_Titanfall( titan ) SetSpawnOption_Warpfall( titan ) + + // modified: do a hotdrop spawnpoint warning + thread HotDrop_Spawnpoint( pos, team, TITANFALL_WARNING_DURATION, false, damagedef_titan_fall ) if ( aiSettings != "" ) SetSpawnOption_AISettings( titan, aiSettings ) @@ -182,12 +190,27 @@ void function AiGameModes_SpawnTitan( vector pos, vector rot, int team, string s thread titanHandler( titan ) } -// entity.ReplaceActiveWeapon gave grunts archers sometimes, this is my replacement for it -void function ReplaceWeapon( entity guy, string weapon, array<string> mods ) +void function SetUpNPCWeapons( entity guy ) { - guy.TakeActiveWeapon() - guy.GiveWeapon( weapon, mods ) - guy.SetActiveWeaponByName( weapon ) + string className = guy.GetClassName() + + array<string> mainWeapons + if ( className in file.npcWeaponsTable ) + mainWeapons = file.npcWeaponsTable[ className ] + + if ( mainWeapons.len() == 0 ) // no valid weapons + return + + // take off existing main weapons, or sometimes they'll have a archer by default + foreach ( entity weapon in guy.GetMainWeapons() ) + guy.TakeWeapon( weapon.GetWeaponClassName() ) + + if ( mainWeapons.len() > 0 ) + { + string weaponName = mainWeapons[ RandomInt( mainWeapons.len() ) ] + guy.GiveWeapon( weaponName ) + guy.SetActiveWeaponByName( weaponName ) + } } // Checks if we can spawn a dropship at a node, this should guarantee dropship ziplines diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_aitdm.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_aitdm.nut index fae778d6..f47ee90f 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_aitdm.nut +++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_aitdm.nut @@ -1,22 +1,36 @@ untyped global function GamemodeAITdm_Init -const SQUADS_PER_TEAM = 3 +// these are now default settings +const int SQUADS_PER_TEAM = 4 -const REAPERS_PER_TEAM = 2 +const int REAPERS_PER_TEAM = 2 -const LEVEL_SPECTRES = 125 -const LEVEL_STALKERS = 380 -const LEVEL_REAPERS = 500 +const int LEVEL_SPECTRES = 125 +const int LEVEL_STALKERS = 380 +const int LEVEL_REAPERS = 500 + +// add settings +global function AITdm_SetSquadsPerTeam +global function AITdm_SetReapersPerTeam +global function AITdm_SetLevelSpectres +global function AITdm_SetLevelStalkers +global function AITdm_SetLevelReapers struct { // Due to team based escalation everything is an array - array< int > levels = [ LEVEL_SPECTRES, LEVEL_SPECTRES ] + array< int > levels = [] // Initilazed in `Spawner_Threaded` array< array< string > > podEntities = [ [ "npc_soldier" ], [ "npc_soldier" ] ] array< bool > reapers = [ false, false ] -} file + // default settings + int squadsPerTeam = SQUADS_PER_TEAM + int reapersPerTeam = REAPERS_PER_TEAM + int levelSpectres = LEVEL_SPECTRES + int levelStalkers = LEVEL_STALKERS + int levelReapers = LEVEL_REAPERS +} file void function GamemodeAITdm_Init() { @@ -34,18 +48,47 @@ void function GamemodeAITdm_Init() if ( GetCurrentPlaylistVarInt( "aitdm_archer_grunts", 0 ) == 0 ) { - AiGameModes_SetGruntWeapons( [ "mp_weapon_rspn101", "mp_weapon_dmr", "mp_weapon_r97", "mp_weapon_lmg" ] ) - AiGameModes_SetSpectreWeapons( [ "mp_weapon_hemlok_smg", "mp_weapon_doubletake", "mp_weapon_mastiff" ] ) + AiGameModes_SetNPCWeapons( "npc_soldier", [ "mp_weapon_rspn101", "mp_weapon_dmr", "mp_weapon_r97", "mp_weapon_lmg" ] ) + AiGameModes_SetNPCWeapons( "npc_spectre", [ "mp_weapon_hemlok_smg", "mp_weapon_doubletake", "mp_weapon_mastiff" ] ) + AiGameModes_SetNPCWeapons( "npc_stalker", [ "mp_weapon_hemlok_smg", "mp_weapon_lstar", "mp_weapon_mastiff" ] ) } else { - AiGameModes_SetGruntWeapons( [ "mp_weapon_rocket_launcher" ] ) - AiGameModes_SetSpectreWeapons( [ "mp_weapon_rocket_launcher" ] ) + AiGameModes_SetNPCWeapons( "npc_soldier", [ "mp_weapon_rocket_launcher" ] ) + AiGameModes_SetNPCWeapons( "npc_spectre", [ "mp_weapon_rocket_launcher" ] ) + AiGameModes_SetNPCWeapons( "npc_stalker", [ "mp_weapon_rocket_launcher" ] ) } ScoreEvent_SetupEarnMeterValuesForMixedModes() } +// add settings +void function AITdm_SetSquadsPerTeam( int squads ) +{ + file.squadsPerTeam = squads +} + +void function AITdm_SetReapersPerTeam( int reapers ) +{ + file.reapersPerTeam = reapers +} + +void function AITdm_SetLevelSpectres( int level ) +{ + file.levelSpectres = level +} + +void function AITdm_SetLevelStalkers( int level ) +{ + file.levelStalkers = level +} + +void function AITdm_SetLevelReapers( int level ) +{ + file.levelReapers = level +} +// + // Starts skyshow, this also requiers AINs but doesn't crash if they're missing void function OnPrematchStart() { @@ -74,11 +117,9 @@ void function HandleScoreEvent( entity victim, entity attacker, var damageInfo ) // Basic checks if ( victim == attacker || !( attacker.IsPlayer() || attacker.IsTitan() ) || GetGameState() != eGameState.Playing ) return - // Hacked spectre filter if ( victim.GetOwner() == attacker ) return - // NPC titans without an owner player will not count towards any team's score if ( attacker.IsNPC() && attacker.IsTitan() && !IsValid( GetPetTitanOwner( attacker ) ) ) return @@ -193,7 +234,7 @@ void function SpawnIntroBatch_Threaded( int team ) int ships = shipNodes.len() - for ( int i = 0; i < SQUADS_PER_TEAM; i++ ) + for ( int i = 0; i < file.squadsPerTeam; i++ ) { if ( pods != 0 || ships == 0 ) { @@ -238,6 +279,7 @@ void function Spawner_Threaded( int team ) // used to index into escalation arrays int index = team == TEAM_MILITIA ? 0 : 1 + file.levels = [ file.levelSpectres, file.levelSpectres ] // due we added settings, should init levels here! while( true ) { @@ -252,7 +294,7 @@ void function Spawner_Threaded( int team ) if ( file.reapers[ index ] ) { array< entity > points = SpawnPoints_GetDropPod() - if ( reaperCount < REAPERS_PER_TEAM ) + if ( reaperCount < file.reapersPerTeam ) { entity node = points[ GetSpawnPointIndex( points, team ) ] waitthread AiGameModes_SpawnReaper( node.GetOrigin(), node.GetAngles(), team, "npc_super_spectre_aitdm", ReaperHandler ) @@ -260,7 +302,7 @@ void function Spawner_Threaded( int team ) } // NORMAL SPAWNS - if ( count < SQUADS_PER_TEAM * 4 - 2 ) + if ( count < file.squadsPerTeam * 4 - 2 ) { string ent = file.podEntities[ index ][ RandomInt( file.podEntities[ index ].len() ) ] @@ -306,19 +348,19 @@ void function Escalate( int team ) // Based on score escalate a team switch ( file.levels[ index ] ) { - case LEVEL_SPECTRES: - file.levels[ index ] = LEVEL_STALKERS + case file.levelSpectres: + file.levels[ index ] = file.levelStalkers file.podEntities[ index ].append( "npc_spectre" ) SetGlobalNetInt( defcon, 2 ) return - case LEVEL_STALKERS: - file.levels[ index ] = LEVEL_REAPERS + case file.levelStalkers: + file.levels[ index ] = file.levelReapers file.podEntities[ index ].append( "npc_stalker" ) SetGlobalNetInt( defcon, 3 ) return - case LEVEL_REAPERS: + case file.levelReapers: file.reapers[ index ] = true SetGlobalNetInt( defcon, 4 ) return @@ -355,30 +397,47 @@ int function GetSpawnPointIndex( array< entity > points, int team ) // AI can also flee deeper into their zone suggesting someone spent way too much time on this void function SquadHandler( array<entity> guys ) { + int team = guys[0].GetTeam() + // show the squad enemy radar + array<entity> players = GetPlayerArrayOfEnemies( team ) + foreach ( entity guy in guys ) + { + if ( IsAlive( guy ) ) + { + foreach ( player in players ) + guy.Minimap_AlwaysShow( 0, player ) + } + } + // Not all maps have assaultpoints / have weird assault points ( looking at you ac ) // So we use enemies with a large radius - array< entity > points = GetNPCArrayOfEnemies( guys[0].GetTeam() ) - - if ( points.len() == 0 ) + while ( GetNPCArrayOfEnemies( team ).len() == 0 ) // if we can't find any enemy npcs, keep waiting + WaitFrame() + + // our waiting is end, check if any soldiers left + bool squadAlive = false + foreach ( entity guy in guys ) + { + if ( IsAlive( guy ) ) + squadAlive = true + else + guys.removebyvalue( guy ) + } + if ( !squadAlive ) return + + array<entity> points = GetNPCArrayOfEnemies( team ) vector point point = points[ RandomInt( points.len() ) ].GetOrigin() - array<entity> players = GetPlayerArrayOfEnemies( guys[0].GetTeam() ) - - // Setup AI + // Setup AI, first assault point foreach ( guy in guys ) { guy.EnableNPCFlag( NPC_ALLOW_PATROL | NPC_ALLOW_INVESTIGATE | NPC_ALLOW_HAND_SIGNALS | NPC_ALLOW_FLEE ) guy.AssaultPoint( point ) guy.AssaultSetGoalRadius( 1600 ) // 1600 is minimum for npc_stalker, works fine for others - - // show on enemy radar - foreach ( player in players ) - guy.Minimap_AlwaysShow( 0, player ) - - + //thread AITdm_CleanupBoredNPCThread( guy ) } @@ -396,16 +455,32 @@ void function SquadHandler( array<entity> guys ) // Stop func if our squad has been killed off if ( guys.len() == 0 ) return + } + + // Get point and send our whole squad to it + points = GetNPCArrayOfEnemies( team ) + if ( points.len() == 0 ) // can't find any points here + { + // Have to wait some amount of time before continuing + // because if we don't the server will continue checking this + // forever, aren't loops fun? + // This definitely didn't waste ~8 hours of my time reverting various + // launcher PRs before finding this mods PR that caused servers to + // freeze forever before having their process killed by the dedi watchdog + // without any logging. If anyone reads this, PLEASE add logging to your scripts + // for when weird edge cases happen, it can literally only help debugging. -Spoon + WaitFrame() + continue + } - // Get point and send guy to it - points = GetNPCArrayOfEnemies( guy.GetTeam() ) - if ( points.len() == 0 ) - continue - - point = points[ RandomInt( points.len() ) ].GetOrigin() - - guy.AssaultPoint( point ) + point = points[ RandomInt( points.len() ) ].GetOrigin() + + foreach ( guy in guys ) + { + if ( IsAlive( guy ) ) + guy.AssaultPoint( point ) } + wait RandomFloatRange(5.0,15.0) } } diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_at.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_at.nut index 915e03e0..c61cb585 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_at.nut +++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_at.nut @@ -1,12 +1,54 @@ +untyped // AddCallback_OnUseEntity() needs this global function GamemodeAt_Init global function RateSpawnpoints_AT -const int BH_AI_TEAM = TEAM_BOTH -const int BOUNTY_TITAN_DAMAGE_POOL = 400 // Rewarded for damage -const int BOUNTY_TITAN_KILL_REWARD = 100 // Rewarded for kill -const float WAVE_STATE_TRANSITION_TIME = 5.0 +// Old bobr note which still applies after a year :) +// IMPLEMENTATION NOTES: +// bounty hunt is a mode that was clearly pretty heavily developed, and had alot of scrapped concepts (i.e. most wanted player bounties, turret bounties, collectable blackbox objectives) +// in the interest of time, this script isn't gonna support any of that atm +// alot of the remote functions also take parameters that aren't used, i'm not gonna populate these and just use default values for now instead +// however, if you do want to mess with this stuff, almost all the remote functions for this stuff are still present in cl_gamemode_at, and should work fine with minimal fuckery in my experience + + +// Bank settings +const float AT_BANKS_OPEN_DURATION = 45.0 // Bank open time +const int AT_BANK_DEPOSIT_RATE = 10 // Amount deposited per second +const int AT_BANK_DEPOSIT_RADIUS = 256 // bank radius for depositing +const float AT_BANK_FORCE_CLOSE_DELAY = 4.0 // If all bonus money has been deposited close the banks after this constant early + +// TODO: The reference function no longer exists, check if this still holds true +// VoyageDB: HACK score events... respawn made things in AT_SetScoreEventOverride() really messed up, have to do some hack here +const array<string> AT_ENABLE_SCOREEVENTS = +[ + // these are disabled in AT_SetScoreEventOverride(), but related scoreEvents are not implemented into gamemode + // needs to re-enable them + "DoomTitan", + "DoomAutoTitan" +] +const array<string> AT_DISABLE_SCOREEVENTS = +[ + // these are missed in AT_SetScoreEventOverride(), but game actually used them + // needs to disable them + "KillStalker" +] + +// Wave settings +// General +const int AT_AI_TEAM = TEAM_BOTH // Allow AI to attack and be attacked by both player teams +const float AT_FIRST_WAVE_START_DELAY = 10.0 // First wave has an extra delay before begining +const float AT_WAVE_TRANSITION_DELAY = 5.0 // Time between each wave and banks opening/closing +const float AT_WAVE_END_ANNOUNCEMENT_DELAY = 1.0 // Extra wait before announcing wave cleaned + +// Squad settings +const int AT_DROPPOD_SQUADS_ALLOWED_ON_FIELD = 4 // default is 4 droppod squads on field, won't use if AT_USE_TOTAL_ALLOWED_ON_FIELD_CHECK turns on // TODO: verify this -const array<string> VALID_BOUNTY_TITAN_SETTINGS = [ +// Titan bounty settings +const float AT_BOUNTY_TITAN_CHECK_DELAY = 10.0 // wait for bounty titans landing before we start checking their life state +const float AT_BOUNTY_TITAN_HEALTH_MULTIPLIER = 3 // TODO: Verify this + +// Titan boss settings, check sh_gamemode_at.nut for more info +const array<string> AT_BOUNTY_TITANS_AI_SETTINGS = +[ "npc_titan_atlas_stickybomb_bounty", "npc_titan_atlas_tracker_bounty", "npc_titan_ogre_minigun_bounty", @@ -16,102 +58,577 @@ const array<string> VALID_BOUNTY_TITAN_SETTINGS = [ "npc_titan_atlas_vanguard_bounty" ] +// Extra +// Respawn didn't use the "totalAllowedOnField" for npc spawning, they only allow 1 squad to be on field for each type of npc. enabling this might cause too much npcs spawning and crash the game +const bool AT_USE_TOTAL_ALLOWED_ON_FIELD_CHECK = false -// IMPLEMENTATION NOTES: -// bounty hunt is a mode that was clearly pretty heavily developed, and had alot of scrapped concepts (i.e. most wanted player bounties, turret bounties, collectable blackbox objectives) -// in the interest of time, this script isn't gonna support any of that atm -// alot of the remote functions also take parameters that aren't used, i'm not gonna populate these and just use default values for now instead -// however, if you do want to mess with this stuff, almost all the remote functions for this stuff are still present in cl_gamemode_at, and should work fine with minimal fuckery in my experience +// Objectives +const int AT_OBJECTIVE_EMPTY = -1 // Remove objective +const int AT_OBJECTIVE_KILL_DZ = 104 // #AT_OBJECTIVE_KILL_DZ +const int AT_OBJECTIVE_KILL_DZ_MULTI = 105 // #AT_OBJECTIVE_KILL_DZ_MULTI +const int AT_OBJECTIVE_KILL_BOSS = 106 // #AT_OBJECTIVE_KILL_BOSS +const int AT_OBJECTIVE_KILL_BOSS_MULTI = 107 // #AT_OBJECTIVE_KILL_BOSS_MULTI +const int AT_OBJECTIVE_BANK_OPEN = 109 // #AT_BANK_OPEN_OBJECTIVE -struct { - array<entity> campsToRegisterOnEntitiesDidLoad +// When a player tries to deposit when they have 0 bonus money +// we show a help mesage, this is the ratelimit for that message +// so that we dont spam it too much +const float AT_PLAYER_HUD_MESSAGE_COOLDOWN = 2.5 +// Due to bad navmeshes NPCs may wonder off to bumfuck nowhere or the game +// might teleport them into the map while trying to correct their position +// This obviously breaks bounty hunt where the objective is to kill ALL ai +// so we try to cleanup the camps after a set amount of time of inactivity +const int AT_CAMP_BORED_NPCS_LEFT_TO_START_CLEANUP = 3 +const float AT_CAMP_BORED_CLEANUP_WAIT = 60.0 +struct +{ array<entity> banks array<AT_WaveOrigin> camps + + // Used to track ScriptmanagedEntArrays of ai squads + table< int, array<int> > campScriptEntArrays - table< int, table< string, int > > trackedCampNPCSpawns + table< entity, bool > titanIsBountyBoss + table< entity, int > bountyTitanRewards + table< entity, int > npcStolenBonus + table< entity, bool > playerBankUploading + table< entity, table<entity, int> > playerSavedBountyDamage + table< entity, float > playerHudMessageAllowedTime } file void function GamemodeAt_Init() { - AddCallback_GameStateEnter( eGameState.Playing, RunATGame ) - + // wave + RegisterSignal( "ATWaveEnd" ) + // camp + RegisterSignal( "ATCampClean" ) + RegisterSignal( "ATAllCampsClean" ) + + // Set-up score callbacks + ScoreEvent_SetupEarnMeterValuesForMixedModes() + AddDamageFinalCallback( "npc_titan", OnNPCTitanFinalDamaged ) + AddCallback_OnPlayerKilled( AT_PlayerOrNPCKilledScoreEvent ) + AddCallback_OnNPCKilled( AT_PlayerOrNPCKilledScoreEvent ) + + // Set npc weapons + AiGameModes_SetNPCWeapons( "npc_soldier", [ "mp_weapon_rspn101", "mp_weapon_dmr", "mp_weapon_r97", "mp_weapon_lmg" ] ) + AiGameModes_SetNPCWeapons( "npc_spectre", [ "mp_weapon_hemlok_smg", "mp_weapon_doubletake", "mp_weapon_mastiff" ] ) + AiGameModes_SetNPCWeapons( "npc_stalker", [ "mp_weapon_hemlok_smg", "mp_weapon_lstar", "mp_weapon_mastiff" ] ) + + // Gamestate callbacks + AddCallback_GameStateEnter( eGameState.Prematch, OnATGamePrematch ) + AddCallback_GameStateEnter( eGameState.Playing, OnATGamePlaying ) + + // Initilaze player AddCallback_OnClientConnected( InitialiseATPlayer ) - AddSpawnCallbackEditorClass( "info_target", "info_attrition_bank", CreateATBank ) - AddSpawnCallbackEditorClass( "info_target", "info_attrition_camp", CreateATCamp ) - AddCallback_EntitiesDidLoad( CreateATCamps_Delayed ) + // Initilaze gamemode entities + AddCallback_EntitiesDidLoad( OnEntitiesDidLoad ) } void function RateSpawnpoints_AT( int checkclass, array<entity> spawnpoints, int team, entity player ) { - RateSpawnpoints_Generic( checkclass, spawnpoints, team, player ) // temp + RateSpawnpoints_Generic( checkclass, spawnpoints, team, player ) } -// world and player inits + + +//////////////////////////////////////// +///// GAMESTATE CALLBACK FUNCTIONS ///// +//////////////////////////////////////// + +void function OnATGamePrematch() +{ + AT_ScoreEventsValueSetUp() +} + +void function OnATGamePlaying() +{ + thread AT_GameLoop_Threaded() +} + +//////////////////////////////////////////// +///// GAMESTATE CALLBACK FUNCTIONS END ///// +//////////////////////////////////////////// + + + +//////////////////////////// +///// PLAYER FUNCTIONS ///// +//////////////////////////// void function InitialiseATPlayer( entity player ) { Remote_CallFunction_NonReplay( player, "ServerCallback_AT_OnPlayerConnected" ) + player.SetPlayerNetInt( "AT_bonusPointMult", 1 ) + file.playerBankUploading[ player ] <- false + file.playerSavedBountyDamage[ player ] <- {} + file.playerHudMessageAllowedTime[ player ] <- 0.0 + thread AT_PlayerTitleThink( player ) + thread AT_PlayerObjectiveThink( player ) } -void function CreateATBank( entity spawnpoint ) +void function AT_PlayerTitleThink( entity player ) { - entity bank = CreatePropDynamic( spawnpoint.GetModelName(), spawnpoint.GetOrigin(), spawnpoint.GetAngles(), SOLID_VPHYSICS ) - bank.SetScriptName( "AT_Bank" ) - - // create tracker ent - // we don't need to store these at all, client just needs to get them - DispatchSpawn( GetAvailableBankTracker( bank ) ) + player.EndSignal( "OnDestroy" ) + + while ( true ) + { + if ( GetGameState() == eGameState.Playing ) + { + // Set player money count + player.SetTitle( "$" + string( AT_GetPlayerBonusPoints( player ) ) ) + } + else if ( GetGameState() >= eGameState.WinnerDetermined ) + { + if ( player.IsTitan() ) + player.SetTitle( GetTitanPlayerTitle( player ) ) + else + player.SetTitle( "" ) + + return + } + + WaitFrame() + } +} + +string function GetTitanPlayerTitle( entity player ) +{ + entity soul = player.GetTitanSoul() + + if ( !IsValid( soul ) ) + return "" - thread PlayAnim( bank, "mh_inactive_idle" ) + string settings = GetSoulPlayerSettings( soul ) + var title = GetPlayerSettingsFieldForClassName( settings, "printname" ) + + if ( title == null ) + return "" - file.banks.append( bank ) + return expect string( title ) } -void function CreateATCamp( entity spawnpoint ) +void function AT_PlayerObjectiveThink( entity player ) { - // delay this so we don't do stuff before all spawns are initialised and that - file.campsToRegisterOnEntitiesDidLoad.append( spawnpoint ) + player.EndSignal( "OnDestroy" ) + + int curObjective = AT_OBJECTIVE_EMPTY + while ( true ) + { + // game entered other state + if ( GetGameState() >= eGameState.WinnerDetermined ) + { + player.SetPlayerNetInt( "gameInfoStatusText", AT_OBJECTIVE_EMPTY ) + return + } + + int nextObjective = AT_OBJECTIVE_EMPTY + + // Determine objective text for player + if ( !IsAlive( player ) ) // Don't show objective to dead players + { + nextObjective = AT_OBJECTIVE_EMPTY + } + else // We're still alive + { + if ( GetGlobalNetBool( "banksOpen" ) ) + { + nextObjective = AT_OBJECTIVE_BANK_OPEN + } + else if ( GetGlobalNetBool( "preBankPhase" ) ) + { + nextObjective = AT_OBJECTIVE_EMPTY + } + else + { + // No checks have passed, try to do a "Kill all x near the marked dropzone" objective + int dropZoneActiveCount = 0 + int bossAliveCount = 0 + array<entity> campEnts + campEnts.append( GetGlobalNetEnt( "camp1Ent" ) ) + campEnts.append( GetGlobalNetEnt( "camp2Ent" ) ) + + foreach ( entity ent in campEnts ) + { + if ( IsValid( ent ) ) + { + if ( ent.IsTitan() ) + bossAliveCount += 1 + else + dropZoneActiveCount += 1 + } + } + + switch( dropZoneActiveCount ) + { + case 1: + nextObjective = AT_OBJECTIVE_KILL_DZ + break + case 2: + nextObjective = AT_OBJECTIVE_KILL_DZ_MULTI + break + } + + switch( bossAliveCount ) + { + case 1: + nextObjective = AT_OBJECTIVE_KILL_BOSS + break + case 2: + nextObjective = AT_OBJECTIVE_KILL_BOSS_MULTI + break + } + + // We couldn't get an objective, set it to empty + if ( dropZoneActiveCount == 0 && bossAliveCount == 0 ) + nextObjective = AT_OBJECTIVE_EMPTY + } + } + + // Set the objective when changed + if ( curObjective != nextObjective ) + { + player.SetPlayerNetInt( "gameInfoStatusText", nextObjective ) + curObjective = nextObjective + } + + WaitFrame() + } } -void function CreateATCamps_Delayed() +//////////////////////////////// +///// PLAYER FUNCTIONS END ///// +//////////////////////////////// + + + +//////////////////////////////////////// +///// GAMEMODE INITILAZE FUNCTIONS ///// +//////////////////////////////////////// + +void function OnEntitiesDidLoad() { - // we delay registering camps until EntitiesDidLoad since they rely on spawnpoints and stuff, which might not all be ready in the creation callback - // unsure if this would be an issue in practice, but protecting against it in case it would be - foreach ( entity camp in file.campsToRegisterOnEntitiesDidLoad ) + foreach ( entity info_target in GetEntArrayByClass_Expensive( "info_target" ) ) { - AT_WaveOrigin campStruct - campStruct.ent = camp - campStruct.origin = camp.GetOrigin() - campStruct.radius = expect string( camp.kv.radius ).tofloat() - campStruct.height = expect string( camp.kv.height ).tofloat() + if( info_target.HasKey( "editorclass" ) ) + { + switch( info_target.kv.editorclass ) + { + case "info_attrition_bank": + entity bank = CreateEntity( "prop_script" ) + bank.SetScriptName( "AT_Bank" ) // VoyageDB: don't know how to make client able to track it + bank.SetOrigin( info_target.GetOrigin() ) + bank.SetAngles( info_target.GetAngles() ) + DispatchSpawn( bank ) + bank.kv.solid = SOLID_VPHYSICS + bank.SetModel( info_target.GetModelName() ) + + // Minimap icon init + bank.Minimap_SetCustomState( eMinimapObject_prop_script.AT_BANK ) + bank.Minimap_SetAlignUpright( true ) + bank.Minimap_SetZOrder( MINIMAP_Z_OBJECT ) + bank.Minimap_Hide( TEAM_IMC, null ) + bank.Minimap_Hide( TEAM_MILITIA, null ) + + // Create tracker ent + // we don't need to store these at all, client just needs to get them + DispatchSpawn( GetAvailableBankTracker( bank ) ) + + // Make sure the bank is in it's disabled pose + thread PlayAnim( bank, "mh_inactive_idle" ) + // Set the bank usable + AddCallback_OnUseEntity( bank, OnPlayerUseBank ) + bank.SetUsable() + bank.SetUsePrompts( "#AT_USE_BANK_CLOSED", "#AT_USE_BANK_CLOSED" ) + + file.banks.append( bank ) + break; + case "info_attrition_camp": + AT_WaveOrigin campStruct + campStruct.ent = info_target + campStruct.origin = info_target.GetOrigin() + campStruct.radius = expect string( info_target.kv.radius ).tofloat() + campStruct.height = expect string( info_target.kv.height ).tofloat() + + // Assumes every info_attrition_camp will have all 9 phases, possibly not a good idea? + // TODO: verify this on all vanilla maps before release + for ( int i = 0; i < 9; i++ ) + campStruct.phaseAllowed.append( expect string( info_target.kv[ "phase_" + ( i + 1 ) ] ) == "1" ) + + // Get droppod spawns within the camp + foreach ( entity spawnpoint in SpawnPoints_GetDropPod() ) + { + vector campPos = info_target.GetOrigin() + vector spawnPos = spawnpoint.GetOrigin() + if ( Distance( campPos, spawnPos ) < campStruct.radius ) + campStruct.dropPodSpawnPoints.append( spawnpoint ) + } + + // Get titan spawns within the camp + foreach ( entity spawnpoint in SpawnPoints_GetTitan() ) + { + vector campPos = info_target.GetOrigin() + vector spawnPos = spawnpoint.GetOrigin() + if ( Distance( campPos, spawnPos ) < campStruct.radius ) + campStruct.titanSpawnPoints.append( spawnpoint ) + } + + file.camps.append( campStruct ) + break; + } + } + } +} + +//////////////////////////////////////////// +///// GAMEMODE INITILAZE FUNCTIONS END ///// +//////////////////////////////////////////// + + + +///////////////////////////// +///// SCORING FUNCTIONS ///// +///////////////////////////// + +// TODO: Don't reward in postmatch +// TODO: Dropping a titan on a bounty with it's dome-shield still up rewards you the bonus, but +// it doesn't actually damage the bounty titan + +void function AT_ScoreEventsValueSetUp() +{ + ScoreEvent_SetEarnMeterValues( "KillTitan", 0.10, 0.15 ) + ScoreEvent_SetEarnMeterValues( "KillAutoTitan", 0.10, 0.15 ) + ScoreEvent_SetEarnMeterValues( "AttritionTitanKilled", 0.10, 0.15 ) + ScoreEvent_SetEarnMeterValues( "KillPilot", 0.10, 0.10 ) + ScoreEvent_SetEarnMeterValues( "AttritionPilotKilled", 0.10, 0.10 ) + ScoreEvent_SetEarnMeterValues( "AttritionBossKilled", 0.10, 0.20 ) + ScoreEvent_SetEarnMeterValues( "AttritionGruntKilled", 0.02, 0.02, 0.5 ) + ScoreEvent_SetEarnMeterValues( "AttritionSpectreKilled", 0.02, 0.02, 0.5 ) + ScoreEvent_SetEarnMeterValues( "AttritionStalkerKilled", 0.02, 0.02, 0.5 ) + ScoreEvent_SetEarnMeterValues( "AttritionSuperSpectreKilled", 0.10, 0.10, 0.5 ) + + // HACK + foreach ( string eventName in AT_ENABLE_SCOREEVENTS ) + ScoreEvent_Enable( GetScoreEvent( eventName ) ) + + foreach ( string eventName in AT_DISABLE_SCOREEVENTS ) + ScoreEvent_Disable( GetScoreEvent( eventName ) ) +} + +void function AT_PlayerOrNPCKilledScoreEvent( entity victim, entity attacker, var damageInfo ) +{ + if ( !IsValid( attacker ) ) + return + + // Suicide + if ( attacker == victim ) + { + if ( victim.IsPlayer() ) + AT_PlayerBonusLoss( victim, AT_GetPlayerBonusPoints( victim ) / 2 ) + + return + } + + // NPC is the attacker + if ( !attacker.IsPlayer() ) + { + if ( attacker.IsTitan() && IsValid( GetPetTitanOwner( attacker ) ) ) // Re-asign attacker + attacker = GetPetTitanOwner( attacker ) + else // NPC steals money from player, killing it will award the stolen bonus + normal reward + AT_NPCTryStealBonusPoints( attacker, victim ) - // assumes every info_attrition_camp will have all 9 phases, possibly not a good idea? - for ( int i = 0; i < 9; i++ ) - campStruct.phaseAllowed.append( expect string( camp.kv[ "phase_" + ( i + 1 ) ] ) == "1" ) + return + } + + // Get event name + string eventName = GetAttritionScoreEventName( victim.GetClassName() ) + + if ( victim.IsTitan() ) // titan specific + eventName = GetAttritionScoreEventNameFromAI( victim ) + + if ( eventName == "" ) // no valid scoreEvent + return + + int scoreVal = ScoreEvent_GetPointValue( GetScoreEvent( eventName ) ) + + // pet titan check + if ( victim.IsTitan() && IsValid( GetPetTitanOwner( victim ) ) ) + { + if( GetPetTitanOwner( victim ) == attacker ) // Player ejected + return - // get droppod spawns - foreach ( entity spawnpoint in SpawnPoints_GetDropPod() ) - if ( Distance( camp.GetOrigin(), spawnpoint.GetOrigin() ) < 1500.0 ) - campStruct.dropPodSpawnPoints.append( spawnpoint ) + if( GetPetTitanOwner( victim ).IsPlayer() ) // Killed player npc titan + return - foreach ( entity spawnpoint in SpawnPoints_GetTitan() ) - if ( Distance( camp.GetOrigin(), spawnpoint.GetOrigin() ) < 1500.0 ) - campStruct.titanSpawnPoints.append( spawnpoint ) + scoreVal = ATTRITION_SCORE_TITAN_MIN + } + + // killed npc + if ( victim.IsNPC() ) + { + int bonusFromNPC = 0 + // If NPC was carrying a bonus award it to the attacker + if ( victim in file.npcStolenBonus ) + { + bonusFromNPC = file.npcStolenBonus[ victim ] + delete file.npcStolenBonus[ victim ] + } + AT_AddPlayerBonusPointsForEntityKilled( attacker, scoreVal, damageInfo, bonusFromNPC ) + AddPlayerScore( attacker, eventName ) // we add scoreEvent here, since basic score events has been overwrited by sh_gamemode_at.nut + // update score difference and scoreboard + AT_AddToPlayerTeamScore( attacker, scoreVal ) + } + + // bonus stealing check + if ( victim.IsPlayer() ) + AT_PlayerTryStealBonusPoints( attacker, victim, damageInfo ) +} + +bool function AT_NPCTryStealBonusPoints( entity attacker, entity victim ) +{ + // basic checks + if ( !attacker.IsNPC() ) + return false + + if ( !victim.IsPlayer() ) + return false + + int victimBonus = AT_GetPlayerBonusPoints( victim ) + int bonusToSteal = victimBonus / 2 // npc always steal half the bonus from player, no extra bonus for killing the player + if ( bonusToSteal == 0 ) // player has no bonus! + return false + + if ( !( attacker in file.npcStolenBonus ) ) // init + file.npcStolenBonus[ attacker ] <- 0 + + file.npcStolenBonus[ attacker ] += bonusToSteal + + AT_PlayerBonusLoss( victim, bonusToSteal ) // tell victim of bonus stolen + + if ( !( attacker in file.titanIsBountyBoss ) ) // if attacker npc is not a bounty titan, we make them highlighted + NPCBountyStolenHighlight( attacker ) + + return true +} + +void function NPCBountyStolenHighlight( entity npc ) +{ + Highlight_SetEnemyHighlight( npc, "enemy_boss_bounty" ) +} + +bool function AT_PlayerTryStealBonusPoints( entity attacker, entity victim, var damageInfo ) +{ + // basic checks + if ( !attacker.IsPlayer() ) + return false - // todo: turret spawns someday maybe + if ( !victim.IsPlayer() ) + return false - file.camps.append( campStruct ) + int victimBonus = AT_GetPlayerBonusPoints( victim ) + + int minScoreCanSteal = ATTRITION_SCORE_PILOT_MIN + if ( victim.IsTitan() ) + minScoreCanSteal = ATTRITION_SCORE_TITAN_MIN + + int bonusToSteal = victimBonus / 2 + int attackerScore = bonusToSteal + bool realStealBonus = true + if ( bonusToSteal <= minScoreCanSteal ) // no enough bonus to steal + { + attackerScore = minScoreCanSteal // give attacker min bonus + realStealBonus = false // we don't do attacker steal events below, just half victim's bonus } + + // servercallback + int victimEHandle = victim.GetEncodedEHandle() + vector damageOrigin = DamageInfo_GetDamagePosition( damageInfo ) + + // only do attacker events if victim has enough bonus to steal + if ( realStealBonus ) + { + Remote_CallFunction_NonReplay( + attacker, + "ServerCallback_AT_PlayerKillScorePopup", + bonusToSteal, // stolenScore + victimEHandle, // victimEHandle + damageOrigin.x, // x + damageOrigin.y, // y + damageOrigin.z // z + ) + } + else // otherwise we do a normal entity killed scoreEvent + { + AT_AddPlayerBonusPointsForEntityKilled( attacker, attackerScore, damageInfo ) + } + + // update score difference and scoreboard + AT_AddToPlayerTeamScore( attacker, minScoreCanSteal ) + + // steal bonus + // only do attacker events if victim has enough bonus to steal + if ( realStealBonus ) + { + AT_AddPlayerBonusPoints( attacker, bonusToSteal ) + AddPlayerScore( attacker, "AttritionBonusStolen" ) + } + + // tell victim of bonus stolen + AT_PlayerBonusLoss( victim, bonusToSteal ) - file.campsToRegisterOnEntitiesDidLoad.clear() + return realStealBonus } -// scoring funcs +void function AT_PlayerBonusLoss( entity player, int bonusLoss ) +{ + AT_AddPlayerBonusPoints( player, -bonusLoss ) + Remote_CallFunction_NonReplay( + player, + "ServerCallback_AT_ShowStolenBonus", + bonusLoss // stolenScore + ) +} -// don't use this where possible as it doesn't set score and stuff -void function AT_SetPlayerCash( entity player, int amount ) +// team score meter +void function AT_AddToPlayerTeamScore( entity player, int amount ) +{ + // do not award any score after the match is ended + if ( GetGameState() > eGameState.Playing ) + return + + // add to scoreboard + player.AddToPlayerGameStat( PGS_ASSAULT_SCORE, amount ) + + // Check score so we dont go over max + if ( GameRules_GetTeamScore(player.GetTeam()) + amount > GetScoreLimit_FromPlaylist() ) + { + amount = GetScoreLimit_FromPlaylist() - GameRules_GetTeamScore(player.GetTeam()) + } + + // update score difference + AddTeamScore( player.GetTeam(), amount ) +} + +// bonus points, players earn from killing +void function AT_AddPlayerBonusPoints( entity player, int amount ) +{ + // do not award any score after the match is ended + if ( GetGameState() > eGameState.Playing ) + return + + // add to scoreboard + player.AddToPlayerGameStat( PGS_SCORE, amount ) + AT_SetPlayerBonusPoints( player, player.GetPlayerNetInt( "AT_bonusPoints" ) + ( player.GetPlayerNetInt( "AT_bonusPoints256" ) * 256 ) + amount ) +} + +int function AT_GetPlayerBonusPoints( entity player ) +{ + return player.GetPlayerNetInt( "AT_bonusPoints" ) + player.GetPlayerNetInt( "AT_bonusPoints256" ) * 256 +} + +void function AT_SetPlayerBonusPoints( entity player, int amount ) { // split into stacks of 256 where necessary int stacks = amount / 256 // automatically rounds down because int division @@ -120,198 +637,986 @@ void function AT_SetPlayerCash( entity player, int amount ) player.SetPlayerNetInt( "AT_bonusPoints", amount - stacks * 256 ) } -void function AT_AddPlayerCash( entity player, int amount ) +// total points, the value player actually uploaded to team score +void function AT_AddPlayerTotalPoints( entity player, int amount ) { - // update score difference - AddTeamScore( player.GetTeam(), amount / 2 ) - AT_SetPlayerCash( player, player.GetPlayerNetInt( "AT_bonusPoints" ) + ( player.GetPlayerNetInt( "AT_bonusPoints256" ) * 256 ) + amount ) + // update score difference and scoreboard, calling this function meaning player has deposited their bonus to team score + AT_AddToPlayerTeamScore( player, amount ) + AT_SetPlayerTotalPoints( player, player.GetPlayerNetInt( "AT_totalPoints" ) + ( player.GetPlayerNetInt( "AT_totalPoints256" ) * 256 ) + amount ) +} + +void function AT_SetPlayerTotalPoints( entity player, int amount ) +{ + // split into stacks of 256 where necessary + int stacks = amount / 256 // automatically rounds down because int division + + player.SetPlayerNetInt( "AT_totalPoints256", stacks ) + player.SetPlayerNetInt( "AT_totalPoints", amount - stacks * 256 ) } -// run gamestate +// earn points, seems not used +void function AT_AddPlayerEarnedPoints( entity player, int amount ) +{ + AT_SetPlayerBonusPoints( player, player.GetPlayerNetInt( "AT_earnedPoints" ) + ( player.GetPlayerNetInt( "AT_earnedPoints256" ) * 256 ) + amount ) +} + +void function AT_SetPlayerEarnedPoints( entity player, int amount ) +{ + // split into stacks of 256 where necessary + int stacks = amount / 256 // automatically rounds down because int division + + player.SetPlayerNetInt( "AT_earnedPoints256", stacks ) + player.SetPlayerNetInt( "AT_earnedPoints", amount - stacks * 256 ) +} + +// damaging bounty +void function AT_AddPlayerBonusPointsForBossDamaged( entity player, entity victim, int amount, var damageInfo ) +{ + AT_AddPlayerBonusPoints( player, amount ) + // update score difference and scoreboard + AT_AddToPlayerTeamScore( player, amount ) + + // send servercallback for damaging + int bossEHandle = victim.GetEncodedEHandle() + vector damageOrigin = DamageInfo_GetDamagePosition( damageInfo ) + + Remote_CallFunction_NonReplay( + player, + "ServerCallback_AT_BossDamageScorePopup", + amount, // damageScore + amount, // damageBonus + bossEHandle, // bossEHandle + damageOrigin.x, // x + damageOrigin.y, // y + damageOrigin.z // z + ) +} -void function RunATGame() +void function AT_AddPlayerBonusPointsForEntityKilled( entity player, int amount, var damageInfo, int extraBonus = 0 ) { - thread RunATGame_Threaded() + AT_AddPlayerBonusPoints( player, amount + extraBonus ) + + // send servercallback for damaging + int attackerEHandle = player.GetEncodedEHandle() + vector damageOrigin = DamageInfo_GetDamagePosition( damageInfo ) + + Remote_CallFunction_NonReplay( + player, + "ServerCallback_AT_ShowATScorePopup", + attackerEHandle, // attackerEHandle + amount, // damageScore + amount + extraBonus, // damageBonus + damageOrigin.x, // damagePosX + damageOrigin.y, // damagePosX + damageOrigin.z, // damagePosX + 0 // damageType ( not used ) + ) } -void function RunATGame_Threaded() +///////////////////////////////// +///// SCORING FUNCTIONS END ///// +///////////////////////////////// + + + +////////////////////////////// +///// GAMELOOP FUNCTIONS ///// +////////////////////////////// + +void function AT_GameLoop_Threaded() { svGlobal.levelEnt.EndSignal( "GameStateChanged" ) - OnThreadEnd( function() - { - SetGlobalNetBool( "banksOpen", false ) - }) + // game end func + // TODO: Cant seem to be able to get this crash ??? + OnThreadEnd + ( + function() + { + // prevent crash before entity creation on map change + if ( GetGameState() >= eGameState.Prematch ) + { + SetGlobalNetBool( "preBankPhase", false ) + SetGlobalNetBool( "banksOpen", false ) + } + } + ) - wait WAVE_STATE_TRANSITION_TIME // initial wait before first wave - - for ( int waveCount = 1; ; waveCount++ ) + // Initial wait before first wave + wait AT_FIRST_WAVE_START_DELAY - AT_WAVE_TRANSITION_DELAY + + int lastWaveId = -1 + for ( int waveCount = 1; ; waveCount++ ) { - wait WAVE_STATE_TRANSITION_TIME + wait AT_WAVE_TRANSITION_DELAY // cap to number of real waves - int waveId = ( waveCount / 2 ) - // last wave is clearly unfinished so don't use, just cap to last actually used one - if ( waveId >= GetWaveDataSize() - 1 ) + int waveId = ( waveCount - 1 ) / 2 + int waveCapAmount = 2 + waveId = int( min( waveId, GetWaveDataSize() - waveCapAmount ) ) + + // New wave dialogue + bool waveChanged = lastWaveId != waveId + if ( waveChanged ) + { + PlayFactionDialogueToTeam( "bh_newWave", TEAM_IMC ) + PlayFactionDialogueToTeam( "bh_newWave", TEAM_MILITIA ) + } + else // same wave, second half { - waveId = GetWaveDataSize() - 2 - waveCount = waveId * 2 + PlayFactionDialogueToTeam( "bh_incoming", TEAM_IMC ) + PlayFactionDialogueToTeam( "bh_incoming", TEAM_MILITIA ) } + + lastWaveId = waveId SetGlobalNetInt( "AT_currentWave", waveId ) - bool isBossWave = waveCount / float( 2 ) > waveId // odd number waveCount means boss wave + bool isBossWave = waveCount % 2 == 0 // even number waveCount means boss wave // announce the wave foreach ( entity player in GetPlayerArray() ) { if ( isBossWave ) + { Remote_CallFunction_NonReplay( player, "ServerCallback_AT_AnnounceBoss" ) + } else - Remote_CallFunction_NonReplay( player, "ServerCallback_AT_AnnouncePreParty", 0.0, waveId ) + { + Remote_CallFunction_NonReplay( + player, + "ServerCallback_AT_AnnouncePreParty", + 0.0, // endTime ( not used ) + waveId // waveNum + ) + } } - wait WAVE_STATE_TRANSITION_TIME + wait AT_WAVE_TRANSITION_DELAY - // run the wave + // Run the wave + thread AT_CampSpawnThink( waveId, isBossWave ) + + if ( !isBossWave ) + { + svGlobal.levelEnt.WaitSignal( "ATAllCampsClean" ) // signaled when all camps cleaned in spawn functions + } + else + { + wait AT_BOUNTY_TITAN_CHECK_DELAY + // wait until all bounty titans killed + while ( IsAlive( GetGlobalNetEnt( "camp1Ent" ) ) || IsAlive( GetGlobalNetEnt( "camp2Ent" ) ) ) + WaitFrame() + } + + // wave end, prebank phase + svGlobal.levelEnt.Signal( "ATWaveEnd" ) // defensive fix, destroy existing campEnts + SetGlobalNetBool( "preBankPhase", true ) + + wait AT_WAVE_END_ANNOUNCEMENT_DELAY - AT_WaveData wave = GetWaveData( waveId ) - array< array<AT_SpawnData> > campSpawnData + // announce wave end + foreach ( entity player in GetPlayerArray() ) + { + Remote_CallFunction_NonReplay( + player, + "ServerCallback_AT_AnnounceWaveOver", + waveId, // waveNum ( not used ) + 0, // militiaDamageTotal ( not used ) + 0, // imcDamageTotal ( not used ) + 0, // milMVP ( not used ) + 0, // imcMVP ( not used ) + 0, // milMVPDamage ( not used ) + 0 // imcMVPDamage ( not used ) + ) + } + + wait AT_WAVE_TRANSITION_DELAY - if ( isBossWave ) - campSpawnData = wave.bossSpawnData - else - campSpawnData = wave.spawnDataArrays + // banking phase + SetGlobalNetBool( "preBankPhase", false ) + SetGlobalNetTime( "AT_bankStartTime", Time() ) + SetGlobalNetTime( "AT_bankEndTime", Time() + AT_BANKS_OPEN_DURATION ) + SetGlobalNetBool( "banksOpen", true ) + + foreach ( entity player in GetPlayerArray() ) + Remote_CallFunction_NonReplay( player, "ServerCallback_AT_BankOpen" ) + + foreach ( entity bank in file.banks ) + thread AT_BankActiveThink( bank ) + - // initialise pending spawns - foreach ( array< AT_SpawnData > campData in campSpawnData ) + float endTime = Time() + AT_BANKS_OPEN_DURATION + bool forceCloseTriggered = false + // wait until no player is holding bonus, or max wait time + while ( Time() <= endTime ) { - foreach ( AT_SpawnData spawnData in campData ) - spawnData.pendingSpawns = spawnData.totalToSpawn + // If everyone has deposited their bonuses close the banks early + if ( !ATAnyPlayerHasBonus() && !forceCloseTriggered ) + { + forceCloseTriggered = true + endTime = Time() + AT_BANK_FORCE_CLOSE_DELAY + } + + WaitFrame() } - // clear tracked spawns - file.trackedCampNPCSpawns = {} - while ( true ) - { - // if this is ever 0 by the end of this loop, wave is complete - int numActiveCampSpawners = 0 + SetGlobalNetBool( "banksOpen", false ) + foreach ( entity player in GetPlayerArray() ) + Remote_CallFunction_NonReplay( player, "ServerCallback_AT_BankClose" ) + } +} + +bool function ATAnyPlayerHasBonus() +{ + foreach ( entity player in GetPlayerArray() ) + { + if ( AT_GetPlayerBonusPoints( player ) ) + return true + } + return false +} + +////////////////////////////////// +///// GAMELOOP FUNCTIONS END ///// +////////////////////////////////// + + + +////////////////////////// +///// CAMP FUNCTIONS ///// +////////////////////////// + +void function AT_CampSpawnThink( int waveId, bool isBossWave ) +{ + AT_WaveData wave = GetWaveData( waveId ) + array< array<AT_SpawnData> > campSpawnData + + if ( isBossWave ) + campSpawnData = wave.bossSpawnData + else + campSpawnData = wave.spawnDataArrays + + array<AT_WaveOrigin> allCampsToUse + foreach ( AT_WaveOrigin campStruct in file.camps ) + { + if ( campStruct.phaseAllowed[ waveId ] ) + allCampsToUse.append( campStruct ) + } + + // HACK + // There's too many phase3 camps on exoplanet and rise, make sure we always have the correct count + int maxCampsForWave = waveId == 0 ? 1 : 2 + while( allCampsToUse.len() > maxCampsForWave ) + { + // Get the required number of camps + array<AT_WaveOrigin> tempCamps + for( int i = 0; i < maxCampsForWave; i++ ) + tempCamps.append( allCampsToUse[RandomInt( allCampsToUse.len() )] ) - // iterate over camp data for wave - for ( int campIdx = 0; campIdx < campSpawnData.len() && campIdx < file.camps.len(); campIdx++ ) + + // Check if they're intersecting, if they are, try again + bool intersecting = false + for( int i = 0; i < tempCamps.len(); i++ ) + { + AT_WaveOrigin campA = tempCamps[i] + for( int j = 0; j < tempCamps.len(); j++ ) { - if ( !( campIdx in file.trackedCampNPCSpawns ) ) - file.trackedCampNPCSpawns[ campIdx ] <- {} + // Don't compare the same two camps + if( j == i ) + continue + + AT_WaveOrigin campB = tempCamps[j] + + if( Distance( campA.origin, campB.origin ) < campA.radius + campB.radius ) + intersecting = true + } + } + + if( !intersecting ) + allCampsToUse = tempCamps + + // If we ever get really unlucky just wait a frame + WaitFrame() + } + + foreach ( int spawnId, AT_WaveOrigin curCampData in allCampsToUse ) + { + array<AT_SpawnData> curSpawnData = campSpawnData[ spawnId ] + + int totalNPCsToSpawn = 0 + // initialise pending spawns and get total npcs + foreach ( AT_SpawnData spawnData in curSpawnData ) + { + spawnData.pendingSpawns = spawnData.totalToSpawn + // add to network variables + string npcNetVar = GetNPCNetVarName( spawnData.aitype, spawnId ) + SetGlobalNetInt( npcNetVar, spawnData.totalToSpawn ) + + totalNPCsToSpawn += spawnData.totalToSpawn + } + + if ( !isBossWave ) + { + // camp Ent, boss wave will use boss themselves as campEnt + string campEntVarName = "camp" + string( spawnId + 1 ) + "Ent" + bool waveNotActive = GetGlobalNetBool( "preBankPhase" ) || GetGlobalNetBool( "banksOpen" ) + if ( !IsValid( GetGlobalNetEnt( campEntVarName ) ) && !waveNotActive ) + SetGlobalNetEnt( campEntVarName, CreateCampTracker( curCampData, spawnId ) ) - // iterate over ai spawn data for camp - foreach ( AT_SpawnData spawnData in campSpawnData[ campIdx ] ) + array<AT_SpawnData> minionSquadDatas + foreach ( AT_SpawnData data in curSpawnData ) + { + switch ( data.aitype ) { - if ( !( spawnData.aitype in file.trackedCampNPCSpawns[ campIdx ] ) ) - file.trackedCampNPCSpawns[ campIdx ][ spawnData.aitype ] <- 0 - - if ( spawnData.pendingSpawns > 0 || file.trackedCampNPCSpawns[ campIdx ][ spawnData.aitype ] > 0 ) - numActiveCampSpawners++ + case "npc_soldier": + case "npc_spectre": + case "npc_stalker": + if ( !AT_USE_TOTAL_ALLOWED_ON_FIELD_CHECK ) + minionSquadDatas.append( data ) + else + thread AT_DroppodSquadEvent_Single( curCampData, spawnId, data ) + break + + case "npc_super_spectre": + thread AT_ReaperEvent( curCampData, spawnId, data ) + break + } + } + + // minions squad spawn + if ( !AT_USE_TOTAL_ALLOWED_ON_FIELD_CHECK ) + { + if ( minionSquadDatas.len() > 0 ) + thread AT_DroppodSquadEvent( curCampData, spawnId, minionSquadDatas ) + } + + // use campProgressThink for handling wave state + thread CampProgressThink( spawnId, totalNPCsToSpawn ) + } + else // bosswave spawn + { + foreach ( AT_SpawnData data in curSpawnData ) + { + if( data.aitype != "npc_titan" ) + continue - // try to spawn as many ai as we can, as long as the camp doesn't already have too many spawned - int spawnCount - for ( spawnCount = 0; spawnCount < spawnData.pendingSpawns && spawnCount < spawnData.totalAllowedOnField - file.trackedCampNPCSpawns[ campIdx ][ spawnData.aitype ]; ) - { - // not doing this in a generic way atm, but could be good for the future if we want to support more ai - switch ( spawnData.aitype ) - { - case "npc_soldier": - case "npc_spectre": - case "npc_stalker": - thread AT_SpawnDroppodSquad( campIdx, spawnData.aitype ) - spawnCount += 4 - break - - case "npc_super_spectre": - thread AT_SpawnReaper( campIdx ) - spawnCount += 1 - break - - case "npc_titan": - thread AT_SpawnBountyTitan( campIdx ) - spawnCount += 1 - break - - default: - print( "BOUNTY HUNT: Tried to spawn unsupported ai of type \"" + "\" at camp " + campIdx ) - } + thread AT_BountyTitanEvent( curCampData, spawnId, data ) + break + } + } + } +} + +void function CampProgressThink( int spawnId, int totalNPCsToSpawn ) +{ + string campLetter = GetCampLetter( spawnId ) + string campProgressName = campLetter + "campProgress" + string campEntVarName = "camp" + string( spawnId + 1 ) + "Ent" + + // initial wait + SetGlobalNetFloat( campProgressName, 1.0 ) + + // TODO: random wait, make this a constant ?? + wait 3.0 + + float cleanUpTime = -1.0 + + while ( true ) + { + int npcsLeft + // get all npcs might be in this camp + for ( int i = 0; i < 5; i++ ) + { + string netVarName = string( i + 1 ) + campLetter + "campCount" + int netVarValue = GetGlobalNetInt( netVarName ) + if ( netVarValue >= 0 ) // uninitialized network var starts from -1, avoid checking them + npcsLeft += netVarValue + } + + float campLeft = float( npcsLeft ) / float( totalNPCsToSpawn ) + SetGlobalNetFloat( campProgressName, campLeft ) + + if( npcsLeft <= AT_CAMP_BORED_NPCS_LEFT_TO_START_CLEANUP && cleanUpTime < 0.0 ) + { + cleanUpTime = Time() + AT_CAMP_BORED_CLEANUP_WAIT + print("Cleanup timer started!") + } + + if( Time() > cleanUpTime && cleanUpTime > 0.0 && spawnId in file.campScriptEntArrays ) + { + foreach( int handle in file.campScriptEntArrays[spawnId] ) + { + array<entity> entities = GetScriptManagedEntArray( handle ) + entities.removebyvalue( null ) + foreach ( entity ent in entities ) + { + if ( IsAlive( ent ) && ent.IsNPC() ) + { + printt( "Killing bored AI " + ent.GetClassName() + " at " + ent.GetOrigin() ) + ent.Die() } - - // track spawns - file.trackedCampNPCSpawns[ campIdx ][ spawnData.aitype ] += spawnCount - spawnData.pendingSpawns -= spawnCount } } + } + + if ( campLeft <= 0.0 ) // camp wiped! + { + PlayFactionDialogueToTeam( "bh_cleared" + campLetter, TEAM_IMC ) + PlayFactionDialogueToTeam( "bh_cleared" + campLetter, TEAM_MILITIA ) + + entity campEnt = GetGlobalNetEnt( campEntVarName ) + if ( IsValid( campEnt ) ) + campEnt.Signal( "ATCampClean" ) // destroy the camp ent + + // check if both camps being destroyed + if ( !IsValid( GetGlobalNetEnt( "camp1Ent" ) ) && !IsValid( GetGlobalNetEnt( "camp2Ent" ) ) ) + svGlobal.levelEnt.Signal( "ATAllCampsClean" ) // end the wave - if ( numActiveCampSpawners == 0 ) - break - - wait 0.5 + return } - - wait WAVE_STATE_TRANSITION_TIME - - // banking phase + + WaitFrame() } } // entity funcs +// camp +entity function CreateCampTracker( AT_WaveOrigin campData, int spawnId ) +{ + // store data + vector campOrigin = campData.origin + float campRadius = campData.radius + float campHeight = campData.height + // add a minimap icon + entity mapIconEnt = CreateEntity( "prop_script" ) + DispatchSpawn( mapIconEnt ) + + mapIconEnt.SetOrigin( campOrigin ) + mapIconEnt.DisableHibernation() + SetTeam( mapIconEnt, AT_AI_TEAM ) + mapIconEnt.Minimap_AlwaysShow( TEAM_IMC, null ) + mapIconEnt.Minimap_AlwaysShow( TEAM_MILITIA, null ) + + mapIconEnt.Minimap_SetCustomState( GetCampMinimapState( spawnId ) ) + mapIconEnt.Minimap_SetAlignUpright( true ) + mapIconEnt.Minimap_SetZOrder( MINIMAP_Z_OBJECT ) + mapIconEnt.Minimap_SetObjectScale( campRadius / 16000.0 ) // proper icon on the map + + // attach a location tracker + entity tracker = GetAvailableLocationTracker() + tracker.SetOwner( mapIconEnt ) // needs a owner to show up + tracker.SetOrigin( campOrigin ) + SetLocationTrackerRadius( tracker, campRadius ) + SetLocationTrackerID( tracker, spawnId ) + DispatchSpawn( tracker ) + + thread TrackWaveEndForCampInfo( tracker, mapIconEnt ) + return tracker +} + +void function TrackWaveEndForCampInfo( entity tracker, entity mapIconEnt ) +{ + tracker.EndSignal( "OnDestroy" ) + tracker.EndSignal( "ATCampClean" ) + + OnThreadEnd + ( + function(): ( tracker, mapIconEnt ) + { + // camp cleaned, wave or game ended, destroy the camp info + if ( IsValid( tracker ) ) + tracker.Destroy() + + if ( IsValid( mapIconEnt ) ) + mapIconEnt.Destroy() + } + ) + + WaitSignal( svGlobal.levelEnt, "GameStateChanged", "ATWaveEnd" ) +} + +string function GetCampLetter( int spawnId ) +{ + return spawnId == 0 ? "A" : "B" +} + +int function GetCampMinimapState( int id ) +{ + switch ( id ) + { + case 0: + return eMinimapObject_prop_script.AT_DROPZONE_A + case 1: + return eMinimapObject_prop_script.AT_DROPZONE_B + case 2: + return eMinimapObject_prop_script.AT_DROPZONE_C + } + + unreachable +} + +////////////////////////////// +///// CAMP FUNCTIONS END ///// +////////////////////////////// + + + +////////////////////////// +///// BANK FUNCTIONS ///// +////////////////////////// + +void function AT_BankActiveThink( entity bank ) +{ + svGlobal.levelEnt.EndSignal( "GameStateChanged" ) + bank.EndSignal( "OnDestroy" ) + + // Banks closed + OnThreadEnd + ( + function(): ( bank ) + { + if ( IsValid( bank ) ) + { + // Update use prompt + if ( GetGameState() != eGameState.Playing ) + bank.UnsetUsable() + else + bank.SetUsePrompts( "#AT_USE_BANK_CLOSED", "#AT_USE_BANK_CLOSED" ) + + thread PlayAnim( bank, "mh_active_2_inactive" ) + FadeOutSoundOnEntity( bank, "Mobile_Hardpoint_Idle", 0.5 ) + bank.Minimap_Hide( TEAM_IMC, null ) + bank.Minimap_Hide( TEAM_MILITIA, null ) + } + } + ) + + // Update use prompt to usable + bank.SetUsable() + bank.SetUsePrompts( "#AT_USE_BANK", "#AT_USE_BANK_PC" ) + + thread PlayAnim( bank, "mh_inactive_2_active" ) + EmitSoundOnEntity( bank, "Mobile_Hardpoint_Idle" ) + + // Show minimap icon for bank + bank.Minimap_AlwaysShow( TEAM_IMC, null ) + bank.Minimap_AlwaysShow( TEAM_MILITIA, null ) + bank.Minimap_SetCustomState( eMinimapObject_prop_script.AT_BANK ) + + // Wait for bank close or game end + while ( GetGlobalNetBool( "banksOpen" ) ) + WaitFrame() +} + +function OnPlayerUseBank( bank, player ) +{ + // Banks are always usable so that we can show the use prompt + // Only allow deposit when banks are open + if ( !GetGlobalNetBool( "banksOpen" ) ) + return + + expect entity( bank ) + expect entity( player ) + + // bank.SetUsableByGroup( "pilot" ) didn't seem to work so we just + // exit here if player is in a titan + if( player.IsTitan() ) + return + + // Player has no bonus, try to send a tip using SendHUDMessage + if ( AT_GetPlayerBonusPoints( player ) == 0 ) + { + ATSendDepositTipToPlayer( player, "#AT_USE_BANK_NO_BONUS_HINT" ) + return + } + + // Prevent more than one instance of this thread running + if ( !file.playerBankUploading[ player ] ) + thread PlayerUploadingBonus_Threaded( bank, player ) +} + +bool function ATSendDepositTipToPlayer( entity player, string message ) +{ + if ( Time() < file.playerHudMessageAllowedTime[ player ] ) + return false + + SendHudMessage( player, message, -1, 0.4, 255, 255, 255, 255, 0.5, 1.0, 0.5 ) + file.playerHudMessageAllowedTime[ player ] = Time() + AT_PLAYER_HUD_MESSAGE_COOLDOWN + + return true +} + +struct AT_playerUploadStruct +{ + bool uploadSuccess = false + int depositedPoints = 0 +} + +void function PlayerUploadingBonus_Threaded( entity bank, entity player ) +{ + bank.EndSignal( "OnDestroy" ) + player.EndSignal( "OnDestroy" ) + player.EndSignal( "OnDeath" ) + + file.playerBankUploading[ player ] = true + + // this literally only exists because structs are passed by ref, + // and primitives like ints and bools are passed by val + // which meant that the OnThreadEnd was just getting 0 and false + AT_playerUploadStruct uploadInfo + + // Cleanup and call finish deposit func + OnThreadEnd + ( + function(): ( player, uploadInfo ) + { + if ( IsValid( player ) ) + { + file.playerBankUploading[ player ] = false + + // Clean up looping sound + StopSoundOnEntity( player, "HUD_MP_BountyHunt_BankBonusPts_Ticker_Loop_1P" ) + StopSoundOnEntity( player, "HUD_MP_BountyHunt_BankBonusPts_Ticker_Loop_3P" ) + + // Do medal event + // TODO: Check if vanilla actually do.s this every time you finish depositing??? + AddPlayerScore( player, "AttritionCashedBonus" ) + + // Do server callback + Remote_CallFunction_NonReplay( + player, + "ServerCallback_AT_FinishDeposit", + uploadInfo.depositedPoints // deposit + ) + + player.SetPlayerNetBool( "AT_playerUploading", false ) + + if ( uploadInfo.uploadSuccess ) // Player deposited all remaining bonus + { + // Emit uploading successful sound + EmitSoundOnEntityOnlyToPlayer( player, player, "HUD_MP_BountyHunt_BankBonusPts_Deposit_End_Successful_1P" ) + EmitSoundOnEntityExceptToPlayer( player, player, "HUD_MP_BountyHunt_BankBonusPts_Deposit_End_Successful_3P" ) + + // player is MVP + int ourScore = player.GetPlayerGameStat( PGS_ASSAULT_SCORE ) + bool isMVP = true + foreach(teamPlayer in GetPlayerArrayOfTeam(player.GetTeam())) + { + if (ourScore < teamPlayer.GetPlayerGameStat( PGS_ASSAULT_SCORE )) + { + isMVP = false + break + } + } + if (isMVP) + PlayFactionDialogueToPlayer( "bh_mvp", player ) + } + else // Player was killed or left the bank radius + { + // Emit uploading failed sound + EmitSoundOnEntityOnlyToPlayer( player, player, "HUD_MP_BountyHunt_BankBonusPts_Deposit_End_Unsuccessful_1P" ) + EmitSoundOnEntityExceptToPlayer( player, player, "HUD_MP_BountyHunt_BankBonusPts_Deposit_End_Unsuccessful_3P" ) + } + } + } + ) + + // Uploading start sound + EmitSoundOnEntityOnlyToPlayer( player, player, "HUD_MP_BountyHunt_BankBonusPts_Deposit_Start_1P" ) + EmitSoundOnEntityExceptToPlayer( player, player, "HUD_MP_BountyHunt_BankBonusPts_Deposit_Start_3P" ) + EmitSoundOnEntityOnlyToPlayer( player, player, "HUD_MP_BountyHunt_BankBonusPts_Ticker_Loop_1P" ) + EmitSoundOnEntityExceptToPlayer( player, player, "HUD_MP_BountyHunt_BankBonusPts_Ticker_Loop_3P" ) + + player.SetPlayerNetBool( "AT_playerUploading", true ) + + // Upload bonus while the player is within range of the bank + while ( Distance( player.GetOrigin(), bank.GetOrigin() ) <= AT_BANK_DEPOSIT_RADIUS && GetGlobalNetBool( "banksOpen" ) ) + { + // Calling this moves the "Uploading..." graphic to the same place it is + // in vanilla + Remote_CallFunction_NonReplay( player, "ServerCallback_AT_ShowRespawnBonusLoss" ) + + int bonusToUpload = int( min( AT_BANK_DEPOSIT_RATE, AT_GetPlayerBonusPoints( player ) ) ) + // No more bonus to upload, return + if ( bonusToUpload == 0 ) + { + uploadInfo.uploadSuccess = true + return + } -void function AT_SpawnDroppodSquad( int camp, string aiType ) + // Remove bonus points and add them to total poins + AT_AddPlayerBonusPoints( player, -bonusToUpload ) + AT_AddPlayerTotalPoints( player, bonusToUpload ) + + uploadInfo.depositedPoints += bonusToUpload + WaitFrame() + } +} + +////////////////////////////// +///// BANK FUNCTIONS END ///// +////////////////////////////// + + + +///////////////////////// +///// NPC FUNCTIONS ///// +///////////////////////// + +int function GetScriptManagedNPCArrayLength_Alive( int scriptManagerId ) +{ + array<entity> entities = GetScriptManagedEntArray( scriptManagerId ) + entities.removebyvalue( null ) + int npcsAlive = 0 + foreach ( entity ent in entities ) + { + if ( IsAlive( ent ) && ent.IsNPC() ) + npcsAlive += 1 + } + return npcsAlive +} + +void function AT_DroppodSquadEvent( AT_WaveOrigin campData, int spawnId, array<AT_SpawnData> minionDatas ) +{ + svGlobal.levelEnt.EndSignal( "GameStateChanged" ) + // create a script managed array for all handled minions + int eventManager = CreateScriptManagedEntArray() + + if( !(spawnId in file.campScriptEntArrays) ) + file.campScriptEntArrays[spawnId] <- [] + + file.campScriptEntArrays[spawnId].append(eventManager) + + int totalAllowedOnField = SQUAD_SIZE * AT_DROPPOD_SQUADS_ALLOWED_ON_FIELD + while ( true ) + { + foreach ( AT_SpawnData data in minionDatas ) + { + string ent = data.aitype + waitthread AT_SpawnDroppodSquad( campData, spawnId, ent, eventManager ) + data.pendingSpawns -= SQUAD_SIZE + if ( data.pendingSpawns <= 0 ) // current spawn data has reached max spawn amount + minionDatas.removebyvalue( data ) // remove this data + if ( GetScriptManagedNPCArrayLength_Alive( eventManager ) >= totalAllowedOnField ) // we have enough npcs on field? + break // stop following spawning functions + } + if ( minionDatas.len() == 0 ) // all spawn data has finished spawn + return + + int npcOnFieldCount = GetScriptManagedNPCArrayLength_Alive( eventManager ) + while ( npcOnFieldCount >= totalAllowedOnField - SQUAD_SIZE ) // wait until we have lost more than 1 squad + { + WaitFrame() + npcOnFieldCount = GetScriptManagedNPCArrayLength_Alive( eventManager ) + } + } +} + +// for AT_USE_TOTAL_ALLOWED_ON_FIELD_CHECK, handles a single spawndata +void function AT_DroppodSquadEvent_Single( AT_WaveOrigin campData, int spawnId, AT_SpawnData data ) +{ + svGlobal.levelEnt.EndSignal( "GameStateChanged" ) + + // get ent and create a script managed array for current event + string ent = data.aitype + int eventManager = CreateScriptManagedEntArray() + + if( !(spawnId in file.campScriptEntArrays) ) + file.campScriptEntArrays[spawnId] <- [] + + file.campScriptEntArrays[spawnId].append(eventManager) + + int totalAllowedOnField = data.totalAllowedOnField // mostly 12 for grunts and spectres, too much! + // start spawner + while ( true ) + { + waitthread AT_SpawnDroppodSquad( campData, spawnId, ent, eventManager ) + data.pendingSpawns -= SQUAD_SIZE + if ( data.pendingSpawns <= 0 ) // we have reached max npcs + return // stop any spawning functions + + int npcOnFieldCount = GetScriptManagedNPCArrayLength_Alive( eventManager ) + while ( npcOnFieldCount >= totalAllowedOnField - SQUAD_SIZE ) // wait until we have less npcs than allowed count + { + WaitFrame() + npcOnFieldCount = GetScriptManagedNPCArrayLength_Alive( eventManager ) + } + } +} + +void function AT_SpawnDroppodSquad( AT_WaveOrigin campData, int spawnId, string aiType, int scriptManagerId ) { entity spawnpoint - if ( file.camps[ camp ].dropPodSpawnPoints.len() == 0 ) - spawnpoint = file.camps[ camp ].ent + if ( campData.dropPodSpawnPoints.len() == 0 ) + spawnpoint = campData.ent else - spawnpoint = file.camps[ camp ].dropPodSpawnPoints.getrandom() + spawnpoint = campData.dropPodSpawnPoints.getrandom() + // anti-crash + if ( !IsValid( spawnpoint ) ) + spawnpoint = campData.ent // add variation to spawns wait RandomFloat( 1.0 ) - AiGameModes_SpawnDropPod( spawnpoint.GetOrigin(), spawnpoint.GetAngles(), BH_AI_TEAM, aiType, void function( array<entity> guys ) : ( camp, aiType ) - { - AT_HandleSquadSpawn( guys, camp, aiType ) - }) + AiGameModes_SpawnDropPod( + spawnpoint.GetOrigin(), + spawnpoint.GetAngles(), + AT_AI_TEAM, + aiType, + // squad handler + void function( array<entity> guys ) : ( campData, spawnId, aiType, scriptManagerId ) + { + AT_HandleSquadSpawn( guys, campData, spawnId, aiType, scriptManagerId ) + }, + eDropPodFlag.DISSOLVE_AFTER_DISEMBARKS + ) } -void function AT_HandleSquadSpawn( array<entity> guys, int camp, string aiType ) +void function AT_HandleSquadSpawn( array<entity> guys, AT_WaveOrigin campData, int spawnId, string aiType, int scriptManagerId ) { foreach ( entity guy in guys ) { - guy.EnableNPCFlag( NPC_ALLOW_PATROL | NPC_ALLOW_INVESTIGATE | NPC_ALLOW_HAND_SIGNALS | NPC_ALLOW_FLEE ) - - // untrack them on death - thread AT_WaitToUntrackNPC( guy, camp, aiType ) + // TODO: NPCs still seem to go outside their camp ??? + //guy.EnableNPCFlag( NPC_ALLOW_PATROL | NPC_ALLOW_HAND_SIGNALS | NPC_ALLOW_FLEE ) + + // tracking lifetime + AddToScriptManagedEntArray( scriptManagerId, guy ) + thread AT_TrackNPCLifeTime( guy, spawnId, aiType ) + + thread AT_ForceAssaultAroundCamp( guy, campData ) + } +} + +void function AT_ForceAssaultAroundCamp( entity guy, AT_WaveOrigin campData ) +{ + guy.EndSignal( "OnDestroy" ) + guy.EndSignal( "OnDeath" ) + + // goal check + vector ornull goalPos = NavMesh_ClampPointForAI(campData.origin, guy) + goalPos = goalPos == null ? campData.origin : goalPos + expect vector(goalPos) + + float goalRadius = campData.radius / 4 + float guyGoalRadius = guy.GetMinGoalRadius() + if ( guyGoalRadius > goalRadius ) // this npc cannot use forced goal radius? + goalRadius = guyGoalRadius + + while( true ) + { + guy.AssaultPoint( goalPos ) + guy.AssaultSetGoalRadius( goalRadius ) + guy.AssaultSetFightRadius( 0 ) + guy.AssaultSetArrivalTolerance( int(goalRadius) ) + + wait RandomFloatRange( 1, 5 ) + } +} + +void function AT_ReaperEvent( AT_WaveOrigin campData, int spawnId, AT_SpawnData data ) +{ + svGlobal.levelEnt.EndSignal( "GameStateChanged" ) + + // create a script managed array for current event + int eventManager = CreateScriptManagedEntArray() + + if( !(spawnId in file.campScriptEntArrays) ) + file.campScriptEntArrays[spawnId] <- [] + + file.campScriptEntArrays[spawnId].append(eventManager) + + int totalAllowedOnField = 1 // 1 allowed at the same time for heavy armor units + if ( AT_USE_TOTAL_ALLOWED_ON_FIELD_CHECK ) + totalAllowedOnField = data.totalAllowedOnField + + while ( true ) + { + waitthread AT_SpawnReaper( campData, spawnId, eventManager ) + data.pendingSpawns -= 1 + if ( data.pendingSpawns <= 0 ) // we have reached max npcs + return // stop any spawning functions + + int npcOnFieldCount = GetScriptManagedNPCArrayLength_Alive( eventManager ) + while ( npcOnFieldCount >= totalAllowedOnField ) // wait until we have less npcs than allowed count + { + WaitFrame() + npcOnFieldCount = GetScriptManagedNPCArrayLength_Alive( eventManager ) + } } } -void function AT_SpawnReaper( int camp ) +void function AT_SpawnReaper( AT_WaveOrigin campData, int spawnId, int scriptManagerId ) { entity spawnpoint - if ( file.camps[ camp ].dropPodSpawnPoints.len() == 0 ) - spawnpoint = file.camps[ camp ].ent + if ( campData.dropPodSpawnPoints.len() == 0 ) + spawnpoint = campData.ent else - spawnpoint = file.camps[ camp ].titanSpawnPoints.getrandom() + spawnpoint = campData.dropPodSpawnPoints.getrandom() + // anti-crash + if ( !IsValid( spawnpoint ) ) + spawnpoint = campData.ent // add variation to spawns wait RandomFloat( 1.0 ) - AiGameModes_SpawnReaper( spawnpoint.GetOrigin(), spawnpoint.GetAngles(), BH_AI_TEAM, "npc_super_spectre",void function( entity reaper ) : ( camp ) + AiGameModes_SpawnReaper( + spawnpoint.GetOrigin(), + spawnpoint.GetAngles(), + AT_AI_TEAM, + "npc_super_spectre_aitdm", + // reaper handler + void function( entity reaper ) : ( campData, spawnId, scriptManagerId ) + { + AT_HandleReaperSpawn( reaper, campData, spawnId, scriptManagerId ) + } + ) +} + +void function AT_HandleReaperSpawn( entity reaper, AT_WaveOrigin campData, int spawnId, int scriptManagerId ) +{ + // tracking lifetime + AddToScriptManagedEntArray( scriptManagerId, reaper ) + thread AT_TrackNPCLifeTime( reaper, spawnId, "npc_super_spectre" ) + + thread AT_ForceAssaultAroundCamp( reaper, campData ) +} + +void function AT_BountyTitanEvent( AT_WaveOrigin campData, int spawnId, AT_SpawnData data ) +{ + svGlobal.levelEnt.EndSignal( "GameStateChanged" ) + + // create a script managed array for current event + int eventManager = CreateScriptManagedEntArray() + + int totalAllowedOnField = 1 // 1 allowed at the same time for heavy armor units + if ( AT_USE_TOTAL_ALLOWED_ON_FIELD_CHECK ) + totalAllowedOnField = data.totalAllowedOnField + while ( true ) { - thread AT_WaitToUntrackNPC( reaper, camp, "npc_super_spectre" ) - }) + waitthread AT_SpawnBountyTitan( campData, spawnId, eventManager ) + data.pendingSpawns -= 1 + if ( data.pendingSpawns <= 0 ) // we have reached max npcs + return // stop any spawning functions + + int npcOnFieldCount = GetScriptManagedNPCArrayLength_Alive( eventManager ) + while ( npcOnFieldCount >= totalAllowedOnField ) // wait until we have less npcs than allowed count + { + WaitFrame() + npcOnFieldCount = GetScriptManagedNPCArrayLength_Alive( eventManager ) + } + } } -void function AT_SpawnBountyTitan( int camp ) +void function AT_SpawnBountyTitan( AT_WaveOrigin campData, int spawnId, int scriptManagerId ) { entity spawnpoint - if ( file.camps[ camp ].dropPodSpawnPoints.len() == 0 ) - spawnpoint = file.camps[ camp ].ent + if ( campData.titanSpawnPoints.len() == 0 ) + spawnpoint = campData.ent else - spawnpoint = file.camps[ camp ].titanSpawnPoints.getrandom() + spawnpoint = campData.titanSpawnPoints.getrandom() + // anti-crash + if ( !IsValid( spawnpoint ) ) + spawnpoint = campData.ent // add variation to spawns wait RandomFloat( 1.0 ) @@ -320,57 +1625,191 @@ void function AT_SpawnBountyTitan( int camp ) int bountyID = 0 try { - bountyID = ReserveBossID( VALID_BOUNTY_TITAN_SETTINGS.getrandom() ) + bountyID = ReserveBossID( AT_BOUNTY_TITANS_AI_SETTINGS.getrandom() ) } catch ( ex ) {} // if we go above the expected wave count that vanilla supports, there's basically no way to ensure that this func won't error, so default 0 after that point string aisettings = GetTypeFromBossID( bountyID ) string titanClass = expect string( Dev_GetAISettingByKeyField_Global( aisettings, "npc_titan_player_settings" ) ) + AiGameModes_SpawnTitan( + spawnpoint.GetOrigin(), + spawnpoint.GetAngles(), + AT_AI_TEAM, + titanClass, + aisettings, + // titan handler + void function( entity titan ) : ( campData, spawnId, bountyID, scriptManagerId ) + { + AT_HandleBossTitanSpawn( titan, campData, spawnId, bountyID, scriptManagerId ) + } + ) +} + +void function AT_HandleBossTitanSpawn( entity titan, AT_WaveOrigin campData, int spawnId, int bountyID, int scriptManagerId ) +{ + // set the bounty to be campEnt, for client tracking + SetGlobalNetEnt( "camp" + string( spawnId + 1 ) + "Ent", titan ) + // set up health + titan.SetMaxHealth( titan.GetMaxHealth() * AT_BOUNTY_TITAN_HEALTH_MULTIPLIER ) + titan.SetHealth( titan.GetMaxHealth() ) + // make minimap always show them and highlight them + titan.Minimap_AlwaysShow( TEAM_IMC, null ) + titan.Minimap_AlwaysShow( TEAM_MILITIA, null ) + thread BountyBossHighlightThink( titan ) + + // set up titan-specific death callbacks, mark it as bounty boss for finalDamageCallbacks to work + file.titanIsBountyBoss[ titan ] <- true + file.bountyTitanRewards[ titan ] <- ATTRITION_SCORE_BOSS_DAMAGE + AddEntityCallback_OnKilled( titan, OnBountyTitanKilled ) - AiGameModes_SpawnTitan( spawnpoint.GetOrigin(), spawnpoint.GetAngles(), BH_AI_TEAM, titanClass, aisettings, void function( entity titan ) : ( camp, bountyID ) + titan.GetTitanSoul().soul.skipDoomState = true + // i feel like this should be localised, but there's nothing for it in r1_english? + titan.SetTitle( GetNameFromBossID( bountyID ) ) + + // tracking lifetime + AddToScriptManagedEntArray( scriptManagerId, titan ) + thread AT_TrackNPCLifeTime( titan, spawnId, "npc_titan" ) +} + +void function BountyBossHighlightThink( entity titan ) +{ + titan.EndSignal( "OnDestroy" ) + titan.EndSignal( "OnDeath" ) + + while ( true ) { - // set up titan-specific death/damage callbacks - AddEntityCallback_OnDamaged( titan, OnBountyDamaged) - AddEntityCallback_OnKilled( titan, OnBountyKilled ) - - titan.GetTitanSoul().soul.skipDoomState = true - // i feel like this should be localised, but there's nothing for it in r1_english? - titan.SetTitle( GetNameFromBossID( bountyID ) ) - thread AT_WaitToUntrackNPC( titan, camp, "npc_titan" ) - } ) + Highlight_SetEnemyHighlight( titan, "enemy_boss_bounty" ) + titan.WaitSignal( "StopPhaseShift" ) // prevent phase shift mess up highlights + } } -// Tracked entities will require their own "wallet" -// for titans it should be used for rounding error compenstation -// for infantry it sould be used to store money if the npc kills a player -void function OnBountyDamaged( entity titan, var damageInfo ) +void function OnNPCTitanFinalDamaged( entity titan, var damageInfo ) +{ + if ( titan in file.titanIsBountyBoss ) + OnBountyTitanDamaged( titan, damageInfo ) +} + +void function OnBountyTitanDamaged( entity titan, var damageInfo ) { entity attacker = DamageInfo_GetAttacker( damageInfo ) + if ( !IsValid( attacker ) ) // delayed by projectile shots + return + // damaged by npc or something? if ( !attacker.IsPlayer() ) - attacker = GetLatestAssistingPlayerInfo( titan ).player - - if ( IsValid( attacker ) && attacker.IsPlayer() ) { - int reward = int ( BOUNTY_TITAN_DAMAGE_POOL * DamageInfo_GetDamage( damageInfo ) / titan.GetMaxHealth() ) - printt ( titan.GetMaxHealth(), DamageInfo_GetDamage( damageInfo ) ) - - AT_AddPlayerCash( attacker, reward ) + attacker = GetBountyBossDamageOwner( attacker, titan ) + if ( !IsValid( attacker ) || !attacker.IsPlayer() ) + return } + + // respawn FUCKED UP pilot weapon against titan's damage calculation, have to copy-paste this check from Titan_NPCTookDamage() + if ( HeavyArmorCriticalHitRequired( damageInfo ) && + CritWeaponInDamageInfo( damageInfo ) && + !IsCriticalHit( attacker, titan, DamageInfo_GetHitBox( damageInfo ), DamageInfo_GetDamage( damageInfo ), DamageInfo_GetDamageType( damageInfo ) ) && + IsValid( attacker ) && + !attacker.IsTitan() ) + return + + int rewardSegment = ATTRITION_SCORE_BOSS_DAMAGE + int healthSegment = titan.GetMaxHealth() / rewardSegment + + // sometimes damage is not enough to add 1 point, we save the damage for player's next attack + if ( !( titan in file.playerSavedBountyDamage[ attacker ] ) ) + file.playerSavedBountyDamage[ attacker ][ titan ] <- 0 + + file.playerSavedBountyDamage[ attacker ][ titan ] += int( DamageInfo_GetDamage( damageInfo ) ) + if ( file.playerSavedBountyDamage[ attacker ][ titan ] < healthSegment ) + return // they can't earn reward from this shot + + int damageSegment = file.playerSavedBountyDamage[ attacker ][ titan ] / healthSegment + int savedDamageLeft = file.playerSavedBountyDamage[ attacker ][ titan ] % healthSegment + file.playerSavedBountyDamage[ attacker ][ titan ] = savedDamageLeft + + float damageFrac = float( damageSegment ) / rewardSegment + int rewardLeft = file.bountyTitanRewards[ titan ] + int reward = int( ATTRITION_SCORE_BOSS_DAMAGE * damageFrac ) + if ( reward >= rewardLeft ) // overloaded shot? + reward = rewardLeft + file.bountyTitanRewards[ titan ] -= reward + + if ( reward > 0 ) + AT_AddPlayerBonusPointsForBossDamaged( attacker, titan, reward, damageInfo ) } -void function OnBountyKilled( entity titan, var damageInfo ) +void function OnBountyTitanKilled( entity titan, var damageInfo ) { entity attacker = DamageInfo_GetAttacker( damageInfo ) + if ( !IsValid( attacker ) ) // delayed by projectile shots + return + // damaged by npc or something? if ( !attacker.IsPlayer() ) - attacker = GetLatestAssistingPlayerInfo( titan ).player + { + attacker = GetBountyBossDamageOwner( attacker, titan ) + if ( !IsValid( attacker ) || !attacker.IsPlayer() ) + return + } + + // add all remaining reward to attacker + // bounty killed bonus handled by AT_PlayerOrNPCKilledScoreEvent() + int rewardLeft = file.bountyTitanRewards[ titan ] + delete file.bountyTitanRewards[ titan ] + if ( rewardLeft > 0 ) + AT_AddPlayerBonusPointsForBossDamaged( attacker, titan, rewardLeft, damageInfo ) + + // remove this bounty's damage saver + foreach ( entity player in GetPlayerArray() ) + { + if ( titan in file.playerSavedBountyDamage[ player ] ) + delete file.playerSavedBountyDamage[ player ][ titan ] + } + + // faction dialogue + int team = attacker.GetTeam() + PlayFactionDialogueToPlayer( "bh_playerKilledBounty", attacker ) + PlayFactionDialogueToTeamExceptPlayer( "bh_bountyClaimedByFriendly", team, attacker ) + PlayFactionDialogueToTeam( "bh_bountyClaimedByEnemy", GetOtherTeam( team ) ) +} + +entity function GetBountyBossDamageOwner( entity attacker, entity titan ) +{ + if ( attacker.IsPlayer() ) // already a player + return attacker - if ( IsValid( attacker ) && attacker.IsPlayer() ) - AT_AddPlayerCash( attacker, BOUNTY_TITAN_KILL_REWARD ) + if ( attacker.IsTitan() ) // attacker is a npc titan + { + // try to find it's pet titan owner + if ( IsValid( GetPetTitanOwner( attacker ) ) ) + return GetPetTitanOwner( attacker ) + } + + // other damages or non-owner npcs, not sure how it happens, just use this titan's last attacker + return GetLatestAssistingPlayerInfo( titan ).player } -void function AT_WaitToUntrackNPC( entity guy, int camp, string aiType ) +void function AT_TrackNPCLifeTime( entity guy, int spawnId, string aiType ) { guy.WaitSignal( "OnDeath", "OnDestroy" ) - file.trackedCampNPCSpawns[ camp ][ aiType ]-- + + string npcNetVar = GetNPCNetVarName( aiType, spawnId ) + SetGlobalNetInt( npcNetVar, GetGlobalNetInt( npcNetVar ) - 1 ) +} + + +// network var +string function GetNPCNetVarName( string className, int spawnId ) +{ + string npcId = string( GetAiTypeInt( className ) + 1 ) + string campLetter = GetCampLetter( spawnId ) + if ( npcId == "0" ) // cannot find this ai support! + { + if ( className == "npc_super_spectre" ) // stupid, reapers are not handled by GetAiTypeInt(), but it must be 4 + return "4" + campLetter + "campCount" + return "" + } + return npcId + campLetter + "campCount" } + +///////////////////////////// +///// NPC FUNCTIONS END ///// +///////////////////////////// diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_cp.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_cp.nut index 705b7836..c5765300 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_cp.nut +++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_cp.nut @@ -471,8 +471,10 @@ void function HardpointThink( HardpointStruct hardpoint ) } else if(cappingTeam==TEAM_UNASSIGNED) // nobody on point { - if((GetHardpointState(hardpoint)==CAPTURE_POINT_STATE_AMPED)||(GetHardpointState(hardpoint)==CAPTURE_POINT_STATE_AMPING)) + if((GetHardpointState(hardpoint)>=CAPTURE_POINT_STATE_AMPED) || (GetHardpointState(hardpoint)==CAPTURE_POINT_STATE_SELF_UNAMPING)) { + if (GetHardpointState(hardpoint) == CAPTURE_POINT_STATE_AMPED) + SetHardpointState(hardpoint,CAPTURE_POINT_STATE_SELF_UNAMPING) // plays a pulsating effect on the UI only when the hardpoint is amped SetHardpointCappingTeam(hardpoint,hardpointEnt.GetTeam()) SetHardpointCaptureProgress(hardpoint,max(1.0,GetHardpointCaptureProgress(hardpoint)-(deltaTime/HARDPOINT_AMPED_DELAY))) if(GetHardpointCaptureProgress(hardpoint)<=1.001) // unamp @@ -546,8 +548,10 @@ void function HardpointThink( HardpointStruct hardpoint ) } else if(file.ampingEnabled)//amping or reamping { - if(GetHardpointState(hardpoint)<CAPTURE_POINT_STATE_AMPING) - SetHardpointState(hardpoint,CAPTURE_POINT_STATE_AMPING) + // i have no idea why but putting it CAPTURE_POINT_STATE_AMPING will say 'CONTESTED' on the UI + // since whether the point is contested is checked above, putting the hardpoint state to a value of 8 fixes it somehow + if(GetHardpointState(hardpoint)<=CAPTURE_POINT_STATE_AMPING) + SetHardpointState( hardpoint, 8 ) SetHardpointCaptureProgress( hardpoint, min( 2.0, GetHardpointCaptureProgress( hardpoint ) + ( deltaTime / HARDPOINT_AMPED_DELAY * capperAmount ) ) ) if(GetHardpointCaptureProgress(hardpoint)==2.0&&!(GetHardpointState(hardpoint)==CAPTURE_POINT_STATE_AMPED)) { @@ -645,7 +649,10 @@ void function OnHardpointEntered( entity trigger, entity player ) hardpoint.militiaCappers.append( player ) foreach(CP_PlayerStruct playerStruct in file.players) if(playerStruct.player == player) + { playerStruct.isOnHardpoint = true + player.SetPlayerNetInt( "playerHardpointID", hardpoint.hardpoint.GetHardpointID() ) + } } void function OnHardpointLeft( entity trigger, entity player ) @@ -661,7 +668,10 @@ void function OnHardpointLeft( entity trigger, entity player ) FindAndRemove( hardpoint.militiaCappers, player ) foreach(CP_PlayerStruct playerStruct in file.players) if(playerStruct.player == player) + { playerStruct.isOnHardpoint = false + player.SetPlayerNetInt( "playerHardpointID", 69 ) // an arbitary number to remove the hud from the player + } } string function CaptureStateToString( int state ) @@ -675,6 +685,7 @@ string function CaptureStateToString( int state ) case CAPTURE_POINT_STATE_CAPTURED: return "CAPTURED" case CAPTURE_POINT_STATE_AMPING: + case 8: return "AMPING" case CAPTURE_POINT_STATE_AMPED: return "AMPED" diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ffa.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ffa.nut index 27eef177..4bff6038 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ffa.nut +++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ffa.nut @@ -6,6 +6,9 @@ void function FFA_Init() ScoreEvent_SetupEarnMeterValuesForMixedModes() AddCallback_OnPlayerKilled( OnPlayerKilled ) + + // modified for northstar + AddCallback_OnClientConnected( OnClientConnected ) } void function OnPlayerKilled( entity victim, entity attacker, var damageInfo ) @@ -16,4 +19,18 @@ void function OnPlayerKilled( entity victim, entity attacker, var damageInfo ) // why isn't this PGS_SCORE? odd game attacker.AddToPlayerGameStat( PGS_ASSAULT_SCORE, 1 ) } +} + +// modified for northstar +void function OnClientConnected( entity player ) +{ + thread FFAPlayerScoreThink( player ) // good to have this! instead of DisconnectCallback this could handle a null player +} + +void function FFAPlayerScoreThink( entity player ) +{ + int team = player.GetTeam() + + player.WaitSignal( "OnDestroy" ) // this can handle disconnecting + AddTeamScore( team, -GameRules_GetTeamScore( team ) ) }
\ No newline at end of file diff --git a/Northstar.CustomServers/mod/scripts/vscripts/mp/_codecallbacks.gnut b/Northstar.CustomServers/mod/scripts/vscripts/mp/_codecallbacks.gnut index 425a8b8b..ff281d6e 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/mp/_codecallbacks.gnut +++ b/Northstar.CustomServers/mod/scripts/vscripts/mp/_codecallbacks.gnut @@ -283,12 +283,26 @@ void function CodeCallback_DamagePlayerOrNPC( entity ent, var damageInfo ) return RunClassDamageFinalCallbacks( ent, damageInfo ) + #if VERBOSE_DAMAGE_PRINTOUTS printt( " after class damage final callbacks:", DamageInfo_GetDamage( damageInfo ) ) #endif if ( DamageInfo_GetDamage( damageInfo ) == 0 ) return + // Added via AddEntityCallback_OnFinalDamaged + foreach ( callbackFunc in ent.e.entFinalDamageCallbacks ) + { + callbackFunc( ent, damageInfo ) + } + + #if VERBOSE_DAMAGE_PRINTOUTS + printt( " afterAddEntityCallback_OnFinalDamaged callbacks:", DamageInfo_GetDamage( damageInfo ) ) + #endif + + if ( DamageInfo_GetDamage( damageInfo ) == 0 ) + return + if ( DamageInfo_GetCustomDamageType( damageInfo ) & DF_DOOMED_HEALTH_LOSS ) DamageInfo_AddDamageFlags( damageInfo, DAMAGEFLAG_NOPAIN ) diff --git a/Northstar.CustomServers/mod/scripts/vscripts/sh_store.gnut b/Northstar.CustomServers/mod/scripts/vscripts/sh_store.gnut new file mode 100644 index 00000000..371cc1c7 --- /dev/null +++ b/Northstar.CustomServers/mod/scripts/vscripts/sh_store.gnut @@ -0,0 +1,933 @@ + +global function Store_Init +global function Store_GetCustomizationRefs +global function Store_GetPatchRefs +global function Store_GetBannerRefs +global function Store_GetCamoRefs + +global struct CamoRef +{ + string ref + string pilotRef + string titanRef +} + +struct RefData +{ + string ref + string parentRef +} + +struct +{ + table< int, array<string> > customizationRefs + table< int, array<string> > patchRefs + table< int, array<string> > bannerRefs + table< int, array<CamoRef> > camoRefs + table< int, array<RefData> > limitedEditionFDRefData +} file + +void function Store_Init() +{ + #if SERVER + AddClientCommandCallback( "SetHasSeenStore", ClientCommand_SetHasSeenStore ) + AddClientCommandCallback( "StoreSetNewItemStatus", ClientCommand_StoreSetNewItemStatus ) + #endif + + file.customizationRefs[ ET_DLC1_ION ] <- [] + file.customizationRefs[ ET_DLC1_ION ].append( "ion_skin_10" ) + file.customizationRefs[ ET_DLC1_ION ].append( "ion_nose_art_17" ) + file.customizationRefs[ ET_DLC1_ION ].append( "ion_nose_art_18" ) + file.customizationRefs[ ET_DLC1_ION ].append( "ion_nose_art_19" ) + file.customizationRefs[ ET_DLC1_ION ].append( "ion_nose_art_20" ) + file.customizationRefs[ ET_DLC1_ION ].append( "ion_nose_art_21" ) + file.customizationRefs[ ET_DLC3_ION ] <- [] + file.customizationRefs[ ET_DLC3_ION ].append( "ion_skin_11" ) + file.customizationRefs[ ET_DLC3_ION ].append( "ion_nose_art_22" ) + file.customizationRefs[ ET_DLC3_ION ].append( "ion_nose_art_23" ) + file.customizationRefs[ ET_DLC3_ION ].append( "ion_nose_art_24" ) + file.customizationRefs[ ET_DLC3_ION ].append( "ion_nose_art_25" ) + file.customizationRefs[ ET_DLC3_ION ].append( "ion_nose_art_26" ) + file.customizationRefs[ ET_DLC5_ION ] <- [] + file.customizationRefs[ ET_DLC5_ION ].append( "ion_skin_07" ) + file.customizationRefs[ ET_DLC5_ION ].append( "ion_nose_art_27" ) + file.customizationRefs[ ET_DLC5_ION ].append( "ion_nose_art_28" ) + file.customizationRefs[ ET_DLC5_ION ].append( "ion_nose_art_29" ) + file.customizationRefs[ ET_DLC5_ION ].append( "ion_nose_art_30" ) + file.customizationRefs[ ET_DLC5_ION ].append( "ion_nose_art_31" ) + + file.customizationRefs[ ET_DLC1_SCORCH ] <- [] + file.customizationRefs[ ET_DLC1_SCORCH ].append( "scorch_skin_07" ) + file.customizationRefs[ ET_DLC1_SCORCH ].append( "scorch_nose_art_15" ) + file.customizationRefs[ ET_DLC1_SCORCH ].append( "scorch_nose_art_16" ) + file.customizationRefs[ ET_DLC1_SCORCH ].append( "scorch_nose_art_17" ) + file.customizationRefs[ ET_DLC1_SCORCH ].append( "scorch_nose_art_18" ) + file.customizationRefs[ ET_DLC1_SCORCH ].append( "scorch_nose_art_19" ) + file.customizationRefs[ ET_DLC3_SCORCH ] <- [] + file.customizationRefs[ ET_DLC3_SCORCH ].append( "scorch_skin_08" ) + file.customizationRefs[ ET_DLC3_SCORCH ].append( "scorch_nose_art_20" ) + file.customizationRefs[ ET_DLC3_SCORCH ].append( "scorch_nose_art_21" ) + file.customizationRefs[ ET_DLC3_SCORCH ].append( "scorch_nose_art_22" ) + file.customizationRefs[ ET_DLC3_SCORCH ].append( "scorch_nose_art_23" ) + file.customizationRefs[ ET_DLC3_SCORCH ].append( "scorch_nose_art_24" ) + file.customizationRefs[ ET_DLC5_SCORCH ] <- [] + file.customizationRefs[ ET_DLC5_SCORCH ].append( "scorch_skin_06" ) + file.customizationRefs[ ET_DLC5_SCORCH ].append( "scorch_nose_art_25" ) + file.customizationRefs[ ET_DLC5_SCORCH ].append( "scorch_nose_art_26" ) + file.customizationRefs[ ET_DLC5_SCORCH ].append( "scorch_nose_art_27" ) + file.customizationRefs[ ET_DLC5_SCORCH ].append( "scorch_nose_art_28" ) + file.customizationRefs[ ET_DLC5_SCORCH ].append( "scorch_nose_art_29" ) + + file.customizationRefs[ ET_DLC1_NORTHSTAR ] <- [] + file.customizationRefs[ ET_DLC1_NORTHSTAR ].append( "northstar_skin_10" ) + file.customizationRefs[ ET_DLC1_NORTHSTAR ].append( "northstar_nose_art_18" ) + file.customizationRefs[ ET_DLC1_NORTHSTAR ].append( "northstar_nose_art_19" ) + file.customizationRefs[ ET_DLC1_NORTHSTAR ].append( "northstar_nose_art_20" ) + file.customizationRefs[ ET_DLC1_NORTHSTAR ].append( "northstar_nose_art_21" ) + file.customizationRefs[ ET_DLC1_NORTHSTAR ].append( "northstar_nose_art_22" ) + file.customizationRefs[ ET_DLC3_NORTHSTAR ] <- [] + file.customizationRefs[ ET_DLC3_NORTHSTAR ].append( "northstar_skin_11" ) + file.customizationRefs[ ET_DLC3_NORTHSTAR ].append( "northstar_nose_art_23" ) + file.customizationRefs[ ET_DLC3_NORTHSTAR ].append( "northstar_nose_art_24" ) + file.customizationRefs[ ET_DLC3_NORTHSTAR ].append( "northstar_nose_art_25" ) + file.customizationRefs[ ET_DLC3_NORTHSTAR ].append( "northstar_nose_art_26" ) + file.customizationRefs[ ET_DLC3_NORTHSTAR ].append( "northstar_nose_art_27" ) + file.customizationRefs[ ET_DLC5_NORTHSTAR ] <- [] + file.customizationRefs[ ET_DLC5_NORTHSTAR ].append( "northstar_skin_06" ) + file.customizationRefs[ ET_DLC5_NORTHSTAR ].append( "northstar_nose_art_28" ) + file.customizationRefs[ ET_DLC5_NORTHSTAR ].append( "northstar_nose_art_29" ) + file.customizationRefs[ ET_DLC5_NORTHSTAR ].append( "northstar_nose_art_30" ) + file.customizationRefs[ ET_DLC5_NORTHSTAR ].append( "northstar_nose_art_31" ) + file.customizationRefs[ ET_DLC5_NORTHSTAR ].append( "northstar_nose_art_32" ) + + file.customizationRefs[ ET_DLC1_RONIN ] <- [] + file.customizationRefs[ ET_DLC1_RONIN ].append( "ronin_skin_10" ) + file.customizationRefs[ ET_DLC1_RONIN ].append( "ronin_nose_art_16" ) + file.customizationRefs[ ET_DLC1_RONIN ].append( "ronin_nose_art_17" ) + file.customizationRefs[ ET_DLC1_RONIN ].append( "ronin_nose_art_18" ) + file.customizationRefs[ ET_DLC1_RONIN ].append( "ronin_nose_art_19" ) + file.customizationRefs[ ET_DLC1_RONIN ].append( "ronin_nose_art_20" ) + file.customizationRefs[ ET_DLC3_RONIN ] <- [] + file.customizationRefs[ ET_DLC3_RONIN ].append( "ronin_skin_11" ) + file.customizationRefs[ ET_DLC3_RONIN ].append( "ronin_nose_art_21" ) + file.customizationRefs[ ET_DLC3_RONIN ].append( "ronin_nose_art_22" ) + file.customizationRefs[ ET_DLC3_RONIN ].append( "ronin_nose_art_23" ) + file.customizationRefs[ ET_DLC3_RONIN ].append( "ronin_nose_art_24" ) + file.customizationRefs[ ET_DLC3_RONIN ].append( "ronin_nose_art_25" ) + file.customizationRefs[ ET_DLC5_RONIN ] <- [] + file.customizationRefs[ ET_DLC5_RONIN ].append( "ronin_skin_07" ) + file.customizationRefs[ ET_DLC5_RONIN ].append( "ronin_nose_art_26" ) + file.customizationRefs[ ET_DLC5_RONIN ].append( "ronin_nose_art_27" ) + file.customizationRefs[ ET_DLC5_RONIN ].append( "ronin_nose_art_28" ) + file.customizationRefs[ ET_DLC5_RONIN ].append( "ronin_nose_art_29" ) + file.customizationRefs[ ET_DLC5_RONIN ].append( "ronin_nose_art_30" ) + + file.customizationRefs[ ET_DLC1_TONE ] <- [] + file.customizationRefs[ ET_DLC1_TONE ].append( "tone_skin_06" ) + file.customizationRefs[ ET_DLC1_TONE ].append( "tone_nose_art_17" ) + file.customizationRefs[ ET_DLC1_TONE ].append( "tone_nose_art_18" ) + file.customizationRefs[ ET_DLC1_TONE ].append( "tone_nose_art_19" ) + file.customizationRefs[ ET_DLC1_TONE ].append( "tone_nose_art_20" ) + file.customizationRefs[ ET_DLC1_TONE ].append( "tone_nose_art_21" ) + file.customizationRefs[ ET_DLC3_TONE ] <- [] + file.customizationRefs[ ET_DLC3_TONE ].append( "tone_skin_07" ) + file.customizationRefs[ ET_DLC3_TONE ].append( "tone_nose_art_22" ) + file.customizationRefs[ ET_DLC3_TONE ].append( "tone_nose_art_23" ) + file.customizationRefs[ ET_DLC3_TONE ].append( "tone_nose_art_24" ) + file.customizationRefs[ ET_DLC3_TONE ].append( "tone_nose_art_25" ) + file.customizationRefs[ ET_DLC3_TONE ].append( "tone_nose_art_26" ) + file.customizationRefs[ ET_DLC5_TONE ] <- [] + file.customizationRefs[ ET_DLC5_TONE ].append( "tone_skin_08" ) + file.customizationRefs[ ET_DLC5_TONE ].append( "tone_nose_art_27" ) + file.customizationRefs[ ET_DLC5_TONE ].append( "tone_nose_art_28" ) + file.customizationRefs[ ET_DLC5_TONE ].append( "tone_nose_art_29" ) + file.customizationRefs[ ET_DLC5_TONE ].append( "tone_nose_art_30" ) + file.customizationRefs[ ET_DLC5_TONE ].append( "tone_nose_art_31" ) + + file.customizationRefs[ ET_DLC1_LEGION ] <- [] + file.customizationRefs[ ET_DLC1_LEGION ].append( "legion_skin_07" ) + file.customizationRefs[ ET_DLC1_LEGION ].append( "legion_nose_art_17" ) + file.customizationRefs[ ET_DLC1_LEGION ].append( "legion_nose_art_18" ) + file.customizationRefs[ ET_DLC1_LEGION ].append( "legion_nose_art_19" ) + file.customizationRefs[ ET_DLC1_LEGION ].append( "legion_nose_art_20" ) + file.customizationRefs[ ET_DLC1_LEGION ].append( "legion_nose_art_21" ) + file.customizationRefs[ ET_DLC3_LEGION ] <- [] + file.customizationRefs[ ET_DLC3_LEGION ].append( "legion_skin_08" ) + file.customizationRefs[ ET_DLC3_LEGION ].append( "legion_nose_art_22" ) + file.customizationRefs[ ET_DLC3_LEGION ].append( "legion_nose_art_23" ) + file.customizationRefs[ ET_DLC3_LEGION ].append( "legion_nose_art_24" ) + file.customizationRefs[ ET_DLC3_LEGION ].append( "legion_nose_art_25" ) + file.customizationRefs[ ET_DLC3_LEGION ].append( "legion_nose_art_26" ) + file.customizationRefs[ ET_DLC5_LEGION ] <- [] + file.customizationRefs[ ET_DLC5_LEGION ].append( "legion_skin_09" ) + file.customizationRefs[ ET_DLC5_LEGION ].append( "legion_nose_art_27" ) + file.customizationRefs[ ET_DLC5_LEGION ].append( "legion_nose_art_28" ) + file.customizationRefs[ ET_DLC5_LEGION ].append( "legion_nose_art_29" ) + file.customizationRefs[ ET_DLC5_LEGION ].append( "legion_nose_art_30" ) + file.customizationRefs[ ET_DLC5_LEGION ].append( "legion_nose_art_31" ) + + file.patchRefs[ ET_DLC1_CALLSIGN ] <- [] + file.patchRefs[ ET_DLC3_CALLSIGN ] <- [] + file.patchRefs[ ET_DLC5_CALLSIGN ] <- [] + + file.patchRefs[ ET_DLC1_CALLSIGN ].append( "gc_icon_64" ) + file.patchRefs[ ET_DLC1_CALLSIGN ].append( "gc_icon_aces" ) + file.patchRefs[ ET_DLC1_CALLSIGN ].append( "gc_icon_alien" ) + file.patchRefs[ ET_DLC1_CALLSIGN ].append( "gc_icon_apex" ) + file.patchRefs[ ET_DLC1_CALLSIGN ].append( "gc_icon_ares" ) + file.patchRefs[ ET_DLC1_CALLSIGN ].append( "gc_icon_controller" ) + file.patchRefs[ ET_DLC1_CALLSIGN ].append( "gc_icon_drone" ) + file.patchRefs[ ET_DLC1_CALLSIGN ].append( "gc_icon_heartbreaker" ) + file.patchRefs[ ET_DLC1_CALLSIGN ].append( "gc_icon_hexes" ) + file.patchRefs[ ET_DLC1_CALLSIGN ].append( "gc_icon_kodai" ) + file.patchRefs[ ET_DLC1_CALLSIGN ].append( "gc_icon_lastimosa" ) + file.patchRefs[ ET_DLC1_CALLSIGN ].append( "gc_icon_lawai" ) + file.patchRefs[ ET_DLC1_CALLSIGN ].append( "gc_icon_mcor" ) + file.patchRefs[ ET_DLC1_CALLSIGN ].append( "gc_icon_phoenix" ) + file.patchRefs[ ET_DLC1_CALLSIGN ].append( "gc_icon_pilot" ) + file.patchRefs[ ET_DLC1_CALLSIGN ].append( "gc_icon_robot" ) + file.patchRefs[ ET_DLC1_CALLSIGN ].append( "gc_icon_sentry" ) + file.patchRefs[ ET_DLC1_CALLSIGN ].append( "gc_icon_super_spectre" ) + file.patchRefs[ ET_DLC1_CALLSIGN ].append( "gc_icon_vinson" ) + file.patchRefs[ ET_DLC1_CALLSIGN ].append( "gc_icon_wonyeon" ) + + file.patchRefs[ ET_DLC3_CALLSIGN ].append( "gc_icon_balance" ) + file.patchRefs[ ET_DLC3_CALLSIGN ].append( "gc_icon_boot" ) + file.patchRefs[ ET_DLC3_CALLSIGN ].append( "gc_icon_bt_eye" ) + file.patchRefs[ ET_DLC3_CALLSIGN ].append( "gc_icon_buzzsaw" ) + file.patchRefs[ ET_DLC3_CALLSIGN ].append( "gc_icon_crossed_lighting" ) + file.patchRefs[ ET_DLC3_CALLSIGN ].append( "gc_icon_flying_bullet" ) + file.patchRefs[ ET_DLC3_CALLSIGN ].append( "gc_icon_hammer2" ) + file.patchRefs[ ET_DLC3_CALLSIGN ].append( "gc_icon_keyboard" ) + file.patchRefs[ ET_DLC3_CALLSIGN ].append( "gc_icon_lightbulb" ) + file.patchRefs[ ET_DLC3_CALLSIGN ].append( "gc_icon_narwhal" ) + file.patchRefs[ ET_DLC3_CALLSIGN ].append( "gc_icon_peace" ) + file.patchRefs[ ET_DLC3_CALLSIGN ].append( "gc_icon_pilot2" ) + file.patchRefs[ ET_DLC3_CALLSIGN ].append( "gc_icon_robot_eye" ) + file.patchRefs[ ET_DLC3_CALLSIGN ].append( "gc_icon_srs" ) + file.patchRefs[ ET_DLC3_CALLSIGN ].append( "gc_icon_starline" ) + file.patchRefs[ ET_DLC3_CALLSIGN ].append( "gc_icon_taco" ) + file.patchRefs[ ET_DLC3_CALLSIGN ].append( "gc_icon_thumbdown" ) + file.patchRefs[ ET_DLC3_CALLSIGN ].append( "gc_icon_thumbup" ) + file.patchRefs[ ET_DLC3_CALLSIGN ].append( "gc_icon_treble" ) + file.patchRefs[ ET_DLC3_CALLSIGN ].append( "gc_icon_vanguard" ) + + file.patchRefs[ ET_DLC5_CALLSIGN ].append( "gc_icon_monarch_dlc5" ) + file.patchRefs[ ET_DLC5_CALLSIGN ].append( "gc_icon_militia" ) + file.patchRefs[ ET_DLC5_CALLSIGN ].append( "gc_icon_militia_alt" ) + file.patchRefs[ ET_DLC5_CALLSIGN ].append( "gc_icon_imc" ) + file.patchRefs[ ET_DLC5_CALLSIGN ].append( "gc_icon_hammond" ) + file.patchRefs[ ET_DLC5_CALLSIGN ].append( "gc_icon_tri_chevron" ) + file.patchRefs[ ET_DLC5_CALLSIGN ].append( "gc_icon_pilot_circle" ) + file.patchRefs[ ET_DLC5_CALLSIGN ].append( "gc_icon_x" ) + file.patchRefs[ ET_DLC5_CALLSIGN ].append( "gc_icon_nessie" ) + file.patchRefs[ ET_DLC5_CALLSIGN ].append( "gc_icon_spicy" ) + file.patchRefs[ ET_DLC5_CALLSIGN ].append( "gc_icon_crown" ) + file.patchRefs[ ET_DLC5_CALLSIGN ].append( "gc_icon_pawn" ) + file.patchRefs[ ET_DLC5_CALLSIGN ].append( "gc_icon_excite" ) + file.patchRefs[ ET_DLC5_CALLSIGN ].append( "gc_icon_duck" ) + file.patchRefs[ ET_DLC5_CALLSIGN ].append( "gc_icon_sock" ) + file.patchRefs[ ET_DLC5_CALLSIGN ].append( "gc_icon_rabbit" ) + file.patchRefs[ ET_DLC5_CALLSIGN ].append( "gc_icon_peanut" ) + file.patchRefs[ ET_DLC5_CALLSIGN ].append( "gc_icon_clock" ) + file.patchRefs[ ET_DLC5_CALLSIGN ].append( "gc_icon_shamrock" ) + file.patchRefs[ ET_DLC5_CALLSIGN ].append( "gc_icon_trident" ) + + file.bannerRefs[ ET_DLC1_CALLSIGN ] <- [] + file.bannerRefs[ ET_DLC3_CALLSIGN ] <- [] + file.bannerRefs[ ET_DLC5_CALLSIGN ] <- [] + + file.bannerRefs[ ET_DLC1_CALLSIGN ].append( "callsign_106_col" ) + file.bannerRefs[ ET_DLC1_CALLSIGN ].append( "callsign_107_col" ) + file.bannerRefs[ ET_DLC1_CALLSIGN ].append( "callsign_108_col" ) + file.bannerRefs[ ET_DLC1_CALLSIGN ].append( "callsign_109_col" ) + file.bannerRefs[ ET_DLC1_CALLSIGN ].append( "callsign_110_col" ) + file.bannerRefs[ ET_DLC1_CALLSIGN ].append( "callsign_111_col" ) + file.bannerRefs[ ET_DLC1_CALLSIGN ].append( "callsign_112_col" ) + file.bannerRefs[ ET_DLC1_CALLSIGN ].append( "callsign_113_col" ) + file.bannerRefs[ ET_DLC1_CALLSIGN ].append( "callsign_114_col" ) + file.bannerRefs[ ET_DLC1_CALLSIGN ].append( "callsign_115_col" ) + file.bannerRefs[ ET_DLC1_CALLSIGN ].append( "callsign_116_col" ) + file.bannerRefs[ ET_DLC1_CALLSIGN ].append( "callsign_117_col" ) + file.bannerRefs[ ET_DLC1_CALLSIGN ].append( "callsign_118_col" ) + file.bannerRefs[ ET_DLC1_CALLSIGN ].append( "callsign_119_col" ) + file.bannerRefs[ ET_DLC1_CALLSIGN ].append( "callsign_120_col" ) + file.bannerRefs[ ET_DLC1_CALLSIGN ].append( "callsign_121_col" ) + file.bannerRefs[ ET_DLC1_CALLSIGN ].append( "callsign_122_col" ) + file.bannerRefs[ ET_DLC1_CALLSIGN ].append( "callsign_123_col" ) + file.bannerRefs[ ET_DLC1_CALLSIGN ].append( "callsign_124_col" ) + file.bannerRefs[ ET_DLC1_CALLSIGN ].append( "callsign_125_col" ) + + file.bannerRefs[ ET_DLC3_CALLSIGN ].append( "callsign_143_col" ) + file.bannerRefs[ ET_DLC3_CALLSIGN ].append( "callsign_144_col" ) + file.bannerRefs[ ET_DLC3_CALLSIGN ].append( "callsign_145_col" ) + file.bannerRefs[ ET_DLC3_CALLSIGN ].append( "callsign_146_col" ) + file.bannerRefs[ ET_DLC3_CALLSIGN ].append( "callsign_147_col" ) + file.bannerRefs[ ET_DLC3_CALLSIGN ].append( "callsign_148_col" ) + file.bannerRefs[ ET_DLC3_CALLSIGN ].append( "callsign_149_col" ) + file.bannerRefs[ ET_DLC3_CALLSIGN ].append( "callsign_150_col" ) + file.bannerRefs[ ET_DLC3_CALLSIGN ].append( "callsign_151_col" ) + file.bannerRefs[ ET_DLC3_CALLSIGN ].append( "callsign_152_col" ) + file.bannerRefs[ ET_DLC3_CALLSIGN ].append( "callsign_153_col" ) + file.bannerRefs[ ET_DLC3_CALLSIGN ].append( "callsign_154_col" ) + file.bannerRefs[ ET_DLC3_CALLSIGN ].append( "callsign_155_col" ) + file.bannerRefs[ ET_DLC3_CALLSIGN ].append( "callsign_156_col" ) + file.bannerRefs[ ET_DLC3_CALLSIGN ].append( "callsign_157_col" ) + file.bannerRefs[ ET_DLC3_CALLSIGN ].append( "callsign_158_col" ) + file.bannerRefs[ ET_DLC3_CALLSIGN ].append( "callsign_159_col" ) + file.bannerRefs[ ET_DLC3_CALLSIGN ].append( "callsign_160_col" ) + file.bannerRefs[ ET_DLC3_CALLSIGN ].append( "callsign_161_col" ) + file.bannerRefs[ ET_DLC3_CALLSIGN ].append( "callsign_162_col" ) + + file.bannerRefs[ ET_DLC5_CALLSIGN ].append( "callsign_166_col" ) + file.bannerRefs[ ET_DLC5_CALLSIGN ].append( "callsign_167_col" ) + file.bannerRefs[ ET_DLC5_CALLSIGN ].append( "callsign_168_col" ) + file.bannerRefs[ ET_DLC5_CALLSIGN ].append( "callsign_169_col" ) + file.bannerRefs[ ET_DLC5_CALLSIGN ].append( "callsign_170_col" ) + file.bannerRefs[ ET_DLC5_CALLSIGN ].append( "callsign_171_col" ) + file.bannerRefs[ ET_DLC5_CALLSIGN ].append( "callsign_172_col" ) + file.bannerRefs[ ET_DLC5_CALLSIGN ].append( "callsign_173_col" ) + file.bannerRefs[ ET_DLC5_CALLSIGN ].append( "callsign_174_col" ) + file.bannerRefs[ ET_DLC5_CALLSIGN ].append( "callsign_175_col" ) + file.bannerRefs[ ET_DLC5_CALLSIGN ].append( "callsign_176_col" ) + file.bannerRefs[ ET_DLC5_CALLSIGN ].append( "callsign_177_col" ) + file.bannerRefs[ ET_DLC5_CALLSIGN ].append( "callsign_178_col" ) + file.bannerRefs[ ET_DLC5_CALLSIGN ].append( "callsign_179_col" ) + file.bannerRefs[ ET_DLC5_CALLSIGN ].append( "callsign_180_col" ) + file.bannerRefs[ ET_DLC5_CALLSIGN ].append( "callsign_181_col" ) + file.bannerRefs[ ET_DLC5_CALLSIGN ].append( "callsign_182_col" ) + file.bannerRefs[ ET_DLC5_CALLSIGN ].append( "callsign_183_col" ) + file.bannerRefs[ ET_DLC5_CALLSIGN ].append( "callsign_184_col" ) + file.bannerRefs[ ET_DLC5_CALLSIGN ].append( "callsign_185_col" ) + + file.camoRefs[ ET_DLC1_CAMO ] <- [] + file.camoRefs[ ET_DLC3_CAMO ] <- [] + file.camoRefs[ ET_DLC5_CAMO ] <- [] + + for ( int i = 101; i <= 120; i++ ) + { + AddCamoRef( ET_DLC1_CAMO, i ) + } + + for ( int i = 121; i <= 140; i++ ) + { + AddCamoRef( ET_DLC3_CAMO, i ) + } + + // You did it reddit! + int numRefs = file.camoRefs[ ET_DLC3_CAMO ].len() + CamoRef tempRef = file.camoRefs[ ET_DLC3_CAMO ][numRefs - 1] + file.camoRefs[ ET_DLC3_CAMO ][numRefs - 1] = file.camoRefs[ ET_DLC3_CAMO ][numRefs - 2] + file.camoRefs[ ET_DLC3_CAMO ][numRefs - 2] = tempRef + + for ( int i = 141; i <= 160; i++ ) + { + AddCamoRef( ET_DLC5_CAMO, i ) + } + + file.limitedEditionFDRefData[ ET_DLC7_ION_WARPAINT ] <- [] + file.limitedEditionFDRefData[ ET_DLC7_ION_WARPAINT ].append( CreateRefData( "ion_skin_fd", "ion" ) ) + file.limitedEditionFDRefData[ ET_DLC7_ION_WARPAINT ].append( CreateRefData( "callsign_fd_ion_dynamic" ) ) + + file.limitedEditionFDRefData[ ET_DLC7_SCORCH_WARPAINT ] <- [] + file.limitedEditionFDRefData[ ET_DLC7_SCORCH_WARPAINT ].append( CreateRefData( "scorch_skin_fd", "scorch" ) ) + file.limitedEditionFDRefData[ ET_DLC7_SCORCH_WARPAINT ].append( CreateRefData( "callsign_fd_scorch_dynamic" ) ) + + file.limitedEditionFDRefData[ ET_DLC7_NORTHSTAR_WARPAINT ] <- [] + file.limitedEditionFDRefData[ ET_DLC7_NORTHSTAR_WARPAINT ].append( CreateRefData( "northstar_skin_fd", "northstar" ) ) + file.limitedEditionFDRefData[ ET_DLC7_NORTHSTAR_WARPAINT ].append( CreateRefData( "callsign_fd_northstar_dynamic" ) ) + + file.limitedEditionFDRefData[ ET_DLC7_RONIN_WARPAINT ] <- [] + file.limitedEditionFDRefData[ ET_DLC7_RONIN_WARPAINT ].append( CreateRefData( "ronin_skin_fd", "ronin" ) ) + file.limitedEditionFDRefData[ ET_DLC7_RONIN_WARPAINT ].append( CreateRefData( "callsign_fd_ronin_dynamic" ) ) + + file.limitedEditionFDRefData[ ET_DLC7_TONE_WARPAINT ] <- [] + file.limitedEditionFDRefData[ ET_DLC7_TONE_WARPAINT ].append( CreateRefData( "tone_skin_fd", "tone" ) ) + file.limitedEditionFDRefData[ ET_DLC7_TONE_WARPAINT ].append( CreateRefData( "callsign_fd_tone_dynamic" ) ) + + file.limitedEditionFDRefData[ ET_DLC7_LEGION_WARPAINT ] <- [] + file.limitedEditionFDRefData[ ET_DLC7_LEGION_WARPAINT ].append( CreateRefData( "legion_skin_fd", "legion" ) ) + file.limitedEditionFDRefData[ ET_DLC7_LEGION_WARPAINT ].append( CreateRefData( "callsign_fd_legion_dynamic" ) ) + + file.limitedEditionFDRefData[ ET_DLC7_MONARCH_WARPAINT ] <- [] + file.limitedEditionFDRefData[ ET_DLC7_MONARCH_WARPAINT ].append( CreateRefData( "monarch_skin_fd", "vanguard" ) ) + file.limitedEditionFDRefData[ ET_DLC7_MONARCH_WARPAINT ].append( CreateRefData( "callsign_fd_monarch_dynamic" ) ) +} + +RefData function CreateRefData( string ref, string parentRef = "" ) +{ + RefData data + data.ref = ref + data.parentRef = parentRef + + return data +} + +array<string> function Store_GetCustomizationRefs( int entitlementId ) +{ + return file.customizationRefs[ entitlementId ] +} + +array<string> function Store_GetPatchRefs( int entitlementId ) +{ + return file.patchRefs[ entitlementId ] +} + +array<string> function Store_GetBannerRefs( int entitlementId ) +{ + return file.bannerRefs[ entitlementId ] +} + +array<CamoRef> function Store_GetCamoRefs( int entitlementId ) +{ + return file.camoRefs[ entitlementId ] +} + +void function AddCamoRef( int entitlementId, int index ) +{ + CamoRef cref + cref.ref = "camo_skin" + index + cref.pilotRef = "pilot_camo_skin" + index + cref.titanRef = "titan_camo_skin" + index + file.camoRefs[ entitlementId ].append( cref ) +} + +#if SERVER +bool function ClientCommand_SetHasSeenStore( entity player, array<string> args ) +{ + player.SetPersistentVar( "hasSeenStore", true ) + + return true +} + +// TODO: refParam is problematic, because it assumes an entitlement maps to a single ref which is not the case for limited edition frontier defense entitlements +void function StoreUpdatePIN( entity player, int entitlementId, string refParam ) +{ + printt( "StoreUpdatePIN", entitlementId, refParam) + + switch ( entitlementId ) + { + case ET_DLC1_BUNDLE: + PIN_BuyItemWithRealMoney( player, false, "dlc1_bundle", 0 ) + break + + case ET_DLC3_BUNDLE: + PIN_BuyItemWithRealMoney( player, false, "dlc3_bundle", 0 ) + break + + case ET_DLC5_BUNDLE: + PIN_BuyItemWithRealMoney( player, false, "dlc5_bundle", 0 ) + break + + case ET_PRIME_TITANS_BUNDLE: + PIN_BuyItemWithRealMoney( player, false, "sprime_titans_bundle", 0 ) + break + + case ET_DLC1_PRIME_ION: + case ET_DLC1_PRIME_SCORCH: + case ET_DLC3_PRIME_NORTHSTAR: + case ET_DLC3_PRIME_LEGION: + case ET_DLC5_PRIME_TONE: + case ET_DLC5_PRIME_RONIN: + Assert( refParam != "" ) + if ( refParam == "" || !ItemDefined( refParam ) ) + return + + PIN_BuyItemWithRealMoney( player, false, refParam, 0 ) + break + + case ET_DLC1_ION: + case ET_DLC1_SCORCH: + case ET_DLC1_NORTHSTAR: + case ET_DLC1_RONIN: + case ET_DLC1_TONE: + case ET_DLC1_LEGION: + Assert( refParam != "" ) + if ( refParam == "" ) + return + + PIN_BuyItemWithRealMoney( player, false, "customization_" + refParam, 0 ) + break + + case ET_DLC3_ION: + case ET_DLC3_SCORCH: + case ET_DLC3_NORTHSTAR: + case ET_DLC3_RONIN: + case ET_DLC3_TONE: + case ET_DLC3_LEGION: + Assert( refParam != "" ) + if ( refParam == "" ) + return + + PIN_BuyItemWithRealMoney( player, false, "dlc3_customization_" + refParam, 0 ) + break + + case ET_DLC5_ION: + case ET_DLC5_SCORCH: + case ET_DLC5_NORTHSTAR: + case ET_DLC5_RONIN: + case ET_DLC5_TONE: + case ET_DLC5_LEGION: + Assert( refParam != "" ) + if ( refParam == "" ) + return + + PIN_BuyItemWithRealMoney( player, false, "dlc5_customization_" + refParam, 0 ) + break + + case ET_DLC1_CAMO: + PIN_BuyItemWithRealMoney( player, false, "dlc_camos", 0 ) + break + + case ET_DLC3_CAMO: + PIN_BuyItemWithRealMoney( player, false, "dlc3_camos", 0 ) + break + + case ET_DLC5_CAMO: + PIN_BuyItemWithRealMoney( player, false, "dlc5_camos", 0 ) + break + + case ET_DLC1_CALLSIGN: + PIN_BuyItemWithRealMoney( player, false, "dlc_callsigns", 0 ) + break + + case ET_DLC3_CALLSIGN: + PIN_BuyItemWithRealMoney( player, false, "dlc3_callsigns", 0 ) + break + + case ET_DLC5_CALLSIGN: + PIN_BuyItemWithRealMoney( player, false, "dlc5_callsigns", 0 ) + break + + case ET_DLC7_TITAN_WARPAINT_BUNDLE: + PIN_BuyItemWithRealMoney( player, false, "dlc7_frontier_titan_warpaint_bundle", 0 ) + break + + case ET_DLC7_ION_WARPAINT: + case ET_DLC7_SCORCH_WARPAINT: + case ET_DLC7_NORTHSTAR_WARPAINT: + case ET_DLC7_RONIN_WARPAINT: + case ET_DLC7_TONE_WARPAINT: + case ET_DLC7_LEGION_WARPAINT: + case ET_DLC7_MONARCH_WARPAINT: + PIN_BuyItemWithRealMoney( player, false, "dlc7_frontier_titan_warpaint_" + file.limitedEditionFDRefData[ entitlementId ][0].ref, 0 ) + break + + case ET_DLC7_WEAPON_BUNDLE: + PIN_BuyItemWithRealMoney( player, false, "dlc7_weapon_warpaint_bundle", 0 ) + break + + case ET_DLC7_R201_WARPAINT: + case ET_DLC7_G2A5_WARPAINT: + case ET_DLC7_FLATLINE_WARPAINT: + case ET_DLC7_CAR_WARPAINT: + case ET_DLC7_ALTERNATOR_WARPAINT: + case ET_DLC7_EVA8_WARPAINT: + case ET_DLC7_WINGMAN_WARPAINT: + case ET_DLC7_ARCHER_WARPAINT: + Assert( refParam != "" ) + if ( refParam == "" ) + return + + PIN_BuyItemWithRealMoney( player, false, "dlc7_weapon_warpaint_" + refParam, 0 ) + break + + case ET_DLC8_WEAPON_WARPAINT_BUNDLE: + PIN_BuyItemWithRealMoney( player, false, "dlc8_weapon_warpaint_bundle", 0 ) + break + + case ET_DLC8_R201_WARPAINT: + case ET_DLC8_HEMLOK_WARPAINT: + case ET_DLC8_R97_WARPAINT: + case ET_DLC8_KRABER_WARPAINT: + case ET_DLC8_SPITFIRE_WARPAINT: + case ET_DLC8_DEVOTION_WARPAINT: + case ET_DLC8_MOZAMBIQUE_WARPAINT: + case ET_DLC8_THUNDERBOLT_WARPAINT: + Assert( refParam != "" ) + if ( refParam == "" ) + return + + PIN_BuyItemWithRealMoney( player, false, "dlc8_weapon_warpaint_" + refParam, 0 ) + break + + case ET_JUMPSTARTERBUNDLE: + PIN_BuyItemWithRealMoney( player, false, "dlc10_jump_starter_pack", 0 ) + + case ET_DLC9_WEAPON_WARPAINT_BUNDLE: + PIN_BuyItemWithRealMoney( player, false, "dlc9_weapon_warpaint_bundle", 0 ) + break + + case ET_DLC9_LSTAR_WARPAINT: + case ET_DLC9_MASTIFF_WARPAINT: + case ET_DLC9_SIDEWINDER_WARPAINT: + case ET_DLC9_R201_WARPAINT: + case ET_DLC9_CAR_WARPAINT: + case ET_DLC9_SPITFIRE_WARPAINT: + Assert( refParam != "" ) + if ( refParam == "" ) + return + + PIN_BuyItemWithRealMoney( player, false, "dlc9_weapon_warpaint_" + refParam, 0 ) + break + + case ET_DLC10_WEAPON_WARPAINT_BUNDLE: + PIN_BuyItemWithRealMoney( player, false, "dlc10_weapon_warpaint_bundle", 0 ) + break + + case ET_DLC10_R101_WARPAINT: + case ET_DLC10_FLATLINE_WARPAINT: + case ET_DLC10_VOLT_WARPAINT: + case ET_DLC10_ALTERNATOR_WARPAINT: + case ET_DLC10_SOFTBALL_WARPAINT: + case ET_DLC10_EPG1_WARPAINT: + Assert( refParam != "" ) + if ( refParam == "" ) + return + + PIN_BuyItemWithRealMoney( player, false, "dlc10_weapon_warpaint_" + refParam, 0 ) + break + + case ET_DLC11_WEAPON_WARPAINT_BUNDLE: + PIN_BuyItemWithRealMoney( player, false, "dlc11_weapon_warpaint_bundle", 0 ) + break + + case ET_DLC11_DMR_WARPAINT: + case ET_DLC11_DOUBLETAKE_WARPAINT: + case ET_DLC11_G2A5_WARPAINT: + case ET_DLC11_COLDWAR_WARPAINT: + case ET_DLC11_R97_WARPAINT: + case ET_DLC11_R101_WARPAINT: + Assert( refParam != "" ) + if ( refParam == "" ) + return + + PIN_BuyItemWithRealMoney( player, false, "dlc11_weapon_warpaint_" + refParam, 0 ) + break + } +} + +bool function ClientCommand_StoreSetNewItemStatus( entity player, array<string> args ) +{ + // fix crash + if( args.len() < 1 ) + return true + + int entitlementId = int( args[0] ) + string refParam + string parentRefParam + + if ( args.len() >= 2 ) + refParam = args[1] + + if ( args.len() >= 3 ) + parentRefParam = args[2] + + StoreUpdatePIN( player, entitlementId, refParam ) + StoreSetNewItemStatus( player, entitlementId, refParam, parentRefParam ) + + return true +} + +bool function StoreSetNewItemStatus( entity player, int entitlementId, string refParam, string parentRefParam ) +{ + string e = entitlementId == ET_JUMPSTARTERBUNDLE ? "ET_JUMPSTARTERBUNDLE" : string( entitlementId ) + printt( "!!!!!!!!!!! StoreSetNewItemStatus() running for entitlement:", e ) + + switch ( entitlementId ) + { + case 3: + // Prime Titans + SetItemNewStatus( player, "ion_prime", "", true ) + SetItemNewStatus( player, "scorch_prime", "", true ) + + // Customization Packs + table< string, array<string> > dlc1BundleCustomizationRefs + dlc1BundleCustomizationRefs[ "ion" ] <- file.customizationRefs[ ET_DLC1_ION ] + dlc1BundleCustomizationRefs[ "tone" ] <- file.customizationRefs[ ET_DLC1_TONE ] + dlc1BundleCustomizationRefs[ "scorch" ] <- file.customizationRefs[ ET_DLC1_SCORCH ] + dlc1BundleCustomizationRefs[ "legion" ] <- file.customizationRefs[ ET_DLC1_LEGION ] + dlc1BundleCustomizationRefs[ "ronin" ] <- file.customizationRefs[ ET_DLC1_RONIN ] + dlc1BundleCustomizationRefs[ "northstar" ] <- file.customizationRefs[ ET_DLC1_NORTHSTAR ] + + foreach ( parentRef, childRefs in dlc1BundleCustomizationRefs ) + { + foreach ( ref in childRefs ) + { + if ( !SubitemDefined( parentRef, ref ) ) + return false + + SetItemNewStatus( player, ref, parentRef, true ) + } + } + + // Callsigns + foreach ( ref in file.patchRefs[ ET_DLC1_CALLSIGN ] ) + { + SetItemNewStatus( player, ref, "", true ) + } + + break + + case ET_DLC3_BUNDLE: + // Prime Titans + SetItemNewStatus( player, "northstar_prime", "", true ) + SetItemNewStatus( player, "legion_prime", "", true ) + + // Customization Packs + table< string, array<string> > dlc3BundleCustomizationRefs + dlc3BundleCustomizationRefs[ "ion" ] <- file.customizationRefs[ ET_DLC3_ION ] + dlc3BundleCustomizationRefs[ "tone" ] <- file.customizationRefs[ ET_DLC3_TONE ] + dlc3BundleCustomizationRefs[ "scorch" ] <- file.customizationRefs[ ET_DLC3_SCORCH ] + dlc3BundleCustomizationRefs[ "legion" ] <- file.customizationRefs[ ET_DLC3_LEGION ] + dlc3BundleCustomizationRefs[ "ronin" ] <- file.customizationRefs[ ET_DLC3_RONIN ] + dlc3BundleCustomizationRefs[ "northstar" ] <- file.customizationRefs[ ET_DLC3_NORTHSTAR ] + + foreach ( parentRef, childRefs in dlc3BundleCustomizationRefs ) + { + foreach ( ref in childRefs ) + { + if ( !SubitemDefined( parentRef, ref ) ) + return false + + SetItemNewStatus( player, ref, parentRef, true ) + } + } + + // Callsigns + foreach ( ref in file.patchRefs[ ET_DLC3_CALLSIGN ] ) + { + SetItemNewStatus( player, ref, "", true ) + } + + break + + case ET_DLC5_BUNDLE: + // Prime Titans + SetItemNewStatus( player, "ronin_prime", "", true ) + SetItemNewStatus( player, "tone_prime", "", true ) + + // Customization Packs + table< string, array<string> > dlc5BundleCustomizationRefs + dlc5BundleCustomizationRefs[ "ion" ] <- file.customizationRefs[ ET_DLC5_ION ] + dlc5BundleCustomizationRefs[ "tone" ] <- file.customizationRefs[ ET_DLC5_TONE ] + dlc5BundleCustomizationRefs[ "scorch" ] <- file.customizationRefs[ ET_DLC5_SCORCH ] + dlc5BundleCustomizationRefs[ "legion" ] <- file.customizationRefs[ ET_DLC5_LEGION ] + dlc5BundleCustomizationRefs[ "ronin" ] <- file.customizationRefs[ ET_DLC5_RONIN ] + dlc5BundleCustomizationRefs[ "northstar" ] <- file.customizationRefs[ ET_DLC5_NORTHSTAR ] + + foreach ( parentRef, childRefs in dlc5BundleCustomizationRefs ) + { + foreach ( ref in childRefs ) + { + if ( !SubitemDefined( parentRef, ref ) ) + return false + + SetItemNewStatus( player, ref, parentRef, true ) + } + } + + // Callsigns + foreach ( ref in file.patchRefs[ ET_DLC5_CALLSIGN ] ) + { + SetItemNewStatus( player, ref, "", true ) + } + + break + + case ET_PRIME_TITANS_BUNDLE: + SetItemNewStatus( player, "ion_prime", "", true ) + SetItemNewStatus( player, "scorch_prime", "", true ) + SetItemNewStatus( player, "northstar_prime", "", true ) + SetItemNewStatus( player, "legion_prime", "", true ) + SetItemNewStatus( player, "ronin_prime", "", true ) + SetItemNewStatus( player, "tone_prime", "", true ) + break + + case ET_DLC1_PRIME_ION: + case ET_DLC1_PRIME_SCORCH: + case ET_DLC3_PRIME_NORTHSTAR: + case ET_DLC3_PRIME_LEGION: + case ET_DLC5_PRIME_TONE: + case ET_DLC5_PRIME_RONIN: + Assert( refParam != "" ) + if ( refParam == "" || !ItemDefined( refParam ) ) + return false + + SetItemNewStatus( player, refParam, "", true ) + break + + case ET_DLC1_ION: + case ET_DLC3_ION: + case ET_DLC5_ION: + case ET_DLC1_SCORCH: + case ET_DLC3_SCORCH: + case ET_DLC5_SCORCH: + case ET_DLC1_NORTHSTAR: + case ET_DLC3_NORTHSTAR: + case ET_DLC5_NORTHSTAR: + case ET_DLC1_RONIN: + case ET_DLC3_RONIN: + case ET_DLC5_RONIN: + case ET_DLC1_TONE: + case ET_DLC3_TONE: + case ET_DLC5_TONE: + case ET_DLC1_LEGION: + case ET_DLC3_LEGION: + case ET_DLC5_LEGION: + Assert( refParam != "" ) + if ( refParam == "" ) + return false + + foreach ( ref in file.customizationRefs[ entitlementId ] ) + { + if ( !SubitemDefined( refParam, ref ) ) + return false + + SetItemNewStatus( player, ref, refParam, true ) + } + break + + case ET_DLC1_CAMO: + case ET_DLC3_CAMO: + case ET_DLC5_CAMO: + // Not implemented, way too many camos would show as new + break + + case ET_DLC1_CALLSIGN: + case ET_DLC3_CALLSIGN: + case ET_DLC5_CALLSIGN: + foreach ( ref in file.patchRefs[ entitlementId ] ) + { + SetItemNewStatus( player, ref, "", true ) + } + + foreach ( ref in file.bannerRefs[ entitlementId ] ) + { + SetItemNewStatus( player, ref, "", true ) + } + break + + case ET_DLC7_TITAN_WARPAINT_BUNDLE: + array<int> childEntitlements = GetChildEntitlements( entitlementId ) + + foreach ( entitlement in childEntitlements ) + { + foreach ( data in file.limitedEditionFDRefData[ entitlement ] ) + SetItemNewStatus( player, data.ref, data.parentRef, true ) + } + break + + case ET_DLC7_ION_WARPAINT: + case ET_DLC7_SCORCH_WARPAINT: + case ET_DLC7_NORTHSTAR_WARPAINT: + case ET_DLC7_RONIN_WARPAINT: + case ET_DLC7_TONE_WARPAINT: + case ET_DLC7_LEGION_WARPAINT: + case ET_DLC7_MONARCH_WARPAINT: + foreach ( data in file.limitedEditionFDRefData[ entitlementId ] ) + SetItemNewStatus( player, data.ref, data.parentRef, true ) + break + + case ET_DLC7_R201_WARPAINT: + case ET_DLC7_G2A5_WARPAINT: + case ET_DLC7_FLATLINE_WARPAINT: + case ET_DLC7_CAR_WARPAINT: + case ET_DLC7_ALTERNATOR_WARPAINT: + case ET_DLC7_EVA8_WARPAINT: + case ET_DLC7_WINGMAN_WARPAINT: + case ET_DLC7_ARCHER_WARPAINT: + case ET_DLC8_R201_WARPAINT: + case ET_DLC8_HEMLOK_WARPAINT: + case ET_DLC8_R97_WARPAINT: + case ET_DLC8_KRABER_WARPAINT: + case ET_DLC8_SPITFIRE_WARPAINT: + case ET_DLC8_DEVOTION_WARPAINT: + case ET_DLC8_MOZAMBIQUE_WARPAINT: + case ET_DLC8_THUNDERBOLT_WARPAINT: + case ET_DLC9_LSTAR_WARPAINT: + case ET_DLC9_MASTIFF_WARPAINT: + case ET_DLC9_SIDEWINDER_WARPAINT: + case ET_DLC9_R201_WARPAINT: + case ET_DLC9_CAR_WARPAINT: + case ET_DLC9_SPITFIRE_WARPAINT: + case ET_DLC10_R101_WARPAINT: + case ET_DLC10_FLATLINE_WARPAINT: + case ET_DLC10_VOLT_WARPAINT: + case ET_DLC10_ALTERNATOR_WARPAINT: + case ET_DLC10_SOFTBALL_WARPAINT: + case ET_DLC10_EPG1_WARPAINT: + case ET_DLC11_DMR_WARPAINT: + case ET_DLC11_DOUBLETAKE_WARPAINT: + case ET_DLC11_G2A5_WARPAINT: + case ET_DLC11_COLDWAR_WARPAINT: + case ET_DLC11_R97_WARPAINT: + case ET_DLC11_R101_WARPAINT: + Assert( refParam != "" ) + if ( refParam == "" || !ItemDefined( refParam ) ) + return false + + Assert( parentRefParam != "" ) + if ( parentRefParam == "" || !ItemDefined( parentRefParam ) ) + return false + + SetItemNewStatus( player, refParam, parentRefParam, true ) + break + + case ET_DLC7_WEAPON_BUNDLE: + SetItemNewStatus( player, "skin_rspn101_wasteland", "mp_weapon_rspn101", true ) + SetItemNewStatus( player, "skin_g2_masterwork", "mp_weapon_g2", true ) + SetItemNewStatus( player, "skin_vinson_blue_fade", "mp_weapon_vinson", true ) + SetItemNewStatus( player, "skin_car_crimson_fury", "mp_weapon_car", true ) + SetItemNewStatus( player, "skin_alternator_patriot", "mp_weapon_alternator_smg", true ) + SetItemNewStatus( player, "skin_shotgun_badlands", "mp_weapon_shotgun", true ) + SetItemNewStatus( player, "skin_wingman_aqua_fade", "mp_weapon_wingman", true ) + SetItemNewStatus( player, "skin_rocket_launcher_psych_spectre", "mp_weapon_rocket_launcher", true ) + break + + case ET_DLC8_WEAPON_WARPAINT_BUNDLE: + SetItemNewStatus( player, "skin_rspn101_patriot", "mp_weapon_rspn101", true ) + SetItemNewStatus( player, "skin_hemlok_mochi", "mp_weapon_hemlok", true ) + SetItemNewStatus( player, "skin_r97_purple_fade", "mp_weapon_r97", true ) + SetItemNewStatus( player, "skin_kraber_masterwork", "mp_weapon_sniper", true ) + SetItemNewStatus( player, "skin_spitfire_lead_farmer", "mp_weapon_lmg", true ) + SetItemNewStatus( player, "skin_devotion_rspn_customs", "mp_weapon_esaw", true ) + SetItemNewStatus( player, "skin_mozambique_crimson_fury", "mp_weapon_shotgun_pistol", true ) + SetItemNewStatus( player, "skin_thunderbolt_8bit", "mp_weapon_arc_launcher", true ) + break + + case ET_DLC9_WEAPON_WARPAINT_BUNDLE: + SetItemNewStatus( player, "skin_lstar_heatsink", "mp_weapon_lstar", true ) + SetItemNewStatus( player, "skin_mastiff_crimson_fury", "mp_weapon_mastiff", true ) + SetItemNewStatus( player, "skin_sidewinder_masterwork", "mp_weapon_smr", true ) + SetItemNewStatus( player, "skin_rspn101_halloween", "mp_weapon_rspn101", true ) + SetItemNewStatus( player, "skin_car_halloween", "mp_weapon_car", true ) + SetItemNewStatus( player, "skin_spitfire_halloween", "mp_weapon_lmg", true ) + break + + case ET_DLC10_WEAPON_WARPAINT_BUNDLE: + SetItemNewStatus( player, "skin_rspn101_og_blue_fade", "mp_weapon_rspn101_og", true ) + SetItemNewStatus( player, "skin_vinson_badlands", "mp_weapon_vinson", true ) + SetItemNewStatus( player, "skin_volt_heatsink", "mp_weapon_hemlok_smg", true ) + SetItemNewStatus( player, "skin_alternator_headhunter", "mp_weapon_alternator_smg", true ) + SetItemNewStatus( player, "skin_softball_masterwork", "mp_weapon_softball", true ) + SetItemNewStatus( player, "skin_epg_mrvn", "mp_weapon_epg", true ) + break + + case ET_DLC11_WEAPON_WARPAINT_BUNDLE: + SetItemNewStatus( player, "skin_dmr_phantom", "mp_weapon_dmr", true ) + SetItemNewStatus( player, "skin_doubletake_masterwork", "mp_weapon_doubletake", true ) + SetItemNewStatus( player, "skin_g2_purple_fade", "mp_weapon_g2", true ) + SetItemNewStatus( player, "skin_coldwar_heatsink", "mp_weapon_pulse_lmg", true ) + SetItemNewStatus( player, "skin_r97_sky", "mp_weapon_r97", true ) + SetItemNewStatus( player, "skin_rspn101_crimson_fury", "mp_weapon_rspn101", true ) + break + + case ET_JUMPSTARTERBUNDLE: + UnlockUltimateEdition( player ) + break + } + + return true +} +#endif |