feat(gameplay): 更新单位属性和材质配置 - 调整多个单位的基础生命值和攻击力数值平衡 - 修改CoreBattleGround材质的纹理缩放比例 - 更新地砖材质颜色为白色 - 优化纹理资源设置,调整最大纹理尺寸并添加PlayableAds平台支持 - 修复弓箭手技能目标查找逻辑,改用统一的敌人检测方法 - 为水晶矿添加中立阵营设置和光球视觉效果同步 - 调整预制体中的全局缩放参数 - 为狼人和石巨人添加NavMeshAgent组件支持 - 优化水晶矿血条显示位置计算方式 ```
185 lines
6.4 KiB
C#
185 lines
6.4 KiB
C#
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
/// <summary>
|
|
/// 红方城堡自动出兵 AI:独立能量、三路目标评估、低费试探与高费推进。
|
|
/// </summary>
|
|
public class EnemyCastleAI : MonoBehaviour
|
|
{
|
|
[Header("AI 阵营")]
|
|
[SerializeField] private int teamId = 1;
|
|
[SerializeField] private int maxMana = 10;
|
|
[SerializeField] private float manaRegenInterval = 3f;
|
|
[SerializeField] private float decisionInterval = 1.2f;
|
|
[SerializeField] private float minimumSpawnInterval = 1.4f;
|
|
|
|
[Header("三路目标")]
|
|
[SerializeField] private float leftLaneX = -10f;
|
|
[SerializeField] private float centerLaneX = 0f;
|
|
[SerializeField] private float rightLaneX = 10f;
|
|
[SerializeField] private float laneHalfWidth = 4f;
|
|
[SerializeField] private float resourceThreatWeight = 2.5f;
|
|
[SerializeField] private float enemyUnitWeight = 1.5f;
|
|
[SerializeField] private float laneRotationWeight = 0.7f;
|
|
|
|
[Header("卡组")]
|
|
[SerializeField] private CharacterData[] deck;
|
|
|
|
private Transform redSpawnPoint;
|
|
private AttackTargetController targetController;
|
|
private float currentMana;
|
|
private float manaTimer;
|
|
private float decisionTimer;
|
|
private float spawnTimer;
|
|
private int lastLane = 1;
|
|
private int consecutiveLaneSpawns;
|
|
private bool initialized;
|
|
|
|
private void Start()
|
|
{
|
|
redSpawnPoint = FindRedSpawnPoint();
|
|
targetController = AttackTargetController.Instance;
|
|
currentMana = maxMana;
|
|
if (deck == null || deck.Length == 0)
|
|
deck = Resources.LoadAll<CharacterData>("Data");
|
|
initialized = redSpawnPoint != null && deck != null && deck.Length > 0;
|
|
if (!initialized)
|
|
Debug.LogWarning("EnemyCastleAI 初始化失败:缺少红方出兵点或角色卡组。");
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
if (!initialized) return;
|
|
|
|
manaTimer += Time.deltaTime;
|
|
while (manaTimer >= manaRegenInterval)
|
|
{
|
|
manaTimer -= manaRegenInterval;
|
|
currentMana = Mathf.Min(maxMana, currentMana + 1f);
|
|
}
|
|
|
|
decisionTimer -= Time.deltaTime;
|
|
spawnTimer -= Time.deltaTime;
|
|
if (decisionTimer > 0f || spawnTimer > 0f) return;
|
|
decisionTimer = decisionInterval;
|
|
|
|
int lane = ChooseLane();
|
|
CharacterData card = ChooseCard(lane);
|
|
if (card == null || card.manaCost > currentMana) return;
|
|
if (Spawn(card, lane))
|
|
{
|
|
currentMana -= card.manaCost;
|
|
spawnTimer = minimumSpawnInterval;
|
|
if (lane == lastLane) consecutiveLaneSpawns++;
|
|
else consecutiveLaneSpawns = 1;
|
|
lastLane = lane;
|
|
}
|
|
}
|
|
|
|
private int ChooseLane()
|
|
{
|
|
float left = ScoreLane(leftLaneX, 0);
|
|
float center = ScoreLane(centerLaneX, 1);
|
|
float right = ScoreLane(rightLaneX, 2);
|
|
|
|
if (consecutiveLaneSpawns >= 2)
|
|
{
|
|
if (lastLane == 0) left -= 3f;
|
|
if (lastLane == 1) center -= 3f;
|
|
if (lastLane == 2) right -= 3f;
|
|
}
|
|
|
|
float best = Mathf.Max(left, Mathf.Max(center, right));
|
|
if (Mathf.Approximately(best, left)) return 0;
|
|
if (Mathf.Approximately(best, right)) return 2;
|
|
return 1;
|
|
}
|
|
|
|
private float ScoreLane(float laneX, int lane)
|
|
{
|
|
float score = 1f;
|
|
score += lane == 1 ? 0.8f : 0f;
|
|
score += lane == lastLane ? laneRotationWeight : 0f;
|
|
|
|
foreach (var unit in FindObjectsOfType<UnitTeam>())
|
|
{
|
|
if (unit == null || unit.teamId != 0) continue;
|
|
Vector3 p = unit.transform.position;
|
|
if (Mathf.Abs(p.x - laneX) <= laneHalfWidth)
|
|
score += enemyUnitWeight;
|
|
}
|
|
|
|
foreach (var mine in FindObjectsOfType<CrystalMineCapture>())
|
|
{
|
|
if (mine == null) continue;
|
|
Vector3 p = mine.transform.position;
|
|
if (Mathf.Abs(p.x - laneX) > laneHalfWidth * 1.8f) continue;
|
|
UnitTeam team = mine.GetComponent<UnitTeam>();
|
|
if (team == null || team.teamId != teamId)
|
|
score += resourceThreatWeight;
|
|
}
|
|
|
|
return score;
|
|
}
|
|
|
|
private CharacterData ChooseCard(int lane)
|
|
{
|
|
CharacterData best = null;
|
|
float bestScore = float.NegativeInfinity;
|
|
foreach (var card in deck)
|
|
{
|
|
if (card == null || card.unitPrefab == null || card.manaCost > currentMana) continue;
|
|
float score = Random.Range(0f, 1f);
|
|
if (card.manaCost <= 2) score += 2f;
|
|
if (currentMana >= 7 && card.manaCost >= 5) score += 2f;
|
|
if (lane != 1 && card.manaCost <= 4) score += 0.5f;
|
|
if (score > bestScore) { bestScore = score; best = card; }
|
|
}
|
|
return best;
|
|
}
|
|
|
|
private bool Spawn(CharacterData data, int lane)
|
|
{
|
|
GameObject unit = Instantiate(data.unitPrefab, redSpawnPoint.position, redSpawnPoint.rotation);
|
|
UnitTeam team = unit.GetComponent<UnitTeam>();
|
|
if (team != null) team.teamId = teamId;
|
|
unit.tag = "Enemy";
|
|
|
|
UnitBase unitBase = unit.GetComponent<UnitBase>();
|
|
if (unitBase != null)
|
|
{
|
|
unitBase.InitializeFromPlayerData(data.GetMaxHealth(1), data.GetAttackPower(1), data.GetMoveSpeed(1));
|
|
}
|
|
|
|
if (targetController != null)
|
|
{
|
|
targetController.redTarget = FindLaneTarget(lane);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private GameObject FindLaneTarget(int lane)
|
|
{
|
|
float x = lane == 0 ? leftLaneX : lane == 2 ? rightLaneX : centerLaneX;
|
|
GameObject best = null;
|
|
float bestDistance = float.PositiveInfinity;
|
|
foreach (var mine in FindObjectsOfType<CrystalMineCapture>())
|
|
{
|
|
if (Mathf.Abs(mine.transform.position.x - x) < laneHalfWidth * 2f)
|
|
{
|
|
float d = Mathf.Abs(mine.transform.position.z - redSpawnPoint.position.z);
|
|
if (d < bestDistance) { bestDistance = d; best = mine.gameObject; }
|
|
}
|
|
}
|
|
return best != null ? best : targetController?.GetEnemyCastleOfTeam(teamId);
|
|
}
|
|
|
|
private Transform FindRedSpawnPoint()
|
|
{
|
|
var manager = GetComponent<CastleSpawnManager>();
|
|
if (manager != null && manager.redSpawnPoint != null) return manager.redSpawnPoint;
|
|
var castles = GameObject.Find("红色城堡");
|
|
return castles != null ? castles.transform.Find("出兵点") : null;
|
|
}
|
|
}
|