DIFF

ScriptedSandstormController.c

/*
	This class implements scripted functionality over the Sandstorm controller.
	Note that certain functionality may be limited to server-only or client-only!
*/
class ScriptedSandstormController : SandstormController
{
	private static const float MAX_SURFACE_DISTANCE = 2.0;
	private static const float MAX_SURFACE_DISTANCE_INVERSE = 1.0 / MAX_SURFACE_DISTANCE;
	private const float SURFACE_TRANSITION_DISTANCE = 10.0;
	private static const float SURFACE_TRANSITION_FRACTION = 1.0 / MAX_SURFACE_DISTANCE;

	private const float SURFACE_INTENSITY_MIN = 0;
	private const float SURFACE_INTENSITY_MAX = 1.0;
	
	private static const float INTERIOR_TRANSITION_DURATION = 1.5; // 1.5 seconds transition
	private const float INTERIOR_TRANSITION_MIN = 0;
	private const float INTERIOR_TRANSITION_MAX = 1.0;
	private static const float INTERIOR_TRANSITION_DURATON_FRACTION = 1.0 / INTERIOR_TRANSITION_DURATION;
	
	private const ref array<typename> EXCLUDED_SHELTER_TYPES = {CrashBase, Plant};
	
	private ref map<string, ref PlayerSandstormData> m_PlayerData = new map<string, ref PlayerSandstormData>();
	
	private ref PPERequester_SandstormEffect m_PPESandstormRequester;
	private float m_StormEffectTimer;
	
	protected ref ScriptInvoker m_OnSandstormStartSI = new ScriptInvoker();
	protected ref ScriptInvoker m_OnSandstormStopSI = new ScriptInvoker();
	
	private void ScriptedSandstormController()  { /*Disallowed in scripts, leave to native.*/ }
	private void ~ScriptedSandstormController() { /*Disallowed in scripts, leave to native.*/ }
	
	private PlayerSandstormData GetPlayerData(string playerId)
	{
		if (!m_PlayerData.Contains(playerId))
		{
			m_PlayerData.Set(playerId, new PlayerSandstormData());
		}
		return m_PlayerData.Get(playerId);
	}
	
	private PlayerSandstormData GetPlayerSandstormData(PlayerBase player)
	{
		if (!player)
			return NULL;
		
		PlayerSandstormData playerData;
		PlayerIdentity identity = player.GetIdentity();
		if (g_Game.IsMultiplayer() || g_Game.IsDedicatedServer())
		{
			if (!identity)
				return NULL;
			
			playerData = GetPlayerData(identity.GetId());
		}
		else // singleplayer override
		{
			playerData = GetPlayerData("-1");
		}
		
		if (!playerData)
		{
			if (g_Game.IsMultiplayer() || g_Game.IsDedicatedServer())
			{
				ErrorEx("Could not get players sandstorm data! Player UID= [" + identity.GetId() + "]", ErrorExSeverity.ERROR);
			}
			else
			{
				ErrorEx("Could not get players sandstorm data! Player UID=[-1]", ErrorExSeverity.ERROR);
			}
		}
		
		return playerData;
	}
	
	void CleanupDisconnectedPlayer(string playerId)
	{
		if (m_PlayerData.Contains(playerId))
		{
			m_PlayerData.Remove(playerId);
		}
	}
	
	void CheckConnectedPlayers()
	{
		array<Man> players = new array<Man>();
		array<string> playerUIDs = new array<string>();
		
		if (g_Game.IsMultiplayer() || g_Game.IsDedicatedServer())
		{
			g_Game.GetPlayers(players);
			foreach(Man player: players)
			{
				if (!player)
					continue;
				
				PlayerIdentity identity = player.GetIdentity();
				if (!identity)
					continue;
				
				string playerUIDToAdd = identity.GetId();
				if (playerUIDs.Find(playerUIDToAdd) == -1)
					playerUIDs.Insert(playerUIDToAdd);
			}
		}
		else // singleplayer override
		{
			playerUIDs.Insert("-1");
		}
		
		//! Check if we have any PlayerSandstormData elements catched for players that are no longer connected.
		for (int i = m_PlayerData.Count() - 1; i >=0; --i)
		{
			string playerUIDToRemove = m_PlayerData.GetKey(i);
			if (playerUIDs.Find(playerUIDToRemove) == -1)
			{
				m_PlayerData.RemoveElement(i);
			}
		}
		
		playerUIDs.Clear();
	}
	
	// Lerps 'intensity' between its outside value and 0 based on whether the player is
	// currently in a shielding state and how far through the transition they are.
	//   progress = 0  -> no change (still fully outside / just entered)
	//   progress = 1  -> fully sheltered (intensity = 0) when entering,
	//                    or fully exposed (intensity = outside) when leaving.
	protected void ApplyShelterFade(inout float intensity, bool isCurrentlySheltered, float transitionProgress)
	{
		if (isCurrentlySheltered)
		{
			intensity = Math.Lerp(intensity, 0.0, transitionProgress);
		}
		else
		{
			intensity = Math.Lerp(0.0, intensity, transitionProgress);
		}
	}
	
	// Get local sandstorm intensity value for player
	override float GetIntensityForPlayer(Man player)
	{
		super.GetIntensityForPlayer(player);
		
		if (!player)
			return 0.0;
		
		PlayerBase playerPB = PlayerBase.Cast(player);
		if (!playerPB)
			return 0.0;
				
		vector headPosition = MiscGameplayFunctions.GetPlayerHeadPosition(playerPB);
		float baseIntensity = GetIntensity(headPosition);
		if (baseIntensity <= 0.0)
			return 0.0;
		
		// Get player-specific data
		PlayerSandstormData playerData = GetPlayerSandstormData(playerPB);
		if (!playerData)
			return 0.0;
		
		// Apply intensity modifiers in sequence - each can modify the result
		float modifiedIntensity = baseIntensity;

		// Apply underground modifier first (highest priority)
		ApplyUndergroundModifier(modifiedIntensity, playerPB, playerData);
		
		// If not underground, apply building modifier
		if (modifiedIntensity > 0.0)
			ApplyBuildingCheckModifier(modifiedIntensity, playerPB, playerData);
		
		// If not underground or in building, apply vehicle modifier
		if (modifiedIntensity > 0.0)
			ApplyVehicleModifier(modifiedIntensity, playerPB, playerData);
		
		return modifiedIntensity;
	}
	
	private void ApplyUndergroundModifier(inout float intensity, PlayerBase player, PlayerSandstormData playerData)
	{
		HumanCommandVehicle vehicleCommand = player.GetCommand_Vehicle();
		if (vehicleCommand)
			return;

		bool isUnderground = false;
		EUndergroundPresence presence = player.GetUndergroundPresence();
		if (presence == EUndergroundPresence.TRANSITIONING || presence == EUndergroundPresence.FULL)
			isUnderground = true;
		
		float currentTime = g_Game.GetTime();
		float elapsedTime = currentTime - playerData.m_UndergroundTransitionTimer;
		float transitionProgress = Math.Clamp(elapsedTime * INTERIOR_TRANSITION_DURATON_FRACTION, INTERIOR_TRANSITION_MIN, INTERIOR_TRANSITION_MAX);
		
		ApplyShelterFade(intensity, isUnderground, transitionProgress);
		ApplySurfaceDistanceModifier(intensity, player);
	}
	
	private void ApplySurfaceDistanceModifier(inout float intensity, PlayerBase player)
	{
		vector playerPos = player.GetPosition();
		//float surfaceHeight = GetInterpolatedSurfaceY(playerPos[0], playerPos[2]);
		float surfaceHeight = g_Game.SurfaceY(playerPos[0], playerPos[2]);
		
		if (playerPos[1] < surfaceHeight)
		{
			float depthBelowSurface = surfaceHeight - playerPos[1];
			float fade = Math.Clamp(depthBelowSurface * MAX_SURFACE_DISTANCE_INVERSE, 0, 1.0);
			intensity *= (1.0 - fade);
		}
	}
		
	private void ApplyBuildingCheckModifier(inout float intensity, PlayerBase player, PlayerSandstormData playerData)
	{
		if (!player)
			return;
		
		if (intensity <= 0)
			return;
		
		Object roofHitObj;
		bool isUnderRoof = MiscGameplayFunctions.IsUnder(player, EXCLUDED_SHELTER_TYPES, roofHitObj, GameConstants.ROOF_CHECK_RAYCAST_DIST, ObjIntersectIFire);
		bool isInInterior = player.IsSoundInsideBuilding();
		
		//ErrorEx(string.Format("Under roof=%1 | In Interior=%2", isUnderRoof, isInInterior), ErrorExSeverity.INFO);
		
		// Determine if player is currently "inside"
		bool isCurrentlyInside = (isUnderRoof && isInInterior);
		bool wasInside = (playerData.GetWasUnderRoof() && playerData.GetWasInInterior());
		float currentTime = g_Game.GetTime();
		
		// Initialize transition ONLY if building state actually changed
		if (isCurrentlyInside != wasInside)
		{
			playerData.m_InteriorTransitionTimer = currentTime; // Only reset timer on state change
			playerData.SetWasUnderRoof(isUnderRoof);
			playerData.SetWasInInterior(isInInterior);
		}
		
		// If not inside and never was inside, no modification needed
		if (!isCurrentlyInside && !wasInside)
			return;
		
		float elapsedTime = currentTime - playerData.m_InteriorTransitionTimer;
		float transitionProgress = Math.Clamp(elapsedTime * INTERIOR_TRANSITION_DURATON_FRACTION, INTERIOR_TRANSITION_MIN, INTERIOR_TRANSITION_MAX);
		
		ApplyShelterFade(intensity, isCurrentlyInside, transitionProgress);
	}
	
	private void ApplyVehicleModifier(inout float intensity, PlayerBase player, PlayerSandstormData playerData)
	{
		if (!player)
			return;
		
		if (intensity <= 0)
			return;
		
		HumanCommandVehicle vehicleCommand = player.GetCommand_Vehicle();
		if (!vehicleCommand)
			return;

		Transport transport = vehicleCommand.GetTransport();

		// no transport or not in Car transport
		if (!transport || transport && !transport.IsAnyInherited({Car}))
			return;
		
		bool isInVehicle = transport != null;
		float currentTime = g_Game.GetTime();
		
		// Initialize transition if vehicle state changed
		if (isInVehicle != playerData.GetWasInVehicle())
		{
			playerData.m_VehicleTransitionTimer = currentTime;
			playerData.SetWasInVehicle(isInVehicle);
		}
		
		float elapsedTime = currentTime - playerData.m_VehicleTransitionTimer;
		float transitionProgress = Math.Clamp(elapsedTime * INTERIOR_TRANSITION_DURATON_FRACTION, INTERIOR_TRANSITION_MIN, INTERIOR_TRANSITION_MAX);
		
		ApplyShelterFade(intensity, isInVehicle, transitionProgress);
	}
	
	// Helper function to interpolate SurfaceY values
	/*private float GetInterpolatedSurfaceY(float x, float z, float sampleRadius = 2.0)
	{
		float totalHeight = 0;
		int validSamples = 0;
		
		// Sample in a 3x3 grid around the point
		for (int i = -1; i <= 1; i++)
		{
			for (int j = -1; j <= 1; j++)
			{
				float sampleX = x + (i * sampleRadius);
				float sampleZ = z + (j * sampleRadius);
				float height = g_Game.SurfaceY(sampleX, sampleZ);
				
				totalHeight += height;
				validSamples++;
			}
		}
		
		return totalHeight / validSamples;
	}*/
	
	/*!
		\see SandstormController::OnStart
	*/
	override void OnStart(bool continuedSession, bool calledByWeather = false)
	{
		super.OnStart(calledByWeather, continuedSession);
		
		// Server is not interested in local effects like post-processing.
		if (!g_Game || g_Game.IsDedicatedServer())
			return;
		
		// Clients may start their post processing effects as soon as the sandstorm starts - this may also be during join in progress so set the intensity accordingly as well.
		m_PPESandstormRequester = PPERequester_SandstormEffect.Cast(PPERequesterBank.GetRequester(PPERequesterBank.REQ_SANDSTORMEFFECT));
		
		PlayerBase player = PlayerBase.Cast(g_Game.GetPlayer());
		if (player && m_PPESandstormRequester)
		{
			float localIntensity = GetIntensityForPlayer(player);
			m_PPESandstormRequester.SetTargetIntensity(localIntensity);
			m_PPESandstormRequester.Start();
		}
		
		if (m_OnSandstormStartSI)
			m_OnSandstormStartSI.Invoke();
	}
	
	/*!
		\see SandstormController::OnUpdate
	*/
	override void OnUpdate(float deltaTime)
	{
		super.OnUpdate(deltaTime);
		
		// Server is not interested in local effects like post-processing.
		if (!g_Game || g_Game.IsDedicatedServer())
			return;
		
		// Clients may update their post-processing effect and its properties
		// on a per-tick basis, to represent the actual state closely
		PlayerBase player = PlayerBase.Cast(g_Game.GetPlayer());
		if (player && m_PPESandstormRequester)
		{
			float localIntensity = GetIntensityForPlayer(player);
			m_PPESandstormRequester.SetTargetIntensity(localIntensity);
		}
	}
	
	/*!
		\see SandstormController::OnStop
	*/
	override void OnStop(bool calledByWeather = false)
	{
		super.OnStop(calledByWeather);
		
		// Server is not interested in local effects like post-processing.
		if (!g_Game || g_Game.IsDedicatedServer())
			return;
		
		// Clients may reset their post processing to default state
		if (m_PPESandstormRequester)
		{
			m_PPESandstormRequester.SetTargetIntensity(0);
			m_PPESandstormRequester.Stop();
		}
		
		if (m_OnSandstormStopSI)
			m_OnSandstormStopSI.Invoke();
	}
	
	ScriptInvoker GetSandstormStartSI()
	{
		return m_OnSandstormStartSI;
	}
	
	ScriptInvoker GetSandstormStopSI()
	{
		return m_OnSandstormStopSI;
	}
}