diff options
Diffstat (limited to 'Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_at.nut')
-rw-r--r-- | Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_at.nut | 366 |
1 files changed, 365 insertions, 1 deletions
diff --git a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_at.nut b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_at.nut index 573ea72fc..915e03e08 100644 --- a/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_at.nut +++ b/Northstar.CustomServers/mod/scripts/vscripts/gamemodes/_gamemode_at.nut @@ -1,12 +1,376 @@ 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 + +const array<string> VALID_BOUNTY_TITAN_SETTINGS = [ + "npc_titan_atlas_stickybomb_bounty", + "npc_titan_atlas_tracker_bounty", + "npc_titan_ogre_minigun_bounty", + "npc_titan_ogre_meteor_bounty", + "npc_titan_stryder_leadwall_bounty", + "npc_titan_stryder_sniper_bounty", + "npc_titan_atlas_vanguard_bounty" +] + + +// 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 + +struct { + array<entity> campsToRegisterOnEntitiesDidLoad + + array<entity> banks + array<AT_WaveOrigin> camps + + table< int, table< string, int > > trackedCampNPCSpawns +} file + void function GamemodeAt_Init() { + AddCallback_GameStateEnter( eGameState.Playing, RunATGame ) + + AddCallback_OnClientConnected( InitialiseATPlayer ) + AddSpawnCallbackEditorClass( "info_target", "info_attrition_bank", CreateATBank ) + AddSpawnCallbackEditorClass( "info_target", "info_attrition_camp", CreateATCamp ) + AddCallback_EntitiesDidLoad( CreateATCamps_Delayed ) } void function RateSpawnpoints_AT( int checkclass, array<entity> spawnpoints, int team, entity player ) { RateSpawnpoints_Generic( checkclass, spawnpoints, team, player ) // temp -}
\ No newline at end of file +} + +// world and player inits + +void function InitialiseATPlayer( entity player ) +{ + Remote_CallFunction_NonReplay( player, "ServerCallback_AT_OnPlayerConnected" ) +} + +void function CreateATBank( entity spawnpoint ) +{ + 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 ) ) + + thread PlayAnim( bank, "mh_inactive_idle" ) + + file.banks.append( bank ) +} + +void function CreateATCamp( entity spawnpoint ) +{ + // delay this so we don't do stuff before all spawns are initialised and that + file.campsToRegisterOnEntitiesDidLoad.append( spawnpoint ) +} + +void function CreateATCamps_Delayed() +{ + // 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 ) + { + 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() + + // 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" ) + + // get droppod spawns + foreach ( entity spawnpoint in SpawnPoints_GetDropPod() ) + if ( Distance( camp.GetOrigin(), spawnpoint.GetOrigin() ) < 1500.0 ) + campStruct.dropPodSpawnPoints.append( spawnpoint ) + + foreach ( entity spawnpoint in SpawnPoints_GetTitan() ) + if ( Distance( camp.GetOrigin(), spawnpoint.GetOrigin() ) < 1500.0 ) + campStruct.titanSpawnPoints.append( spawnpoint ) + + // todo: turret spawns someday maybe + + file.camps.append( campStruct ) + } + + file.campsToRegisterOnEntitiesDidLoad.clear() +} + +// scoring funcs + +// don't use this where possible as it doesn't set score and stuff +void function AT_SetPlayerCash( entity player, int amount ) +{ + // split into stacks of 256 where necessary + int stacks = amount / 256 // automatically rounds down because int division + + player.SetPlayerNetInt( "AT_bonusPoints256", stacks ) + player.SetPlayerNetInt( "AT_bonusPoints", amount - stacks * 256 ) +} + +void function AT_AddPlayerCash( 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 ) +} + +// run gamestate + +void function RunATGame() +{ + thread RunATGame_Threaded() +} + +void function RunATGame_Threaded() +{ + svGlobal.levelEnt.EndSignal( "GameStateChanged" ) + + OnThreadEnd( function() + { + SetGlobalNetBool( "banksOpen", false ) + }) + + wait WAVE_STATE_TRANSITION_TIME // initial wait before first wave + + for ( int waveCount = 1; ; waveCount++ ) + { + wait WAVE_STATE_TRANSITION_TIME + + // 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 ) + { + waveId = GetWaveDataSize() - 2 + waveCount = waveId * 2 + } + + SetGlobalNetInt( "AT_currentWave", waveId ) + bool isBossWave = waveCount / float( 2 ) > waveId // odd 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 ) + } + + wait WAVE_STATE_TRANSITION_TIME + + // run the wave + + AT_WaveData wave = GetWaveData( waveId ) + array< array<AT_SpawnData> > campSpawnData + + if ( isBossWave ) + campSpawnData = wave.bossSpawnData + else + campSpawnData = wave.spawnDataArrays + + // initialise pending spawns + foreach ( array< AT_SpawnData > campData in campSpawnData ) + { + foreach ( AT_SpawnData spawnData in campData ) + spawnData.pendingSpawns = spawnData.totalToSpawn + } + + // clear tracked spawns + file.trackedCampNPCSpawns = {} + while ( true ) + { + // if this is ever 0 by the end of this loop, wave is complete + int numActiveCampSpawners = 0 + + // iterate over camp data for wave + for ( int campIdx = 0; campIdx < campSpawnData.len() && campIdx < file.camps.len(); campIdx++ ) + { + if ( !( campIdx in file.trackedCampNPCSpawns ) ) + file.trackedCampNPCSpawns[ campIdx ] <- {} + + // iterate over ai spawn data for camp + foreach ( AT_SpawnData spawnData in campSpawnData[ campIdx ] ) + { + if ( !( spawnData.aitype in file.trackedCampNPCSpawns[ campIdx ] ) ) + file.trackedCampNPCSpawns[ campIdx ][ spawnData.aitype ] <- 0 + + if ( spawnData.pendingSpawns > 0 || file.trackedCampNPCSpawns[ campIdx ][ spawnData.aitype ] > 0 ) + numActiveCampSpawners++ + + // 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 ) + } + } + + // track spawns + file.trackedCampNPCSpawns[ campIdx ][ spawnData.aitype ] += spawnCount + spawnData.pendingSpawns -= spawnCount + } + } + + if ( numActiveCampSpawners == 0 ) + break + + wait 0.5 + } + + wait WAVE_STATE_TRANSITION_TIME + + // banking phase + } +} + +// entity funcs + +void function AT_SpawnDroppodSquad( int camp, string aiType ) +{ + entity spawnpoint + if ( file.camps[ camp ].dropPodSpawnPoints.len() == 0 ) + spawnpoint = file.camps[ camp ].ent + else + spawnpoint = file.camps[ camp ].dropPodSpawnPoints.getrandom() + + // 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 ) + }) +} + +void function AT_HandleSquadSpawn( array<entity> guys, int camp, string aiType ) +{ + 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 ) + } +} + +void function AT_SpawnReaper( int camp ) +{ + entity spawnpoint + if ( file.camps[ camp ].dropPodSpawnPoints.len() == 0 ) + spawnpoint = file.camps[ camp ].ent + else + spawnpoint = file.camps[ camp ].titanSpawnPoints.getrandom() + + // 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 ) + { + thread AT_WaitToUntrackNPC( reaper, camp, "npc_super_spectre" ) + }) +} + +void function AT_SpawnBountyTitan( int camp ) +{ + entity spawnpoint + if ( file.camps[ camp ].dropPodSpawnPoints.len() == 0 ) + spawnpoint = file.camps[ camp ].ent + else + spawnpoint = file.camps[ camp ].titanSpawnPoints.getrandom() + + // add variation to spawns + wait RandomFloat( 1.0 ) + + // look up titan to use + int bountyID = 0 + try + { + bountyID = ReserveBossID( VALID_BOUNTY_TITAN_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(), BH_AI_TEAM, titanClass, aisettings, void function( entity titan ) : ( camp, bountyID ) + { + // 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" ) + } ) +} + +// 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 ) +{ + entity attacker = DamageInfo_GetAttacker( damageInfo ) + 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 ) + } +} + +void function OnBountyKilled( entity titan, var damageInfo ) +{ + entity attacker = DamageInfo_GetAttacker( damageInfo ) + if ( !attacker.IsPlayer() ) + attacker = GetLatestAssistingPlayerInfo( titan ).player + + if ( IsValid( attacker ) && attacker.IsPlayer() ) + AT_AddPlayerCash( attacker, BOUNTY_TITAN_KILL_REWARD ) +} + +void function AT_WaitToUntrackNPC( entity guy, int camp, string aiType ) +{ + guy.WaitSignal( "OnDeath", "OnDestroy" ) + file.trackedCampNPCSpawns[ camp ][ aiType ]-- +} |