aboutsummaryrefslogtreecommitdiff
path: root/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_fw.nut
diff options
context:
space:
mode:
Diffstat (limited to 'Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_fw.nut')
-rw-r--r--Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_fw.nut2391
1 files changed, 2391 insertions, 0 deletions
diff --git a/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_fw.nut b/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_fw.nut
new file mode 100644
index 00000000..fa66c2f7
--- /dev/null
+++ b/Northstar.Custom/mod/scripts/vscripts/gamemodes/_gamemode_fw.nut
@@ -0,0 +1,2391 @@
+untyped
+global function GamemodeFW_Init
+
+// spawn points
+global function RateSpawnpointsPilot_FW
+global function RateSpawnpointsTitan_FW
+//global function RateSpawnpoints_FW
+
+// for battery_port.gnut to work
+global function FW_ReplaceMegaTurret
+
+// fw specific titanfalls
+global function FW_IsPlayerInFriendlyTerritory
+global function FW_IsPlayerInEnemyTerritory
+
+// Callbacks for mods to reduce harvester damage of modded weapons
+global function FW_AddHarvesterDamageSourceModifier
+global function FW_RemoveHarvesterDamageSourceModifier
+
+// you need to deal this much damage to trigger "FortWarTowerDamage" score event
+const int FW_HARVESTER_DAMAGE_SEGMENT = 5250
+
+// basically needs to match "waves count - bosswaves count"
+const int FW_MAX_LEVELS = 3
+
+// to confirm it's a npc from camps..
+const string FW_NPC_SCRIPTNAME = "fw_npcsFromCamp"
+const int FW_AI_TEAM = TEAM_BOTH
+const float WAVE_STATE_TRANSITION_TIME = 5.0
+
+// from sh_gamemode_fw, if half of these npcs cleared in one camp, it gets escalate
+const int FW_GRUNT_COUNT = 36//32
+const int FW_SPECTRE_COUNT = 24
+const int FW_REAPER_COUNT = 2
+
+// max deployment each camp
+const int FW_GRUNT_MAX_DEPLOYED = 8
+const int FW_SPECTRE_MAX_DEPLOYED = 8
+const int FW_REAPER_MAX_DEPLOYED = 1
+
+// if other camps been cleaned many times, we levelDown
+const int FW_CAMP_IGNORE_NEEDED = 2
+
+// debounce for showing damaged infos
+const float FW_HARVESTER_DAMAGED_DEBOUNCE = 5.0
+const float FW_TURRET_DAMAGED_DEBOUNCE = 2.0
+
+global HarvesterStruct& fw_harvesterMlt
+global HarvesterStruct& fw_harvesterImc
+
+// these are not using respawn's remaining code( sh_gamemode_fw.nut )!
+
+// respawn already have a FW_TowerData struct! this struct is only for score events
+struct HarvesterDamageStruct
+{
+ float recentDamageTime
+ int storedDamage
+}
+
+struct TurretSiteStruct
+{
+ entity site
+ entity turret
+ entity minimapstate
+ string turretflagid
+}
+
+// respawn already have a FW_CampData, FW_WaveOrigin and FW_SpawnData struct!
+struct CampSiteStruct
+{
+ entity camp
+ entity info
+ entity tracker
+ array<entity> validDropPodSpawns
+ array<entity> validTitanSpawns
+ string campId // "A", "B", "C"
+ int npcsAlive
+ int ignoredSinceLastClean
+}
+
+struct CampSpawnStruct
+{
+ string spawnContent // what npcs to spawn
+ int maxSpawnCount // max spawn count on this camp
+ int countPerSpawn // how many npcs to deploy per spawn, for droppods most be 4
+ int killsToEscalate // how many kills needed to escalate
+}
+
+struct
+{
+ array<HarvesterStruct> harvesters
+
+ // Stores damage source IDs and the modifier applied to them when they damage a harvester
+ table< int, float > harvesterDamageSourceMods
+
+ // save camp's info_target, we spawn camps after game starts, or player's first life won't show up correct camp icons
+ array<entity> camps
+
+ array<entity> fwTerritories
+
+ array<TurretSiteStruct> turretsites
+
+ array<CampSiteStruct> fwCampSites
+
+ // respawn already have a FW_TowerData struct! this table is only for score events
+ table< entity, HarvesterDamageStruct > playerDamageHarvester // team, table< player, time >
+
+ // this is for saving territory's connecting time, try not to make faction dialogues play together
+ table< int, float > teamTerrLastConnectTime // team, time
+
+ // unused
+ array<entity> etitaninmlt
+ array<entity> etitaninimc
+
+ entity harvesterMlt_info
+ entity harvesterImc_info
+
+ table<int, CampSpawnStruct> fwNpcLevel // basically use to powerup certian camp, sync with alertLevel
+ table< string, table< string, int > > trackedCampNPCSpawns
+}file
+
+void function GamemodeFW_Init()
+{
+ // _battery_port.gnut needs this
+ RegisterSignal( "BatteryActivate" )
+
+ 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" ] )
+
+ AddCallback_EntitiesDidLoad( LoadEntities )
+ AddCallback_GameStateEnter( eGameState.Prematch, OnFWGamePrematch )
+ AddCallback_GameStateEnter( eGameState.Playing, OnFWGamePlaying )
+
+ AddSpawnCallback( "item_powerup", FWAddPowerUpIcon )
+ AddSpawnCallback( "npc_turret_mega", OnFWTurretSpawned )
+
+ AddCallback_OnClientConnected( OnFWPlayerConnected )
+ AddCallback_PlayerClassChanged( OnFWPlayerClassChanged )
+ AddCallback_OnPlayerKilled( OnFWPlayerKilled )
+ AddCallback_OnPilotBecomesTitan( OnFWPilotBecomesTitan )
+ AddCallback_OnTitanBecomesPilot( OnFWTitanBecomesPilot )
+
+ ScoreEvent_SetupEarnMeterValuesForMixedModes()
+ SetRecalculateRespawnAsTitanStartPointCallback( FW_ForcedTitanStartPoint )
+ SetRecalculateTitanReplacementPointCallback( FW_ReCalculateTitanReplacementPoint )
+ SetRequestTitanAllowedCallback( FW_RequestTitanAllowed )
+}
+
+
+
+//////////////////////////
+///// HACK FUNCTIONS /////
+//////////////////////////
+
+const array<string> HACK_CLEANUP_MAPS =
+[
+ "mp_grave",
+ "mp_homestead",
+ "mp_complex3"
+]
+
+//if npcs outside the map try to fire( like in death animation ), it will cause a engine error
+
+// in mp_grave, npcs will sometimes stuck underground
+const float GRAVE_CHECK_HEIGHT = 1700 // the map's lowest ground is 1950+, npcs will stuck under -4000 or -400
+// in mp_homestead, npcs will sometimes stuck in the sky
+const float HOMESTEAD_CHECK_HIEGHT = 8000 // the map's highest part is 7868+, npcs will stuck above 13800+
+// in mp_complex3, npcs will sometimes stuck in the sky
+const float COMPLEX_CHECK_HEIGHT = 7000 // the map's highest part is 6716+, npcs will stuck above 9700+
+
+// do a hack
+void function HACK_ForceDestroyNPCs()
+{
+ thread HACK_ForceDestroyNPCs_Threaded()
+}
+
+void function HACK_ForceDestroyNPCs_Threaded()
+{
+ string mapName = GetMapName()
+ if( !( HACK_CLEANUP_MAPS.contains( mapName ) ) )
+ return
+
+ while( true )
+ {
+ if( mapName == "mp_grave" )
+ {
+ foreach( entity npc in GetNPCArray() )
+ {
+ if( npc.GetOrigin().z <= GRAVE_CHECK_HEIGHT )
+ {
+ npc.ClearParent()
+ npc.Destroy()
+ }
+ }
+ }
+ if( mapName == "mp_homestead" )
+ {
+ foreach( entity npc in GetNPCArray() )
+ {
+ // neither spawning from droppod nor hotdropping
+ if( !IsValid( npc.GetParent() ) && !npc.e.isHotDropping )
+ {
+ if( npc.GetOrigin().z >= HOMESTEAD_CHECK_HIEGHT )
+ {
+ npc.Destroy()
+ }
+ }
+ }
+ }
+ if( mapName == "mp_complex3" )
+ {
+ foreach( entity npc in GetNPCArray() )
+ {
+ // neither spawning from droppod nor hotdropping
+ if( !IsValid( npc.GetParent() ) && !npc.e.isHotDropping )
+ {
+ if( npc.GetOrigin().z >= COMPLEX_CHECK_HEIGHT )
+ {
+ npc.Destroy()
+ }
+ }
+ }
+ }
+ WaitFrame()
+ }
+}
+
+//////////////////////////////
+///// HACK FUNCTIONS END /////
+//////////////////////////////
+
+
+
+////////////////////////////////
+///// SPAWNPOINT FUNCTIONS /////
+////////////////////////////////
+
+void function RateSpawnpointsPilot_FW( int checkClass, array<entity> spawnpoints, int team, entity player )
+{
+ array<entity> startSpawns = SpawnPoints_GetPilotStart( team )
+ RateSpawnpoints_FW( startSpawns, checkClass, spawnpoints, team, player )
+}
+
+void function RateSpawnpointsTitan_FW( int checkClass, array<entity> spawnpoints, int team, entity player )
+{
+ array<entity> startSpawns = SpawnPoints_GetTitanStart( team )
+ RateSpawnpoints_FW( startSpawns, checkClass, spawnpoints, team, player )
+}
+
+void function RateSpawnpoints_FW( array<entity> startSpawns, int checkClass, array<entity> spawnpoints, int team, entity player )
+{
+ if ( HasSwitchedSides() )
+ team = GetOtherTeam( team )
+
+ // average out startspawn positions
+ vector averageFriendlySpawns
+ foreach ( entity spawnpoint in startSpawns )
+ averageFriendlySpawns += spawnpoint.GetOrigin()
+
+ averageFriendlySpawns /= startSpawns.len()
+
+ entity friendlyTerritory
+ foreach ( entity territory in file.fwTerritories )
+ {
+ if ( team == territory.GetTeam() )
+ {
+ friendlyTerritory = territory
+ break
+ }
+ }
+
+ vector ratingPos
+ if ( IsValid( friendlyTerritory ) )
+ ratingPos = friendlyTerritory.GetOrigin()
+ else
+ ratingPos = averageFriendlySpawns
+
+ foreach ( entity spawnpoint in spawnpoints )
+ {
+ // idk about magic number here really
+ float rating = 1.0 - ( Distance2D( spawnpoint.GetOrigin(), ratingPos ) / 1000.0 )
+ spawnpoint.CalculateRating( checkClass, player.GetTeam(), rating, rating )
+ }
+}
+
+////////////////////////////////////
+///// SPAWNPOINT FUNCTIONS END /////
+////////////////////////////////////
+
+
+
+//////////////////////////////
+///// CALLBACK FUNCTIONS /////
+//////////////////////////////
+
+void function OnFWGamePrematch()
+{
+ InitFWScoreEvents()
+ FW_createHarvester()
+ InitFWCampSites()
+ InitCampSpawnerLevel()
+}
+
+void function OnFWGamePlaying()
+{
+ startFWHarvester()
+ FWAreaThreatLevelThink()
+ StartFWCampThink()
+ InitTurretSettings()
+ FWPlayerObjectiveState()
+
+ HACK_ForceDestroyNPCs()
+}
+
+void function OnFWPlayerConnected( entity player )
+{
+ InitFWPlayers( player )
+}
+
+void function OnFWPlayerClassChanged( entity player )
+{
+ // give player a friendly highlight
+ Highlight_SetFriendlyHighlight( player, "fw_friendly" )
+}
+
+void function OnFWPlayerKilled( entity victim, entity attacker, var damageInfo )
+{
+ HandleFWPlayerKilledScoreEvent( victim, attacker )
+}
+
+void function OnFWPilotBecomesTitan( entity player, entity titan )
+{
+ // objective stuff
+ SetTitanObjective( player, titan )
+}
+
+void function OnFWTitanBecomesPilot( entity player, entity titan )
+{
+ // objective stuff
+ SetPilotObjective( player, titan )
+}
+
+//////////////////////////////////
+///// CALLBACK FUNCTIONS END /////
+//////////////////////////////////
+
+
+/////////////////////////////////
+///// SCORE EVENT FUNCTIONS /////
+/////////////////////////////////
+
+void function InitFWScoreEvents()
+{
+ // common scoreEvents
+ ScoreEvent_SetEarnMeterValues( "KillHeavyTurret", 0.0, 0.20 ) // can only adds to titan's in this mode
+
+ // fw special: save for later use of scoreEvents
+
+ // combat
+ ScoreEvent_SetEarnMeterValues( "FortWarAssault", 0.0, 0.05, 0.0 ) // titans don't earn
+ ScoreEvent_SetEarnMeterValues( "FortWarDefense", 0.0, 0.05, 0.0 ) // titans don't earn
+ ScoreEvent_SetEarnMeterValues( "FortWarPerimeterDefense", 0.0, 0.05 ) // unused
+ ScoreEvent_SetEarnMeterValues( "FortWarSiege", 0.0, 0.05 ) // unused
+ ScoreEvent_SetEarnMeterValues( "FortWarSnipe", 0.0, 0.05 ) // unused
+
+ // constructions
+ ScoreEvent_SetEarnMeterValues( "FortWarBaseConstruction", 0.0, 0.15 )
+ ScoreEvent_SetEarnMeterValues( "FortWarForwardConstruction", 0.0, 0.15 )
+ ScoreEvent_SetEarnMeterValues( "FortWarInvasiveConstruction", 0.0, 0.25 ) // unused
+ ScoreEvent_SetEarnMeterValues( "FortWarResourceDenial", 0.0, 0.05 ) // unused
+ ScoreEvent_SetEarnMeterValues( "FortWarSecuringGatheredResources", 0.0, 0.05 ) // unused
+
+ // tower
+ ScoreEvent_SetEarnMeterValues( "FortWarTowerDamage", 0.0, 0.10, 0.0 ) // using the const FW_HARVESTER_DAMAGE_SEGMENT, titans don't earn
+ ScoreEvent_SetEarnMeterValues( "FortWarTowerDefense", 0.0, 0.10, 0.0 ) // titans don't earn
+ ScoreEvent_SetEarnMeterValues( "FortWarShieldDestroyed", 0.0, 0.15 )
+
+ // turrets
+ ScoreEvent_SetEarnMeterValues( "FortWarTeamTurretControlBonus_One", 0.0, 0.15, 0.5 ) // give more meter if no turret left
+ ScoreEvent_SetEarnMeterValues( "FortWarTeamTurretControlBonus_Two", 0.0, 0.15, 0.5 )
+ ScoreEvent_SetEarnMeterValues( "FortWarTeamTurretControlBonus_Three", 0.0, 0.10, 0.5 )
+ ScoreEvent_SetEarnMeterValues( "FortWarTeamTurretControlBonus_Four", 0.0, 0.10, 0.5 ) // give less meter if controlled most turrets
+ ScoreEvent_SetEarnMeterValues( "FortWarTeamTurretControlBonus_Five", 0.0, 0.05, 0.5 )
+ ScoreEvent_SetEarnMeterValues( "FortWarTeamTurretControlBonus_Six", 0.0, 0.05, 0.5 )
+}
+
+// consider this means victim recently damaged harvester
+const float TOWER_DEFENSE_REQURED_TIME = 10.0
+
+void function HandleFWPlayerKilledScoreEvent( entity victim, entity attacker )
+{
+ // this function only handles player's kills
+ if( !attacker.IsPlayer() )
+ return
+
+ // suicide don't get scores
+ if( attacker == victim )
+ return
+
+ int attackerTeam = attacker.GetTeam()
+ int victimTeam = victim.GetTeam()
+
+ string scoreEvent = ""
+ int secondaryScore = 0
+ entity attackerHarvester = FW_GetTeamHarvesterProp( attackerTeam )
+
+ if( FW_IsPlayerInEnemyTerritory( victim ) ) // victim is in enemy territory
+ {
+ scoreEvent = "FortWarDefense" // enemy earn score from defense
+ secondaryScore = POINTVALUE_FW_DEFENSE
+ }
+
+ if( FW_IsPlayerInFriendlyTerritory( victim ) ) // victim is in friendly territory
+ {
+ scoreEvent = "FortWarAssault" // enemy earn score from assault
+ secondaryScore = POINTVALUE_FW_ASSAULT
+ }
+
+ if( victim in file.playerDamageHarvester ) // victim has damaged the harvester this life
+ {
+ float damageTime = file.playerDamageHarvester[ victim ].recentDamageTime
+
+ // is victim recently damaged havester?
+ if( damageTime + TOWER_DEFENSE_REQURED_TIME >= Time() )
+ {
+ scoreEvent = "FortWarTowerDefense" // you defend the tower!
+ secondaryScore = POINTVALUE_FW_TOWER_DEFENSE
+ }
+
+ }
+
+ if( scoreEvent != "" )
+ {
+ AddPlayerScore( attacker, scoreEvent, victim )
+ attacker.AddToPlayerGameStat( PGS_DEFENSE_SCORE, secondaryScore )
+ }
+}
+
+/////////////////////////////////////
+///// SCORE EVENT FUNCTIONS END /////
+/////////////////////////////////////
+
+
+
+//////////////////////////////////////
+///// FACTION DIALOGUE FUNCTIONS /////
+//////////////////////////////////////
+
+const float FW_TERRYTORY_DIALOGUE_DEBOUNCE = 5.0
+
+// WORKING IN PROGRESS
+bool function TryFWTerritoryDialogue( entity territory, entity player )
+{
+ bool thisTimeIsTitan = player.IsTitan()
+ int terrTeam = territory.GetTeam()
+ int enemyTeam = GetOtherTeam( terrTeam )
+ bool sameTeam = terrTeam == player.GetTeam()
+ bool isInDebounce = file.teamTerrLastConnectTime[ terrTeam ] + FW_TERRYTORY_DIALOGUE_DEBOUNCE >= Time()
+
+ // the territory trigger will only save players and titans
+ array<entity> allEntsInside = GetAllEntitiesInTrigger( territory )
+ allEntsInside.removebyvalue( null ) // since we're using a fake trigger, need to check this
+ array<entity> friendliesInside // this means territory's friendly team
+ array<entity> enemiesInside // this means territory's enemy team
+ array<entity> enemyTitansInside
+ foreach( entity ent in allEntsInside )
+ {
+ if( !IsValid( ent ) ) // since we're using a fake trigger, need to check this
+ continue
+ if( ent.GetTeam() == terrTeam )
+ friendliesInside.append( ent )
+ }
+ foreach( entity ent in allEntsInside )
+ {
+ if( !IsValid( ent ) ) // since we're using a fake trigger, need to check this
+ continue
+ if( ent.GetTeam() != terrTeam )
+ enemiesInside.append( ent )
+ }
+ foreach( entity enemy in enemiesInside )
+ {
+ if( !IsValid( enemy ) ) // since we're using a fake trigger, need to check this
+ continue
+ if( enemy.IsTitan() )
+ enemyTitansInside.append( enemy )
+ }
+
+ print( "enemy in territory: " + string( enemiesInside.len() ) )
+ print( "friendly in territory: " + string( friendliesInside.len() ) )
+
+ print( "sameTeam: " + string( sameTeam ) )
+ print( "isInDebounce: " + string( isInDebounce ) )
+ print( "thisTimeIsTitan: " + string( thisTimeIsTitan ) )
+
+ if( enemiesInside.len() > 3 || friendliesInside.len() > 1 ) // already have some players triggered dialogue
+ return false
+
+ // notify player enemy's behaves
+ if( !sameTeam ) // player is not the same team as territory
+ {
+ // consider this means all enemies has left friendly territory, should use a debounce
+ if( enemiesInside.len() == 0 && !isInDebounce )
+ {
+ PlayFactionDialogueToTeam( "fortwar_terEnemyExpelled", terrTeam )
+ return true
+ }
+ // has more than 3 titans inside including new one, ignores debounce
+ else if( enemyTitansInside.len() >= 3 && thisTimeIsTitan )
+ {
+ PlayFactionDialogueToTeam( "fortwar_terPresentEnemyTitans", terrTeam )
+ return true
+ }
+ // only the player inside terrytory
+ else if( enemyTitansInside.len() == 1 )
+ {
+ // entered territory as titan, ignores debounce
+ if( thisTimeIsTitan )
+ {
+ PlayFactionDialogueToTeam( "fortwar_terEnteredEnemyPilot", terrTeam )
+ return true
+ }
+ // entered territory as pilot
+ else if( !isInDebounce )
+ {
+ PlayFactionDialogueToTeam( "fortwar_terEnteredEnemyPilot", terrTeam )
+ return true
+ }
+ }
+
+ // notify player friendly's behaves
+ // consider this means all friendlies has left enemy territory
+ if( friendliesInside.len() == 0 && !sameTeam && !isInDebounce )
+ {
+ PlayFactionDialogueToTeam( "fortwar_terFriendlyExpelled", terrTeam )
+ return true
+ }
+ }
+
+ return false
+}
+
+//////////////////////////////////////////
+///// FACTION DIALOGUE FUNCTIONS END /////
+//////////////////////////////////////////
+
+
+
+/////////////////////////////////////////
+///// GAMEMODE INITIALIZE FUNCTIONS /////
+/////////////////////////////////////////
+
+void function LoadEntities()
+{
+ // info_target
+ foreach ( entity info_target in GetEntArrayByClass_Expensive( "info_target" ) )
+ {
+ if( info_target.HasKey( "editorclass" ) )
+ {
+ switch( info_target.kv.editorclass )
+ {
+ case "info_fw_team_tower":
+ if ( info_target.GetTeam() == TEAM_IMC )
+ {
+ file.harvesterImc_info = info_target
+ //print("fw_tower tracker spawned")
+ }
+ if ( info_target.GetTeam() == TEAM_MILITIA )
+ {
+ file.harvesterMlt_info = info_target
+ //print("fw_tower tracker spawned")
+ }
+ break
+ case "info_fw_camp":
+ file.camps.append( info_target )
+ //InitCampTracker( info_target )
+ //print("fw_camp spawned")
+ break
+ case "info_fw_turret_site":
+ string idString = expect string(info_target.kv.turretId)
+ int id = int( info_target.kv.turretId )
+ //print("info_fw_turret_siteID : " + idString )
+
+ // set this for replace function to find
+ TurretSiteStruct turretsite
+ file.turretsites.append( turretsite )
+
+ turretsite.site = info_target
+
+ // create turret, spawn with no team and set it after game starts
+ entity turret = CreateNPC( "npc_turret_mega", TEAM_UNASSIGNED, info_target.GetOrigin(), info_target.GetAngles() )
+ SetSpawnOption_AISettings( turret, "npc_turret_mega_fortwar" )
+ DispatchSpawn( turret )
+
+ turretsite.turret = turret
+
+ // init turret settings
+ turret.s.minimapstate <- null // entity, for saving turret's minimap handler
+ turret.s.baseTurret <- false // bool, is this turret from base
+ turret.s.turretflagid <- "" // string, turret's id like "1", "2", "3"
+ turret.s.lastDamagedTime <- 0.0 // float, for showing turret underattack icons
+ turret.s.relatedBatteryPort <- null // entity, corssfile
+
+ // minimap icons holder
+ entity minimapstate = CreateEntity( "prop_script" )
+ minimapstate.SetValueForModelKey( info_target.GetModelName() ) // these info must have model to work
+ minimapstate.Hide() // hide the model! it will still work on minimaps
+ minimapstate.SetOrigin( info_target.GetOrigin() )
+ minimapstate.SetAngles( info_target.GetAngles() )
+ //SetTeam( minimapstate, info_target.GetTeam() ) // setTeam() for icons is done in TurretStateWatcher()
+ minimapstate.kv.solid = SOLID_VPHYSICS
+ DispatchSpawn( minimapstate )
+ // show on minimaps
+ minimapstate.Minimap_AlwaysShow( TEAM_IMC, null )
+ minimapstate.Minimap_AlwaysShow( TEAM_MILITIA, null )
+ minimapstate.Minimap_SetCustomState( eMinimapObject_prop_script.FW_BUILDSITE_SHIELDED )
+
+ turretsite.minimapstate = minimapstate
+ turret.s.minimapstate = minimapstate
+
+ break
+ }
+ }
+ }
+
+ // script_ref
+ foreach ( entity script_ref in GetEntArrayByClass_Expensive( "script_ref" ) )
+ {
+ if( script_ref.HasKey( "editorclass" ) )
+ {
+ switch( script_ref.kv.editorclass )
+ {
+ case "info_fw_foundation_plate":
+ entity prop = CreatePropScript( script_ref.GetModelName(), script_ref.GetOrigin(), script_ref.GetAngles(), 6 )
+ break
+ case "info_fw_battery_port":
+ entity batteryPort = CreatePropScript( script_ref.GetModelName(), script_ref.GetOrigin(), script_ref.GetAngles(), 6 )
+ FW_InitBatteryPort(batteryPort)
+
+ break
+ }
+ }
+ }
+
+ // trigger_multiple
+ foreach ( entity trigger_multiple in GetEntArrayByClass_Expensive( "trigger_multiple" ) )
+ {
+ if( trigger_multiple.HasKey( "editorclass" ) )
+ {
+ switch( trigger_multiple.kv.editorclass )
+ {
+ case "trigger_fw_territory":
+ SetupFWTerritoryTrigger( trigger_multiple )
+ break
+ }
+ }
+ }
+
+ // maybe for tick_spawning reapers?
+ ValidateAndFinalizePendingStationaryPositions()
+}
+
+void function InitCampSpawnerLevel() // can edit this to make more spawns, alertLevel icons supports max to lv3( 0,1,2 )
+{
+ // lv1 spawns: grunts
+ CampSpawnStruct campSpawnLv1
+ campSpawnLv1.spawnContent = "npc_soldier"
+ campSpawnLv1.maxSpawnCount = FW_GRUNT_MAX_DEPLOYED
+ campSpawnLv1.countPerSpawn = 4 // how many npcs to deploy per spawn, for droppods most be 4
+ campSpawnLv1.killsToEscalate = FW_GRUNT_COUNT / 2
+
+ file.fwNpcLevel[0] <- campSpawnLv1
+
+ // lv2 spawns: spectres
+ CampSpawnStruct campSpawnLv2
+ campSpawnLv2.spawnContent = "npc_spectre"
+ campSpawnLv2.maxSpawnCount = FW_SPECTRE_MAX_DEPLOYED
+ campSpawnLv2.countPerSpawn = 4 // how many npcs to deploy per spawn, for droppods most be 4
+ campSpawnLv2.killsToEscalate = FW_SPECTRE_COUNT / 2
+
+ file.fwNpcLevel[1] <- campSpawnLv2
+
+ // lv3 spawns: reapers
+ CampSpawnStruct campSpawnLv3
+ campSpawnLv3.spawnContent = "npc_super_spectre"
+ campSpawnLv3.maxSpawnCount = FW_REAPER_MAX_DEPLOYED
+ campSpawnLv3.countPerSpawn = 1 // how many npcs to deploy per spawn
+ campSpawnLv3.killsToEscalate = FW_REAPER_COUNT / 2 // only 1 kill needed to spawn the boss?
+
+ file.fwNpcLevel[2] <- campSpawnLv3
+}
+
+/////////////////////////////////////////////
+///// GAMEMODE INITIALIZE FUNCTIONS END /////
+/////////////////////////////////////////////
+
+
+
+///////////////////////////////////////
+///// PLAYER INITIALIZE FUNCTIONS /////
+///////////////////////////////////////
+
+void function InitFWPlayers( entity player )
+{
+ HarvesterDamageStruct emptyStruct
+ file.playerDamageHarvester[ player ] <- emptyStruct
+
+ // objective stuff
+ player.s.notifiedTitanfall <- false
+
+ // notification stuff
+ player.s.lastTurretNotifyTime <- 0.0
+}
+
+///////////////////////////////////////////
+///// PLAYER INITIALIZE FUNCTIONS END /////
+///////////////////////////////////////////
+
+
+
+/////////////////////////////
+///// POWERUP FUNCTIONS /////
+/////////////////////////////
+
+void function FWAddPowerUpIcon( entity powerup )
+{
+ powerup.Minimap_SetAlignUpright( true )
+ powerup.Minimap_SetZOrder( MINIMAP_Z_OBJECT )
+ powerup.Minimap_SetClampToEdge( false )
+ powerup.Minimap_AlwaysShow( TEAM_MILITIA, null )
+ powerup.Minimap_AlwaysShow( TEAM_IMC, null )
+}
+
+/////////////////////////////////
+///// POWERUP FUNCTIONS END /////
+/////////////////////////////////
+
+
+
+/////////////////////////////
+///// AICAMPS FUNCTIONS /////
+/////////////////////////////
+
+void function InitFWCampSites()
+{
+ // init here
+ foreach( entity info_target in file.camps )
+ {
+ InitCampTracker( info_target )
+ }
+
+ // camps don't have a id, set them manually
+ foreach( int index, CampSiteStruct campsite in file.fwCampSites )
+ {
+ entity campInfo = campsite.camp
+ float radius = float( campInfo.kv.radius )
+
+ // get droppod spawns
+ foreach ( entity spawnpoint in SpawnPoints_GetDropPod() )
+ if ( Distance( campInfo.GetOrigin(), spawnpoint.GetOrigin() ) < radius )
+ campsite.validDropPodSpawns.append( spawnpoint )
+
+ // get titan spawns
+ foreach ( entity spawnpoint in SpawnPoints_GetTitan() )
+ if ( Distance( campInfo.GetOrigin(), spawnpoint.GetOrigin() ) < radius )
+ campsite.validTitanSpawns.append( spawnpoint )
+
+ if ( index == 0 )
+ {
+ campsite.campId = "A"
+ SetGlobalNetInt( "fwCampAlertA", 0 )
+ SetGlobalNetFloat( "fwCampStressA", 0.0 ) // start from empty
+ SetLocationTrackerID( campsite.tracker, 0 )
+ file.trackedCampNPCSpawns["A"] <- {}
+ continue
+ }
+ if ( index == 1 )
+ {
+ campsite.campId = "B"
+ SetGlobalNetInt( "fwCampAlertB", 0 )
+ SetGlobalNetFloat( "fwCampStressB", 0.0 ) // start from empty
+ SetLocationTrackerID( campsite.tracker, 1 )
+ file.trackedCampNPCSpawns["B"] <- {}
+ continue
+ }
+ if ( index == 2 )
+ {
+ campsite.campId = "C"
+ SetGlobalNetInt( "fwCampAlertC", 0 )
+ SetGlobalNetFloat( "fwCampStressC", 0.0 ) // start from empty
+ SetLocationTrackerID( campsite.tracker, 2 )
+ file.trackedCampNPCSpawns["C"] <- {}
+ continue
+ }
+ }
+}
+
+void function InitCampTracker( entity camp )
+{
+ //print("InitCampTracker")
+ CampSiteStruct campsite
+ campsite.camp = camp
+ file.fwCampSites.append( campsite )
+
+ entity placementHelper = CreateEntity( "info_placement_helper" )
+ placementHelper.SetOrigin( camp.GetOrigin() ) // tracker needs a owner to display
+ campsite.info = placementHelper
+ DispatchSpawn( placementHelper )
+
+ float radius = float( camp.kv.radius ) // radius to show up icon and spawn ais
+
+ entity tracker = GetAvailableCampLocationTracker()
+ tracker.SetOwner( placementHelper )
+ campsite.tracker = tracker
+ SetLocationTrackerRadius( tracker, radius )
+ DispatchSpawn( tracker )
+}
+
+void function StartFWCampThink()
+{
+ foreach( CampSiteStruct camp in file.fwCampSites )
+ {
+ //print( "has " + string( file.fwCampSites.len() ) + " camps in total" )
+ //print( "campId is " + camp.campId )
+ thread FWAiCampThink( camp )
+ }
+}
+
+// this is not using respawn's remaining code!
+void function FWAiCampThink( CampSiteStruct campsite )
+{
+ string campId = campsite.campId
+ string alertVarName = "fwCampAlert" + campId
+ string stressVarName = "fwCampStress" + campId
+
+
+ bool firstSpawn = true
+ while( GamePlayingOrSuddenDeath() )
+ {
+ wait WAVE_STATE_TRANSITION_TIME
+
+ int alertLevel = GetGlobalNetInt( alertVarName )
+ //print( "campsite" + campId + ".ignoredSinceLastClean: " + string( campsite.ignoredSinceLastClean ) )
+ if( campsite.ignoredSinceLastClean >= FW_CAMP_IGNORE_NEEDED && alertLevel > 0 ) // has been ignored many times, level > 0
+ alertLevel = 0 // reset level
+ else if( !firstSpawn ) // not the first spawn!
+ alertLevel += 1 // level up
+
+ if( alertLevel >= FW_MAX_LEVELS - 1 ) // reached max level?
+ alertLevel = FW_MAX_LEVELS - 1 // stay
+
+ // update netVars, don't know how client update these, sometimes they can't catch up
+ SetGlobalNetInt( alertVarName, alertLevel )
+ SetGlobalNetFloat( stressVarName, 1.0 ) // refill
+
+ // under attack, clean this
+ campsite.ignoredSinceLastClean = 0
+
+ CampSpawnStruct curSpawnStruct = file.fwNpcLevel[alertLevel]
+ string npcToSpawn = curSpawnStruct.spawnContent
+ int maxSpawnCount = curSpawnStruct.maxSpawnCount
+ int countPerSpawn = curSpawnStruct.countPerSpawn
+ int killsToEscalate = curSpawnStruct.killsToEscalate
+
+ // for this time's loop
+ file.trackedCampNPCSpawns[campId] = {}
+ int killsNeeded = killsToEscalate
+ int lastNpcLeft
+ while( true )
+ {
+ WaitFrame()
+
+ //print( alertVarName + " : " + string( GetGlobalNetInt( alertVarName ) ) )
+ //print( stressVarName + " : " + string( GetGlobalNetFloat( stressVarName ) ) )
+ //print( "campsite" + campId + ".ignoredSinceLastClean: " + string( campsite.ignoredSinceLastClean ) )
+
+ if( !( npcToSpawn in file.trackedCampNPCSpawns[campId] ) ) // init it
+ file.trackedCampNPCSpawns[campId][npcToSpawn] <- 0
+
+ int npcsLeft = file.trackedCampNPCSpawns[campId][npcToSpawn]
+ killsNeeded -= lastNpcLeft - npcsLeft
+
+ if( killsNeeded <= 0 ) // check if needs more kills
+ {
+ SetGlobalNetFloat( stressVarName, 0.0 ) // empty
+ AddIgnoredCountToOtherCamps( campsite )
+ break
+ }
+
+ // update stress bar
+ float campStressLeft = float( killsNeeded ) / float( killsToEscalate )
+ SetGlobalNetFloat( stressVarName, campStressLeft )
+ //print( "campStressLeft: " + string( campStressLeft ) )
+
+ if( maxSpawnCount - npcsLeft >= countPerSpawn && killsNeeded >= countPerSpawn ) // keep spawning
+ {
+ // spawn functions, for fw we only spawn one kind of enemy each time
+ // light units
+ if( npcToSpawn == "npc_soldier"
+ || npcToSpawn == "npc_spectre"
+ || npcToSpawn == "npc_stalker" )
+ thread FW_SpawnDroppodSquad( campsite, npcToSpawn )
+
+ // reapers
+ if( npcToSpawn == "npc_super_spectre" )
+ thread FW_SpawnReaper( campsite )
+
+ file.trackedCampNPCSpawns[campId][npcToSpawn] += countPerSpawn
+
+ // titans?
+ //else if( npcToSpawn == "npc_titan" )
+ //{
+ // file.trackedCampNPCSpawns[campId][npcToSpawn] += 4
+ //}
+ }
+
+ lastNpcLeft = file.trackedCampNPCSpawns[campId][npcToSpawn]
+ }
+
+ // first loop ends
+ firstSpawn = false
+ }
+}
+
+void function AddIgnoredCountToOtherCamps( CampSiteStruct senderCamp )
+{
+ foreach( CampSiteStruct camp in file.fwCampSites )
+ {
+ //print( "senderCampId is: " + senderCamp.campId )
+ //print( "curCampId is " + camp.campId )
+ if( camp.campId != senderCamp.campId ) // other camps
+ {
+ camp.ignoredSinceLastClean += 1
+ }
+ }
+}
+
+// functions from at
+void function FW_SpawnDroppodSquad( CampSiteStruct campsite, string aiType )
+{
+ entity spawnpoint
+ if ( campsite.validDropPodSpawns.len() == 0 )
+ spawnpoint = campsite.tracker // no spawnPoints valid, use camp itself to spawn
+ else
+ spawnpoint = campsite.validDropPodSpawns.getrandom()
+
+ // add variation to spawns
+ wait RandomFloat( 1.0 )
+
+ AiGameModes_SpawnDropPod( spawnpoint.GetOrigin(), spawnpoint.GetAngles(), FW_AI_TEAM, aiType, void function( array<entity> guys ) : ( campsite, aiType )
+ {
+ FW_HandleSquadSpawn( guys, campsite, aiType )
+ })
+}
+
+void function FW_HandleSquadSpawn( array<entity> guys, CampSiteStruct campsite, string aiType )
+{
+ foreach ( entity guy in guys )
+ {
+ guy.EnableNPCFlag( NPC_ALLOW_PATROL | NPC_ALLOW_HAND_SIGNALS | NPC_ALLOW_FLEE ) // NPC_ALLOW_INVESTIGATE is not allowed
+ guy.SetScriptName( FW_NPC_SCRIPTNAME ) // well no need
+ // show on minimap to let players kill them
+ guy.Minimap_AlwaysShow( TEAM_MILITIA, null )
+ guy.Minimap_AlwaysShow( TEAM_IMC, null )
+
+ // untrack them on death
+ thread FW_WaitToUntrackNPC( guy, campsite.campId, aiType )
+ }
+ // at least don't let them running around
+ thread FW_ForceAssaultInCamp( guys, campsite.camp )
+}
+
+void function FW_SpawnReaper( CampSiteStruct campsite )
+{
+ entity spawnpoint
+ if ( campsite.validDropPodSpawns.len() == 0 )
+ spawnpoint = campsite.tracker // no spawnPoints valid, use camp itself to spawn
+ else
+ spawnpoint = campsite.validDropPodSpawns.getrandom()
+
+ // add variation to spawns
+ wait RandomFloat( 1.0 )
+
+ AiGameModes_SpawnReaper( spawnpoint.GetOrigin(), spawnpoint.GetAngles(), FW_AI_TEAM, "npc_super_spectre_aitdm",void function( entity reaper ) : ( campsite )
+ {
+ reaper.SetScriptName( FW_NPC_SCRIPTNAME ) // no neet rn
+ // show on minimap to let players kill them
+ reaper.Minimap_AlwaysShow( TEAM_MILITIA, null )
+ reaper.Minimap_AlwaysShow( TEAM_IMC, null )
+
+ // at least don't let them running around
+ thread FW_ForceAssaultInCamp( [reaper], campsite.camp )
+ // untrack them on death
+ thread FW_WaitToUntrackNPC( reaper, campsite.campId, "npc_super_spectre" )
+ })
+}
+
+// maybe this will make them stay around the camp
+void function FW_ForceAssaultInCamp( array<entity> guys, entity camp )
+{
+ while( true )
+ {
+ bool oneGuyValid = false
+ foreach( entity guy in guys )
+ {
+ if( IsValid( guy ) )
+ {
+ guy.AssaultPoint( camp.GetOrigin() )
+ guy.AssaultSetGoalRadius( float( camp.kv.radius ) ) // the camp's radius
+ guy.AssaultSetFightRadius( 0 )
+ oneGuyValid = true
+ }
+ }
+ if( !oneGuyValid ) // no guys left
+ return
+
+ wait RandomFloatRange( 10, 15 ) // make randomness
+ }
+}
+
+void function FW_WaitToUntrackNPC( entity guy, string campId, string aiType )
+{
+ guy.WaitSignal( "OnDeath", "OnDestroy" )
+ if( aiType in file.trackedCampNPCSpawns[ campId ] ) // maybe escalated?
+ file.trackedCampNPCSpawns[ campId ][ aiType ]--
+}
+
+/////////////////////////////////
+///// AICAMPS FUNCTIONS END /////
+/////////////////////////////////
+
+
+
+///////////////////////////////
+///// TERRITORY FUNCTIONS /////
+///////////////////////////////
+
+void function SetupFWTerritoryTrigger( entity trigger )
+{
+ //print("trigger_fw_territory detected")
+ file.fwTerritories.append( trigger )
+ trigger.ConnectOutput( "OnStartTouch", EntityEnterFWTrig )
+ trigger.ConnectOutput( "OnEndTouch", EntityLeaveFWTrig )
+
+ // respawn didn't leave a key for trigger's team, let's set it manually.
+ if( Distance( trigger.GetOrigin(), file.harvesterMlt_info.GetOrigin() ) > Distance( trigger.GetOrigin(), file.harvesterImc_info.GetOrigin() ) )
+ SetTeam( trigger, TEAM_IMC )
+ else
+ SetTeam( trigger, TEAM_MILITIA )
+
+ // init
+ file.teamTerrLastConnectTime[ trigger.GetTeam() ] <- 0.0
+
+ thread FWTerritoryTriggerThink( trigger )
+}
+
+// since we're using a trigger_multiple, needs this to remove invalid keys
+void function FWTerritoryTriggerThink( entity trigger )
+{
+ trigger.EndSignal( "OnDestroy" )
+
+ while( true )
+ {
+ if( null in trigger.e.scriptTriggerData.entities )
+ delete trigger.e.scriptTriggerData.entities[ null ]
+ WaitFrame()
+ }
+}
+
+void function EntityEnterFWTrig( entity trigger, entity ent, entity caller, var value )
+{
+ if( !IsValid( ent ) ) // post-spawns
+ return
+ if( !ent.IsPlayer() && !ent.IsTitan() ) // no neet to add props and grunts i guess
+ return
+ // functions that trigger_multiple missing
+ if( IsValid( ent ) )
+ {
+ ScriptTriggerAddEntity( trigger, ent )
+ thread ScriptTriggerPlayerDisconnectThink( trigger, ent )
+ //TryFWTerritoryDialogue( trigger, ent ) // WIP
+ file.teamTerrLastConnectTime[ trigger.GetTeam() ] = Time()
+ }
+
+ if( !IsValid(ent) )
+ return
+ if ( ent.IsPlayer() ) // notifications for player
+ {
+ MessageToPlayer( ent, eEventNotifications.Clear ) // clean up last message
+ bool sameTeam = ent.GetTeam() == trigger.GetTeam()
+ if ( sameTeam )
+ {
+ Remote_CallFunction_NonReplay( ent , "ServerCallback_FW_NotifyEnterFriendlyArea" )
+ ent.SetPlayerNetInt( "indicatorId", 1 ) // 1 means "FRIENDLY TERRITORY"
+ }
+ else
+ {
+ Remote_CallFunction_NonReplay( ent , "ServerCallback_FW_NotifyEnterEnemyArea" )
+ ent.SetPlayerNetInt( "indicatorId", 2 ) // 2 means "ENEMY TERRITORY"
+ }
+ }
+}
+
+void function EntityLeaveFWTrig( entity trigger, entity ent, entity caller, var value )
+{
+ if( !IsValid( ent ) ) // post-spawns
+ return
+ if( !ent.IsPlayer() && !ent.IsTitan() ) // no neet to add props and grunts i guess
+ return
+ // functions that trigger_multiple missing
+ if( IsValid( ent ) )
+ {
+ if( ent in trigger.e.scriptTriggerData.entities ) // need to check this!
+ {
+ ScriptTriggerRemoveEntity( trigger, ent )
+ //TryFWTerritoryDialogue( trigger, ent ) // WIP
+ file.teamTerrLastConnectTime[ trigger.GetTeam() ] = Time()
+ }
+ }
+
+ if( !IsValid(ent) )
+ return
+ if ( ent.IsPlayer() ) // notifications for player
+ {
+ MessageToPlayer( ent, eEventNotifications.Clear ) // clean up
+ bool sameTeam = ent.GetTeam() == trigger.GetTeam()
+ if ( sameTeam )
+ Remote_CallFunction_NonReplay( ent , "ServerCallback_FW_NotifyExitFriendlyArea" )
+ else
+ Remote_CallFunction_NonReplay( ent , "ServerCallback_FW_NotifyExitEnemyArea" )
+ ent.SetPlayerNetInt( "indicatorId", 4 ) // 4 means "NO MAN'S LAND"
+ }
+}
+
+// globlized!
+bool function FW_IsPlayerInFriendlyTerritory( entity player )
+{
+ foreach( entity trigger in file.fwTerritories )
+ {
+ if( trigger.GetTeam() == player.GetTeam() ) // is it friendly one?
+ {
+ if( GetAllEntitiesInTrigger( trigger ).contains( player ) ) // is player inside?
+ return true
+ }
+ }
+ return false // can't find the player
+}
+
+// globlized!
+bool function FW_IsPlayerInEnemyTerritory( entity player )
+{
+ foreach( entity trigger in file.fwTerritories )
+ {
+ if( trigger.GetTeam() != player.GetTeam() ) // is it enemy one?
+ {
+ if( GetAllEntitiesInTrigger( trigger ).contains( player ) ) // is player inside?
+ return true
+ }
+ }
+ return false // can't find the player
+}
+
+///////////////////////////////////
+///// TERRITORY FUNCTIONS END /////
+///////////////////////////////////
+
+
+
+////////////////////////////////
+///// TITANSPAWN FUNCTIONS /////
+////////////////////////////////
+
+// territory trigger don't have a kv.radius, let's use a const
+// 2800 will pretty much get harvester's near titan startpoints
+const float FW_SPAWNPOINT_SEARCH_RADIUS = 2800.0
+
+
+Point function FW_ReCalculateTitanReplacementPoint( Point basePoint, entity player )
+{
+ int team = player.GetTeam()
+ // find team's harvester
+ entity teamHarvester = FW_GetTeamHarvesterProp( team )
+
+ if ( !IsValid( teamHarvester ) ) // team's havester has been destroyed!
+ return basePoint // return given value
+
+ if( Distance2D( basePoint.origin, teamHarvester.GetOrigin() ) <= FW_SPAWNPOINT_SEARCH_RADIUS ) // close enough!
+ return basePoint // this origin is good enough
+
+ // if not close enough to base, re-calculate
+ array<entity> fortWarPoints = FW_GetTitanSpawnPointsForTeam( team )
+ entity validPoint = GetClosest( fortWarPoints, basePoint.origin )
+ basePoint.origin = validPoint.GetOrigin()
+ return basePoint
+}
+
+bool function FW_RequestTitanAllowed( entity player, array< string > args )
+{
+ if( !FW_IsPlayerInFriendlyTerritory( player ) ) // is player in friendly base?
+ {
+ PlayFactionDialogueToPlayer( "tw_territoryNag", player ) // notify player
+ TryPlayTitanfallNegativeSoundToPlayer( player )
+ int objectiveID = 101 // which means "#FW_OBJECTIVE_TITANFALL"
+ Remote_CallFunction_NonReplay( player, "ServerCallback_FW_SetObjective", objectiveID )
+ return false
+ }
+ return true
+}
+
+bool function TryPlayTitanfallNegativeSoundToPlayer( entity player )
+{
+ if( !( "lastNegativeSound" in player.s ) )
+ player.s.lastNegativeSound <- 0.0 // float
+ if( player.s.lastNegativeSound + 1.0 > Time() ) // in sound cooldown
+ return false
+
+ // use a sound to notify player they can't titanfall here
+ EmitSoundOnEntityOnlyToPlayer( player, player, "titan_dryfire" )
+ player.s.lastNegativeSound = Time()
+
+ return true
+}
+
+array<entity> function FW_GetTitanSpawnPointsForTeam( int team )
+{
+ array<entity> validSpawnPoints
+ // find team's harvester
+ entity teamHarvester = FW_GetTeamHarvesterProp( team )
+
+ array<entity> allPoints
+ // same as _replacement_titans_drop.gnut does
+ allPoints.extend( GetEntArrayByClass_Expensive( "info_spawnpoint_titan" ) )
+ allPoints.extend( GetEntArrayByClass_Expensive( "info_spawnpoint_titan_start" ) )
+ allPoints.extend( GetEntArrayByClass_Expensive( "info_replacement_titan_spawn" ) )
+
+ // get valid points from all points
+ foreach( entity point in allPoints )
+ {
+ if( Distance2D( point.GetOrigin(), teamHarvester.GetOrigin() ) <= FW_SPAWNPOINT_SEARCH_RADIUS )
+ validSpawnPoints.append( point )
+ }
+
+ return validSpawnPoints
+}
+
+// some maps have reversed startpoints! we need a hack
+const array<string> TITAN_POINT_REVERSED_MAPS =
+[
+ "mp_grave"
+]
+
+// "Respawn as Titan" don't follow the rateSpawnPoints, fix it manually
+entity function FW_ForcedTitanStartPoint( entity player, entity basePoint )
+{
+ int team = player.GetTeam()
+ if ( TITAN_POINT_REVERSED_MAPS.contains( GetMapName() ) )
+ team = GetOtherTeam( player.GetTeam() )
+ array<entity> startPoints = SpawnPoints_GetTitanStart( team )
+ entity validPoint = startPoints[ RandomInt( startPoints.len() ) ] // choose a random( maybe not safe ) start point
+ return validPoint
+}
+
+////////////////////////////////////
+///// TITANSPAWN FUNCTIONS END /////
+////////////////////////////////////
+
+
+
+/////////////////////////////////
+///// THREATLEVEL FUNCTIONS /////
+/////////////////////////////////
+
+void function FWAreaThreatLevelThink()
+{
+ thread FWAreaThreatLevelThink_Threaded()
+}
+
+void function FWAreaThreatLevelThink_Threaded()
+{
+ entity imcTerritory
+ entity mltTerritory
+ foreach( entity territory in file.fwTerritories )
+ {
+ if( territory.GetTeam() == TEAM_IMC )
+ imcTerritory = territory
+ else
+ mltTerritory = territory
+ }
+
+ float lastWarningTime // for debounce
+ bool warnImcTitanApproach
+ bool warnMltTitanApproach
+ bool warnImcTitanInArea
+ bool warnMltTitanInArea
+
+ while( GamePlayingOrSuddenDeath() )
+ {
+ //print( " imc threat level is: " + string( GetGlobalNetInt( "imcTowerThreatLevel" ) ) )
+ //print( " mlt threat level is: " + string( GetGlobalNetInt( "milTowerThreatLevel" ) ) )
+ float imcLastDamage = fw_harvesterImc.lastDamage
+ float mltLastDamage = fw_harvesterMlt.lastDamage
+ bool imcShieldDown = fw_harvesterImc.harvesterShieldDown
+ bool mltShieldDown = fw_harvesterMlt.harvesterShieldDown
+
+ // imc threatLevel
+ if( imcLastDamage + FW_HARVESTER_DAMAGED_DEBOUNCE >= Time() && imcShieldDown )
+ SetGlobalNetInt( "imcTowerThreatLevel", 3 ) // 3 will show a "harvester being damaged" warning to player
+ else if( warnImcTitanInArea )
+ SetGlobalNetInt( "imcTowerThreatLevel", 2 ) // 2 will show a "titan in area" warning to player
+ else if( warnImcTitanApproach )
+ SetGlobalNetInt( "imcTowerThreatLevel", 1 ) // 1 will show a "titan approach" waning to player
+ else
+ SetGlobalNetInt( "imcTowerThreatLevel", 0 ) // 0 will hide all warnings
+
+ // militia threatLevel
+ if( mltLastDamage + FW_HARVESTER_DAMAGED_DEBOUNCE >= Time() && mltShieldDown )
+ SetGlobalNetInt( "milTowerThreatLevel", 3 ) // 3 will show a "harvester being damaged" warning to player
+ else if( warnMltTitanInArea )
+ SetGlobalNetInt( "milTowerThreatLevel", 2 ) // 2 will show a "titan in area" warning to player
+ else if( warnMltTitanApproach )
+ SetGlobalNetInt( "milTowerThreatLevel", 1 ) // 1 will show a "titan approach" waning to player
+ else
+ SetGlobalNetInt( "milTowerThreatLevel", 0 ) // 0 will hide all warnings
+
+
+ // clean it here
+ warnImcTitanInArea = false
+ warnMltTitanInArea = false
+ warnImcTitanApproach = false
+ warnMltTitanApproach = false
+
+ // get valid titans
+ array<entity> allTitans = GetNPCArrayByClass( "npc_titan" )
+ array<entity> allPlayers = GetPlayerArray()
+ foreach( entity player in allPlayers )
+ {
+ if( IsAlive( player ) && player.IsTitan() )
+ {
+ allTitans.append( player )
+ }
+ }
+
+ // check threats
+ array<entity> imcEntArray = GetAllEntitiesInTrigger( imcTerritory )
+ array<entity> mltEntArray = GetAllEntitiesInTrigger( mltTerritory )
+ imcEntArray.removebyvalue( null ) // since we're using a fake trigger, need to check this
+ mltEntArray.removebyvalue( null )
+ foreach( entity ent in imcEntArray )
+ {
+ //print( ent )
+ if( !IsValid( ent ) ) // since we're using a fake trigger, need to check this
+ continue
+ if( ent.IsPlayer() || ent.IsNPC() )
+ {
+ if( ent.IsTitan() && ent.GetTeam() != TEAM_IMC )
+ warnImcTitanInArea = true
+ }
+ }
+ foreach( entity ent in mltEntArray )
+ {
+ //print( ent )
+ if( !IsValid( ent ) ) // since we're using a fake trigger, need to check this
+ continue
+ if( ent.IsPlayer() || ent.IsNPC() )
+ {
+ if( ent.IsTitan() && ent.GetTeam() != TEAM_MILITIA )
+ warnMltTitanInArea = true
+ }
+ }
+
+ foreach( entity titan in allTitans )
+ {
+ if( !imcEntArray.contains( titan )
+ && !mltEntArray.contains( titan )
+ && titan.GetTeam() != TEAM_IMC
+ && !titan.e.isHotDropping )
+ warnImcTitanApproach = true // this titan must be in natural space
+
+ if( !mltEntArray.contains( titan )
+ && !imcEntArray.contains( titan )
+ && titan.GetTeam() != TEAM_MILITIA
+ && !titan.e.isHotDropping )
+ warnMltTitanApproach = true // this titan must be in natural space
+ }
+
+ WaitFrame()
+ }
+}
+
+/////////////////////////////////////
+///// THREATLEVEL FUNCTIONS END /////
+/////////////////////////////////////
+
+
+
+////////////////////////////
+///// TURRET FUNCTIONS /////
+////////////////////////////
+
+void function OnFWTurretSpawned( entity turret )
+{
+ turret.EnableTurret() // always enabled
+ SetDefaultMPEnemyHighlight( turret ) // for sonar highlights to work
+ AddEntityCallback_OnDamaged( turret, OnMegaTurretDamaged )
+ thread FWTurretHighlightThink( turret )
+}
+
+// this will clear turret's highlight upon their death, for notifying players to fix them
+void function FWTurretHighlightThink( entity turret )
+{
+ turret.EndSignal( "OnDestroy" )
+ WaitFrame() // wait a frame for other turret spawn options to set up
+ Highlight_SetFriendlyHighlight( turret, "fw_friendly" ) // initialize the highlight, they will show upon player's next respawn
+
+ turret.WaitSignal( "OnDeath" )
+ Highlight_ClearFriendlyHighlight( turret )
+}
+
+// for battery_port, replace the turret with new one
+entity function FW_ReplaceMegaTurret( entity perviousTurret )
+{
+ if( !IsValid( perviousTurret ) ) // previous turret not exist!
+ return
+
+ entity turret = CreateNPC( "npc_turret_mega", perviousTurret.GetTeam(), perviousTurret.GetOrigin(), perviousTurret.GetAngles() )
+ SetSpawnOption_AISettings( turret, "npc_turret_mega_fortwar" )
+ DispatchSpawn( turret )
+
+ // apply settings to new turret, must up on date
+ turret.s.baseTurret <- perviousTurret.s.baseTurret
+ turret.s.minimapstate <- perviousTurret.s.minimapstate
+ turret.s.turretflagid <- perviousTurret.s.turretflagid
+ turret.s.lastDamagedTime <- perviousTurret.s.lastDamagedTime
+ turret.s.relatedBatteryPort <- perviousTurret.s.relatedBatteryPort
+
+ int maxHealth = perviousTurret.GetMaxHealth()
+ int maxShield = perviousTurret.GetShieldHealthMax()
+ turret.SetMaxHealth( maxHealth )
+ turret.SetHealth( maxHealth )
+ turret.SetShieldHealth( maxShield )
+ turret.SetShieldHealthMax( maxShield )
+
+ // update turretSiteStruct
+ foreach( TurretSiteStruct turretsite in file.turretsites )
+ {
+ if( turretsite.turret == perviousTurret )
+ {
+ turretsite.turret = turret // only changed this
+ }
+ }
+
+ perviousTurret.Destroy() // destroy previous one
+
+ return turret
+}
+
+// avoid notifications overrides itself
+const float TURRET_NOTIFICATION_DEBOUNCE = 10.0
+
+void function OnMegaTurretDamaged( entity turret, var damageInfo )
+{
+ int damageSourceID = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ float damageAmount = DamageInfo_GetDamage( damageInfo )
+ int scriptType = DamageInfo_GetCustomDamageType( damageInfo )
+ int turretTeam = turret.GetTeam()
+
+ if ( !damageSourceID && !damageAmount && !attacker )
+ return
+
+ if( turret.GetShieldHealth() - damageAmount <= 0 && scriptType != damageTypes.rodeoBatteryRemoval ) // this shot breaks shield
+ {
+ if ( !attacker.IsTitan() && !IsSuperSpectre( attacker ) )
+ {
+ if( attacker.IsPlayer() && attacker.GetTeam() != turret.GetTeam() ) // good to have
+ {
+ // avoid notifications overrides itself
+ if( attacker.s.lastTurretNotifyTime + TURRET_NOTIFICATION_DEBOUNCE < Time() )
+ {
+ MessageToPlayer( attacker, eEventNotifications.Clear ) // clean up last message
+ MessageToPlayer( attacker, eEventNotifications.TurretTitanDamageOnly )
+ attacker.s.lastTurretNotifyTime = Time()
+ }
+ }
+ DamageInfo_SetDamage( damageInfo, turret.GetShieldHealth() ) // destroy shields
+ return
+ }
+ }
+
+ // successfully damaged turret
+ turret.s.lastDamagedTime = Time()
+
+ if ( damageSourceID == eDamageSourceId.mp_titanweapon_heat_shield ||
+ damageSourceID == eDamageSourceId.mp_titanweapon_meteor_thermite ||
+ damageSourceID == eDamageSourceId.mp_titanweapon_flame_wall ||
+ damageSourceID == eDamageSourceId.mp_titanability_slow_trap ||
+ damageSourceID == eDamageSourceId.mp_titancore_flame_wave_secondary
+ ) // scorch's thermite damages
+ DamageInfo_SetDamage( damageInfo, DamageInfo_GetDamage( damageInfo )/2 ) // nerf scorch
+
+ // faction dialogue
+ damageAmount = DamageInfo_GetDamage( damageInfo )
+ if( turret.GetHealth() - damageAmount <= 0 ) // turret killed this shot
+ {
+ if( GamePlayingOrSuddenDeath() )
+ PlayFactionDialogueToTeam( "fortwar_turretDestroyedFriendly", turretTeam )
+ }
+}
+
+void function InitTurretSettings()
+{
+ foreach( TurretSiteStruct turretSite in file.turretsites )
+ {
+ entity turret = turretSite.turret
+ entity minimapstate = turretSite.minimapstate
+ int teamNum = turretSite.site.GetTeam()
+ int id = int( string( turretSite.site.kv.turretId ) )
+ string idString = string( id + 1 )
+ int team = int( string( turretSite.site.kv.teamnumber ) )
+
+ int stateFlag = 1 // netural
+
+ // spawn with teamNumber?
+ if( team == TEAM_IMC || team == TEAM_MILITIA )
+ turret.s.baseTurret = true
+
+ //SetTeam( minimapstate, team ) // setTeam() for icons is done in TurretStateWatcher()
+ SetTeam( turret, team )
+
+ //print( "Try to set globatNetEnt: " + "turretSite" + idString )
+
+ turret.s.turretflagid = idString
+ turretSite.turretflagid = idString
+
+ thread TurretStateWatcher( turretSite )
+ }
+}
+
+// about networkvar "turretStateFlags" value
+// 1 means destoryed/netural
+// 2 means imc turret
+// 4 means mlt turret
+// 10 means shielded imc turret
+// 13 means shielded mlt turret
+// 16 means destoryed/netural being attacked
+// 18 means imc turret being attacked
+// 20 means mlt turret being attacked
+// 26 means shielded imc turret being attacked
+// 28 means shielded mlt turret being attacked
+
+// unsure:
+// 24 means destroyed imc turret being attacked?
+// 40 means destroyed imc turret?
+// 48 means destroyed mlt turret being attacked?
+
+const int TURRET_DESTROYED_FLAG = 1
+const int TURRET_NATURAL_FLAG = 1
+const int TURRET_IMC_FLAG = 2
+const int TURRET_MLT_FLAG = 4
+const int TURRET_SHIELDED_IMC_FLAG = 10
+const int TURRET_SHIELDED_MLT_FLAG = 13
+
+const int TURRET_UNDERATTACK_NATURAL_FLAG = 16
+const int TURRET_UNDERATTACK_IMC_FLAG = 18
+const int TURRET_UNDERATTACK_MLT_FLAG = 20
+// natural turret noramlly can't get shielded
+const int TURRET_SHIELDED_UNDERATTACK_IMC_FLAG = 26
+const int TURRET_SHIELDED_UNDERATTACK_MLT_FLAG = 28
+
+void function TurretStateWatcher( TurretSiteStruct turretSite )
+{
+ entity mapIcon = turretSite.minimapstate
+ entity turret = turretSite.turret
+ entity batteryPort = expect entity( turret.s.relatedBatteryPort )
+
+ int turretHealth = GetCurrentPlaylistVarInt( "fw_turret_health", FW_DEFAULT_TURRET_HEALTH )
+ int turretShield = GetCurrentPlaylistVarInt( "fw_turret_shield", FW_DEFAULT_TURRET_SHIELD )
+ turret.SetMaxHealth( turretHealth )
+ turret.SetHealth( turretHealth )
+ turret.SetShieldHealthMax( turretShield )
+
+ string idString = turretSite.turretflagid
+ string siteVarName = "turretSite" + idString
+ string stateVarName = "turretStateFlags" + idString
+
+ // battery overlay icons holder
+ entity overlayState = CreateEntity( "prop_script" )
+ overlayState.SetValueForModelKey( $"models/communication/flag_base.mdl" ) // requires a model to show overlays
+ overlayState.Hide() // this can still show players overlay icons
+ overlayState.SetOrigin( batteryPort.GetOrigin() ) // tracking batteryPort's positions
+ overlayState.SetAngles( batteryPort.GetAngles() )
+ overlayState.kv.solid = SOLID_VPHYSICS
+ DispatchSpawn( overlayState )
+
+ svGlobal.levelEnt.EndSignal( "CleanUpEntitiesForRoundEnd" ) // end dialogues is good
+ mapIcon.EndSignal( "OnDestroy" ) // mapIcon should be valid all time, tracking it
+ batteryPort.EndSignal( "OnDestroy" ) // also track this
+ overlayState.EndSignal( "OnDestroy" )
+
+ SetGlobalNetEnt( siteVarName, overlayState ) // tracking batteryPort's positions and team
+ SetGlobalNetInt( stateVarName, TURRET_NATURAL_FLAG ) // init for all turrets
+
+ int lastFrameTeam
+ bool lastFrameIsAlive
+
+ while( true )
+ {
+ WaitFrame() // start of the loop
+
+ turret = turretSite.turret // need to keep updating, for sometimes it being replaced
+
+ if( !IsValid( turret ) ) // replacing turret this frame
+ continue // skip the loop once
+
+ bool isBaseTurret = expect bool( turret.s.baseTurret )
+ int turretTeam = turret.GetTeam()
+ bool turretAlive = IsAlive( turret )
+
+ bool changedTeamThisFrame = lastFrameTeam != turretTeam // turret has changed team?
+ bool killedThisFrame = lastFrameIsAlive != turretAlive // turret has no health left?
+
+ if( !turretAlive ) // turret down, waiting to be repaired
+ {
+ if( !isBaseTurret ) // never reset base turret's team
+ {
+ SetTeam( turret, TEAM_UNASSIGNED )
+ SetTeam( mapIcon, TEAM_UNASSIGNED )
+ SetTeam( batteryPort, TEAM_UNASSIGNED )
+ SetTeam( overlayState, TEAM_UNASSIGNED )
+ batteryPort.SetUsableByGroup( "pilot" ) // show hints to any pilot
+ }
+ SetGlobalNetInt( stateVarName, TURRET_DESTROYED_FLAG )
+ continue
+ }
+
+ // wrong dialogue, it will say "The turret you requested is on the way"
+ //if( changedTeamThisFrame ) // has been hacked!
+ // PlayFactionDialogueToTeam( "fortwar_turretDeployFriendly", turretTeam )
+
+ int iconTeam = turretTeam == TEAM_BOTH ? TEAM_UNASSIGNED : turretTeam // specific check
+ SetTeam( mapIcon, iconTeam ) // update icon's team
+ SetTeam( batteryPort, turretTeam ) // update batteryPort's team
+ SetTeam( overlayState, iconTeam ) // update overlayEnt's team
+
+ if( turretTeam != TEAM_BOTH && turretTeam != TEAM_UNASSIGNED ) // not a natural turret nor dead
+ batteryPort.SetUsableByGroup( "friendlies pilot" ) // only show hint to friendlies
+
+ float lastDamagedTime = expect float( turret.s.lastDamagedTime )
+ int stateFlag = TURRET_NATURAL_FLAG
+
+ // imc states
+ if( iconTeam == TEAM_IMC )
+ {
+ if( lastDamagedTime + FW_TURRET_DAMAGED_DEBOUNCE >= Time() ) // recent underattack
+ {
+ if( turret.GetShieldHealth() > 0 ) // has shields
+ stateFlag = TURRET_SHIELDED_UNDERATTACK_IMC_FLAG
+ else
+ stateFlag = TURRET_UNDERATTACK_IMC_FLAG
+
+ // these dialogue have 30s debounce inside
+ if( isBaseTurret )
+ PlayFactionDialogueToTeam( "fortwar_baseTurretsUnderAttack", TEAM_IMC )
+ else
+ PlayFactionDialogueToTeam( "fortwar_awayTurretsUnderAttack", TEAM_IMC )
+ }
+ else if( turret.GetShieldHealth() > 0 ) // has shields left
+ stateFlag = TURRET_SHIELDED_IMC_FLAG
+ else
+ stateFlag = TURRET_IMC_FLAG
+ }
+
+ // mlt states
+ if( iconTeam == TEAM_MILITIA )
+ {
+ if( lastDamagedTime + FW_TURRET_DAMAGED_DEBOUNCE >= Time() ) // recent underattack
+ {
+ if( turret.GetShieldHealth() > 0 ) // has shields
+ stateFlag = TURRET_SHIELDED_UNDERATTACK_MLT_FLAG
+ else
+ stateFlag = TURRET_UNDERATTACK_MLT_FLAG
+
+ // these dialogue have 30s debounce inside
+ if( isBaseTurret )
+ PlayFactionDialogueToTeam( "fortwar_baseTurretsUnderAttack", TEAM_MILITIA )
+ else
+ PlayFactionDialogueToTeam( "fortwar_awayTurretsUnderAttack", TEAM_MILITIA )
+ }
+ else if( turret.GetShieldHealth() > 0 ) // has shields left
+ stateFlag = TURRET_SHIELDED_MLT_FLAG
+ else
+ stateFlag = TURRET_MLT_FLAG
+ }
+
+ // natural states
+ if( iconTeam == TEAM_UNASSIGNED )
+ {
+ if( lastDamagedTime + FW_TURRET_DAMAGED_DEBOUNCE >= Time() ) // recent underattack
+ stateFlag = TURRET_UNDERATTACK_NATURAL_FLAG
+ else
+ stateFlag = TURRET_NATURAL_FLAG
+ }
+
+ SetGlobalNetInt( stateVarName, stateFlag )
+
+ // update these
+ lastFrameTeam = turretTeam
+ lastFrameIsAlive = turretAlive
+
+ WaitFrame()
+ }
+}
+
+////////////////////////////////
+///// TURRET FUNCTIONS END /////
+////////////////////////////////
+
+
+
+///////////////////////////////
+///// HARVESTER FUNCTIONS /////
+///////////////////////////////
+
+void function startFWHarvester()
+{
+ thread HarvesterThink(fw_harvesterImc)
+ thread HarvesterAlarm(fw_harvesterImc)
+ thread UpdateHarvesterHealth( TEAM_IMC )
+
+ thread HarvesterThink(fw_harvesterMlt)
+ thread HarvesterAlarm(fw_harvesterMlt)
+ thread UpdateHarvesterHealth( TEAM_MILITIA )
+}
+
+entity function FW_GetTeamHarvesterProp( int team )
+{
+ if( team == TEAM_IMC )
+ return fw_harvesterImc.harvester
+ else if( team == TEAM_MILITIA )
+ return fw_harvesterMlt.harvester
+
+ unreachable // crash the game
+}
+
+void function FW_createHarvester()
+{
+ // imc havester spawn
+ fw_harvesterImc = SpawnHarvester( file.harvesterImc_info.GetOrigin(), file.harvesterImc_info.GetAngles(), GetCurrentPlaylistVarInt( "fw_harvester_health", FW_DEFAULT_HARVESTER_HEALTH ), GetCurrentPlaylistVarInt( "fw_harvester_shield", FW_DEFAULT_HARVESTER_SHIELD ), TEAM_IMC )
+ fw_harvesterImc.harvester.SetArmorType( ARMOR_TYPE_HEAVY )
+ fw_harvesterImc.harvester.Minimap_SetAlignUpright( true )
+ fw_harvesterImc.harvester.Minimap_AlwaysShow( TEAM_IMC, null )
+ fw_harvesterImc.harvester.Minimap_AlwaysShow( TEAM_MILITIA, null )
+ fw_harvesterImc.harvester.Minimap_SetHeightTracking( true )
+ fw_harvesterImc.harvester.Minimap_SetZOrder( MINIMAP_Z_OBJECT )
+ fw_harvesterImc.harvester.Minimap_SetCustomState( eMinimapObject_prop_script.FD_HARVESTER )
+ AddEntityCallback_OnFinalDamaged( fw_harvesterImc.harvester, OnHarvesterDamaged )
+ AddEntityCallback_OnPostDamaged( fw_harvesterImc.harvester, OnHarvesterPostDamaged )
+
+ // imc havester settings
+ // don't set this, or sonar pulse will try to find it and failed to set highlight
+ //fw_harvesterMlt.harvester.SetScriptName("fw_team_tower")
+ file.harvesters.append(fw_harvesterImc)
+ entity trackerImc = GetAvailableBaseLocationTracker()
+ trackerImc.SetOwner( fw_harvesterImc.harvester )
+ DispatchSpawn( trackerImc )
+ SetLocationTrackerRadius( trackerImc, 1 ) // whole map
+
+ // scores starts from 100, TeamScore means harvester health; TeamScore2 means shield bar
+ GameRules_SetTeamScore( TEAM_MILITIA , 100 )
+ GameRules_SetTeamScore2( TEAM_MILITIA , 100 )
+
+
+ // mlt havester spawn
+ fw_harvesterMlt = SpawnHarvester( file.harvesterMlt_info.GetOrigin(), file.harvesterMlt_info.GetAngles(), GetCurrentPlaylistVarInt( "fw_harvester_health", FW_DEFAULT_HARVESTER_HEALTH ), GetCurrentPlaylistVarInt( "fw_harvester_shield", FW_DEFAULT_HARVESTER_SHIELD ), TEAM_MILITIA )
+ fw_harvesterMlt.harvester.SetArmorType( ARMOR_TYPE_HEAVY )
+ fw_harvesterMlt.harvester.Minimap_SetAlignUpright( true )
+ fw_harvesterMlt.harvester.Minimap_AlwaysShow( TEAM_IMC, null )
+ fw_harvesterMlt.harvester.Minimap_AlwaysShow( TEAM_MILITIA, null )
+ fw_harvesterMlt.harvester.Minimap_SetHeightTracking( true )
+ fw_harvesterMlt.harvester.Minimap_SetZOrder( MINIMAP_Z_OBJECT )
+ fw_harvesterMlt.harvester.Minimap_SetCustomState( eMinimapObject_prop_script.FD_HARVESTER )
+ AddEntityCallback_OnFinalDamaged( fw_harvesterMlt.harvester, OnHarvesterDamaged )
+ AddEntityCallback_OnPostDamaged( fw_harvesterMlt.harvester, OnHarvesterPostDamaged )
+
+ // mlt havester settings
+ // don't set this, or sonar pulse will try to find it and failed to set highlight
+ //fw_harvesterImc.harvester.SetScriptName("fw_team_tower")
+ file.harvesters.append(fw_harvesterMlt)
+ entity trackerMlt = GetAvailableBaseLocationTracker()
+ trackerMlt.SetOwner( fw_harvesterMlt.harvester )
+ DispatchSpawn( trackerMlt )
+ SetLocationTrackerRadius( trackerMlt, 1 ) // whole map
+
+ // scores starts from 100, TeamScore means harvester health; TeamScore2 means shield bar
+ GameRules_SetTeamScore( TEAM_IMC , 100 )
+ GameRules_SetTeamScore2( TEAM_IMC , 100 )
+
+ InitHarvesterDamageMods()
+}
+
+void function FW_AddHarvesterDamageSourceModifier( int id, float mod )
+{
+ if ( !( id in file.harvesterDamageSourceMods ) )
+ file.harvesterDamageSourceMods[id] <- 1.0
+
+ file.harvesterDamageSourceMods[id] *= mod
+}
+
+void function FW_RemoveHarvesterDamageSourceModifier( int id, float mod )
+{
+ if ( !( id in file.harvesterDamageSourceMods ) )
+ return
+
+ file.harvesterDamageSourceMods[id] /= mod
+
+ if ( file.harvesterDamageSourceMods[id] == 1.0 )
+ delete file.harvesterDamageSourceMods[id]
+}
+
+void function InitHarvesterDamageMods()
+{
+ // Damage balancing
+ const float CORE_DAMAGE_FRAC = 0.67
+ const float NUKE_EJECT_DAMAGE_FRAC = 0.25
+ const float DOT_DAMAGE_FRAC = 0.5
+
+ // Core balancing
+ FW_AddHarvesterDamageSourceModifier( eDamageSourceId.mp_titancore_laser_cannon, CORE_DAMAGE_FRAC )
+ FW_AddHarvesterDamageSourceModifier( eDamageSourceId.mp_titancore_salvo_core, CORE_DAMAGE_FRAC )
+ FW_AddHarvesterDamageSourceModifier( eDamageSourceId.mp_titanweapon_flightcore_rockets, CORE_DAMAGE_FRAC )
+ FW_AddHarvesterDamageSourceModifier( eDamageSourceId.mp_titancore_shift_core, CORE_DAMAGE_FRAC )
+ // Flame Core is not included since its single target damage is low compared to the others
+
+ // Nuke eject balancing
+ FW_AddHarvesterDamageSourceModifier( eDamageSourceId.damagedef_nuclear_core, NUKE_EJECT_DAMAGE_FRAC )
+
+ // Damage over time balancing
+ FW_AddHarvesterDamageSourceModifier( eDamageSourceId.mp_titanweapon_dumbfire_rockets, DOT_DAMAGE_FRAC )
+ FW_AddHarvesterDamageSourceModifier( eDamageSourceId.mp_titanweapon_meteor_thermite, DOT_DAMAGE_FRAC )
+ FW_AddHarvesterDamageSourceModifier( eDamageSourceId.mp_titanweapon_flame_wall, DOT_DAMAGE_FRAC )
+ FW_AddHarvesterDamageSourceModifier( eDamageSourceId.mp_titanability_slow_trap, DOT_DAMAGE_FRAC )
+ FW_AddHarvesterDamageSourceModifier( eDamageSourceId.mp_titancore_flame_wave_secondary, DOT_DAMAGE_FRAC )
+ FW_AddHarvesterDamageSourceModifier( eDamageSourceId.mp_titanweapon_heat_shield, DOT_DAMAGE_FRAC )
+}
+
+// this function can't handle specific damageSourceID, such as plasma railgun, but is the best to scale both shield and health damage
+void function OnHarvesterDamaged( entity harvester, var damageInfo )
+{
+ if ( !IsValid( harvester ) )
+ return
+
+ // Entities (non-Players and non-NPCs) don't consider damaged entity lists, which makes ground attacks (e.g. Arc Wave) and thermite hit more than they should
+ entity inflictor = DamageInfo_GetInflictor( damageInfo )
+ if ( IsValid( inflictor ) && ( inflictor.e.onlyDamageEntitiesOnce || inflictor.e.onlyDamageEntitiesOncePerTick ) )
+ {
+ if ( inflictor.e.damagedEntities.contains( harvester ) )
+ {
+ DamageInfo_SetDamage( damageInfo, 0 )
+ return
+ }
+ else
+ {
+ inflictor.e.damagedEntities.append( harvester )
+ }
+ }
+
+ int friendlyTeam = harvester.GetTeam()
+ int enemyTeam = GetOtherTeam( friendlyTeam )
+ int damageSourceID = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+
+ HarvesterStruct harvesterstruct // current harveter's struct
+ if( friendlyTeam == TEAM_MILITIA )
+ harvesterstruct = fw_harvesterMlt
+ if( friendlyTeam == TEAM_IMC )
+ harvesterstruct = fw_harvesterImc
+
+ float damageAmount = DamageInfo_GetDamage( damageInfo )
+ if((harvester.GetShieldHealth()-damageAmount)<0)
+ {
+ if( !harvesterstruct.harvesterShieldDown )
+ {
+ PlayFactionDialogueToTeam( "fortwar_baseShieldDownFriendly", friendlyTeam )
+ PlayFactionDialogueToTeam( "fortwar_baseShieldDownEnemy", enemyTeam )
+ harvesterstruct.harvesterShieldDown = true // prevent shield dialogues from repeating
+ }
+ }
+
+ // always reset harvester's recharge delay
+ harvesterstruct.lastDamage = Time()
+
+ // Should be moved to a final damage callback once those are added
+ if ( damageSourceID in file.harvesterDamageSourceMods )
+ DamageInfo_ScaleDamage( damageInfo, file.harvesterDamageSourceMods[ damageSourceID ] )
+}
+void function OnHarvesterPostDamaged( entity harvester, var damageInfo )
+{
+ if ( !IsValid( harvester ) )
+ return
+
+ int friendlyTeam = harvester.GetTeam()
+ int enemyTeam = GetOtherTeam( friendlyTeam )
+
+ GameRules_SetTeamScore( friendlyTeam , 1.0 * GetHealthFrac( harvester ) * 100 )
+
+ int damageSourceID = DamageInfo_GetDamageSourceIdentifier( damageInfo )
+ entity attacker = DamageInfo_GetAttacker( damageInfo )
+ int scriptType = DamageInfo_GetCustomDamageType( damageInfo )
+ float damageAmount = DamageInfo_GetDamage( damageInfo )
+
+ if(damageAmount == 0)
+ return
+
+ if ( !damageSourceID && !damageAmount && !attacker ) // actually not dealing any damage?
+ return
+
+ // prevent player from sniping the harvester cross-map
+ if ( attacker.IsPlayer() && !FW_IsPlayerInEnemyTerritory( attacker ) )
+ {
+ Remote_CallFunction_NonReplay( attacker , "ServerCallback_FW_NotifyNeedsEnterEnemyArea" )
+ DamageInfo_SetDamage( damageInfo, 0 )
+ DamageInfo_SetCustomDamageType( damageInfo, scriptType | DF_NO_INDICATOR ) // hide the hitmarker
+ return // these damage won't do anything to the harvester
+ }
+
+ HarvesterStruct harvesterstruct // current harveter's struct
+ if( friendlyTeam == TEAM_MILITIA )
+ harvesterstruct = fw_harvesterMlt
+ if( friendlyTeam == TEAM_IMC )
+ harvesterstruct = fw_harvesterImc
+
+
+ damageAmount = DamageInfo_GetDamage( damageInfo ) // get damageAmount again after all damage adjustments
+
+ if ( !attacker.IsTitan() )
+ {
+ if( attacker.IsPlayer() )
+ Remote_CallFunction_NonReplay( attacker , "ServerCallback_FW_NotifyTitanRequired" )
+ DamageInfo_SetDamage( damageInfo, harvester.GetShieldHealth() )
+ damageAmount = 0 // never damage haveter's prop
+ }
+
+ if( !harvesterstruct.harvesterShieldDown )
+ {
+ PlayFactionDialogueToTeam( "fortwar_baseShieldDownFriendly", friendlyTeam )
+ PlayFactionDialogueToTeam( "fortwar_baseShieldDownEnemy", enemyTeam )
+ harvesterstruct.harvesterShieldDown = true // prevent shield dialogues from repeating
+ }
+
+ harvesterstruct.harvesterDamageTaken = harvesterstruct.harvesterDamageTaken + damageAmount // track damage for wave recaps
+ float newHealth = harvester.GetHealth() - damageAmount
+ float oldhealthpercent = ( ( harvester.GetHealth().tofloat() / harvester.GetMaxHealth() ) * 100 )
+ float healthpercent = ( ( newHealth / harvester.GetMaxHealth() ) * 100 )
+
+ if (healthpercent <= 75 && oldhealthpercent > 75) // we don't want the dialogue to keep saying "Harvester is below 75% health" everytime they take additional damage
+ {
+ PlayFactionDialogueToTeam( "fortwar_baseDmgFriendly75", friendlyTeam )
+ PlayFactionDialogueToTeam( "fortwar_baseDmgEnemy75", enemyTeam )
+ }
+
+ if (healthpercent <= 50 && oldhealthpercent > 50)
+ {
+ PlayFactionDialogueToTeam( "fortwar_baseDmgFriendly50", friendlyTeam )
+ PlayFactionDialogueToTeam( "fortwar_baseDmgEnemy50", enemyTeam )
+ }
+
+ if (healthpercent <= 25 && oldhealthpercent > 25)
+ {
+ PlayFactionDialogueToTeam( "fortwar_baseDmgFriendly25", friendlyTeam )
+ PlayFactionDialogueToTeam( "fortwar_baseDmgEnemy25", enemyTeam )
+ }
+
+ if( newHealth <= 0 )
+ {
+ EmitSoundAtPosition(TEAM_UNASSIGNED,harvesterstruct.harvester.GetOrigin(),"coop_generator_destroyed")
+ newHealth = 0
+ harvesterstruct.rings.Destroy()
+ harvesterstruct.harvester.Dissolve( ENTITY_DISSOLVE_CORE, Vector( 0, 0, 0 ), 500 )
+ }
+
+ harvester.SetHealth( newHealth )
+ harvesterstruct.havesterWasDamaged = true
+
+ if ( attacker.IsPlayer() )
+ {
+ // dialogue for enemy attackers
+ if( !harvesterstruct.harvesterShieldDown )
+ PlayFactionDialogueToTeam( "fortwar_baseEnemyAllyAttacking", enemyTeam )
+
+ attacker.NotifyDidDamage( harvester, DamageInfo_GetHitBox( damageInfo ), DamageInfo_GetDamagePosition( damageInfo ), DamageInfo_GetCustomDamageType( damageInfo ), DamageInfo_GetDamage( damageInfo ), DamageInfo_GetDamageFlags( damageInfo ), DamageInfo_GetHitGroup( damageInfo ), DamageInfo_GetWeapon( damageInfo ), DamageInfo_GetDistFromAttackOrigin( damageInfo ) )
+
+ // get newest damage for adding score!
+ int scoreDamage = int( DamageInfo_GetDamage( damageInfo ) )
+ // score events
+ attacker.AddToPlayerGameStat( PGS_ASSAULT_SCORE, scoreDamage )
+
+ // add to player structs
+ file.playerDamageHarvester[ attacker ].recentDamageTime = Time()
+ file.playerDamageHarvester[ attacker ].storedDamage += scoreDamage
+
+ // enough to earn score?
+ if( file.playerDamageHarvester[ attacker ].storedDamage >= FW_HARVESTER_DAMAGE_SEGMENT )
+ {
+ AddPlayerScore( attacker, "FortWarTowerDamage", attacker )
+ attacker.AddToPlayerGameStat( PGS_DEFENSE_SCORE, POINTVALUE_FW_TOWER_DAMAGE )
+ file.playerDamageHarvester[ attacker ].storedDamage -= FW_HARVESTER_DAMAGE_SEGMENT // reset stored damage
+ }
+ }
+
+ // harvester down!
+ if ( harvester.GetHealth() == 0 )
+ {
+ // force deciding winner
+ SetWinner( enemyTeam )
+ //PlayFactionDialogueToTeam( "scoring_wonMercy", enemyTeam )
+ //PlayFactionDialogueToTeam( "fortwar_matchLoss", friendlyTeam )
+ GameRules_SetTeamScore2( friendlyTeam, 0 ) // force set score2 to 0( shield bar will empty )
+ GameRules_SetTeamScore( friendlyTeam, 0 ) // force set score to 0( health 0% )
+ }
+}
+
+void function HarvesterThink( HarvesterStruct fw_harvester )
+{
+ entity harvester = fw_harvester.harvester
+
+
+ EmitSoundOnEntity( harvester, "coop_generator_startup" )
+
+ float lastTime = Time()
+ wait 4
+ int lastShieldHealth = harvester.GetShieldHealth()
+ generateBeamFX( fw_harvester )
+ generateShieldFX( fw_harvester )
+
+ EmitSoundOnEntity( harvester, "coop_generator_ambient_healthy" )
+
+ bool isRegening = false // stops the regenning sound to keep stacking on top of each other
+
+ while ( IsAlive( harvester ) )
+ {
+ float currentTime = Time()
+ float deltaTime = currentTime -lastTime
+
+ if ( IsValid( fw_harvester.particleShield ) )
+ {
+ vector shieldColor = GetShieldTriLerpColor( 1.0 - ( harvester.GetShieldHealth().tofloat() / harvester.GetShieldHealthMax().tofloat() ) )
+ EffectSetControlPointVector( fw_harvester.particleShield, 1, shieldColor )
+ }
+
+ if( IsValid( fw_harvester.particleBeam ) )
+ {
+ vector beamColor = GetShieldTriLerpColor( 1.0 - ( harvester.GetHealth().tofloat() / harvester.GetMaxHealth().tofloat() ) )
+ EffectSetControlPointVector( fw_harvester.particleBeam, 1, beamColor )
+ }
+
+ if ( fw_harvester.harvester.GetShieldHealth() == 0 )
+ if( IsValid( fw_harvester.particleShield ) )
+ fw_harvester.particleShield.Destroy()
+
+ if ( ( ( currentTime-fw_harvester.lastDamage ) >= GetCurrentPlaylistVarFloat( "fw_harvester_regen_delay", FW_DEFAULT_HARVESTER_REGEN_DELAY ) ) && ( harvester.GetShieldHealth() < harvester.GetShieldHealthMax() ) )
+ {
+ if( !IsValid( fw_harvester.particleShield ) )
+ generateShieldFX( fw_harvester )
+
+ if( harvester.GetShieldHealth() == 0 )
+ EmitSoundOnEntity( harvester, "coop_generator_shieldrecharge_start" )
+
+ if (!isRegening)
+ {
+ EmitSoundOnEntity( harvester, "coop_generator_shieldrecharge_resume" )
+ fw_harvester.harvesterShieldDown = false
+ isRegening = true
+ }
+
+ float newShieldHealth = ( harvester.GetShieldHealthMax() / GetCurrentPlaylistVarFloat( "fw_harvester_regen_time", FW_DEFAULT_HARVESTER_REGEN_TIME ) * deltaTime ) + harvester.GetShieldHealth()
+
+ // shield full
+ if ( newShieldHealth >= harvester.GetShieldHealthMax() )
+ {
+ StopSoundOnEntity( harvester, "coop_generator_shieldrecharge_resume" )
+ harvester.SetShieldHealth( harvester.GetShieldHealthMax() )
+ EmitSoundOnEntity( harvester, "coop_generator_shieldrecharge_end" )
+ PlayFactionDialogueToTeam( "fortwar_baseShieldUpFriendly", harvester.GetTeam() )
+ isRegening = false
+ }
+ else
+ {
+ harvester.SetShieldHealth( newShieldHealth )
+ }
+ }
+ else if ( ( ( currentTime-fw_harvester.lastDamage ) < GENERATOR_SHIELD_REGEN_DELAY ) && ( harvester.GetShieldHealth() < harvester.GetShieldHealthMax() ) )
+ {
+ isRegening = false
+ }
+
+ if ( ( lastShieldHealth > 0 ) && ( harvester.GetShieldHealth() == 0 ) )
+ {
+ EmitSoundOnEntity( harvester, "TitanWar_Harvester_ShieldDown" ) // add this
+ EmitSoundOnEntity( harvester, "coop_generator_shielddown" )
+ }
+
+ lastShieldHealth = harvester.GetShieldHealth()
+ lastTime = currentTime
+ WaitFrame()
+ }
+}
+
+void function HarvesterAlarm( HarvesterStruct fw_harvester )
+{
+ while( IsAlive( fw_harvester.harvester ) )
+ {
+ if( fw_harvester.harvester.GetShieldHealth() == 0 )
+ {
+ wait EmitSoundOnEntity( fw_harvester.harvester, "coop_generator_underattack_alarm" )
+ }
+ else
+ {
+ WaitFrame()
+ }
+ }
+}
+
+void function UpdateHarvesterHealth( int team )
+{
+ entity harvester
+ if( team == TEAM_MILITIA )
+ harvester = fw_harvesterMlt.harvester
+ if( team == TEAM_IMC )
+ harvester = fw_harvesterImc.harvester
+
+ while( true )
+ {
+ if( IsValid(harvester) )
+ {
+ GameRules_SetTeamScore2( team, 1.0 * harvester.GetShieldHealth() / harvester.GetShieldHealthMax() * 100 )
+ WaitFrame()
+ }
+ else // harvester down
+ {
+ int winnerTeam = GetOtherTeam(team)
+ SetWinner( winnerTeam )
+ //PlayFactionDialogueToTeam( "scoring_wonMercy", winnerTeam )
+ //PlayFactionDialogueToTeam( "fortwar_matchLoss", team )
+ GameRules_SetTeamScore2( team, 0 ) // force set score2 to 0( shield bar will empty )
+ GameRules_SetTeamScore( team, 0 ) // force set score to 0( health 0% )
+ break
+ }
+ }
+}
+
+///////////////////////////////////
+///// HARVESTER FUNCTIONS END /////
+///////////////////////////////////
+
+
+
+//////////////////////////////////////
+///// PLAYER OBJECTIVE FUNCTIONS /////
+//////////////////////////////////////
+
+const int APPLY_BATTERY_TEXT_INDEX = 96 // notify player to use batteries on turrets
+const int EARN_TITAN_TEXT_INDEX = 100 // notify player to earn titans
+const int CALL_IN_TITAN_TEXT_INDEX = 101 // notify player to call in titans in territory
+const int EMBARK_TITAN_TEXT_INDEX = 102 // notify player to embark titans
+const int ATTACK_HARVESTER_TEXT_INDEX = 103 // notify player to attack harvester
+
+void function FWPlayerObjectiveState()
+{
+ thread FWPlayerObjectiveState_Threaded()
+}
+
+void function FWPlayerObjectiveState_Threaded()
+{
+ while( GamePlayingOrSuddenDeath() )
+ {
+ foreach( player in GetPlayerArray() )
+ {
+ entity petTitan = player.GetPetTitan()
+ entity titanSoul
+ if( IsValid( petTitan ) )
+ titanSoul = petTitan.GetTitanSoul()
+
+ if ( IsValid( GetBatteryOnBack( player ) ) )
+ player.SetPlayerNetInt( "gameInfoStatusText", APPLY_BATTERY_TEXT_INDEX )
+ else if ( IsTitanAvailable( player ) )
+ {
+ if( !player.s.notifiedTitanfall ) // first notification, also do a objective announcement
+ {
+ SetObjective( player, CALL_IN_TITAN_TEXT_INDEX )
+ player.s.notifiedTitanfall = true
+ }
+ else
+ player.SetPlayerNetInt( "gameInfoStatusText", CALL_IN_TITAN_TEXT_INDEX )
+ }
+ else if ( IsValid( petTitan ) )
+ player.SetPlayerNetInt( "gameInfoStatusText", EMBARK_TITAN_TEXT_INDEX )
+ else if ( IsAlive( player ) && !player.IsTitan() )
+ player.SetPlayerNetInt( "gameInfoStatusText", EARN_TITAN_TEXT_INDEX )
+ else if( !IsValid( titanSoul ) ) // titan died or player first embarked
+ player.s.notifiedTitanfall = false
+
+ if ( !IsAlive( player ) ) // don't show objetive for dying players
+ player.SetPlayerNetInt( "gameInfoStatusText", -1 )
+ }
+ WaitFrame()
+ }
+
+ // game entered other state, clean this
+ foreach( player in GetPlayerArray() )
+ {
+ player.SetPlayerNetInt( "gameInfoStatusText", -1 )
+ }
+}
+
+void function SetObjective( entity player, int stringid )
+{
+ Remote_CallFunction_NonReplay( player, "ServerCallback_FW_SetObjective", stringid )
+ player.SetPlayerNetInt( "gameInfoStatusText", stringid )
+}
+
+void function SetTitanObjective( entity player, entity titan )
+{
+ SetObjective( player, ATTACK_HARVESTER_TEXT_INDEX )
+}
+
+void function SetPilotObjective( entity player, entity titan )
+{
+ if( titan.GetTitanSoul().IsEjecting() ) // this time titan is ejecting
+ {
+ SetObjective( player, EARN_TITAN_TEXT_INDEX )
+ player.s.notifiedTitanfall = false
+ }
+ else
+ player.SetPlayerNetInt( "gameInfoStatusText", EMBARK_TITAN_TEXT_INDEX )
+}
+
+//////////////////////////////////////////
+///// PLAYER OBJECTIVE FUNCTIONS END /////
+//////////////////////////////////////////
+
+
+
+/////////////////////////////////
+///// BatteryPort Functions /////
+/////////////////////////////////
+
+void function FW_InitBatteryPort( entity batteryPort )
+{
+ batteryPort.kv.fadedist = 10000 // try not to fade
+ InitTurretBatteryPort( batteryPort )
+
+ batteryPort.s.relatedTurret <- null // entity, for saving batteryPort's nearest turret
+
+ entity turret = GetNearestMegaTurret( batteryPort ) // consider this is the port's related turret
+
+ bool isBaseTurret = expect bool( turret.s.baseTurret )
+ SetTeam( batteryPort, turret.GetTeam() )
+ batteryPort.s.relatedTurret = turret
+ batteryPort.s.isUsable <- FW_IsBatteryPortUsable
+ batteryPort.s.useBattery <- FW_UseBattery
+ if( isBaseTurret ) // this is a base turret!
+ {
+ batteryPort.s.hackAvaliable = false
+ batteryPort.SetUsableByGroup( "friendlies pilot" ) // only show hint to friendlies
+ } // it can never be hacked!
+
+ turret.s.relatedBatteryPort = batteryPort // do it here
+}
+
+function FW_IsBatteryPortUsable( batteryPortvar, playervar ) //actually bool function( entity, entity )
+{
+ entity batteryPort = expect entity( batteryPortvar )
+ entity player = expect entity( playervar )
+ entity turret = expect entity( batteryPort.s.relatedTurret )
+ if( !IsValid( turret ) ) // turret has been destroyed!
+ return false
+
+ // get turret's settings, decide behavior
+ bool validTeam = turret.GetTeam() == player.GetTeam() || turret.GetTeam() == TEAM_BOTH || turret.GetTeam() == TEAM_UNASSIGNED
+ bool isBaseTurret = expect bool( turret.s.baseTurret )
+ // is this port able to be hacked
+ bool portHackAvaliable = expect bool( batteryPort.s.hackAvaliable )
+
+ // player has a battery, team valid or able to hack && not a base turret
+ return ( PlayerHasBattery( player ) && ( validTeam || ( portHackAvaliable && !isBaseTurret ) ) )
+}
+
+function FW_UseBattery( batteryPortvar, playervar ) //actually void function( entity, entity )
+{
+ entity batteryPort = expect entity( batteryPortvar )
+ entity player = expect entity( playervar )
+ // change turret settings
+ entity turret = expect entity( batteryPort.s.relatedTurret ) // consider this is the port's related turret
+
+ int playerTeam = player.GetTeam()
+ bool turretReplaced = false
+ bool sameTeam = turret.GetTeam() == player.GetTeam()
+
+ if( !IsAlive( turret ) ) // turret has been killed!
+ {
+ turret = FW_ReplaceMegaTurret( turret )
+ if( !IsValid( turret ) ) // replace failed!
+ return
+ batteryPort.s.relatedTurret = turret
+ turretReplaced = true // if turret has been replaced, mostly reset team!
+ }
+
+ bool teamChanged = false
+ bool isBaseTurret = expect bool( turret.s.baseTurret )
+ if( ( !sameTeam || turretReplaced ) && !isBaseTurret ) // is there a need to change team?
+ {
+ SetTeam( turret, playerTeam )
+ teamChanged = true
+ }
+
+ // restore turret health
+ int newHealth = int ( min( turret.GetMaxHealth(), turret.GetHealth() + ( turret.GetMaxHealth() * GetCurrentPlaylistVarFloat( "fw_turret_fixed_health", TURRET_FIXED_HEALTH_PERCENTAGE ) ) ) )
+ if( turretReplaced || teamChanged ) // replaced/hacked turret will spawn with 50% health
+ newHealth = int ( turret.GetMaxHealth() * GetCurrentPlaylistVarFloat( "fw_turret_hacked_health", TURRET_HACKED_HEALTH_PERCENTAGE ) )
+ // restore turret shield
+ int newShield = int ( min( turret.GetShieldHealthMax(), turret.GetShieldHealth() + ( turret.GetShieldHealth() * GetCurrentPlaylistVarFloat( "fw_turret_fixed_shield", TURRET_FIXED_SHIELD_PERCENTAGE ) ) ) )
+ if( turretReplaced || teamChanged ) // replaced/hacked turret will spawn with 50% shield
+ newShield = int ( turret.GetShieldHealthMax() * GetCurrentPlaylistVarFloat( "fw_turret_hacked_shield", TURRET_HACKED_SHIELD_PERCENTAGE ) )
+ // only do team score event if turret's shields down, encourage players to hack more turrets
+ bool additionalScore = turret.GetShieldHealth() <= 0
+ // this can be too much powerful
+ turret.SetHealth( newHealth )
+ turret.SetShieldHealth( newShield )
+
+ // score event
+ string scoreEvent = "FortWarForwardConstruction"
+ int secondaryScore = POINTVALUE_FW_FORWARD_CONSTRUCTION
+ if( isBaseTurret ) // this is a base turret
+ {
+ scoreEvent = "FortWarBaseConstruction"
+ secondaryScore = POINTVALUE_FW_BASE_CONSTRUCTION
+ }
+ AddPlayerScore( player, scoreEvent, player ) // player themself gets more meter
+ player.AddToPlayerGameStat( PGS_DEFENSE_SCORE, secondaryScore )
+
+ // only do team score event if turret's shields down
+ if( additionalScore )
+ {
+ // get turrets alive, for adding scores
+ string teamTurretCount = GetTeamAliveTurretCount_ReturnString( playerTeam )
+ foreach( entity friendly in GetPlayerArrayOfTeam( playerTeam ) )
+ AddPlayerScore( friendly, "FortWarTeamTurretControlBonus_" + teamTurretCount, friendly )
+
+ PlayFactionDialogueToTeam( "fortwar_turretShieldedByFriendlyPilot", playerTeam )
+ }
+
+}
+
+// get nearest turret, consider it belongs to the port
+entity function GetNearestMegaTurret( entity ent )
+{
+ array<entity> allTurrets = GetNPCArrayByClass( "npc_turret_mega" )
+ entity turret = GetClosest( allTurrets, ent.GetOrigin() )
+ return turret
+}
+
+// this will get english name of the count, since the "FortWarTeamTurretControlBonus_" score event uses it
+string function GetTeamAliveTurretCount_ReturnString( int team )
+{
+ int turretCount
+ foreach( entity turret in GetNPCArrayByClass( "npc_turret_mega" ) )
+ {
+ if( turret.GetTeam() == team && IsAlive( turret ) )
+ turretCount += 1
+ }
+
+ switch( turretCount )
+ {
+ case 1:
+ return "One"
+ case 2:
+ return "Two"
+ case 3:
+ return "Three"
+ case 4:
+ return "Four"
+ case 5:
+ return "Five"
+ case 6:
+ return "Six"
+ }
+
+ return ""
+}
+
+/////////////////////////////////////
+///// BatteryPort Functions End /////
+/////////////////////////////////////