first commit

This commit is contained in:
2026-06-04 15:09:10 +03:00
commit c94f0cc648
62 changed files with 9224 additions and 0 deletions
+313
View File
@@ -0,0 +1,313 @@
using System;
using UnityEngine;
[RequireComponent(typeof(SphereCollider))]
public class CaptureZone : MonoBehaviour
{
public enum Owner
{
Neutral,
Player,
Enemy
}
public enum CaptureState
{
Neutral,
CapturingByPlayer,
CapturedByPlayer,
CapturingByEnemy,
CapturedByEnemy,
Contested
}
[Header("Identity")]
public string zoneName = "Zone";
[Header("Capture")]
public Owner currentOwner = Owner.Neutral;
[Range(0f, 1f)] public float captureProgress = 0.5f;
public float captureTime = 5f;
public int playersInside;
public int enemiesInside;
public int pointsPerSecond = 5;
[Header("Visuals")]
public Renderer zoneRenderer;
public Color neutralColor = new Color(0.55f, 0.55f, 0.55f, 0.45f);
public Color playerColor = new Color(0.1f, 0.35f, 1f, 0.55f);
public Color enemyColor = new Color(1f, 0.12f, 0.08f, 0.55f);
public Color contestedColor = new Color(1f, 0.82f, 0.05f, 0.65f);
public event Action<CaptureZone> onCapturedByPlayer;
public event Action<CaptureZone> onCapturedByEnemy;
private Owner lastNotifiedOwner;
private Vector3 baseScale;
private float pulseTimer;
private float scoreTimer;
private void Awake()
{
SphereCollider trigger = GetComponent<SphereCollider>();
trigger.isTrigger = true;
lastNotifiedOwner = currentOwner;
baseScale = transform.localScale;
ApplyOwnerProgress(currentOwner);
UpdateVisual();
}
private void Update()
{
UpdateCaptureProgress();
TickScoreIncome();
UpdateVisual();
UpdateCapturePulse();
}
private void OnTriggerEnter(Collider other)
{
if (other.CompareTag("Player"))
{
playersInside++;
}
else if (other.CompareTag("Enemy"))
{
enemiesInside++;
}
}
private void OnTriggerExit(Collider other)
{
if (other.CompareTag("Player"))
{
playersInside = Mathf.Max(0, playersInside - 1);
}
else if (other.CompareTag("Enemy"))
{
enemiesInside = Mathf.Max(0, enemiesInside - 1);
}
}
public float GetProgress()
{
return captureProgress;
}
public Owner GetOwner()
{
return currentOwner;
}
public string GetStateLabel()
{
switch (GetCaptureState())
{
case CaptureState.CapturingByPlayer:
return "Capturing by Player";
case CaptureState.CapturedByPlayer:
return "Player";
case CaptureState.CapturingByEnemy:
return "Capturing by Enemy";
case CaptureState.CapturedByEnemy:
return "Enemy";
case CaptureState.Contested:
return "Contested";
default:
return "Neutral";
}
}
public CaptureState GetCaptureState()
{
bool hasPlayer = playersInside > 0;
bool hasEnemy = enemiesInside > 0;
if (hasPlayer && hasEnemy)
{
return CaptureState.Contested;
}
if (hasPlayer && currentOwner != Owner.Player)
{
return CaptureState.CapturingByPlayer;
}
if (hasEnemy && currentOwner != Owner.Enemy)
{
return CaptureState.CapturingByEnemy;
}
if (currentOwner == Owner.Player)
{
return CaptureState.CapturedByPlayer;
}
if (currentOwner == Owner.Enemy)
{
return CaptureState.CapturedByEnemy;
}
return CaptureState.Neutral;
}
public void ForceSetOwner(Owner owner)
{
SetOwner(owner, true);
}
private void UpdateCaptureProgress()
{
bool hasPlayer = playersInside > 0;
bool hasEnemy = enemiesInside > 0;
// Спорная зона замораживает прогресс, чтобы игрок видел механику contest.
if (hasPlayer == hasEnemy)
{
return;
}
float step = Time.deltaTime / Mathf.Max(0.1f, captureTime);
if (hasPlayer)
{
captureProgress = Mathf.MoveTowards(captureProgress, 1f, step);
if (Mathf.Approximately(captureProgress, 1f))
{
SetOwner(Owner.Player, false);
}
}
else if (hasEnemy)
{
captureProgress = Mathf.MoveTowards(captureProgress, 0f, step);
if (Mathf.Approximately(captureProgress, 0f))
{
SetOwner(Owner.Enemy, false);
}
}
}
private void TickScoreIncome()
{
if (currentOwner == Owner.Neutral || ResourceManager.Instance == null)
{
scoreTimer = 0f;
return;
}
scoreTimer += Time.deltaTime;
if (scoreTimer < 1f)
{
return;
}
int ticks = Mathf.FloorToInt(scoreTimer);
scoreTimer -= ticks;
int amount = pointsPerSecond * ticks;
if (currentOwner == Owner.Player)
{
ResourceManager.Instance.AddPlayerPoints(amount);
}
else if (currentOwner == Owner.Enemy)
{
ResourceManager.Instance.AddEnemyPoints(amount);
}
}
private void SetOwner(Owner owner, bool forceNotify)
{
if (currentOwner == owner && !forceNotify)
{
return;
}
Owner previousOwner = currentOwner;
currentOwner = owner;
ApplyOwnerProgress(owner);
pulseTimer = 0.25f;
if (previousOwner == owner && !forceNotify)
{
return;
}
if (lastNotifiedOwner != currentOwner || forceNotify)
{
lastNotifiedOwner = currentOwner;
if (currentOwner == Owner.Player)
{
onCapturedByPlayer?.Invoke(this);
}
else if (currentOwner == Owner.Enemy)
{
onCapturedByEnemy?.Invoke(this);
}
}
}
private void ApplyOwnerProgress(Owner owner)
{
if (owner == Owner.Player)
{
captureProgress = 1f;
}
else if (owner == Owner.Enemy)
{
captureProgress = 0f;
}
else if (Mathf.Approximately(captureProgress, 0f) || Mathf.Approximately(captureProgress, 1f))
{
captureProgress = 0.5f;
}
}
private void UpdateVisual()
{
if (zoneRenderer == null)
{
return;
}
Color color = GetVisualColor();
zoneRenderer.material.color = color;
}
private Color GetVisualColor()
{
CaptureState state = GetCaptureState();
if (state == CaptureState.Contested || state == CaptureState.CapturingByPlayer || state == CaptureState.CapturingByEnemy)
{
float pulse = Mathf.Lerp(0.75f, 1f, Mathf.PingPong(Time.time * 3f, 1f));
return contestedColor * pulse;
}
if (currentOwner == Owner.Player)
{
return playerColor;
}
if (currentOwner == Owner.Enemy)
{
return enemyColor;
}
return neutralColor;
}
private void UpdateCapturePulse()
{
if (pulseTimer <= 0f)
{
transform.localScale = baseScale;
return;
}
pulseTimer -= Time.deltaTime;
float t = Mathf.Clamp01(pulseTimer / 0.25f);
float scale = 1f + Mathf.Sin(t * Mathf.PI) * 0.12f;
transform.localScale = baseScale * scale;
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f3db71fa977ba6416b4d4c8af7de96a0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+164
View File
@@ -0,0 +1,164 @@
using UnityEngine;
using UnityEngine.AI;
[RequireComponent(typeof(CharacterController))]
public class EnemyAI : MonoBehaviour
{
public CaptureZone[] zones;
public float moveSpeed = 3.5f;
public float retargetInterval = 2f;
public float stoppingDistance = 0.7f;
public float randomFactor = 4f;
private CharacterController characterController;
private NavMeshAgent navMeshAgent;
private CaptureZone targetZone;
private Vector3 targetOffset;
private float retargetTimer;
private Vector3 verticalVelocity;
private void Awake()
{
characterController = GetComponent<CharacterController>();
navMeshAgent = GetComponent<NavMeshAgent>();
if (navMeshAgent != null)
{
navMeshAgent.speed = moveSpeed;
navMeshAgent.stoppingDistance = stoppingDistance;
}
}
private void Start()
{
ChooseTarget();
}
private void Update()
{
retargetTimer -= Time.deltaTime;
if (targetZone == null || targetZone.GetOwner() == CaptureZone.Owner.Enemy || retargetTimer <= 0f)
{
ChooseTarget();
}
MoveToTarget();
}
private void ChooseTarget()
{
retargetTimer = retargetInterval + Random.Range(0f, 0.75f);
targetZone = null;
CaptureZone bestPlayerZone = FindBestZone(CaptureZone.Owner.Player);
if (bestPlayerZone != null)
{
SetTarget(bestPlayerZone);
return;
}
CaptureZone bestNeutralZone = FindBestZone(CaptureZone.Owner.Neutral);
if (bestNeutralZone != null)
{
SetTarget(bestNeutralZone);
}
}
private CaptureZone FindBestZone(CaptureZone.Owner owner)
{
if (zones == null || zones.Length == 0)
{
return null;
}
CaptureZone best = null;
float bestScore = float.MaxValue;
for (int i = 0; i < zones.Length; i++)
{
CaptureZone zone = zones[i];
if (zone == null || zone.GetOwner() != owner)
{
continue;
}
float distance = Vector3.Distance(transform.position, zone.transform.position);
float score = distance + Random.Range(0f, randomFactor);
if (score < bestScore)
{
bestScore = score;
best = zone;
}
}
return best;
}
private void SetTarget(CaptureZone zone)
{
targetZone = zone;
Vector2 randomCircle = Random.insideUnitCircle * 1.5f;
targetOffset = new Vector3(randomCircle.x, 0f, randomCircle.y);
if (CanUseNavMeshAgent())
{
navMeshAgent.SetDestination(GetTargetPosition());
}
}
private void MoveToTarget()
{
if (targetZone == null)
{
return;
}
Vector3 targetPosition = GetTargetPosition();
if (CanUseNavMeshAgent())
{
if (!navMeshAgent.hasPath || Vector3.Distance(navMeshAgent.destination, targetPosition) > 0.5f)
{
navMeshAgent.SetDestination(targetPosition);
}
return;
}
Vector3 direction = targetPosition - transform.position;
direction.y = 0f;
if (direction.magnitude <= stoppingDistance)
{
return;
}
Vector3 horizontalMove = direction.normalized * moveSpeed;
if (characterController.isGrounded && verticalVelocity.y < 0f)
{
verticalVelocity.y = -1f;
}
verticalVelocity.y += Physics.gravity.y * Time.deltaTime;
characterController.Move((horizontalMove + verticalVelocity) * Time.deltaTime);
if (horizontalMove.sqrMagnitude > 0.001f)
{
transform.rotation = Quaternion.LookRotation(horizontalMove.normalized, Vector3.up);
}
}
private Vector3 GetTargetPosition()
{
return targetZone.transform.position + targetOffset;
}
private bool CanUseNavMeshAgent()
{
return navMeshAgent != null &&
navMeshAgent.enabled &&
navMeshAgent.isActiveAndEnabled &&
navMeshAgent.isOnNavMesh;
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 84755881e5af5195ea4faf7db39fdb29
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+153
View File
@@ -0,0 +1,153 @@
using UnityEngine;
using UnityEngine.UI;
public class GameManager : MonoBehaviour
{
public CaptureZone[] zones;
public TerritoryUI territoryUI;
public GameObject winPanel;
public Text winText;
public int scoreLimit = 100;
private bool gameOver;
private void Start()
{
Time.timeScale = 1f;
if (territoryUI != null)
{
territoryUI.HideGameOver();
}
SubscribeToZones();
}
private void Update()
{
if (gameOver)
{
return;
}
if (territoryUI != null)
{
territoryUI.UpdateZoneStatus(zones);
}
CheckVictoryConditions();
}
private void SubscribeToZones()
{
if (zones == null)
{
return;
}
for (int i = 0; i < zones.Length; i++)
{
if (zones[i] == null)
{
continue;
}
zones[i].onCapturedByPlayer += OnZoneCaptured;
zones[i].onCapturedByEnemy += OnZoneCaptured;
}
}
private void OnDestroy()
{
if (zones == null)
{
return;
}
for (int i = 0; i < zones.Length; i++)
{
if (zones[i] == null)
{
continue;
}
zones[i].onCapturedByPlayer -= OnZoneCaptured;
zones[i].onCapturedByEnemy -= OnZoneCaptured;
}
}
private void OnZoneCaptured(CaptureZone zone)
{
CheckVictoryConditions();
}
private void CheckVictoryConditions()
{
if (zones == null || zones.Length == 0)
{
return;
}
bool allPlayer = true;
bool allEnemy = true;
for (int i = 0; i < zones.Length; i++)
{
if (zones[i] == null)
{
allPlayer = false;
allEnemy = false;
continue;
}
allPlayer &= zones[i].GetOwner() == CaptureZone.Owner.Player;
allEnemy &= zones[i].GetOwner() == CaptureZone.Owner.Enemy;
}
if (allPlayer)
{
GameOver("Победа игрока: все зоны захвачены!");
return;
}
if (allEnemy)
{
GameOver("Победа врага: все зоны потеряны!");
return;
}
if (ResourceManager.Instance != null && ResourceManager.Instance.playerScore >= scoreLimit)
{
GameOver("Победа игрока: достигнут лимит очков!");
}
}
private void GameOver(string message)
{
if (gameOver)
{
return;
}
gameOver = true;
if (territoryUI != null)
{
territoryUI.ShowGameOver(message);
}
else
{
if (winPanel != null)
{
winPanel.SetActive(true);
}
if (winText != null)
{
winText.text = message;
}
}
Time.timeScale = 0f;
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 333c0b0a17a11865b8bced65a4497db2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+43
View File
@@ -0,0 +1,43 @@
using UnityEngine;
[RequireComponent(typeof(CharacterController))]
public class PlayerController : MonoBehaviour
{
[Header("Movement")]
public float moveSpeed = 6f;
public float gravity = -20f;
private CharacterController characterController;
private Vector3 verticalVelocity;
private void Awake()
{
characterController = GetComponent<CharacterController>();
}
private void Update()
{
float horizontal = Input.GetAxisRaw("Horizontal");
float vertical = Input.GetAxisRaw("Vertical");
Vector3 input = new Vector3(horizontal, 0f, vertical);
input = Vector3.ClampMagnitude(input, 1f);
Vector3 move = input * moveSpeed;
if (characterController.isGrounded && verticalVelocity.y < 0f)
{
verticalVelocity.y = -1f;
}
verticalVelocity.y += gravity * Time.deltaTime;
move += verticalVelocity;
characterController.Move(move * Time.deltaTime);
if (input.sqrMagnitude > 0.001f)
{
transform.rotation = Quaternion.LookRotation(input, Vector3.up);
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 853685d0012a45352a9b673f28614946
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+60
View File
@@ -0,0 +1,60 @@
using UnityEngine;
using UnityEngine.UI;
public class ResourceManager : MonoBehaviour
{
public static ResourceManager Instance { get; private set; }
public int playerScore;
public int enemyScore;
public Text pointsText;
public Text enemyPointsText;
public TerritoryUI territoryUI;
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
}
private void Start()
{
UpdateScoreUI();
}
public void AddPlayerPoints(int amount)
{
playerScore += Mathf.Max(0, amount);
UpdateScoreUI();
}
public void AddEnemyPoints(int amount)
{
enemyScore += Mathf.Max(0, amount);
UpdateScoreUI();
}
private void UpdateScoreUI()
{
if (pointsText != null)
{
pointsText.text = "Player Points: " + playerScore;
}
if (enemyPointsText != null)
{
enemyPointsText.text = "Enemy Points: " + enemyScore;
}
if (territoryUI != null)
{
territoryUI.SetScore(playerScore, enemyScore);
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bebf81aaa988aac5487915a1a02b5bed
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+81
View File
@@ -0,0 +1,81 @@
using System.Text;
using UnityEngine;
using UnityEngine.UI;
public class TerritoryUI : MonoBehaviour
{
public Text playerPointsText;
public Text enemyPointsText;
public Text zoneStatusText;
public GameObject winPanel;
public Text winText;
private readonly StringBuilder zoneStatusBuilder = new StringBuilder();
public void UpdateZoneStatus(CaptureZone[] zones)
{
if (zoneStatusText == null || zones == null)
{
return;
}
zoneStatusBuilder.Length = 0;
for (int i = 0; i < zones.Length; i++)
{
CaptureZone zone = zones[i];
if (zone == null)
{
continue;
}
int percent = Mathf.RoundToInt(zone.GetProgress() * 100f);
zoneStatusBuilder.Append(zone.zoneName)
.Append(": ")
.Append(zone.GetOwner())
.Append(", ")
.Append(percent)
.Append('%');
if (i < zones.Length - 1)
{
zoneStatusBuilder.AppendLine();
}
}
zoneStatusText.text = zoneStatusBuilder.ToString();
}
public void ShowGameOver(string message)
{
if (winPanel != null)
{
winPanel.SetActive(true);
}
if (winText != null)
{
winText.text = message;
}
}
public void HideGameOver()
{
if (winPanel != null)
{
winPanel.SetActive(false);
}
}
public void SetScore(int playerScore, int enemyScore)
{
if (playerPointsText != null)
{
playerPointsText.text = "Player Points: " + playerScore;
}
if (enemyPointsText != null)
{
enemyPointsText.text = "Enemy Points: " + enemyScore;
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 49dcf13b53d69fbb9b2208d2160e6de5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+70
View File
@@ -0,0 +1,70 @@
using UnityEngine;
using UnityEngine.UI;
public class ZoneUI : MonoBehaviour
{
public CaptureZone zone;
public Slider progressSlider;
public Text labelText;
public Image fillImage;
public Camera targetCamera;
public Color neutralColor = Color.gray;
public Color playerColor = new Color(0.2f, 0.45f, 1f);
public Color enemyColor = new Color(1f, 0.2f, 0.15f);
public Color contestedColor = new Color(1f, 0.82f, 0.08f);
private void LateUpdate()
{
if (zone == null)
{
return;
}
if (progressSlider != null)
{
progressSlider.value = zone.GetProgress();
}
Color color = GetColorForZone();
if (fillImage != null)
{
fillImage.color = color;
}
if (labelText != null)
{
labelText.text = zone.zoneName + ": " + zone.GetOwner();
labelText.color = color;
}
Camera cameraToUse = targetCamera != null ? targetCamera : Camera.main;
if (cameraToUse != null)
{
transform.rotation = Quaternion.LookRotation(transform.position - cameraToUse.transform.position, Vector3.up);
}
}
private Color GetColorForZone()
{
CaptureZone.CaptureState state = zone.GetCaptureState();
if (state == CaptureZone.CaptureState.Contested ||
state == CaptureZone.CaptureState.CapturingByPlayer ||
state == CaptureZone.CaptureState.CapturingByEnemy)
{
return contestedColor;
}
if (zone.GetOwner() == CaptureZone.Owner.Player)
{
return playerColor;
}
if (zone.GetOwner() == CaptureZone.Owner.Enemy)
{
return enemyColor;
}
return neutralColor;
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 245f8e31fc2a8036db5d5bf4201e986a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: