aboutsummaryrefslogtreecommitdiff
path: root/Northstar.CustomServers/mod/scripts/vscripts/gamemodes
diff options
context:
space:
mode:
authorGeckoEidechse <40122905+GeckoEidechse@users.noreply.github.com>2024-08-14 17:31:06 +0200
committerGitHub <noreply@github.com>2024-08-14 17:31:06 +0200
commitfa4e319c0b60cd68a3dccaa4322e3c35cfa1e385 (patch)
tree4a78391c5008a4aa50d81fbca5d9f26bb475cd96 /Northstar.CustomServers/mod/scripts/vscripts/gamemodes
parentbcec5a5e9edd2a2af3a017ea4b250a9ba1112e6f (diff)
parent7aa3958ccd8e32970736654dfae0c7a87f0798bb (diff)
downloadNorthstarMods-fa4e319c0b60cd68a3dccaa4322e3c35cfa1e385.tar.gz
NorthstarMods-fa4e319c0b60cd68a3dccaa4322e3c35cfa1e385.zip
Merge branch 'main' into permanent-amped-weapons-fix-pr
Diffstat (limited to 'Northstar.CustomServers/mod/scripts/vscripts/gamemodes')
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_ai_gamemodes.gnut144
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_aitdm.nut162
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_at.nut1831
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_cp.nut58
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ctf.nut73
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ffa.nut17
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_fra.nut1
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_lts.nut33
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_mfd.nut22
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ps.nut1
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_speedball.nut1
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_tdm.nut1
-rw-r--r--Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ttdm.nut31
13 files changed, 2062 insertions, 313 deletions
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_ai_gamemodes.gnut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_ai_gamemodes.gnut
index d6d578bb..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 )
-{
- file.gruntWeapons = weapons
-}
-
-void function AiGameModes_SetSpectreWeapons( array< string > weapons )
+void function AiGameModes_SetNPCWeapons( string npcClass, 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,37 +86,100 @@ 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 )
{
+ float reaperLandTime = REAPER_WARPFALL_DELAY + 1.2 // reaper takes ~1.2s to warpfall
+ thread HotDrop_Spawnpoint( pos, team, reaperLandTime, false, damagedef_reaper_fall )
+
+ 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, < 1,1,1 > )
+ SetDefaultMPEnemyHighlight( reaper )
+ Highlight_SetEnemyHighlight( reaper, "enemy_titan" )
+
SetSpawnOption_Titanfall( reaper )
SetSpawnOption_Warpfall( reaper )
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 HotDrop_Spawnpoint( vector origin, int team, float impactTime, bool hasFriendlyWarning = false, int damageDef = -1 )
+{
+ array<entity> targetEffects = []
+ vector surfaceNormal = < 0, 0, 1 >
+
+ int index = GetParticleSystemIndex( $"P_ar_titan_droppoint" )
+
+ if( hasFriendlyWarning )
+ {
+ entity effectFriendly = StartParticleEffectInWorld_ReturnEntity( index, origin, surfaceNormal )
+ SetTeam( effectFriendly, team )
+ 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, 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 )
@@ -141,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 9a94b848..7d73c926 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,48 @@ 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()
+ SetupGenericTDMChallenge()
}
+// 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,10 +118,12 @@ 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
// Split score so we can check if we are over the score max
// without showing the wrong value on client
@@ -189,7 +235,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 )
{
@@ -234,6 +280,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 )
{
@@ -248,7 +295,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 )
@@ -256,7 +303,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() ) ]
@@ -302,19 +349,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
@@ -351,30 +398,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 )
}
@@ -392,16 +456,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)
}
}
@@ -507,4 +587,4 @@ void function AITdm_CleanupBoredNPCThread( entity guy )
print( "cleaning up bored npc: " + guy + " from team " + guy.GetTeam() )
guy.Destroy()
-} \ No newline at end of file
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_at.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_at.nut
index 915e03e0..9cf0021d 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,582 @@ 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()
+ 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 ) ) )
+
+ if( AT_GetPlayerBonusPoints( player ) >= 600 && !HasPlayerCompletedMeritScore( player ) ) //Challenge is: "Earn $600."
+ {
+ AddPlayerScore( player, "ChallengeATAssault" )
+ SetPlayerChallengeMeritScore( 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 AT_PlayerObjectiveThink( entity player )
+{
+ 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 CreateATCamp( entity spawnpoint )
+////////////////////////////////
+///// PLAYER FUNCTIONS END /////
+////////////////////////////////
+
+
+
+////////////////////////////////////////
+///// GAMEMODE INITILAZE FUNCTIONS /////
+////////////////////////////////////////
+
+void function OnEntitiesDidLoad()
{
- // delay this so we don't do stuff before all spawns are initialised and that
- file.campsToRegisterOnEntitiesDidLoad.append( spawnpoint )
+ foreach ( entity info_target in GetEntArrayByClass_Expensive( "info_target" ) )
+ {
+ 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 CreateATCamps_Delayed()
+void function AT_PlayerOrNPCKilledScoreEvent( entity victim, entity attacker, var damageInfo )
{
- // 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 )
+ if ( !IsValid( attacker ) )
+ return
+
+ // Suicide
+ if ( attacker == victim )
{
- 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 ( victim.IsPlayer() )
+ AT_PlayerBonusLoss( victim, AT_GetPlayerBonusPoints( victim ) / 2 )
- // 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
+ }
+
+ // 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 )
- // get droppod spawns
- foreach ( entity spawnpoint in SpawnPoints_GetDropPod() )
- if ( Distance( camp.GetOrigin(), spawnpoint.GetOrigin() ) < 1500.0 )
- campStruct.dropPodSpawnPoints.append( spawnpoint )
+ 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
- foreach ( entity spawnpoint in SpawnPoints_GetTitan() )
- if ( Distance( camp.GetOrigin(), spawnpoint.GetOrigin() ) < 1500.0 )
- campStruct.titanSpawnPoints.append( spawnpoint )
+ if( GetPetTitanOwner( victim ).IsPlayer() ) // Killed player npc titan
+ return
+
+ 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
- // todo: turret spawns someday maybe
+ 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.camps.append( campStruct )
+ 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
+
+ if ( !victim.IsPlayer() )
+ return false
+
+ 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
+ )
+}
+
+// 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
+}
-// don't use this where possible as it doesn't set score and stuff
-void function AT_SetPlayerCash( entity player, int amount )
+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 +642,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 )
+}
+
+// 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 )
}
-// run gamestate
+// 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 )
-void function RunATGame()
+ Remote_CallFunction_NonReplay(
+ player,
+ "ServerCallback_AT_BossDamageScorePopup",
+ amount, // damageScore
+ amount, // damageBonus
+ bossEHandle, // bossEHandle
+ damageOrigin.x, // x
+ damageOrigin.y, // y
+ damageOrigin.z // z
+ )
+}
+
+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
+ }
+
+ // Remove bonus points and add them to total poins
+ AT_AddPlayerBonusPoints( player, -bonusToUpload )
+ AT_AddPlayerTotalPoints( player, bonusToUpload )
+
+ uploadInfo.depositedPoints += bonusToUpload
+ WaitFrame()
+ }
+}
+
+//////////////////////////////
+///// BANK FUNCTIONS END /////
+//////////////////////////////
-void function AT_SpawnDroppodSquad( int camp, string aiType )
+
+
+/////////////////////////
+///// 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 +1630,178 @@ 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
+ file.titanIsBountyBoss[ titan ] <- true
+ file.bountyTitanRewards[ titan ] <- ATTRITION_SCORE_BOSS_DAMAGE
+ AddEntityCallback_OnPostDamaged( titan, OnBountyTitanPostDamage )
+ 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 OnBountyTitanPostDamage( 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
}
+
+ 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..d8b0c9bd 100644
--- a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_cp.nut
+++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_cp.nut
@@ -29,6 +29,8 @@ struct {
array<HardpointStruct> hardpoints
array<CP_PlayerStruct> players
+ table<entity,int> playerAssaultPoints
+ table<entity,int> playerDefensePoints
} file
void function GamemodeCP_Init()
@@ -112,11 +114,13 @@ void function GamemodeCP_OnPlayerKilled(entity victim, entity attacker, var dama
{
AddPlayerScore( attacker , "HardpointDefense", victim )
attacker.AddToPlayerGameStat(PGS_DEFENSE_SCORE,POINTVALUE_HARDPOINT_DEFENSE)
+ UpdatePlayerScoreForChallenge(attacker,0,POINTVALUE_HARDPOINT_DEFENSE)
}
else if((victimCP.hardpoint.GetTeam()==victim.GetTeam())||(GetHardpointCappingTeam(victimCP)==victim.GetTeam()))
{
AddPlayerScore( attacker, "HardpointAssault", victim )
attacker.AddToPlayerGameStat(PGS_ASSAULT_SCORE,POINTVALUE_HARDPOINT_ASSAULT)
+ UpdatePlayerScoreForChallenge(attacker,POINTVALUE_HARDPOINT_ASSAULT,0)
}
}
}
@@ -127,10 +131,12 @@ void function GamemodeCP_OnPlayerKilled(entity victim, entity attacker, var dama
{
AddPlayerScore( attacker , "HardpointSnipe", victim )
attacker.AddToPlayerGameStat(PGS_ASSAULT_SCORE,POINTVALUE_HARDPOINT_SNIPE)
+ UpdatePlayerScoreForChallenge(attacker,POINTVALUE_HARDPOINT_SNIPE,0)
}
else{
AddPlayerScore( attacker , "HardpointSiege", victim )
attacker.AddToPlayerGameStat(PGS_ASSAULT_SCORE,POINTVALUE_HARDPOINT_SIEGE)
+ UpdatePlayerScoreForChallenge(attacker,POINTVALUE_HARDPOINT_SIEGE,0)
}
}
else if(attackerCP.hardpoint!=null)//Perimeter Defense
@@ -138,6 +144,7 @@ void function GamemodeCP_OnPlayerKilled(entity victim, entity attacker, var dama
if(attackerCP.hardpoint.GetTeam()==attacker.GetTeam())
AddPlayerScore( attacker , "HardpointPerimeterDefense", victim)
attacker.AddToPlayerGameStat(PGS_DEFENSE_SCORE,POINTVALUE_HARDPOINT_PERIMETER_DEFENSE)
+ UpdatePlayerScoreForChallenge(attacker,0,POINTVALUE_HARDPOINT_PERIMETER_DEFENSE)
}
foreach(CP_PlayerStruct player in file.players) //Reset Victim Holdtime Counter
@@ -308,6 +315,7 @@ void function CapturePointForTeam(HardpointStruct hardpoint, int Team)
if(player.IsPlayer()){
AddPlayerScore(player,"ControlPointCapture")
player.AddToPlayerGameStat(PGS_ASSAULT_SCORE,POINTVALUE_HARDPOINT_CAPTURE)
+ UpdatePlayerScoreForChallenge(player,POINTVALUE_HARDPOINT_CAPTURE,0)
}
}
}
@@ -319,12 +327,17 @@ void function GamemodeCP_InitPlayer(entity player)
playerStruct.timeOnPoints = [0.0,0.0,0.0]
playerStruct.isOnHardpoint = false
file.players.append(playerStruct)
+ file.playerAssaultPoints[player] <- 0
+ file.playerDefensePoints[player] <- 0
thread PlayerThink(playerStruct)
}
void function GamemodeCP_RemovePlayer(entity player)
{
-
+ if(player in file.playerAssaultPoints)
+ delete file.playerAssaultPoints[player]
+ if(player in file.playerDefensePoints)
+ delete file.playerDefensePoints[player]
foreach(index,CP_PlayerStruct playerStruct in file.players)
{
if(playerStruct.player==player)
@@ -376,11 +389,13 @@ void function PlayerThink(CP_PlayerStruct player)
{
AddPlayerScore(player.player,"ControlPointAmpedHold")
player.player.AddToPlayerGameStat( PGS_DEFENSE_SCORE, POINTVALUE_HARDPOINT_AMPED_HOLD )
+ UpdatePlayerScoreForChallenge(player.player,0,POINTVALUE_HARDPOINT_AMPED_HOLD)
}
else
{
AddPlayerScore(player.player,"ControlPointHold")
player.player.AddToPlayerGameStat( PGS_DEFENSE_SCORE, POINTVALUE_HARDPOINT_HOLD )
+ UpdatePlayerScoreForChallenge(player.player,0,POINTVALUE_HARDPOINT_HOLD)
}
}
break
@@ -471,8 +486,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 +563,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))
{
@@ -570,6 +589,7 @@ void function HardpointThink( HardpointStruct hardpoint )
{
AddPlayerScore(player,"ControlPointAmped")
player.AddToPlayerGameStat(PGS_DEFENSE_SCORE,POINTVALUE_HARDPOINT_AMPED)
+ UpdatePlayerScoreForChallenge(player,0,POINTVALUE_HARDPOINT_AMPED)
}
}
}
@@ -645,7 +665,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 +684,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 +701,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"
@@ -703,3 +730,26 @@ string function GetHardpointGroup(entity hardpoint) //Hardpoint Entity B on Home
return string(hardpoint.kv.hardpointGroup)
}
+
+void function UpdatePlayerScoreForChallenge(entity player,int assaultpoints = 0,int defensepoints = 0)
+{
+ if(player in file.playerAssaultPoints)
+ {
+ file.playerAssaultPoints[player] += assaultpoints
+ if( file.playerAssaultPoints[player] >= 1000 && !HasPlayerCompletedMeritScore(player) )
+ {
+ AddPlayerScore(player,"ChallengeCPAssault")
+ SetPlayerChallengeMeritScore(player)
+ }
+ }
+
+ if(player in file.playerDefensePoints)
+ {
+ file.playerDefensePoints[player] += defensepoints
+ if( file.playerDefensePoints[player] >= 500 && !HasPlayerCompletedMeritScore(player) )
+ {
+ AddPlayerScore(player,"ChallengeCPDefense")
+ SetPlayerChallengeMeritScore(player)
+ }
+ }
+}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ctf.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ctf.nut
index 99f34164..97addc24 100644
--- a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ctf.nut
+++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ctf.nut
@@ -26,6 +26,8 @@ void function CaptureTheFlag_Init()
{
PrecacheModel( CTF_FLAG_MODEL )
PrecacheModel( CTF_FLAG_BASE_MODEL )
+ PrecacheParticleSystem( FLAG_FX_FRIENDLY )
+ PrecacheParticleSystem( FLAG_FX_ENEMY )
CaptureTheFlagShared_Init()
SetSwitchSidesBased( true )
@@ -73,27 +75,20 @@ void function RateSpawnpoints_CTF( int checkClass, array<entity> spawnpoints, in
bool function VerifyCTFSpawnpoint( entity spawnpoint, int team )
{
// ensure spawnpoints aren't too close to enemy base
+ vector allyFlagSpot
+ vector enemyFlagSpot
+ foreach ( entity spawn in GetEntArrayByClass_Expensive( "info_spawnpoint_flag" ) )
+ {
+ if( spawn.GetTeam() == team )
+ allyFlagSpot = spawn.GetOrigin()
+ else
+ enemyFlagSpot = spawn.GetOrigin()
+ }
- if ( HasSwitchedSides() )
- team = GetOtherTeam( team )
-
- array<entity> startSpawns = SpawnPoints_GetPilotStart( team )
- array<entity> enemyStartSpawns = SpawnPoints_GetPilotStart( GetOtherTeam( team ) )
-
- vector averageFriendlySpawns
- vector averageEnemySpawns
-
- foreach ( entity spawn in startSpawns )
- averageFriendlySpawns += spawn.GetOrigin()
-
- averageFriendlySpawns /= startSpawns.len()
-
- foreach ( entity spawn in enemyStartSpawns )
- averageEnemySpawns += spawn.GetOrigin()
-
- averageEnemySpawns /= startSpawns.len()
+ if( Distance2D( spawnpoint.GetOrigin(), allyFlagSpot ) > Distance2D( spawnpoint.GetOrigin(), enemyFlagSpot ) )
+ return false
- return Distance2D( spawnpoint.GetOrigin(), averageEnemySpawns ) / Distance2D( averageFriendlySpawns, averageEnemySpawns ) > 0.35
+ return true
}
void function CTFInitPlayer( entity player )
@@ -164,6 +159,9 @@ void function CreateFlags()
flag.SetValueForModelKey( CTF_FLAG_MODEL )
SetTeam( flag, flagTeam )
flag.MarkAsNonMovingAttachment()
+ flag.Minimap_AlwaysShow( TEAM_IMC, null ) // show flag icon on minimap
+ flag.Minimap_AlwaysShow( TEAM_MILITIA, null )
+ flag.Minimap_SetAlignUpright( true )
DispatchSpawn( flag )
flag.SetModel( CTF_FLAG_MODEL )
flag.SetOrigin( spawn.GetOrigin() + < 0, 0, base.GetBoundingMaxs().z * 2 > ) // ensure flag doesn't spawn clipped into geometry
@@ -291,7 +289,7 @@ void function GiveFlag( entity player, entity flag )
PlayFactionDialogueToTeamExceptPlayer( "ctf_flagPickupFriendly", player.GetTeam(), player )
MessageToTeam( flag.GetTeam(), eEventNotifications.PlayerHasFriendlyFlag, player, player )
- EmitSoundOnEntityToTeam( flag, "UI_CTF_EnemyGrabFlag", flag.GetTeam() )
+ EmitSoundOnEntityToTeam( flag, "UI_CTF_3P_EnemyGrabFlag", flag.GetTeam() )
SetFlagStateForTeam( flag.GetTeam(), eFlagState.Away ) // used for held
}
@@ -339,14 +337,13 @@ void function DropFlag( entity player, bool realDrop = true )
file.imcCaptureAssistList.append( player )
else
file.militiaCaptureAssistList.append( player )
-
+
// do notifications
MessageToPlayer( player, eEventNotifications.YouDroppedTheEnemyFlag )
EmitSoundOnEntityOnlyToPlayer( player, player, "UI_CTF_1P_FlagDrop" )
-
+
MessageToTeam( player.GetTeam(), eEventNotifications.PlayerDroppedEnemyFlag, player, player )
// todo need a sound here maybe
-
MessageToTeam( GetOtherTeam( player.GetTeam() ), eEventNotifications.PlayerDroppedFriendlyFlag, player, player )
// todo need a sound here maybe
}
@@ -417,20 +414,33 @@ void function CaptureFlag( entity player, entity flag )
assistList = file.militiaCaptureAssistList
foreach( entity assistPlayer in assistList )
+ {
if ( player != assistPlayer )
AddPlayerScore( assistPlayer, "FlagCaptureAssist", player )
+ if( !HasPlayerCompletedMeritScore( assistPlayer ) )
+ {
+ AddPlayerScore( assistPlayer, "ChallengeCTFCapAssist" )
+ SetPlayerChallengeMeritScore( assistPlayer )
+ }
+ }
assistList.clear()
-
+
// notifs
MessageToPlayer( player, eEventNotifications.YouCapturedTheEnemyFlag )
EmitSoundOnEntityOnlyToPlayer( player, player, "UI_CTF_1P_PlayerScore" )
+ if( !HasPlayerCompletedMeritScore( player ) )
+ {
+ AddPlayerScore( player, "ChallengeCTFRetAssist" )
+ SetPlayerChallengeMeritScore( player )
+ }
+
MessageToTeam( team, eEventNotifications.PlayerCapturedEnemyFlag, player, player )
EmitSoundOnEntityToTeamExceptPlayer( flag, "UI_CTF_3P_TeamScore", player.GetTeam(), player )
MessageToTeam( GetOtherTeam( team ), eEventNotifications.PlayerCapturedFriendlyFlag, player, player )
- EmitSoundOnEntityToTeam( flag, "UI_CTF_3P_EnemyScore", flag.GetTeam() )
+ EmitSoundOnEntityToTeam( flag, "UI_CTF_3P_EnemyScores", flag.GetTeam() )
if ( GameRules_GetTeamScore( team ) == GameMode_GetRoundScoreLimit( GAMETYPE ) - 1 )
{
@@ -446,6 +456,9 @@ void function OnPlayerEntersFlagReturnTrigger( entity trigger, entity player )
flag = file.imcFlag
else
flag = file.militiaFlag
+
+ if( !IsValid( flag ) || !IsValid( player ) )
+ return
if ( !player.IsPlayer() || player.IsTitan() || player.GetTeam() != flag.GetTeam() || IsFlagHome( flag ) || flag.GetParent() != null )
return
@@ -481,6 +494,7 @@ void function TryReturnFlag( entity player, entity flag )
})
player.EndSignal( "FlagReturnEnded" )
+ flag.EndSignal( "FlagReturnEnded" ) // avoid multiple players to return one flag at once
player.EndSignal( "OnDeath" )
wait CTF_GetFlagReturnTime()
@@ -488,12 +502,19 @@ void function TryReturnFlag( entity player, entity flag )
// flag return succeeded
// return flag
ResetFlag( flag )
-
+ flag.Signal( "FlagReturnEnded" )
+
// do notifications for return
MessageToPlayer( player, eEventNotifications.YouReturnedFriendlyFlag )
AddPlayerScore( player, "FlagReturn", player )
player.AddToPlayerGameStat( PGS_DEFENSE_SCORE, 1 )
+ if( !HasPlayerCompletedMeritScore( player ) )
+ {
+ AddPlayerScore( player, "ChallengeCTFRetAssist" )
+ SetPlayerChallengeMeritScore( player )
+ }
+
MessageToTeam( flag.GetTeam(), eEventNotifications.PlayerReturnedFriendlyFlag, null, player )
EmitSoundOnEntityToTeam( flag, "UI_CTF_3P_TeamReturnsFlag", flag.GetTeam() )
PlayFactionDialogueToTeam( "ctf_flagReturnedFriendly", flag.GetTeam() )
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/gamemodes/_gamemode_fra.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_fra.nut
index 9d8f84b5..6d0fd3c7 100644
--- a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_fra.nut
+++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_fra.nut
@@ -17,6 +17,7 @@ void function GamemodeFRA_Init()
ScoreEvent_SetEarnMeterValues( "PilotBatteryPickup", 0.0, 0.34 )
EarnMeterMP_SetPassiveMeterGainEnabled( false )
PilotBattery_SetMaxCount( 3 )
+ SetupGenericFFAChallenge()
AddCallback_OnPlayerKilled( FRARemoveEarnMeter )
}
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_lts.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_lts.nut
index 31c85a57..8999231d 100644
--- a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_lts.nut
+++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_lts.nut
@@ -8,6 +8,8 @@ struct {
float lastDamageInfoTime
bool shouldDoHighlights
+
+ table< entity, int > pilotstreak
} file
void function GamemodeLts_Init()
@@ -34,6 +36,37 @@ void function GamemodeLts_Init()
ClassicMP_SetCustomIntro( ClassicMP_DefaultNoIntro_Setup, ClassicMP_DefaultNoIntro_GetLength() )
ClassicMP_ForceDisableEpilogue( true )
AddCallback_GameStateEnter( eGameState.Playing, WaitForThirtySecondsLeft )
+
+ AddCallback_OnClientConnected( SetupPlayerLTSChallenges ) //Just to make up the Match Goals tracking
+ AddCallback_OnClientDisconnected( RemovePlayerLTSChallenges ) //Safety removal of data to prevent crashes
+ AddCallback_OnPlayerKilled( LTSChallengeForPlayerKilled )
+}
+
+void function SetupPlayerLTSChallenges( entity player )
+{
+ file.pilotstreak[ player ] <- 0
+}
+
+void function RemovePlayerLTSChallenges( entity player )
+{
+ if( player in file.pilotstreak )
+ delete file.pilotstreak[ player ]
+}
+
+void function LTSChallengeForPlayerKilled( entity victim, entity attacker, var damageInfo )
+{
+ if ( victim == attacker || !attacker.IsPlayer() || GetGameState() != eGameState.Playing )
+ return
+
+ if ( victim.IsPlayer() && attacker in file.pilotstreak )
+ {
+ file.pilotstreak[attacker]++
+ if( file.pilotstreak[attacker] >= 2 && !HasPlayerCompletedMeritScore( attacker ) )
+ {
+ AddPlayerScore( attacker, "ChallengeLTS" )
+ SetPlayerChallengeMeritScore( attacker )
+ }
+ }
}
void function WaitForThirtySecondsLeft()
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_mfd.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_mfd.nut
index 659dbb7a..768bbde1 100644
--- a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_mfd.nut
+++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_mfd.nut
@@ -180,6 +180,12 @@ void function MarkPlayers( entity imcMark, entity militiaMark )
entity livingMark = GetMarked( GetOtherTeam( deadMark.GetTeam() ) )
livingMark.SetPlayerGameStat( PGS_DEFENSE_SCORE, livingMark.GetPlayerGameStat( PGS_DEFENSE_SCORE ) + 1 )
+ if( !HasPlayerCompletedMeritScore( livingMark ) )
+ {
+ AddPlayerScore( livingMark, "ChallengeMFD" )
+ SetPlayerChallengeMeritScore( livingMark )
+ }
+
// thread this so we don't kill our own thread
thread AddTeamScore( livingMark.GetTeam(), 1 )
}
@@ -188,10 +194,22 @@ void function UpdateMarksForKill( entity victim, entity attacker, var damageInfo
{
if ( victim == GetMarked( victim.GetTeam() ) )
{
- MessageToAll( eEventNotifications.MarkedForDeathKill, null, victim, attacker.GetEncodedEHandle() )
+ // handle suicides. Not sure what the actual message is that vanilla shows for this
+ // but this will prevent crashing for now
+ bool isSuicide = IsSuicide( victim, attacker, DamageInfo_GetDamageSourceIdentifier( damageInfo ) )
+ entity actualAttacker = isSuicide ? victim : attacker
+
+ MessageToAll( eEventNotifications.MarkedForDeathKill, null, victim, actualAttacker.GetEncodedEHandle() )
svGlobal.levelEnt.Signal( "MarkKilled", { mark = victim } )
- if ( attacker.IsPlayer() )
+ if ( !isSuicide && attacker.IsPlayer() )
+ {
attacker.SetPlayerGameStat( PGS_ASSAULT_SCORE, attacker.GetPlayerGameStat( PGS_ASSAULT_SCORE ) + 1 )
+ if( !HasPlayerCompletedMeritScore( attacker ) )
+ {
+ AddPlayerScore( attacker, "ChallengeMFD" )
+ SetPlayerChallengeMeritScore( attacker )
+ }
+ }
}
} \ No newline at end of file
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ps.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ps.nut
index 57355ad8..c91c27d1 100644
--- a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ps.nut
+++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ps.nut
@@ -19,6 +19,7 @@ void function GamemodePs_Init()
AddCallback_OnPlayerKilled( GiveScoreForPlayerKill )
ScoreEvent_SetupEarnMeterValuesForMixedModes()
SetTimeoutWinnerDecisionFunc( CheckScoreForDraw )
+ SetupGenericFFAChallenge()
// spawnzone stuff
SetShouldCreateMinimapSpawnZones( true )
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_speedball.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_speedball.nut
index cb277b00..4617476e 100644
--- a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_speedball.nut
+++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_speedball.nut
@@ -18,6 +18,7 @@ void function GamemodeSpeedball_Init()
Riff_ForceTitanAvailability( eTitanAvailability.Never )
Riff_ForceSetEliminationMode( eEliminationMode.Pilots )
ScoreEvent_SetupEarnMeterValuesForMixedModes()
+ SetupGenericFFAChallenge()
AddSpawnCallbackEditorClass( "script_ref", "info_speedball_flag", CreateFlag )
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_tdm.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_tdm.nut
index 5c0e6fec..61ede2d4 100644
--- a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_tdm.nut
+++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_tdm.nut
@@ -6,6 +6,7 @@ void function GamemodeTdm_Init()
AddCallback_OnPlayerKilled( GiveScoreForPlayerKill )
ScoreEvent_SetupEarnMeterValuesForMixedModes()
SetTimeoutWinnerDecisionFunc( CheckScoreForDraw )
+ SetupGenericTDMChallenge()
}
void function GiveScoreForPlayerKill( entity victim, entity attacker, var damageInfo )
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ttdm.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ttdm.nut
index 6b30a399..3ba84394 100644
--- a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ttdm.nut
+++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_ttdm.nut
@@ -2,6 +2,11 @@ global function GamemodeTTDM_Init
const float TTDMIntroLength = 15.0
+struct
+{
+ table< entity, int > challengeCount
+} file
+
void function GamemodeTTDM_Init()
{
Riff_ForceSetSpawnAsTitan( eSpawnAsTitan.Always )
@@ -14,6 +19,8 @@ void function GamemodeTTDM_Init()
ClassicMP_ForceDisableEpilogue( true )
SetTimeoutWinnerDecisionFunc( CheckScoreForDraw )
+ AddCallback_OnClientConnected( SetupPlayerTTDMChallenges ) //Just to make up the Match Goals tracking
+ AddCallback_OnClientDisconnected( RemovePlayerTTDMChallenges ) //Safety removal of data to prevent crashes
AddCallback_OnPlayerKilled( AddTeamScoreForPlayerKilled ) // dont have to track autotitan kills since you cant leave your titan in this mode
// probably needs scoreevent earnmeter values
@@ -56,6 +63,17 @@ void function TTDMIntroShowIntermissionCam( entity player )
thread PlayerWatchesTTDMIntroIntermissionCam( player )
}
+void function SetupPlayerTTDMChallenges( entity player )
+{
+ file.challengeCount[ player ] <- 0
+}
+
+void function RemovePlayerTTDMChallenges( entity player )
+{
+ if( player in file.challengeCount )
+ delete file.challengeCount[ player ]
+}
+
void function PlayerWatchesTTDMIntroIntermissionCam( entity player )
{
player.EndSignal( "OnDestroy" )
@@ -79,6 +97,19 @@ void function AddTeamScoreForPlayerKilled( entity victim, entity attacker, var d
if ( victim == attacker || !victim.IsPlayer() || !attacker.IsPlayer() && GetGameState() == eGameState.Playing )
return
+ if( victim in file.challengeCount )
+ file.challengeCount[victim] = 0
+
+ if( attacker in file.challengeCount )
+ {
+ file.challengeCount[attacker]++
+ if( file.challengeCount[attacker] >= 2 && !HasPlayerCompletedMeritScore( attacker ) )
+ {
+ AddPlayerScore( attacker, "ChallengeTTDM" )
+ SetPlayerChallengeMeritScore( attacker )
+ }
+ }
+
AddTeamScore( GetOtherTeam( victim.GetTeam() ), 1 )
}