```
feat(gameplay): 添加水晶矿资源产出系统和改进AI卡组管理 - 实现水晶矿占领后的能量资源产出功能,包括manaInterval和manaAmount配置 - 添加敌方AI的能量管理系统,支持通过AddMana方法增加能量 - 实现AI卡组轮转机制,包含手牌管理和抽卡队列功能 - 优化单位AI的目标选择和战斗逻辑,修复目标切换问题 - 水晶矿现在可以按设定间隔为所属阵营提供能量资源 -
This commit is contained in:
1 parent
b185e5768b
commit
06940a849c
21 files changed
+555
-520
No files matched your search
@@ -0,0 +1,130 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// 混沌之门:被占领后按固定间隔从所属阵营卡组随机出兵。
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(CrystalMineCapture))]
|
||||
public class ChaosGateSpawner : MonoBehaviour
|
||||
{
|
||||
[Header("出兵配置")]
|
||||
[Min(1f)] public float spawnInterval = 12f;
|
||||
[Min(0f)] public float spawnRadius = 1.5f;
|
||||
public bool spawnImmediatelyOnCapture = false;
|
||||
|
||||
[Header("蓝方卡组")]
|
||||
[SerializeField] private string[] blueDeckDataNames =
|
||||
{
|
||||
"人类士兵Data", "弓箭手Data", "盾卫Data", "精锐士兵Data",
|
||||
"法师Data", "狂暴狼人Data", "石巨人Data", "弓箭猎人Data"
|
||||
};
|
||||
|
||||
private CrystalMineCapture capture;
|
||||
private UnitTeam unitTeam;
|
||||
private float spawnTimer;
|
||||
private int previousTeamId = 2;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
capture = GetComponent<CrystalMineCapture>();
|
||||
unitTeam = GetComponent<UnitTeam>();
|
||||
spawnTimer = spawnInterval;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (unitTeam == null)
|
||||
{
|
||||
unitTeam = GetComponent<UnitTeam>();
|
||||
if (unitTeam == null) return;
|
||||
}
|
||||
|
||||
int currentTeamId = unitTeam.teamId;
|
||||
if (currentTeamId != previousTeamId)
|
||||
{
|
||||
previousTeamId = currentTeamId;
|
||||
spawnTimer = spawnImmediatelyOnCapture ? 0f : spawnInterval;
|
||||
}
|
||||
|
||||
if (currentTeamId != 0 && currentTeamId != 1)
|
||||
{
|
||||
spawnTimer = spawnInterval;
|
||||
return;
|
||||
}
|
||||
|
||||
spawnTimer -= Time.deltaTime;
|
||||
if (spawnTimer > 0f) return;
|
||||
|
||||
if (TrySpawn(currentTeamId))
|
||||
spawnTimer = spawnInterval;
|
||||
else
|
||||
spawnTimer = 1f;
|
||||
}
|
||||
|
||||
private bool TrySpawn(int teamId)
|
||||
{
|
||||
Vector3 position = transform.position;
|
||||
position.x += Random.Range(-spawnRadius, spawnRadius);
|
||||
position.z += Random.Range(-spawnRadius, spawnRadius);
|
||||
|
||||
if (teamId == 1)
|
||||
{
|
||||
EnemyCastleAI enemyAI = FindObjectOfType<EnemyCastleAI>();
|
||||
return enemyAI != null && enemyAI.TrySpawnFromPortal(position, transform.rotation);
|
||||
}
|
||||
|
||||
CharacterData card = ChooseBlueCard();
|
||||
if (card == null || CastleSpawnManager.Instance == null)
|
||||
return false;
|
||||
|
||||
return SpawnBlueUnit(card, position, transform.rotation);
|
||||
}
|
||||
|
||||
private CharacterData ChooseBlueCard()
|
||||
{
|
||||
CharacterData[] cards = new CharacterData[blueDeckDataNames.Length];
|
||||
int validCount = 0;
|
||||
foreach (string dataName in blueDeckDataNames)
|
||||
{
|
||||
CharacterData data = Resources.Load<CharacterData>("Data/" + dataName);
|
||||
if (data != null && data.unitPrefab != null)
|
||||
cards[validCount++] = data;
|
||||
}
|
||||
|
||||
if (validCount == 0) return null;
|
||||
|
||||
float totalWeight = 0f;
|
||||
for (int i = 0; i < validCount; i++)
|
||||
totalWeight += 1f / Mathf.Max(1, cards[i].manaCost);
|
||||
|
||||
float roll = Random.value * totalWeight;
|
||||
for (int i = 0; i < validCount; i++)
|
||||
{
|
||||
roll -= 1f / Mathf.Max(1, cards[i].manaCost);
|
||||
if (roll <= 0f) return cards[i];
|
||||
}
|
||||
return cards[validCount - 1];
|
||||
}
|
||||
|
||||
private bool SpawnBlueUnit(CharacterData data, Vector3 position, Quaternion rotation)
|
||||
{
|
||||
if (ManaSystem.Instance == null || !ManaSystem.Instance.TryConsumeMana(data.manaCost))
|
||||
return false;
|
||||
|
||||
GameObject unit = Instantiate(data.unitPrefab, position, rotation);
|
||||
UnitTeam team = unit.GetComponent<UnitTeam>();
|
||||
if (team != null)
|
||||
{
|
||||
team.teamId = 0;
|
||||
unit.tag = "Friend";
|
||||
}
|
||||
|
||||
UnitBase unitBase = unit.GetComponent<UnitBase>();
|
||||
if (unitBase != null)
|
||||
{
|
||||
unitBase.InitializeFromPlayerData(data.GetMaxHealth(1), data.GetAttackPower(1), data.GetMoveSpeed(1));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: WnsW5HmqByrprA9NWfcR8/RRH5kmtPm0TGQevEsLKuNlTEbLzjkqpVA=
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -10,7 +10,12 @@ public class CrystalMineCapture : MonoBehaviour
|
||||
[Tooltip("是否自动添加中立阵营(teamId=2)。拆家版关闭以避免单位自动锁敌水晶矿")]
|
||||
public bool autoSetNeutralTeam = true;
|
||||
|
||||
[Header("水晶矿光球")]
|
||||
[Header("资源产出")]
|
||||
[Tooltip("占领后是否按固定间隔给所属阵营增加能量")]
|
||||
public bool producesMana = true;
|
||||
[Min(1f)] public float manaInterval = 8f;
|
||||
[Min(1)] public int manaAmount = 1;
|
||||
|
||||
[SerializeField] private AdjustableGlowOrb glowOrb;
|
||||
[Tooltip("混沌裂缝传送门(随机出兵点),随争夺进度同步变色,可留空")]
|
||||
[SerializeField] private RiftPortalVisual riftVisual;
|
||||
@@ -31,6 +36,7 @@ public class CrystalMineCapture : MonoBehaviour
|
||||
private Image redFill;
|
||||
private RectTransform marker;
|
||||
private UnitTeam unitTeam;
|
||||
private float manaTimer;
|
||||
private static Sprite roundedSprite;
|
||||
private static readonly int BaseColorId = Shader.PropertyToID("_Color");
|
||||
|
||||
@@ -76,6 +82,8 @@ public class CrystalMineCapture : MonoBehaviour
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
UpdateManaProduction();
|
||||
|
||||
if (barRect == null) return;
|
||||
|
||||
barRect.position = transform.position + Vector3.up * GetBarHeight();
|
||||
@@ -109,6 +117,30 @@ public class CrystalMineCapture : MonoBehaviour
|
||||
return barHeightCache.Value;
|
||||
}
|
||||
|
||||
private void UpdateManaProduction()
|
||||
{
|
||||
if (!producesMana || unitTeam == null || (unitTeam.teamId != 0 && unitTeam.teamId != 1))
|
||||
{
|
||||
manaTimer = 0f;
|
||||
return;
|
||||
}
|
||||
|
||||
manaTimer += Time.deltaTime;
|
||||
while (manaTimer >= manaInterval)
|
||||
{
|
||||
manaTimer -= manaInterval;
|
||||
if (unitTeam.teamId == 0)
|
||||
{
|
||||
ManaSystem.Instance?.AddMana(manaAmount);
|
||||
}
|
||||
else
|
||||
{
|
||||
EnemyCastleAI enemyAI = FindObjectOfType<EnemyCastleAI>();
|
||||
enemyAI?.AddMana(manaAmount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ReceiveCaptureDamage(float damage, GameObject attacker)
|
||||
{
|
||||
if (damage <= 0f || attacker == null) return false;
|
||||
|
||||
+118
-11
@@ -22,9 +22,17 @@ public class EnemyCastleAI : MonoBehaviour
|
||||
[SerializeField] private float enemyUnitWeight = 1.5f;
|
||||
[SerializeField] private float laneRotationWeight = 0.7f;
|
||||
|
||||
[Header("卡组")]
|
||||
[Header("卡组轮转")]
|
||||
[SerializeField] private int visibleSlotCount = 4;
|
||||
[SerializeField] private string[] deckDataNames =
|
||||
{
|
||||
"人类士兵Data", "弓箭手Data", "盾卫Data", "精锐士兵Data",
|
||||
"法师Data", "狂暴狼人Data", "石巨人Data", "弓箭猎人Data"
|
||||
};
|
||||
[SerializeField] private CharacterData[] deck;
|
||||
|
||||
private readonly List<CharacterData> hand = new List<CharacterData>();
|
||||
private readonly Queue<CharacterData> drawQueue = new Queue<CharacterData>();
|
||||
private Transform redSpawnPoint;
|
||||
private AttackTargetController targetController;
|
||||
private float currentMana;
|
||||
@@ -40,9 +48,9 @@ public class EnemyCastleAI : MonoBehaviour
|
||||
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;
|
||||
BuildDeck();
|
||||
BuildHand();
|
||||
initialized = redSpawnPoint != null && hand.Count > 0;
|
||||
if (!initialized)
|
||||
Debug.LogWarning("EnemyCastleAI 初始化失败:缺少红方出兵点或角色卡组。");
|
||||
}
|
||||
@@ -65,10 +73,11 @@ public class EnemyCastleAI : MonoBehaviour
|
||||
|
||||
int lane = ChooseLane();
|
||||
CharacterData card = ChooseCard(lane);
|
||||
if (card == null || card.manaCost > currentMana) return;
|
||||
if (card == null) return;
|
||||
if (Spawn(card, lane))
|
||||
{
|
||||
currentMana -= card.manaCost;
|
||||
RotatePlayedCard(card);
|
||||
spawnTimer = minimumSpawnInterval;
|
||||
if (lane == lastLane) consecutiveLaneSpawns++;
|
||||
else consecutiveLaneSpawns = 1;
|
||||
@@ -76,6 +85,50 @@ public class EnemyCastleAI : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
public void AddMana(int amount)
|
||||
{
|
||||
if (amount <= 0) return;
|
||||
currentMana = Mathf.Min(maxMana, currentMana + amount);
|
||||
}
|
||||
|
||||
public int CurrentMana => Mathf.FloorToInt(currentMana);
|
||||
|
||||
public bool TrySpawnFromPortal(Vector3 spawnPosition, Quaternion spawnRotation)
|
||||
{
|
||||
if (!initialized) return false;
|
||||
|
||||
List<CharacterData> affordable = new List<CharacterData>();
|
||||
foreach (CharacterData card in hand)
|
||||
{
|
||||
if (card != null && card.unitPrefab != null && card.manaCost <= currentMana)
|
||||
affordable.Add(card);
|
||||
}
|
||||
|
||||
if (affordable.Count == 0) return false;
|
||||
|
||||
CharacterData selected = SelectWeightedCard(affordable);
|
||||
if (!SpawnAt(selected, spawnPosition, spawnRotation, teamId)) return false;
|
||||
|
||||
currentMana -= selected.manaCost;
|
||||
RotatePlayedCard(selected);
|
||||
return true;
|
||||
}
|
||||
|
||||
private CharacterData SelectWeightedCard(List<CharacterData> cards)
|
||||
{
|
||||
float totalWeight = 0f;
|
||||
foreach (CharacterData card in cards)
|
||||
totalWeight += 1f / Mathf.Max(1, card.manaCost);
|
||||
|
||||
float roll = Random.value * totalWeight;
|
||||
foreach (CharacterData card in cards)
|
||||
{
|
||||
roll -= 1f / Mathf.Max(1, card.manaCost);
|
||||
if (roll <= 0f) return card;
|
||||
}
|
||||
return cards[cards.Count - 1];
|
||||
}
|
||||
|
||||
private int ChooseLane()
|
||||
{
|
||||
float left = ScoreLane(leftLaneX, 0);
|
||||
@@ -126,24 +179,71 @@ public class EnemyCastleAI : MonoBehaviour
|
||||
{
|
||||
CharacterData best = null;
|
||||
float bestScore = float.NegativeInfinity;
|
||||
foreach (var card in deck)
|
||||
foreach (var card in hand)
|
||||
{
|
||||
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; }
|
||||
if (score > bestScore)
|
||||
{
|
||||
bestScore = score;
|
||||
best = card;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
private bool Spawn(CharacterData data, int lane)
|
||||
private void BuildDeck()
|
||||
{
|
||||
GameObject unit = Instantiate(data.unitPrefab, redSpawnPoint.position, redSpawnPoint.rotation);
|
||||
if (deck != null && deck.Length > 0) return;
|
||||
|
||||
var loadedDeck = new List<CharacterData>();
|
||||
foreach (string dataName in deckDataNames)
|
||||
{
|
||||
CharacterData data = Resources.Load<CharacterData>("Data/" + dataName);
|
||||
if (data == null)
|
||||
{
|
||||
Debug.LogWarning($"敌方卡组缺少角色数据: Resources/Data/{dataName}.asset");
|
||||
continue;
|
||||
}
|
||||
loadedDeck.Add(data);
|
||||
}
|
||||
deck = loadedDeck.ToArray();
|
||||
}
|
||||
|
||||
private void BuildHand()
|
||||
{
|
||||
hand.Clear();
|
||||
drawQueue.Clear();
|
||||
if (deck == null) return;
|
||||
|
||||
int handCount = Mathf.Min(visibleSlotCount, deck.Length);
|
||||
for (int i = 0; i < handCount; i++)
|
||||
hand.Add(deck[i]);
|
||||
for (int i = handCount; i < deck.Length; i++)
|
||||
drawQueue.Enqueue(deck[i]);
|
||||
}
|
||||
|
||||
private void RotatePlayedCard(CharacterData playedCard)
|
||||
{
|
||||
int handIndex = hand.IndexOf(playedCard);
|
||||
if (handIndex < 0 || drawQueue.Count == 0) return;
|
||||
|
||||
hand[handIndex] = drawQueue.Dequeue();
|
||||
drawQueue.Enqueue(playedCard);
|
||||
}
|
||||
|
||||
private bool SpawnAt(CharacterData data, Vector3 position, Quaternion rotation, int spawnTeamId)
|
||||
{
|
||||
GameObject unit = Instantiate(data.unitPrefab, position, rotation);
|
||||
UnitTeam team = unit.GetComponent<UnitTeam>();
|
||||
if (team != null) team.teamId = teamId;
|
||||
unit.tag = "Enemy";
|
||||
if (team != null)
|
||||
{
|
||||
team.teamId = spawnTeamId;
|
||||
unit.tag = spawnTeamId == 0 ? "Friend" : "Enemy";
|
||||
}
|
||||
|
||||
UnitBase unitBase = unit.GetComponent<UnitBase>();
|
||||
if (unitBase != null)
|
||||
@@ -151,6 +251,13 @@ public class EnemyCastleAI : MonoBehaviour
|
||||
unitBase.InitializeFromPlayerData(data.GetMaxHealth(1), data.GetAttackPower(1), data.GetMoveSpeed(1));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool Spawn(CharacterData data, int lane)
|
||||
{
|
||||
if (!SpawnAt(data, redSpawnPoint.position, redSpawnPoint.rotation, teamId)) return false;
|
||||
|
||||
if (targetController != null)
|
||||
{
|
||||
targetController.redTarget = FindLaneTarget(lane);
|
||||
|
||||
+41
-25
@@ -207,8 +207,17 @@ public class UnitBase : MonoBehaviour
|
||||
{
|
||||
navAgent.enabled = false;
|
||||
}
|
||||
|
||||
|
||||
animator.SetInteger("speed", 0);
|
||||
|
||||
// 目标死亡、失活或离开攻击范围时,解除锁定并重新寻敌。
|
||||
if (!IsTargetValid(target) || !IsWithinAttackReach(target))
|
||||
{
|
||||
target = null;
|
||||
state = State.Alert;
|
||||
return;
|
||||
}
|
||||
|
||||
attackElapsedTime += Time.deltaTime;
|
||||
CheckAttackAnimationFinished();
|
||||
return;
|
||||
@@ -221,8 +230,10 @@ public class UnitBase : MonoBehaviour
|
||||
return;
|
||||
}
|
||||
|
||||
//目标决策:周围有敌人打敌人,否则直奔全局攻击标志建筑(默认敌方主城堡)
|
||||
// 移动/追踪状态持续重新评估目标,确保相遇时能及时攻击敌方单位。
|
||||
// Attack 状态在上方单独处理,因此不会被这里打断或切换目标。
|
||||
target = FindTarget();
|
||||
|
||||
if (target != null)
|
||||
{
|
||||
// 召唤师模式:不主动追击,站定且攻击开关开启时才攻击射程内目标。
|
||||
@@ -247,21 +258,6 @@ public class UnitBase : MonoBehaviour
|
||||
}
|
||||
else
|
||||
{
|
||||
//到达使命建筑发现是我方的(增援/占领完成):本单位使命转向敌方主城堡,不影响全局标志
|
||||
if (target.CompareTag("Building") && IsFriendlyBuilding(target))
|
||||
{
|
||||
var myTeam = GetComponent<UnitTeam>();
|
||||
GameObject castle = AttackTargetController.Instance != null && myTeam != null
|
||||
? AttackTargetController.Instance.GetEnemyCastleOfTeam(myTeam.teamId)
|
||||
: null;
|
||||
if (castle != null)
|
||||
{
|
||||
missionTarget = castle; // 使命完成,改打主城堡(单位自身,一次性的)
|
||||
target = castle;
|
||||
move(castle.transform.position - transform.position);
|
||||
return;
|
||||
}
|
||||
}
|
||||
attack(target);
|
||||
}
|
||||
|
||||
@@ -279,6 +275,24 @@ public class UnitBase : MonoBehaviour
|
||||
|
||||
public GameObject Target => target;
|
||||
|
||||
private bool IsTargetValid(GameObject candidate)
|
||||
{
|
||||
if (candidate == null || !candidate.activeInHierarchy)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
WorldHealthBar healthBar = candidate.GetComponentInParent<WorldHealthBar>();
|
||||
if (healthBar != null && healthBar.IsDead)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
UnitTeam myTeam = GetComponent<UnitTeam>();
|
||||
UnitTeam targetTeam = candidate.GetComponentInParent<UnitTeam>();
|
||||
return myTeam == null || targetTeam == null || myTeam.IsEnemy(targetTeam.gameObject);
|
||||
}
|
||||
|
||||
public GameObject FindNearestEnemyForSkill()
|
||||
{
|
||||
return FindNearestEnemyUnit();
|
||||
@@ -378,16 +392,19 @@ public class UnitBase : MonoBehaviour
|
||||
GameObject enemy = FindNearestEnemyUnit();
|
||||
if (enemy != null) return enemy;
|
||||
|
||||
// Phase 2: 出生使命建筑;被销毁时兜底敌方主城堡
|
||||
if (missionTarget != null && missionTarget.activeInHierarchy)
|
||||
// Phase 2: 出生使命建筑;只有仍属于敌方的使命建筑才继续攻击。
|
||||
if (IsTargetValid(missionTarget))
|
||||
{
|
||||
return missionTarget;
|
||||
}
|
||||
|
||||
// 使命建筑已被己方占领、失活或销毁时,改攻敌方主城堡。
|
||||
var myTeam = GetComponent<UnitTeam>();
|
||||
if (myTeam != null && AttackTargetController.Instance != null)
|
||||
{
|
||||
return AttackTargetController.Instance.GetEnemyCastleOfTeam(myTeam.teamId);
|
||||
GameObject castle = AttackTargetController.Instance.GetEnemyCastleOfTeam(myTeam.teamId);
|
||||
missionTarget = castle;
|
||||
return castle;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -1064,17 +1081,16 @@ public class UnitBase : MonoBehaviour
|
||||
//攻击动画结束后恢复当前攻速倍率。
|
||||
animator.speed = attackSpeedMultiplier;
|
||||
|
||||
//攻击动画结束后,目标仍在攻击范围内则立即进行下一次攻击。
|
||||
target = FindTarget();
|
||||
if (target != null && IsWithinAttackReach(target))
|
||||
//攻击结束后只继续攻击当前锁定目标,不在这里重新寻敌。
|
||||
if (IsTargetValid(target) && IsWithinAttackReach(target))
|
||||
{
|
||||
// 攻击结束后可能已切换到新的近距离目标,先更新朝向再开始下一次攻击。
|
||||
FaceTarget(target);
|
||||
attack(target);
|
||||
return;
|
||||
}
|
||||
|
||||
//目标离开攻击范围,回到待机逻辑。
|
||||
//目标失效或离开攻击范围,解除锁定,下一帧重新寻敌。
|
||||
target = null;
|
||||
state = State.Alert;
|
||||
animator.SetInteger("speed", 0);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user