Files
AsanoRema ffa1064063 Squashed commit of the following:
commit 428243f4a6
Author: AsanoRema <asanorema2036@gmail.com>
Date:   Sat Aug 29 13:43:13 2026 +0300

    Squashed commit of the following:

    commit 68e78fa464
    Author: AsanoRema <asanorema2036@gmail.com>
    Date:   Sat Aug 29 13:41:49 2026 +0300

        Updated .gitignore

commit 9f87122a34
Author: AsanoRema <asanorema2036@gmail.com>
Date:   Sat Aug 29 13:34:57 2026 +0300

    Added Dohya

commit f6a7547e84
Author: AsanoRema <asanorema2036@gmail.com>
Date:   Fri Aug 21 17:59:13 2026 +0300

    Fixed

commit b98901c366
Author: AsanoRema <asanorema2036@gmail.com>
Date:   Tue Aug 18 17:37:22 2026 +0300

    Added new particle Package

commit bb2f4a3b16
Author: AsanoRema <asanorema2036@gmail.com>
Date:   Tue Aug 18 17:17:48 2026 +0300

    Map mehanic ready to multiplayer

commit c01ff800f6
Author: AsanoRema <asanorema2036@gmail.com>
Date:   Tue Aug 18 16:16:19 2026 +0300

    Squashed commit of the following:

    commit 173bbb165c
    Author: henjkey <henjkey@gmail.com>
    Date:   Fri Aug 14 18:51:23 2026 +0500

        Mappp

    commit d559ab338a
    Author: henjkey <henjkey@gmail.com>
    Date:   Thu Aug 13 18:42:41 2026 +0500

        Map

        Controller
        Coordinates
        Target System
        Crosshair System
        Radar System
2026-08-29 13:45:55 +03:00

621 lines
22 KiB
C#

using System;
using Unity.Netcode;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Serialization;
[DisallowMultipleComponent]
[RequireComponent(typeof(NetworkObject))]
public sealed class MapController : NetworkBehaviour
{
[Header("Main References")]
[SerializeField] private MapCrosshairController crosshair;
[SerializeField] private MapCoordinateSystem coordinateSystem;
[SerializeField] private MapPlayer mapPlayer;
[SerializeField] private RoundGameLoopController roundGameLoop;
[SerializeField] private NetworkBaseHealth baseHealth;
[SerializeField] private MapEnemyBalanceConfig enemyBalance;
[Header("Target System")]
[SerializeField] private MapTargetController targetController;
[SerializeField] private Transform targetParent;
[Header("Radar System")]
[SerializeField] private MapRadarController radarController;
[FormerlySerializedAs("targetPrefab")]
[SerializeField] private MapTarget radarMarkPrefab;
[Header("Crosshair Objects")]
[SerializeField] private Transform relayX;
[SerializeField] private Transform relayY;
[SerializeField] private Transform crosshairVisual;
[Header("Shot Rules")]
[SerializeField, Min(0.01f)] private float hitRadius = 0.65f;
[SerializeField, Min(1)] private int shotDamage = 1;
[SerializeField] private MissileType pseudoMissileType = MissileType.Standard;
[FormerlySerializedAs("ignoreMissileTypeForPrototype")]
[SerializeField] private bool ignoreMissileTypeRequirements = true;
[Header("Network Events")]
[SerializeField] private UnityEvent<int> waveStarted;
[SerializeField] private UnityEvent<bool> shotResolved;
[SerializeField] private UnityEvent allTargetsResolved;
[SerializeField] private UnityEvent allTargetsDestroyed;
[Header("Shot Presentation")]
[SerializeField] private MapImpactMarkerView impactMarkerView;
private readonly NetworkVariable<Vector2> crosshairCoordinates = new(
new Vector2(5f, 5f), NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private readonly NetworkVariable<int> currentWave = new(
0, NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private readonly NetworkVariable<double> radarReadyAt = new(
0d, NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private readonly NetworkVariable<MapShotResultState> lastShotResult = new(
default, NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private readonly NetworkVariable<uint> resolvedSequence = new(
0, NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private readonly NetworkVariable<uint> destroyedSequence = new(
0, NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private readonly NetworkList<MapTargetNetworkState> activeTargets = new();
private readonly NetworkList<Vector2> radarMarks = new();
private float simulationAccumulator;
private int nextTargetId = 1;
private int initialTargetCount;
private int escapedTargetCount;
private bool waveIsActive;
private bool roundEventsBound;
public MapCoordinateSystem CoordinateSystem => coordinateSystem;
public Transform CrosshairVisual => crosshairVisual;
public Vector2 CrosshairCoordinates => crosshairCoordinates.Value;
public int CurrentWave => currentWave.Value;
public int ActiveTargetCount => activeTargets.Count;
public bool WaveIsActive => waveIsActive;
public MapEnemyBalanceConfig EnemyBalance => enemyBalance;
public float HitRadius => hitRadius;
public int ShotDamage => shotDamage;
public bool IgnoreMissileTypeRequirements => ignoreMissileTypeRequirements;
public double RadarCooldownRemaining => IsSpawned && NetworkManager != null
? Math.Max(0d, radarReadyAt.Value - NetworkManager.ServerTime.Time)
: 0d;
public event Action<int> WaveStarted;
public event Action<MapShotResultState> ShotResolved;
public event Action AllTargetsResolved;
public event Action AllTargetsDestroyed;
private void Awake() => ConfigurePresentation();
private void OnValidate()
{
hitRadius = Mathf.Max(0.01f, hitRadius);
shotDamage = Mathf.Max(1, shotDamage);
}
public override void OnNetworkSpawn()
{
activeTargets.OnListChanged += HandleTargetListChanged;
radarMarks.OnListChanged += HandleRadarListChanged;
crosshairCoordinates.OnValueChanged += HandleCrosshairChanged;
currentWave.OnValueChanged += HandleWaveChanged;
radarReadyAt.OnValueChanged += HandleRadarReadyAtChanged;
lastShotResult.OnValueChanged += HandleShotResultChanged;
resolvedSequence.OnValueChanged += HandleResolvedSequenceChanged;
destroyedSequence.OnValueChanged += HandleDestroyedSequenceChanged;
ConfigurePresentation();
BindRoundEvents();
ApplyPresentation();
if (IsServer && roundGameLoop != null &&
roundGameLoop.Phase == RoundPhase.WorkInProgress && currentWave.Value == 0)
StartNextWaveOnServer();
}
public override void OnNetworkDespawn()
{
activeTargets.OnListChanged -= HandleTargetListChanged;
radarMarks.OnListChanged -= HandleRadarListChanged;
crosshairCoordinates.OnValueChanged -= HandleCrosshairChanged;
currentWave.OnValueChanged -= HandleWaveChanged;
radarReadyAt.OnValueChanged -= HandleRadarReadyAtChanged;
lastShotResult.OnValueChanged -= HandleShotResultChanged;
resolvedSequence.OnValueChanged -= HandleResolvedSequenceChanged;
destroyedSequence.OnValueChanged -= HandleDestroyedSequenceChanged;
UnbindRoundEvents();
}
private void OnDestroy()
{
activeTargets.Dispose();
radarMarks.Dispose();
}
private void Update()
{
if (!roundEventsBound)
BindRoundEvents();
if (radarController != null)
radarController.SetCooldownRemaining((float)RadarCooldownRemaining);
if (!IsServer || !IsSpawned || !waveIsActive)
return;
float step = enemyBalance != null ? enemyBalance.SimulationStep : 0.1f;
simulationAccumulator += Time.deltaTime;
while (simulationAccumulator >= step)
{
simulationAccumulator -= step;
SimulateTargets(step);
}
}
public void SetHorizontalNormalized(float value)
{
Vector2 next = crosshairCoordinates.Value;
next.x = Mathf.Clamp01(value) * MapCoordinateSystem.CoordinateMaximum;
RequestCrosshairCoordinates(next);
}
public void SetVerticalNormalized(float value)
{
Vector2 next = crosshairCoordinates.Value;
next.y = Mathf.Clamp01(value) * MapCoordinateSystem.CoordinateMaximum;
RequestCrosshairCoordinates(next);
}
public void SetHorizontalAxis(float value) =>
SetHorizontalNormalized(Mathf.InverseLerp(-1f, 1f, value));
public void SetVerticalAxis(float value) =>
SetVerticalNormalized(Mathf.InverseLerp(-1f, 1f, value));
public Vector2 GetCrosshairCoordinates() => crosshairCoordinates.Value;
public void RequestPseudoFire()
{
if (!IsSpawned)
return;
if (IsServer)
ResolveShotOnServer(pseudoMissileType, ignoreMissileTypeRequirements);
else
RequestPseudoFireRpc();
}
public bool RequestShot(MissileType missileType)
{
if (!IsSpawned || !IsServer || missileType == MissileType.None)
return false;
ResolveShotOnServer(missileType, ignoreMissileTypeRequirements);
return true;
}
public void RequestRadarScan()
{
if (!IsSpawned)
return;
if (IsServer)
ScanRadarOnServer();
else
RequestRadarScanRpc();
}
public bool ActivateRadar()
{
if (!IsSpawned || RadarCooldownRemaining > 0d)
return false;
RequestRadarScan();
return true;
}
public void CreateAndPlaceTargets() => StartNextWave();
public bool StartNextWave()
{
if (!IsSpawned)
return false;
if (IsServer)
return StartNextWaveOnServer();
RequestStartNextWaveRpc();
return true;
}
public bool TryGetNearestTarget(out MapTarget target, out float distance)
{
if (crosshair != null)
return crosshair.TryGetNearestTarget(out target, out distance);
target = null;
distance = -1f;
return false;
}
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
private void SetCrosshairCoordinatesRpc(Vector2 coordinates) =>
SetCrosshairCoordinatesOnServer(coordinates);
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
private void RequestPseudoFireRpc() =>
ResolveShotOnServer(pseudoMissileType, ignoreMissileTypeRequirements);
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
private void RequestRadarScanRpc() => ScanRadarOnServer();
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
private void RequestStartNextWaveRpc() => StartNextWaveOnServer();
private void RequestCrosshairCoordinates(Vector2 coordinates)
{
if (!IsSpawned)
return;
if (IsServer)
SetCrosshairCoordinatesOnServer(coordinates);
else
SetCrosshairCoordinatesRpc(coordinates);
}
private void SetCrosshairCoordinatesOnServer(Vector2 coordinates)
{
if (!IsServer)
return;
crosshairCoordinates.Value = coordinateSystem != null
? coordinateSystem.ClampCoordinates(coordinates)
: new Vector2(
Mathf.Clamp(coordinates.x, 0f, MapCoordinateSystem.CoordinateMaximum),
Mathf.Clamp(coordinates.y, 0f, MapCoordinateSystem.CoordinateMaximum));
}
private bool StartNextWaveOnServer()
{
if (!IsServer || targetController == null || enemyBalance == null ||
!targetController.HasValidConfiguration())
{
Debug.LogWarning(
"[Map] Cannot start target wave: map, enemy balance or target configs are incomplete.",
this);
return false;
}
activeTargets.Clear();
radarMarks.Clear();
currentWave.Value++;
escapedTargetCount = 0;
initialTargetCount = 0;
waveIsActive = true;
simulationAccumulator = 0f;
for (int index = 0; index < enemyBalance.TargetsPerWave; index++)
{
int configIndex = targetController.GetRandomValidConfigIndex();
TargetConfig config = targetController.GetConfig(configIndex);
if (config == null)
continue;
Vector2 coordinates = CreateSpawnCoordinates();
activeTargets.Add(new MapTargetNetworkState(
nextTargetId++, configIndex, coordinates,
enemyBalance.GetHealth(config),
enemyBalance.GetAttackDelay(config)));
initialTargetCount++;
}
Debug.Log($"[Map] Wave {currentWave.Value} started with {initialTargetCount} targets.", this);
if (initialTargetCount == 0)
CompleteWaveOnServer();
return true;
}
private void SimulateTargets(float deltaTime)
{
for (int index = activeTargets.Count - 1; index >= 0; index--)
{
MapTargetNetworkState state = activeTargets[index];
TargetConfig config = targetController.GetConfig(state.ConfigIndex);
if (config == null)
{
ResolveEscapedTarget(index, state, null);
continue;
}
bool isAirTarget = config.TargetPrefab is AirTarget;
float movementSpeed = enemyBalance.GetMovementSpeed(config);
if (isAirTarget && mapPlayer != null && movementSpeed > 0f)
{
Vector2 destination = mapPlayer.MapCoordinates;
state.Coordinates = Vector2.MoveTowards(
state.Coordinates, destination, movementSpeed * deltaTime);
if (Vector2.Distance(state.Coordinates, destination) <= config.StoppingDistance)
{
ResolveEscapedTarget(index, state, config);
continue;
}
}
else
{
state.AttackTimeRemaining = Mathf.Max(
0f, state.AttackTimeRemaining - deltaTime);
if (state.AttackTimeRemaining <= 0f)
{
ResolveEscapedTarget(index, state, config);
continue;
}
}
activeTargets[index] = state;
}
TryCompleteWaveOnServer();
}
private void ResolveEscapedTarget(int index, MapTargetNetworkState target, TargetConfig config)
{
int damage = enemyBalance != null
? enemyBalance.GetBaseDamage(config)
: config != null ? config.BaseDamage : 1;
escapedTargetCount++;
activeTargets.RemoveAt(index);
if (baseHealth != null)
baseHealth.ApplyDamage(damage);
Debug.Log($"[Map] Target {target.TargetId} reached the base. Base damage: {damage}.", this);
}
private void ResolveShotOnServer(MissileType missileType, bool ignoreMissileType)
{
if (!IsServer || !waveIsActive)
{
PublishShotResult(
false, -1, crosshairCoordinates.Value, -1f, missileType);
Debug.Log(
$"[Map] MISS with {missileType}: there is no active target wave.",
this);
return;
}
int nearestIndex = -1;
float nearestDistance = float.PositiveInfinity;
for (int index = 0; index < activeTargets.Count; index++)
{
float distance = Vector2.Distance(
crosshairCoordinates.Value, activeTargets[index].Coordinates);
if (distance >= nearestDistance)
continue;
nearestDistance = distance;
nearestIndex = index;
}
if (nearestIndex >= 0 && nearestDistance <= hitRadius)
{
MapTargetNetworkState target = activeTargets[nearestIndex];
TargetConfig config = targetController.GetConfig(target.ConfigIndex);
bool missileMatches = ignoreMissileType || config == null ||
config.RequiredMissileType == MissileType.None ||
config.RequiredMissileType == missileType;
if (!missileMatches)
{
PublishShotResult(
false, target.TargetId, crosshairCoordinates.Value,
nearestDistance, missileType);
Debug.Log(
$"[Map] MISS target {target.TargetId}: {missileType} does not " +
$"match required {config.RequiredMissileType}. Distance " +
$"{nearestDistance:0.00} is inside hit radius {hitRadius:0.00}.",
this);
return;
}
target.Health -= shotDamage;
if (target.Health <= 0)
activeTargets.RemoveAt(nearestIndex);
else
activeTargets[nearestIndex] = target;
PublishShotResult(
true, target.TargetId, target.Coordinates,
nearestDistance, missileType);
Debug.Log(
$"[Map] HIT target {target.TargetId} with {missileType} " +
$"at {nearestDistance:0.00} map units. Damage: {shotDamage}.",
this);
TryCompleteWaveOnServer();
return;
}
int missedTargetId = nearestIndex >= 0 ? activeTargets[nearestIndex].TargetId : -1;
PublishShotResult(
false, missedTargetId, crosshairCoordinates.Value,
nearestDistance, missileType);
if (nearestIndex < 0)
{
Debug.Log($"[Map] MISS with {missileType}: no active targets.", this);
return;
}
Debug.Log(
$"[Map] MISS target {activeTargets[nearestIndex].TargetId}: distance " +
$"{nearestDistance:0.00} exceeds hit radius {hitRadius:0.00}.",
this);
}
private void PublishShotResult(
bool hit,
int targetId,
Vector2 coordinates,
float distance,
MissileType missileType)
{
lastShotResult.Value = new MapShotResultState(
lastShotResult.Value.Sequence + 1, hit, targetId,
coordinates,
float.IsPositiveInfinity(distance) ? -1f : distance,
missileType);
}
private void ScanRadarOnServer()
{
if (!IsServer || radarController == null || NetworkManager == null ||
NetworkManager.ServerTime.Time < radarReadyAt.Value)
return;
radarMarks.Clear();
foreach (MapTargetNetworkState target in activeTargets)
radarMarks.Add(target.Coordinates);
radarReadyAt.Value = NetworkManager.ServerTime.Time + radarController.Cooldown;
Debug.Log($"[Map] Radar scan found {radarMarks.Count} targets.", this);
}
private void TryCompleteWaveOnServer()
{
if (waveIsActive && activeTargets.Count == 0)
CompleteWaveOnServer();
}
private void CompleteWaveOnServer()
{
waveIsActive = false;
resolvedSequence.Value++;
if (initialTargetCount > 0 && escapedTargetCount == 0)
destroyedSequence.Value++;
Debug.Log(
$"[Map] Wave {currentWave.Value} resolved. " +
$"Destroyed: {Mathf.Max(0, initialTargetCount - escapedTargetCount)}, " +
$"escaped: {escapedTargetCount}.", this);
}
private Vector2 CreateSpawnCoordinates()
{
Vector2 baseCoordinates = mapPlayer != null
? mapPlayer.MapCoordinates
: new Vector2(5f, 5f);
Vector2 farthestCandidate = Vector2.zero;
float farthestDistance = -1f;
for (int attempt = 0; attempt < enemyBalance.SpawnPositionAttempts; attempt++)
{
Vector2 candidate = new(
UnityEngine.Random.Range(0f, MapCoordinateSystem.CoordinateMaximum),
UnityEngine.Random.Range(0f, MapCoordinateSystem.CoordinateMaximum));
float distance = Vector2.Distance(candidate, baseCoordinates);
if (distance >= enemyBalance.MinimumSpawnDistanceFromBase)
return candidate;
if (distance > farthestDistance)
{
farthestDistance = distance;
farthestCandidate = candidate;
}
}
return farthestCandidate;
}
private void ConfigurePresentation()
{
mapPlayer?.Configure(coordinateSystem);
targetController?.Configure(coordinateSystem, targetParent, mapPlayer);
radarController?.Configure(coordinateSystem, radarMarkPrefab);
impactMarkerView?.Configure(coordinateSystem);
crosshair?.Configure(
coordinateSystem, relayX, relayY, crosshairVisual, targetController);
}
private void ApplyPresentation()
{
crosshair?.SetCoordinates(crosshairCoordinates.Value);
targetController?.ApplyNetworkTargets(CreateTargetSnapshot(), currentWave.Value);
radarController?.ApplyRadarMarks(CreateRadarSnapshot());
if (radarController != null)
radarController.SetCooldownRemaining((float)RadarCooldownRemaining);
}
private void BindRoundEvents()
{
if (roundEventsBound)
return;
if (roundGameLoop == null)
roundGameLoop = FindFirstObjectByType<RoundGameLoopController>();
if (roundGameLoop == null || !roundGameLoop.IsSpawned)
return;
roundGameLoop.PhaseChanged += HandleRoundPhaseChanged;
roundEventsBound = true;
}
private void UnbindRoundEvents()
{
if (!roundEventsBound || roundGameLoop == null)
return;
roundGameLoop.PhaseChanged -= HandleRoundPhaseChanged;
roundEventsBound = false;
}
private void HandleRoundPhaseChanged(RoundPhase phase)
{
if (IsServer && phase == RoundPhase.WorkInProgress && !waveIsActive)
StartNextWaveOnServer();
}
private void HandleTargetListChanged(NetworkListEvent<MapTargetNetworkState> _) =>
targetController?.ApplyNetworkTargets(CreateTargetSnapshot(), currentWave.Value);
private void HandleRadarListChanged(NetworkListEvent<Vector2> _) =>
radarController?.ApplyRadarMarks(CreateRadarSnapshot());
private void HandleCrosshairChanged(Vector2 _, Vector2 current) =>
crosshair?.SetCoordinates(current);
private void HandleWaveChanged(int _, int current)
{
targetController?.ApplyNetworkTargets(CreateTargetSnapshot(), current);
WaveStarted?.Invoke(current);
waveStarted?.Invoke(current);
}
private void HandleRadarReadyAtChanged(double _, double __)
{
if (radarController != null)
radarController.SetCooldownRemaining((float)RadarCooldownRemaining);
}
private void HandleShotResultChanged(MapShotResultState previous, MapShotResultState current)
{
if (current.Sequence == 0 || current.Sequence == previous.Sequence)
return;
ShotResolved?.Invoke(current);
shotResolved?.Invoke(current.Hit);
impactMarkerView?.Show(current);
}
private void HandleResolvedSequenceChanged(uint previous, uint current)
{
if (current == previous)
return;
AllTargetsResolved?.Invoke();
allTargetsResolved?.Invoke();
}
private void HandleDestroyedSequenceChanged(uint previous, uint current)
{
if (current == previous)
return;
AllTargetsDestroyed?.Invoke();
allTargetsDestroyed?.Invoke();
}
private MapTargetNetworkState[] CreateTargetSnapshot()
{
MapTargetNetworkState[] snapshot = new MapTargetNetworkState[activeTargets.Count];
for (int index = 0; index < snapshot.Length; index++)
snapshot[index] = activeTargets[index];
return snapshot;
}
private Vector2[] CreateRadarSnapshot()
{
Vector2[] snapshot = new Vector2[radarMarks.Count];
for (int index = 0; index < snapshot.Length; index++)
snapshot[index] = radarMarks[index];
return snapshot;
}
}