```
feat: 移除自适应性能相关配置并调整动画控制器 移除了Unity自适应性能(Adaptive Performance)的相关配置文件, 包括Adaptive Performance文件夹及其子目录中的所有配置文件, 同时移除了移动通知(NotificationSettings)相关配置, 以及第一章第一关(level_chapter1_level1)的关卡配置文件, 还删除了多个材质文件如AdjustableGlowOrb、AdjustableGlowOrbHalo 和Arena相关材质。另外微调了base-人类士兵.controller中状态机 的位置坐标,将x坐标从320调整为330。 ```
This commit is contained in:
1 parent
13d0c80335
commit
f77fa20f82
413 files changed
+13494
-63858
No files matched your search
@@ -87,11 +87,7 @@ public class AISummoner : MonoBehaviour
|
||||
{
|
||||
if (characters[i] != null && characters[i].unitPrefab != null)
|
||||
{
|
||||
// 检查能量
|
||||
if (unlimitedMana || manaSystem == null || manaSystem.CurrentManaInt >= characters[i].manaCost)
|
||||
{
|
||||
validSlots[validCount++] = i + 1; // slot 是 1-based
|
||||
}
|
||||
validSlots[validCount++] = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ public class Barracks : MonoBehaviour
|
||||
|
||||
if (characterData != null)
|
||||
{
|
||||
spawnInterval = Mathf.Max(0.1f, characterData.productionInterval);
|
||||
spawnInterval = 5f;
|
||||
|
||||
// 设置角色头像
|
||||
if (characterIcon != null && characterData.cardIcon != null)
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// 拆家版固定八卡组、四张手牌轮转管理器。
|
||||
/// </summary>
|
||||
public class BattleDeckCycle : MonoBehaviour
|
||||
{
|
||||
public static BattleDeckCycle Instance { get; private set; }
|
||||
|
||||
[SerializeField] private Transform cardRoot;
|
||||
[SerializeField] private int visibleSlotCount = 4;
|
||||
[SerializeField] private string[] deckDataNames =
|
||||
{
|
||||
"人类士兵Data", "弓箭手Data", "盾卫Data", "精锐士兵Data",
|
||||
"法师Data", "狂暴狼人Data", "石巨人Data", "弓箭猎人Data"
|
||||
};
|
||||
|
||||
private readonly List<CharacterData> deck = new List<CharacterData>();
|
||||
private readonly List<CharacterData> hand = new List<CharacterData>();
|
||||
private readonly Queue<CharacterData> drawQueue = new Queue<CharacterData>();
|
||||
private SummonCardButton[] buttons;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
Instance = this;
|
||||
BuildDeck();
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (cardRoot == null)
|
||||
cardRoot = GameObject.Find("角色卡片")?.transform;
|
||||
|
||||
buttons = cardRoot != null
|
||||
? cardRoot.GetComponentsInChildren<SummonCardButton>(true)
|
||||
: FindObjectsOfType<SummonCardButton>(true);
|
||||
|
||||
System.Array.Sort(buttons, (a, b) => a.summonSlot.CompareTo(b.summonSlot));
|
||||
HideExtraSlots();
|
||||
hand.Clear();
|
||||
drawQueue.Clear();
|
||||
|
||||
for (int i = 0; i < visibleSlotCount && i < deck.Count; i++)
|
||||
hand.Add(deck[i]);
|
||||
for (int i = visibleSlotCount; i < deck.Count; i++)
|
||||
drawQueue.Enqueue(deck[i]);
|
||||
|
||||
RefreshAllCards();
|
||||
}
|
||||
|
||||
public CharacterData GetCard(int slot)
|
||||
{
|
||||
int index = slot - 1;
|
||||
return index >= 0 && index < hand.Count ? hand[index] : null;
|
||||
}
|
||||
|
||||
public bool TryPlay(int slot)
|
||||
{
|
||||
CharacterData card = GetCard(slot);
|
||||
if (card == null || card.unitPrefab == null || CastleSpawnManager.Instance == null)
|
||||
return false;
|
||||
|
||||
if (!CastleSpawnManager.Instance.TrySpawn(card, 0))
|
||||
return false;
|
||||
|
||||
int index = slot - 1;
|
||||
CharacterData played = hand[index];
|
||||
hand[index] = drawQueue.Dequeue();
|
||||
drawQueue.Enqueue(played);
|
||||
RefreshAllCards();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void BuildDeck()
|
||||
{
|
||||
deck.Clear();
|
||||
foreach (string dataName in deckDataNames)
|
||||
{
|
||||
CharacterData data = Resources.Load<CharacterData>("Data/" + dataName);
|
||||
if (data == null)
|
||||
{
|
||||
Debug.LogWarning($"卡组缺少角色数据: Resources/Data/{dataName}.asset");
|
||||
continue;
|
||||
}
|
||||
deck.Add(data);
|
||||
}
|
||||
}
|
||||
|
||||
private void HideExtraSlots()
|
||||
{
|
||||
foreach (var button in buttons)
|
||||
{
|
||||
if (button != null && button.summonSlot > visibleSlotCount)
|
||||
button.gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshAllCards()
|
||||
{
|
||||
if (buttons == null) return;
|
||||
foreach (var button in buttons)
|
||||
{
|
||||
if (button == null || button.summonSlot > visibleSlotCount) continue;
|
||||
button.SetDeckCharacter(GetCard(button.summonSlot));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// 战斗场景的出战生成:开战前按阵型数据在对应格位生成英雄。
|
||||
/// 优先使用阵型编排场景保存的数据;没有编排数据时按名单使用默认阵型(近战前排 / 远程后排)。
|
||||
/// </summary>
|
||||
public class BattleFormationSpawner : MonoBehaviour
|
||||
{
|
||||
[Header("出战英雄名单(没有编排数据时的默认来源)")]
|
||||
public HeroRoster roster;
|
||||
|
||||
[Header("生成到哪个父节点(留空则放在场景根)")]
|
||||
public Transform heroParent;
|
||||
|
||||
[Header("玩家阵营 ID")]
|
||||
public int playerTeamId = 0;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
SpawnHeroes();
|
||||
}
|
||||
|
||||
private void SpawnHeroes()
|
||||
{
|
||||
// 1. 玩家编排过的阵型
|
||||
if (FormationData.HasCustomFormation
|
||||
&& FormationData.HeroPrefabs != null
|
||||
&& FormationData.HeroPrefabs.Length > 0)
|
||||
{
|
||||
for (int i = 0; i < FormationData.HeroPrefabs.Length; i++)
|
||||
{
|
||||
int cell = i < FormationData.CellIndices.Length ? FormationData.CellIndices[i] : -1;
|
||||
if (FormationData.HeroPrefabs[i] == null || !DeployGrid.IsValidIndex(cell))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Spawn(FormationData.HeroPrefabs[i], cell);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 没有编排数据:默认阵型
|
||||
SpawnDefaultFormation();
|
||||
}
|
||||
|
||||
private void SpawnDefaultFormation()
|
||||
{
|
||||
if (roster == null || roster.heroes == null) return;
|
||||
|
||||
int meleeTotal = 0, rangedTotal = 0;
|
||||
foreach (HeroEntry entry in roster.heroes)
|
||||
{
|
||||
if (entry == null) continue;
|
||||
if (entry.isMelee) meleeTotal++; else rangedTotal++;
|
||||
}
|
||||
|
||||
var used = new HashSet<int>();
|
||||
int meleeIndex = 0, rangedIndex = 0;
|
||||
|
||||
foreach (HeroEntry entry in roster.heroes)
|
||||
{
|
||||
if (entry == null || entry.prefab == null) continue;
|
||||
|
||||
int cell = entry.isMelee
|
||||
? DeployGrid.DefaultCell(true, meleeIndex++, meleeTotal)
|
||||
: DeployGrid.DefaultCell(false, rangedIndex++, rangedTotal);
|
||||
|
||||
if (used.Contains(cell))
|
||||
{
|
||||
cell = NextFreeCell(cell, used);
|
||||
}
|
||||
if (cell < 0) continue;
|
||||
|
||||
used.Add(cell);
|
||||
Spawn(entry.prefab, cell);
|
||||
}
|
||||
}
|
||||
|
||||
private static int NextFreeCell(int from, HashSet<int> used)
|
||||
{
|
||||
for (int i = 0; i < DeployGrid.CellCount; i++)
|
||||
{
|
||||
int candidate = (from + i) % DeployGrid.CellCount;
|
||||
if (!used.Contains(candidate)) return candidate;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private void Spawn(GameObject prefab, int cell)
|
||||
{
|
||||
CharacterData characterData = FindCharacterDataForPrefab(prefab);
|
||||
GameObject unit = Instantiate(prefab, DeployGrid.CellPosition(cell), Quaternion.identity);
|
||||
unit.name = "英雄_" + prefab.name;
|
||||
|
||||
if (heroParent != null)
|
||||
{
|
||||
unit.transform.SetParent(heroParent, true);
|
||||
}
|
||||
|
||||
UnitTeam team = unit.GetComponent<UnitTeam>();
|
||||
if (team != null)
|
||||
{
|
||||
team.teamId = playerTeamId;
|
||||
}
|
||||
|
||||
UnitBase unitBase = unit.GetComponent<UnitBase>();
|
||||
if (unitBase != null)
|
||||
{
|
||||
unitBase.returnToAnchorWhenIdle = true;
|
||||
unitBase.anchorArrivalDistance = 0.35f;
|
||||
}
|
||||
|
||||
if (characterData != null && unitBase != null)
|
||||
{
|
||||
PlayerCharacterData playerData = PlayerDataManager.Instance != null
|
||||
? PlayerDataManager.Instance.GetCharacterData(characterData.characterId)
|
||||
: null;
|
||||
int level = playerData != null ? playerData.level : 1;
|
||||
unitBase.InitializeFromPlayerData(
|
||||
characterData.GetMaxHealth(level),
|
||||
characterData.GetAttackPower(level),
|
||||
characterData.GetMoveSpeed(level));
|
||||
}
|
||||
}
|
||||
|
||||
private CharacterData FindCharacterDataForPrefab(GameObject prefab)
|
||||
{
|
||||
if (prefab == null) return null;
|
||||
return Resources.LoadAll<CharacterData>("Data")
|
||||
.FirstOrDefault(data => data != null && data.unitPrefab == prefab);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: CylKsXykBSg0uwlH7D3NMglVNcpDcGUo3b0yJbyu49RAp3cINkikdpc=
|
||||
guid: XXgb53itAX7klkhotwyGbzIxindfYhwCY6UY2IP7bdLgsYgx8EIjOxA=
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
@@ -1,557 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
|
||||
// 决战玩法管理器:布阵阶段在己方半场网格放置单位,开战后生成敌方阵型,全灭判定胜负。
|
||||
public class BattleSetupManager : MonoBehaviour
|
||||
{
|
||||
public static BattleSetupManager Instance { get; private set; }
|
||||
|
||||
public enum BattleState { Setup, Battle, Ended }
|
||||
|
||||
[Header("布阵网格")]
|
||||
public float cellSize = 2f;
|
||||
public Vector2 xRange = new Vector2(-8f, 8f);
|
||||
public Vector2 zRange = new Vector2(-20f, -8f);
|
||||
|
||||
[Header("人口")]
|
||||
public int maxPopulation = 30;
|
||||
|
||||
[Header("可用兵种(生成底部卡片)")]
|
||||
public CharacterData[] availableCharacters;
|
||||
|
||||
[Header("敌方阵型")]
|
||||
public CharacterData[] enemyFrontLine;
|
||||
public CharacterData[] enemyBackLine;
|
||||
public float enemyFrontZ = 14f;
|
||||
public float enemyBackZ = 18f;
|
||||
public float enemySpacing = 2f;
|
||||
|
||||
public BattleState State { get; private set; } = BattleState.Setup;
|
||||
public int RemainingPopulation => maxPopulation - usedPopulation;
|
||||
|
||||
// 已放置单位 -> 格子与数据,移除时退回人口。
|
||||
private class PlacementRecord
|
||||
{
|
||||
public Vector2Int cell;
|
||||
public CharacterData data;
|
||||
}
|
||||
|
||||
private readonly HashSet<Vector2Int> occupiedCells = new HashSet<Vector2Int>();
|
||||
private readonly Dictionary<GameObject, PlacementRecord> placements = new Dictionary<GameObject, PlacementRecord>();
|
||||
private readonly Dictionary<CharacterData, Image> cardImages = new Dictionary<CharacterData, Image>();
|
||||
|
||||
private CharacterData selectedCharacter;
|
||||
private int usedPopulation;
|
||||
private Camera mainCamera;
|
||||
private GameObject cellHighlight;
|
||||
private Material highlightMaterial;
|
||||
private GameObject startButton;
|
||||
private Text populationText;
|
||||
private Text resultText;
|
||||
private float endCheckTimer;
|
||||
|
||||
private static readonly Color ValidCellColor = new Color(0.2f, 1f, 0.3f, 0.35f);
|
||||
private static readonly Color InvalidCellColor = new Color(1f, 0.25f, 0.2f, 0.35f);
|
||||
private static readonly Color CardNormalColor = new Color(0.15f, 0.15f, 0.2f, 0.85f);
|
||||
private static readonly Color CardSelectedColor = new Color(0.2f, 0.55f, 0.25f, 0.95f);
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
mainCamera = Camera.main;
|
||||
CreateCellHighlight();
|
||||
CreateCardUI();
|
||||
CreateStartButton();
|
||||
CreateResultText();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (State == BattleState.Setup)
|
||||
{
|
||||
HandleSetupInput();
|
||||
}
|
||||
else if (State == BattleState.Battle)
|
||||
{
|
||||
CheckBattleEnd();
|
||||
}
|
||||
}
|
||||
|
||||
// 布阵阶段输入:选兵种后点格子放置,点击已放单位移除。
|
||||
private void HandleSetupInput()
|
||||
{
|
||||
if (mainCamera == null)
|
||||
{
|
||||
mainCamera = Camera.main;
|
||||
if (mainCamera == null) return;
|
||||
}
|
||||
|
||||
bool overUI = EventSystem.current != null && EventSystem.current.IsPointerOverGameObject();
|
||||
bool highlightVisible = !overUI && selectedCharacter != null;
|
||||
if (cellHighlight != null)
|
||||
{
|
||||
cellHighlight.SetActive(highlightVisible);
|
||||
}
|
||||
|
||||
if (!highlightVisible) return;
|
||||
if (!RaycastGround(out Vector3 groundPoint)) return;
|
||||
|
||||
Vector2Int cell = WorldToCell(groundPoint);
|
||||
bool cellValid = IsCellInRange(cell) && !occupiedCells.Contains(cell);
|
||||
|
||||
cellHighlight.transform.position = new Vector3(cell.x * cellSize, 0.02f, cell.y * cellSize);
|
||||
highlightMaterial.color = cellValid ? ValidCellColor : InvalidCellColor;
|
||||
|
||||
if (Input.GetMouseButtonDown(0))
|
||||
{
|
||||
// 优先移除点中的己方单位。
|
||||
if (TryRemoveUnitAtMouse()) return;
|
||||
|
||||
if (cellValid)
|
||||
{
|
||||
PlaceUnit(selectedCharacter, cell);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 卡片点击入口:选中/再点取消。
|
||||
public void SelectCharacter(CharacterData data)
|
||||
{
|
||||
if (State != BattleState.Setup || data == null) return;
|
||||
|
||||
if (selectedCharacter == data)
|
||||
{
|
||||
selectedCharacter = null;
|
||||
}
|
||||
else if (RemainingPopulation >= data.manaCost)
|
||||
{
|
||||
selectedCharacter = data;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log($"人口不足,{data.characterName}需要{data.manaCost}人口,剩余{RemainingPopulation}");
|
||||
return;
|
||||
}
|
||||
|
||||
RefreshCardSelection();
|
||||
}
|
||||
|
||||
// 人口是否足够,供卡片按钮判断可用性。
|
||||
public bool CanAfford(CharacterData data)
|
||||
{
|
||||
return data != null && RemainingPopulation >= data.manaCost;
|
||||
}
|
||||
|
||||
public void StartBattle()
|
||||
{
|
||||
if (State != BattleState.Setup) return;
|
||||
if (placements.Count == 0)
|
||||
{
|
||||
Debug.Log("请先放置至少一个单位再开战");
|
||||
return;
|
||||
}
|
||||
|
||||
selectedCharacter = null;
|
||||
RefreshCardSelection();
|
||||
SpawnEnemyLine(enemyFrontLine, enemyFrontZ);
|
||||
SpawnEnemyLine(enemyBackLine, enemyBackZ);
|
||||
|
||||
State = BattleState.Battle;
|
||||
if (startButton != null) startButton.SetActive(false);
|
||||
if (populationText != null) populationText.gameObject.SetActive(false);
|
||||
if (cellHighlight != null) cellHighlight.SetActive(false);
|
||||
Debug.Log("战斗开始!");
|
||||
}
|
||||
|
||||
// 每0.5秒统计双方存活,全灭判定胜负。
|
||||
private void CheckBattleEnd()
|
||||
{
|
||||
endCheckTimer += Time.deltaTime;
|
||||
if (endCheckTimer < 0.5f) return;
|
||||
endCheckTimer = 0f;
|
||||
|
||||
int friends = CountActiveUnits("Friend");
|
||||
int enemies = CountActiveUnits("Enemy");
|
||||
|
||||
if (friends == 0 && enemies == 0)
|
||||
{
|
||||
EndBattle("平局!");
|
||||
}
|
||||
else if (enemies == 0)
|
||||
{
|
||||
EndBattle("胜利!");
|
||||
}
|
||||
else if (friends == 0)
|
||||
{
|
||||
EndBattle("失败");
|
||||
}
|
||||
}
|
||||
|
||||
private static int CountActiveUnits(string tag)
|
||||
{
|
||||
// 单位死亡时会被立即隐藏并销毁,只统计激活对象。
|
||||
GameObject[] objects = GameObject.FindGameObjectsWithTag(tag);
|
||||
int count = 0;
|
||||
foreach (GameObject obj in objects)
|
||||
{
|
||||
if (obj.activeInHierarchy) count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private void EndBattle(string result)
|
||||
{
|
||||
State = BattleState.Ended;
|
||||
if (resultText != null)
|
||||
{
|
||||
resultText.text = result;
|
||||
resultText.gameObject.SetActive(true);
|
||||
}
|
||||
Debug.Log($"战斗结束:{result}");
|
||||
}
|
||||
|
||||
// ===== 放置与移除 =====
|
||||
|
||||
private void PlaceUnit(CharacterData data, Vector2Int cell)
|
||||
{
|
||||
if (data == null || data.unitPrefab == null) return;
|
||||
if (RemainingPopulation < data.manaCost) return;
|
||||
if (!occupiedCells.Add(cell)) return;
|
||||
|
||||
Vector3 position = CellToWorld(cell);
|
||||
GameObject unit = Instantiate(data.unitPrefab, position, Quaternion.identity);
|
||||
unit.name = "Friend_" + data.characterName;
|
||||
unit.tag = "Friend";
|
||||
|
||||
// 设置阵营 ID(玩家单位为 team 0)
|
||||
var unitTeam = unit.GetComponent<UnitTeam>();
|
||||
if (unitTeam != null)
|
||||
{
|
||||
unitTeam.teamId = 0;
|
||||
}
|
||||
|
||||
var unitBase = unit.GetComponent<UnitBase>();
|
||||
if (unitBase != null)
|
||||
{
|
||||
unitBase.InitializeFromPlayerData(
|
||||
data.GetMaxHealth(1),
|
||||
data.GetAttackPower(1),
|
||||
data.GetMoveSpeed(1));
|
||||
}
|
||||
|
||||
placements[unit] = new PlacementRecord { cell = cell, data = data };
|
||||
usedPopulation += data.manaCost;
|
||||
UpdatePopulationText();
|
||||
|
||||
if (RemainingPopulation <= 0)
|
||||
{
|
||||
selectedCharacter = null;
|
||||
RefreshCardSelection();
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryRemoveUnitAtMouse()
|
||||
{
|
||||
Ray ray = mainCamera.ScreenPointToRay(Input.mousePosition);
|
||||
RaycastHit[] hits = Physics.RaycastAll(ray, 200f);
|
||||
|
||||
GameObject closest = null;
|
||||
float closestDistance = float.MaxValue;
|
||||
foreach (RaycastHit hit in hits)
|
||||
{
|
||||
var unitBase = hit.collider.GetComponentInParent<UnitBase>();
|
||||
if (unitBase != null && unitBase.CompareTag("Friend") && placements.ContainsKey(unitBase.gameObject))
|
||||
{
|
||||
if (hit.distance < closestDistance)
|
||||
{
|
||||
closestDistance = hit.distance;
|
||||
closest = unitBase.gameObject;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (closest == null) return false;
|
||||
|
||||
if (placements.TryGetValue(closest, out PlacementRecord record))
|
||||
{
|
||||
occupiedCells.Remove(record.cell);
|
||||
usedPopulation -= record.data.manaCost;
|
||||
placements.Remove(closest);
|
||||
UpdatePopulationText();
|
||||
}
|
||||
|
||||
Destroy(closest);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ===== 敌方阵型 =====
|
||||
|
||||
private void SpawnEnemyLine(CharacterData[] line, float z)
|
||||
{
|
||||
if (line == null) return;
|
||||
|
||||
float startX = -(line.Length - 1) * 0.5f * enemySpacing;
|
||||
for (int i = 0; i < line.Length; i++)
|
||||
{
|
||||
CharacterData data = line[i];
|
||||
if (data == null || data.unitPrefab == null) continue;
|
||||
|
||||
GameObject unit = Instantiate(
|
||||
data.unitPrefab,
|
||||
new Vector3(startX + i * enemySpacing, 0f, z),
|
||||
Quaternion.Euler(0f, 180f, 0f));
|
||||
unit.name = "Enemy_" + data.characterName;
|
||||
unit.tag = "Enemy";
|
||||
|
||||
var unitBase = unit.GetComponent<UnitBase>();
|
||||
if (unitBase != null)
|
||||
{
|
||||
unitBase.InitializeFromPlayerData(
|
||||
data.GetMaxHealth(1),
|
||||
data.GetAttackPower(1),
|
||||
data.GetMoveSpeed(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 网格工具 =====
|
||||
|
||||
private Vector2Int WorldToCell(Vector3 world)
|
||||
{
|
||||
return new Vector2Int(
|
||||
Mathf.RoundToInt(world.x / cellSize),
|
||||
Mathf.RoundToInt(world.z / cellSize));
|
||||
}
|
||||
|
||||
private Vector3 CellToWorld(Vector2Int cell)
|
||||
{
|
||||
return new Vector3(cell.x * cellSize, 0f, cell.y * cellSize);
|
||||
}
|
||||
|
||||
private bool IsCellInRange(Vector2Int cell)
|
||||
{
|
||||
float x = cell.x * cellSize;
|
||||
float z = cell.y * cellSize;
|
||||
return x >= xRange.x && x <= xRange.y && z >= zRange.x && z <= zRange.y;
|
||||
}
|
||||
|
||||
private bool RaycastGround(out Vector3 point)
|
||||
{
|
||||
point = Vector3.zero;
|
||||
if (mainCamera == null) return false;
|
||||
|
||||
Ray ray = mainCamera.ScreenPointToRay(Input.mousePosition);
|
||||
if (Physics.Raycast(ray, out RaycastHit hit, 200f))
|
||||
{
|
||||
point = hit.point;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ===== UI 创建(代码生成,无需场景预制) =====
|
||||
|
||||
private Canvas EnsureCanvas()
|
||||
{
|
||||
Canvas canvas = FindObjectOfType<Canvas>();
|
||||
if (canvas == null)
|
||||
{
|
||||
GameObject canvasObject = new GameObject("Canvas", typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster));
|
||||
canvas = canvasObject.GetComponent<Canvas>();
|
||||
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
|
||||
var scaler = canvasObject.GetComponent<CanvasScaler>();
|
||||
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
||||
scaler.referenceResolution = new Vector2(1080f, 1920f);
|
||||
scaler.matchWidthOrHeight = 0.5f;
|
||||
}
|
||||
|
||||
if (FindObjectOfType<EventSystem>() == null)
|
||||
{
|
||||
new GameObject("EventSystem", typeof(EventSystem), typeof(StandaloneInputModule));
|
||||
}
|
||||
|
||||
return canvas;
|
||||
}
|
||||
|
||||
private void CreateCellHighlight()
|
||||
{
|
||||
cellHighlight = GameObject.CreatePrimitive(PrimitiveType.Quad);
|
||||
cellHighlight.name = "CellHighlight";
|
||||
// 高亮块不能挡射线,删掉自带的碰撞体。
|
||||
Destroy(cellHighlight.GetComponent<Collider>());
|
||||
cellHighlight.transform.rotation = Quaternion.Euler(90f, 0f, 0f);
|
||||
cellHighlight.transform.localScale = new Vector3(cellSize * 0.95f, cellSize * 0.95f, 1f);
|
||||
cellHighlight.transform.position = new Vector3(0f, 0.02f, 0f);
|
||||
|
||||
highlightMaterial = new Material(Shader.Find("Sprites/Default"));
|
||||
highlightMaterial.color = ValidCellColor;
|
||||
cellHighlight.GetComponent<Renderer>().sharedMaterial = highlightMaterial;
|
||||
cellHighlight.SetActive(false);
|
||||
}
|
||||
|
||||
private void CreateCardUI()
|
||||
{
|
||||
if (availableCharacters == null || availableCharacters.Length == 0) return;
|
||||
|
||||
Canvas canvas = EnsureCanvas();
|
||||
const float cardWidth = 108f;
|
||||
const float cardHeight = 150f;
|
||||
const float gap = 10f;
|
||||
|
||||
int count = availableCharacters.Length;
|
||||
float totalWidth = count * cardWidth + (count - 1) * gap;
|
||||
float startX = -totalWidth * 0.5f + cardWidth * 0.5f;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
CharacterData data = availableCharacters[i];
|
||||
if (data == null) continue;
|
||||
|
||||
GameObject card = new GameObject("卡片_" + data.characterName, typeof(RectTransform), typeof(Image), typeof(Button));
|
||||
card.transform.SetParent(canvas.transform, false);
|
||||
|
||||
var cardRect = card.GetComponent<RectTransform>();
|
||||
cardRect.anchorMin = cardRect.anchorMax = new Vector2(0.5f, 0f);
|
||||
cardRect.pivot = new Vector2(0.5f, 0f);
|
||||
cardRect.anchoredPosition = new Vector2(startX + i * (cardWidth + gap), 24f);
|
||||
cardRect.sizeDelta = new Vector2(cardWidth, cardHeight);
|
||||
|
||||
var cardImage = card.GetComponent<Image>();
|
||||
cardImage.color = CardNormalColor;
|
||||
cardImages[data] = cardImage;
|
||||
|
||||
// 头像占上半部。
|
||||
GameObject iconObject = new GameObject("Icon", typeof(RectTransform), typeof(Image));
|
||||
iconObject.transform.SetParent(card.transform, false);
|
||||
var iconRect = iconObject.GetComponent<RectTransform>();
|
||||
iconRect.anchorMin = new Vector2(0.08f, 0.35f);
|
||||
iconRect.anchorMax = new Vector2(0.92f, 0.95f);
|
||||
iconRect.offsetMin = Vector2.zero;
|
||||
iconRect.offsetMax = Vector2.zero;
|
||||
var iconImage = iconObject.GetComponent<Image>();
|
||||
if (data.cardIcon != null)
|
||||
{
|
||||
iconImage.sprite = data.cardIcon;
|
||||
iconImage.preserveAspect = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
iconImage.color = new Color(0.4f, 0.4f, 0.45f, 1f);
|
||||
}
|
||||
|
||||
// 名字占底部。
|
||||
CreateChildText(card.transform, "Name", data.characterName, 26,
|
||||
new Vector2(0.05f, 0.02f), new Vector2(0.95f, 0.28f));
|
||||
|
||||
// 人口占顶部。
|
||||
CreateChildText(card.transform, "Cost", data.manaCost + "人口", 28,
|
||||
new Vector2(0.05f, 0.72f), new Vector2(0.95f, 0.98f), new Color(1f, 0.85f, 0.3f));
|
||||
|
||||
// 挂通用卡片按钮:点击进入选兵,人口不足自动置灰。
|
||||
var summonButton = card.AddComponent<SummonCardButton>();
|
||||
summonButton.characterData = data;
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateStartButton()
|
||||
{
|
||||
Canvas canvas = EnsureCanvas();
|
||||
|
||||
startButton = new GameObject("开战按钮", typeof(RectTransform), typeof(Image), typeof(Button));
|
||||
startButton.transform.SetParent(canvas.transform, false);
|
||||
|
||||
var buttonRect = startButton.GetComponent<RectTransform>();
|
||||
buttonRect.anchorMin = buttonRect.anchorMax = new Vector2(0.5f, 1f);
|
||||
buttonRect.pivot = new Vector2(0.5f, 1f);
|
||||
buttonRect.anchoredPosition = new Vector2(0f, -30f);
|
||||
buttonRect.sizeDelta = new Vector2(220f, 90f);
|
||||
|
||||
startButton.GetComponent<Image>().color = new Color(0.75f, 0.2f, 0.15f, 0.95f);
|
||||
CreateChildText(startButton.transform, "Label", "开 战", 44,
|
||||
Vector2.zero, Vector2.one, Color.white);
|
||||
|
||||
startButton.GetComponent<Button>().onClick.AddListener(StartBattle);
|
||||
|
||||
// 人口显示在开战按钮下方。
|
||||
GameObject populationObject = new GameObject("PopulationText", typeof(RectTransform), typeof(Text));
|
||||
populationObject.transform.SetParent(canvas.transform, false);
|
||||
var populationRect = populationObject.GetComponent<RectTransform>();
|
||||
populationRect.anchorMin = populationRect.anchorMax = new Vector2(0.5f, 1f);
|
||||
populationRect.pivot = new Vector2(0.5f, 1f);
|
||||
populationRect.anchoredPosition = new Vector2(0f, -130f);
|
||||
populationRect.sizeDelta = new Vector2(300f, 50f);
|
||||
|
||||
populationText = populationObject.GetComponent<Text>();
|
||||
populationText.alignment = TextAnchor.MiddleCenter;
|
||||
populationText.fontSize = 34;
|
||||
populationText.color = Color.white;
|
||||
UpdatePopulationText();
|
||||
}
|
||||
|
||||
private void CreateResultText()
|
||||
{
|
||||
Canvas canvas = EnsureCanvas();
|
||||
|
||||
GameObject resultObject = new GameObject("ResultText", typeof(RectTransform), typeof(Text));
|
||||
resultObject.transform.SetParent(canvas.transform, false);
|
||||
var resultRect = resultObject.GetComponent<RectTransform>();
|
||||
resultRect.anchorMin = resultRect.anchorMax = Vector2.one * 0.5f;
|
||||
resultRect.sizeDelta = new Vector2(600f, 160f);
|
||||
|
||||
resultText = resultObject.GetComponent<Text>();
|
||||
resultText.alignment = TextAnchor.MiddleCenter;
|
||||
resultText.fontSize = 90;
|
||||
resultText.fontStyle = FontStyle.Bold;
|
||||
resultText.color = new Color(1f, 0.9f, 0.3f);
|
||||
resultText.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
private static Text CreateChildText(Transform parent, string name, string content, int fontSize,
|
||||
Vector2 anchorMin, Vector2 anchorMax, Color? color = null)
|
||||
{
|
||||
GameObject textObject = new GameObject(name, typeof(RectTransform), typeof(Text));
|
||||
textObject.transform.SetParent(parent, false);
|
||||
|
||||
var rect = textObject.GetComponent<RectTransform>();
|
||||
rect.anchorMin = anchorMin;
|
||||
rect.anchorMax = anchorMax;
|
||||
rect.offsetMin = Vector2.zero;
|
||||
rect.offsetMax = Vector2.zero;
|
||||
|
||||
Text text = textObject.GetComponent<Text>();
|
||||
text.text = content;
|
||||
text.fontSize = fontSize;
|
||||
text.alignment = TextAnchor.MiddleCenter;
|
||||
text.color = color ?? Color.white;
|
||||
return text;
|
||||
}
|
||||
|
||||
private void RefreshCardSelection()
|
||||
{
|
||||
foreach (var pair in cardImages)
|
||||
{
|
||||
pair.Value.color = pair.Key == selectedCharacter ? CardSelectedColor : CardNormalColor;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdatePopulationText()
|
||||
{
|
||||
if (populationText != null)
|
||||
{
|
||||
populationText.text = $"人口 {usedPopulation}/{maxPopulation}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -159,7 +159,7 @@ public class BuildingPlacementManager : MonoBehaviour
|
||||
return;
|
||||
}
|
||||
|
||||
int cost = currentCharacterData != null ? currentCharacterData.buildingCost : 100;
|
||||
int cost = 100;
|
||||
if (GoldSystem.Instance == null || !GoldSystem.Instance.TrySpend(cost))
|
||||
{
|
||||
Debug.Log($"金币不足,需要{cost}");
|
||||
|
||||
@@ -22,12 +22,6 @@ public class CastleSpawnManager : MonoBehaviour
|
||||
public bool TrySpawn(CharacterData data, int teamId)
|
||||
{
|
||||
if (data == null || data.unitPrefab == null) return false;
|
||||
if (ManaSystem.Instance == null || !ManaSystem.Instance.TryConsumeMana(data.manaCost))
|
||||
{
|
||||
Debug.Log($"能量不足,需要 {data.manaCost}");
|
||||
return false;
|
||||
}
|
||||
|
||||
Transform spawnPoint = teamId == 0 ? blueSpawnPoint : redSpawnPoint;
|
||||
if (spawnPoint == null)
|
||||
{
|
||||
@@ -70,8 +64,6 @@ public class CastleSpawnManager : MonoBehaviour
|
||||
|
||||
public bool CanAfford(CharacterData data)
|
||||
{
|
||||
return data != null
|
||||
&& ManaSystem.Instance != null
|
||||
&& ManaSystem.Instance.CurrentManaInt >= data.manaCost;
|
||||
return data != null && data.unitPrefab != null;
|
||||
}
|
||||
}
|
||||
@@ -92,24 +92,11 @@ public class ChaosGateSpawner : MonoBehaviour
|
||||
|
||||
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];
|
||||
return cards[Random.Range(0, validCount)];
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
@@ -13,22 +13,9 @@ public class CharacterData : ScriptableObject
|
||||
[Header("UI 显示")]
|
||||
public Sprite cardIcon;
|
||||
|
||||
[Header("召唤配置")]
|
||||
[Header("角色基础配置")]
|
||||
public GameObject unitPrefab;
|
||||
|
||||
[Min(0)]
|
||||
public int manaCost = 1;
|
||||
|
||||
[Min(0f)]
|
||||
public float cooldownTime = 1f;
|
||||
|
||||
[Header("兵营生产")]
|
||||
[Min(0.1f)]
|
||||
public float productionInterval = 5f;
|
||||
|
||||
[Min(0)]
|
||||
public int buildingCost = 100;
|
||||
|
||||
[Header("基础属性(1级)")]
|
||||
[Min(0f)]
|
||||
public float baseMaxHealth = 100f;
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
public class DefenseWaveLevelController : MonoBehaviour
|
||||
{
|
||||
[System.Serializable]
|
||||
private class LevelWaveFile
|
||||
{
|
||||
public string levelId;
|
||||
public string levelName;
|
||||
public WaveEntry[] waves;
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
private class WaveEntry
|
||||
{
|
||||
public int waveIndex;
|
||||
public float restBeforeWave = 2f;
|
||||
public float spawnInterval = 1f;
|
||||
public string[] enemyIds;
|
||||
public int[] counts;
|
||||
public float[] spawnX;
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
public class EnemyPrefabEntry
|
||||
{
|
||||
public string id;
|
||||
public GameObject prefab;
|
||||
}
|
||||
|
||||
[Header("关卡 JSON")]
|
||||
public TextAsset levelJson;
|
||||
|
||||
[Header("JSON enemyId 对应 Prefab")]
|
||||
public EnemyPrefabEntry[] enemyPrefabs;
|
||||
|
||||
[Header("自动模拟")]
|
||||
public bool autoStart = true;
|
||||
public float spawnZ = 25f;
|
||||
public float spawnY = 0.16f;
|
||||
public float waveCompleteDelay = 3f;
|
||||
|
||||
private LevelWaveFile level;
|
||||
private readonly List<GameObject> activeEnemies = new List<GameObject>();
|
||||
private Slider progressSlider;
|
||||
private Text progressText;
|
||||
private int currentWaveIndex = -1;
|
||||
private int totalEnemies;
|
||||
private int defeatedEnemies;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (!LoadLevel()) return;
|
||||
CreateProgressBar();
|
||||
if (autoStart) StartCoroutine(RunLevel());
|
||||
}
|
||||
|
||||
private bool LoadLevel()
|
||||
{
|
||||
if (levelJson == null)
|
||||
{
|
||||
Debug.LogError("DefenseWaveLevelController: 未配置关卡 JSON。");
|
||||
return false;
|
||||
}
|
||||
|
||||
level = JsonUtility.FromJson<LevelWaveFile>(levelJson.text);
|
||||
if (level == null || level.waves == null || level.waves.Length == 0)
|
||||
{
|
||||
Debug.LogError("DefenseWaveLevelController: JSON 没有有效波次。");
|
||||
return false;
|
||||
}
|
||||
|
||||
totalEnemies = level.waves.Sum(w => TotalCount(w));
|
||||
defeatedEnemies = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
private IEnumerator RunLevel()
|
||||
{
|
||||
for (int i = 0; i < level.waves.Length; i++)
|
||||
{
|
||||
currentWaveIndex = i;
|
||||
var wave = level.waves[i];
|
||||
yield return new WaitForSeconds(Mathf.Max(0f, wave.restBeforeWave));
|
||||
yield return SpawnWave(wave);
|
||||
yield return WaitForWaveCleared();
|
||||
yield return new WaitForSeconds(Mathf.Max(0f, waveCompleteDelay));
|
||||
}
|
||||
|
||||
UpdateProgress(level.waves.Length, level.waves.Length, "通关");
|
||||
Debug.Log("DefenseWaveLevelController: 第一关波次全部完成。");
|
||||
}
|
||||
|
||||
private IEnumerator SpawnWave(WaveEntry wave)
|
||||
{
|
||||
int total = TotalCount(wave);
|
||||
int spawned = 0;
|
||||
UpdateProgress(wave.waveIndex, level.waves.Length, $"第 {wave.waveIndex} 波 0/{total}");
|
||||
|
||||
for (int typeIndex = 0; typeIndex < wave.enemyIds.Length; typeIndex++)
|
||||
{
|
||||
var prefab = FindPrefab(wave.enemyIds[typeIndex]);
|
||||
if (prefab == null) continue;
|
||||
|
||||
int count = wave.counts != null && typeIndex < wave.counts.Length ? wave.counts[typeIndex] : 0;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
float x = wave.spawnX != null && wave.spawnX.Length > 0
|
||||
? wave.spawnX[(spawned) % wave.spawnX.Length]
|
||||
: (spawned % 2 == 0 ? -3f : 3f);
|
||||
var enemy = Instantiate(prefab, new Vector3(x, spawnY, spawnZ), Quaternion.identity);
|
||||
enemy.tag = "Enemy";
|
||||
ApplyCharacterData(enemy, wave.enemyIds[typeIndex]);
|
||||
activeEnemies.Add(enemy);
|
||||
spawned++;
|
||||
UpdateProgress(wave.waveIndex, level.waves.Length, $"第 {wave.waveIndex} 波 {spawned}/{total}");
|
||||
yield return new WaitForSeconds(Mathf.Max(0.1f, wave.spawnInterval));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator WaitForWaveCleared()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
for (int i = activeEnemies.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var enemy = activeEnemies[i];
|
||||
if (enemy == null || !enemy.activeInHierarchy)
|
||||
{
|
||||
activeEnemies.RemoveAt(i);
|
||||
defeatedEnemies++;
|
||||
}
|
||||
}
|
||||
|
||||
if (activeEnemies.Count == 0) yield break;
|
||||
yield return new WaitForSeconds(0.25f);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyCharacterData(GameObject enemy, string characterId)
|
||||
{
|
||||
var data = Resources.Load<CharacterData>("Data/" + characterId);
|
||||
var unit = enemy.GetComponent<UnitBase>();
|
||||
if (data == null || unit == null) return;
|
||||
unit.InitializeFromPlayerData(data.GetMaxHealth(1), data.GetAttackPower(1), data.GetMoveSpeed(1));
|
||||
}
|
||||
|
||||
private int TotalCount(WaveEntry wave)
|
||||
{
|
||||
if (wave == null || wave.counts == null) return 0;
|
||||
return wave.counts.Sum(c => Mathf.Max(0, c));
|
||||
}
|
||||
|
||||
private GameObject FindPrefab(string id)
|
||||
{
|
||||
if (enemyPrefabs == null) return null;
|
||||
var entry = enemyPrefabs.FirstOrDefault(e => e != null && e.id == id);
|
||||
if (entry == null || entry.prefab == null)
|
||||
{
|
||||
Debug.LogError("DefenseWaveLevelController: 找不到小怪 Prefab: " + id);
|
||||
return null;
|
||||
}
|
||||
return entry.prefab;
|
||||
}
|
||||
|
||||
private void CreateProgressBar()
|
||||
{
|
||||
var canvasObject = new GameObject("WaveProgressHUD", typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster));
|
||||
var canvas = canvasObject.GetComponent<Canvas>();
|
||||
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
canvas.sortingOrder = 500;
|
||||
|
||||
var root = canvasObject.GetComponent<RectTransform>();
|
||||
var panel = new GameObject("WaveProgressPanel", typeof(RectTransform), typeof(Image));
|
||||
panel.transform.SetParent(root, false);
|
||||
var panelRect = panel.GetComponent<RectTransform>();
|
||||
panelRect.anchorMin = new Vector2(0.5f, 1f);
|
||||
panelRect.anchorMax = new Vector2(0.5f, 1f);
|
||||
panelRect.pivot = new Vector2(0.5f, 1f);
|
||||
panelRect.anchoredPosition = new Vector2(0f, -20f);
|
||||
panelRect.sizeDelta = new Vector2(420f, 64f);
|
||||
panel.GetComponent<Image>().color = new Color(0f, 0f, 0f, 0.45f);
|
||||
|
||||
var sliderObject = new GameObject("WaveProgress", typeof(RectTransform), typeof(Slider));
|
||||
sliderObject.transform.SetParent(panel.transform, false);
|
||||
var sliderRect = sliderObject.GetComponent<RectTransform>();
|
||||
sliderRect.anchorMin = new Vector2(0.5f, 0f);
|
||||
sliderRect.anchorMax = new Vector2(0.5f, 0f);
|
||||
sliderRect.pivot = new Vector2(0.5f, 0f);
|
||||
sliderRect.anchoredPosition = new Vector2(0f, 10f);
|
||||
sliderRect.sizeDelta = new Vector2(360f, 16f);
|
||||
progressSlider = sliderObject.GetComponent<Slider>();
|
||||
progressSlider.minValue = 0f;
|
||||
progressSlider.maxValue = 1f;
|
||||
progressSlider.value = 0f;
|
||||
progressSlider.interactable = false;
|
||||
|
||||
var textObject = new GameObject("WaveProgressText", typeof(RectTransform), typeof(Text));
|
||||
textObject.transform.SetParent(panel.transform, false);
|
||||
var textRect = textObject.GetComponent<RectTransform>();
|
||||
textRect.anchorMin = new Vector2(0.5f, 1f);
|
||||
textRect.anchorMax = new Vector2(0.5f, 1f);
|
||||
textRect.pivot = new Vector2(0.5f, 1f);
|
||||
textRect.anchoredPosition = new Vector2(0f, -8f);
|
||||
textRect.sizeDelta = new Vector2(390f, 28f);
|
||||
progressText = textObject.GetComponent<Text>();
|
||||
progressText.alignment = TextAnchor.MiddleCenter;
|
||||
progressText.font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
|
||||
progressText.fontSize = 18;
|
||||
progressText.color = Color.white;
|
||||
}
|
||||
|
||||
private void UpdateProgress(int waveNumber, int waveCount, string text)
|
||||
{
|
||||
if (progressSlider != null)
|
||||
{
|
||||
float defeatedRatio = totalEnemies > 0 ? (float)defeatedEnemies / totalEnemies : 0f;
|
||||
progressSlider.value = Mathf.Clamp01((waveNumber - 1 + defeatedRatio) / Mathf.Max(1, waveCount));
|
||||
}
|
||||
if (progressText != null) progressText.text = text;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: XXob4XilUXnOhMYzExpqM+4zrmqzWWfAaYPtwHjVN2gIEPjdfifecQI=
|
||||
guid: DigavSykVirvFLHuKxjXELl00pWlLKlkL08/kmuQbvSZLhiGeAqKyrY=
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
@@ -0,0 +1,69 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// 部署网格的几何参数与坐标换算。
|
||||
/// 阵型编排场景和战斗场景共用同一份参数,保证两边的格位完全对齐。
|
||||
/// </summary>
|
||||
public static class DeployGrid
|
||||
{
|
||||
public const int Cols = 6;
|
||||
public const int Rows = 5;
|
||||
public const float ColStep = 2.8f;
|
||||
public const float RowStep = 3.0f;
|
||||
public const float BackZ = 2.5f; // 最靠后一排的中心 Z(让部署区落在底部卡片栏上方)
|
||||
public const float CellWidth = 2.5f;
|
||||
public const float CellDepth = 2.7f;
|
||||
public const float CellHeight = 0.03f;
|
||||
public const float GridY = 0.12f; // 格位可视块的中心高度
|
||||
public const float UnitY = 0.16f; // 单位落点高度(与 NavMesh 高度一致)
|
||||
|
||||
public const int CellCount = Cols * Rows;
|
||||
|
||||
public static int ToIndex(int row, int col) => row * Cols + col;
|
||||
public static int RowOf(int index) => index / Cols;
|
||||
public static int ColOf(int index) => index % Cols;
|
||||
public static bool IsValidIndex(int index) => index >= 0 && index < CellCount;
|
||||
|
||||
/// <summary>格位中心的世界坐标(单位落点高度)。</summary>
|
||||
public static Vector3 CellPosition(int index)
|
||||
{
|
||||
int r = RowOf(index);
|
||||
int c = ColOf(index);
|
||||
float x = (c - (Cols - 1) * 0.5f) * ColStep;
|
||||
float z = BackZ + r * RowStep;
|
||||
return new Vector3(x, UnitY, z);
|
||||
}
|
||||
|
||||
/// <summary>把地面上的世界坐标吸附到最近格位;不在网格范围内返回 -1。</summary>
|
||||
public static int SnapToCell(Vector3 world)
|
||||
{
|
||||
int c = Mathf.RoundToInt(world.x / ColStep + (Cols - 1) * 0.5f);
|
||||
int r = Mathf.RoundToInt((world.z - BackZ) / RowStep);
|
||||
if (c < 0 || c >= Cols || r < 0 || r >= Rows)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
Vector3 center = CellPosition(ToIndex(r, c));
|
||||
if (Mathf.Abs(center.x - world.x) > ColStep * 0.5f) return -1;
|
||||
if (Mathf.Abs(center.z - world.z) > RowStep * 0.5f) return -1;
|
||||
return ToIndex(r, c);
|
||||
}
|
||||
|
||||
/// <summary>把一组单位在网格宽度内均匀铺开,返回列号。</summary>
|
||||
public static int SpreadColumn(int indexInGroup, int groupCount)
|
||||
{
|
||||
if (groupCount <= 1) return Cols / 2;
|
||||
if (groupCount >= Cols) return indexInGroup % Cols;
|
||||
|
||||
float step = (Cols - 1f) / (groupCount + 1f);
|
||||
return Mathf.RoundToInt(step * (indexInGroup + 1));
|
||||
}
|
||||
|
||||
/// <summary>默认阵型格位:近战在最前排(靠近敌人),远程在最后排。</summary>
|
||||
public static int DefaultCell(bool isMelee, int indexInGroup, int groupCount)
|
||||
{
|
||||
int row = isMelee ? Rows - 1 : 0;
|
||||
return ToIndex(row, SpreadColumn(indexInGroup, groupCount));
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: DCtO4X/+USoIwkGoI7+2kj00a9R1yMRAYBXPILcLhDHrIqmIIORGtp8=
|
||||
guid: C3pKtSitVSquK/sQ7Eb3i3YczecXTDqEUKZN0UN+6S6o0f7Nzl8IZHk=
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
@@ -76,7 +76,7 @@ public class EnemyCastleAI : MonoBehaviour
|
||||
if (card == null) return;
|
||||
if (Spawn(card, lane))
|
||||
{
|
||||
currentMana -= card.manaCost;
|
||||
currentMana -= 1f;
|
||||
RotatePlayedCard(card);
|
||||
spawnTimer = minimumSpawnInterval;
|
||||
if (lane == lastLane) consecutiveLaneSpawns++;
|
||||
@@ -100,33 +100,23 @@ public class EnemyCastleAI : MonoBehaviour
|
||||
List<CharacterData> affordable = new List<CharacterData>();
|
||||
foreach (CharacterData card in hand)
|
||||
{
|
||||
if (card != null && card.unitPrefab != null && card.manaCost <= currentMana)
|
||||
if (card != null && card.unitPrefab != null)
|
||||
affordable.Add(card);
|
||||
}
|
||||
|
||||
if (affordable.Count == 0) return false;
|
||||
|
||||
CharacterData selected = SelectWeightedCard(affordable);
|
||||
CharacterData selected = affordable[Random.Range(0, affordable.Count)];
|
||||
if (!SpawnAt(selected, spawnPosition, spawnRotation, teamId)) return false;
|
||||
|
||||
currentMana -= selected.manaCost;
|
||||
currentMana -= 1f;
|
||||
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];
|
||||
return cards == null || cards.Count == 0 ? null : cards[Random.Range(0, cards.Count)];
|
||||
}
|
||||
|
||||
private int ChooseLane()
|
||||
@@ -181,11 +171,8 @@ public class EnemyCastleAI : MonoBehaviour
|
||||
float bestScore = float.NegativeInfinity;
|
||||
foreach (var card in hand)
|
||||
{
|
||||
if (card == null || card.unitPrefab == null || card.manaCost > currentMana) continue;
|
||||
if (card == null || card.unitPrefab == null) 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;
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
|
||||
/// <summary>
|
||||
/// 底部英雄卡片的拖拽:拖到网格格位上放置英雄。
|
||||
/// 吸附与放置由 FormationManager 负责,卡片只负责拖动表现和回调。
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(CanvasGroup))]
|
||||
public class FormationCardDrag : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
|
||||
{
|
||||
[Header("界面引用")]
|
||||
public Image background;
|
||||
public Image icon;
|
||||
public Text nameText;
|
||||
|
||||
[HideInInspector] public FormationManager manager;
|
||||
[HideInInspector] public int heroIndex;
|
||||
|
||||
private RectTransform rectTransform;
|
||||
private CanvasGroup canvasGroup;
|
||||
private Vector2 homePosition;
|
||||
|
||||
private static readonly Color NormalColor = new Color(0.15f, 0.15f, 0.2f, 0.92f);
|
||||
private static readonly Color PlacedColor = new Color(0.18f, 0.18f, 0.22f, 0.45f);
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
rectTransform = GetComponent<RectTransform>();
|
||||
canvasGroup = GetComponent<CanvasGroup>();
|
||||
homePosition = rectTransform.anchoredPosition;
|
||||
}
|
||||
|
||||
/// <summary>填入名字与头像。</summary>
|
||||
public void SetContent(string displayName, Sprite iconSprite)
|
||||
{
|
||||
if (nameText != null) nameText.text = displayName;
|
||||
if (icon != null)
|
||||
{
|
||||
if (iconSprite != null)
|
||||
{
|
||||
icon.sprite = iconSprite;
|
||||
icon.preserveAspect = true;
|
||||
icon.color = Color.white;
|
||||
}
|
||||
else
|
||||
{
|
||||
icon.color = new Color(0.4f, 0.4f, 0.45f, 1f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>已放置的卡片置灰并禁止再次拖拽。</summary>
|
||||
public void SetPlaced(bool isPlaced)
|
||||
{
|
||||
if (background != null) background.color = isPlaced ? PlacedColor : NormalColor;
|
||||
canvasGroup.alpha = isPlaced ? 0.55f : 1f;
|
||||
}
|
||||
|
||||
public void OnBeginDrag(PointerEventData eventData)
|
||||
{
|
||||
if (manager == null || !manager.CanDrag(heroIndex))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
canvasGroup.blocksRaycasts = false;
|
||||
rectTransform.position = eventData.position;
|
||||
manager.OnCardDragStart(heroIndex);
|
||||
}
|
||||
|
||||
public void OnDrag(PointerEventData eventData)
|
||||
{
|
||||
if (manager == null || !manager.CanDrag(heroIndex))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
rectTransform.position = eventData.position;
|
||||
manager.OnCardDragging(heroIndex, eventData.position);
|
||||
}
|
||||
|
||||
public void OnEndDrag(PointerEventData eventData)
|
||||
{
|
||||
canvasGroup.blocksRaycasts = true;
|
||||
|
||||
if (manager == null || !manager.CanDrag(heroIndex))
|
||||
{
|
||||
rectTransform.anchoredPosition = homePosition;
|
||||
return;
|
||||
}
|
||||
|
||||
rectTransform.anchoredPosition = homePosition;
|
||||
manager.OnCardDragEnd(heroIndex, eventData.position);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: CigetXv+VSpp9gA0m88lS7VLDwIk2XT3cg+F1ub+vqbAzjQne5UPqKQ=
|
||||
guid: Cn1L5Hv5UH9/CqdBWvIW50FTi5P+1x+AwqHwUMoKIYzAIyHnYChHwBk=
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
@@ -0,0 +1,24 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// 阵型数据:阵型编排场景写入,战斗场景读取。
|
||||
/// 静态字段在同一个运行会话内跨场景保留;重启游戏会回到「默认阵型」。
|
||||
/// </summary>
|
||||
public static class FormationData
|
||||
{
|
||||
/// <summary>出战英雄的 Prefab(按卡位顺序)。</summary>
|
||||
public static GameObject[] HeroPrefabs = new GameObject[0];
|
||||
|
||||
/// <summary>每个英雄所在的格位索引(-1 表示未放置)。</summary>
|
||||
public static int[] CellIndices = new int[0];
|
||||
|
||||
/// <summary>玩家是否手动编排过阵型。</summary>
|
||||
public static bool HasCustomFormation;
|
||||
|
||||
public static void Clear()
|
||||
{
|
||||
HeroPrefabs = new GameObject[0];
|
||||
CellIndices = new int[0];
|
||||
HasCustomFormation = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: XCgW4yL7VHwdwptg/AYrWGWdmJaYnd7aOzP109HLCYYTIzSnnR4597U=
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,419 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEngine.UI;
|
||||
|
||||
/// <summary>
|
||||
/// 阵型编排页控制器:底部英雄卡拖到网格格位放置英雄,「开始战斗」保存阵型并进入战斗场景。
|
||||
/// 什么都不摆直接开始 = 默认阵型(近战最前排 / 远程最后排)。
|
||||
/// </summary>
|
||||
public class FormationManager : MonoBehaviour
|
||||
{
|
||||
[Header("出战英雄名单")]
|
||||
public HeroRoster roster;
|
||||
|
||||
[Header("战斗场景")]
|
||||
public string battleSceneName = "DefenseTest";
|
||||
|
||||
[Header("界面引用")]
|
||||
public Text hintText;
|
||||
public Button defaultButton;
|
||||
public Button startButton;
|
||||
public FormationCardDrag[] cards = new FormationCardDrag[0];
|
||||
|
||||
// 格位索引 -> 放置记录
|
||||
private class Placement
|
||||
{
|
||||
public int heroIndex;
|
||||
public GameObject preview;
|
||||
}
|
||||
|
||||
private readonly Dictionary<int, Placement> placed = new Dictionary<int, Placement>();
|
||||
private readonly HashSet<int> placedHeroes = new HashSet<int>();
|
||||
|
||||
private Camera mainCamera;
|
||||
private GameObject dropHighlight;
|
||||
private Material highlightMaterial;
|
||||
private int draggingHero = -1;
|
||||
|
||||
private static readonly Color ValidColor = new Color(0.2f, 1f, 0.35f, 0.40f);
|
||||
private static readonly Color InvalidColor = new Color(1f, 0.25f, 0.2f, 0.40f);
|
||||
|
||||
private void Start()
|
||||
{
|
||||
mainCamera = Camera.main;
|
||||
CreateDropHighlight();
|
||||
SetupCards();
|
||||
|
||||
if (defaultButton != null) defaultButton.onClick.AddListener(ApplyDefaultFormation);
|
||||
if (startButton != null) startButton.onClick.AddListener(StartBattle);
|
||||
|
||||
UpdateHint();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (draggingHero >= 0)
|
||||
{
|
||||
UpdateDropHighlight(Input.mousePosition);
|
||||
}
|
||||
|
||||
// 点击场上的英雄把它收回卡片
|
||||
if (Input.GetMouseButtonDown(0) && !IsPointerOverUI())
|
||||
{
|
||||
TryRemoveAtPointer();
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 卡片初始化 =====
|
||||
|
||||
private void SetupCards()
|
||||
{
|
||||
if (cards == null) return;
|
||||
|
||||
for (int i = 0; i < cards.Length; i++)
|
||||
{
|
||||
FormationCardDrag card = cards[i];
|
||||
if (card == null) continue;
|
||||
|
||||
card.manager = this;
|
||||
card.heroIndex = i;
|
||||
|
||||
HeroEntry entry = GetHero(i);
|
||||
card.SetContent(entry != null ? entry.displayName : $"英雄{i + 1}",
|
||||
entry != null ? entry.icon : null);
|
||||
card.SetPlaced(false);
|
||||
}
|
||||
}
|
||||
|
||||
private HeroEntry GetHero(int index)
|
||||
{
|
||||
if (roster == null || roster.heroes == null) return null;
|
||||
if (index < 0 || index >= roster.heroes.Length) return null;
|
||||
return roster.heroes[index];
|
||||
}
|
||||
|
||||
private int HeroCount => roster != null && roster.heroes != null ? roster.heroes.Length : 0;
|
||||
|
||||
// ===== 拖拽回调(由 FormationCardDrag 调用) =====
|
||||
|
||||
public bool CanDrag(int heroIndex)
|
||||
{
|
||||
return GetHero(heroIndex) != null && !placedHeroes.Contains(heroIndex);
|
||||
}
|
||||
|
||||
public void OnCardDragStart(int heroIndex)
|
||||
{
|
||||
draggingHero = heroIndex;
|
||||
UpdateDropHighlight(Input.mousePosition);
|
||||
}
|
||||
|
||||
public void OnCardDragging(int heroIndex, Vector2 screenPosition)
|
||||
{
|
||||
draggingHero = heroIndex;
|
||||
UpdateDropHighlight(screenPosition);
|
||||
}
|
||||
|
||||
public void OnCardDragEnd(int heroIndex, Vector2 screenPosition)
|
||||
{
|
||||
draggingHero = -1;
|
||||
if (dropHighlight != null) dropHighlight.SetActive(false);
|
||||
|
||||
int cell = ScreenToCell(screenPosition);
|
||||
if (cell < 0)
|
||||
{
|
||||
return; // 没拖到网格范围内,忽略
|
||||
}
|
||||
|
||||
if (placed.ContainsKey(cell))
|
||||
{
|
||||
Debug.Log("这个格位已经有英雄了");
|
||||
return;
|
||||
}
|
||||
|
||||
PlaceHero(heroIndex, cell);
|
||||
}
|
||||
|
||||
// ===== 放置 / 移除 =====
|
||||
|
||||
private void PlaceHero(int heroIndex, int cell)
|
||||
{
|
||||
HeroEntry entry = GetHero(heroIndex);
|
||||
if (entry == null || entry.prefab == null || !DeployGrid.IsValidIndex(cell)) return;
|
||||
if (placed.ContainsKey(cell)) return;
|
||||
|
||||
GameObject preview = Instantiate(entry.prefab, DeployGrid.CellPosition(cell), Quaternion.identity);
|
||||
preview.name = $"预览_{entry.displayName}";
|
||||
PreparePreview(preview);
|
||||
|
||||
placed[cell] = new Placement { heroIndex = heroIndex, preview = preview };
|
||||
placedHeroes.Add(heroIndex);
|
||||
|
||||
if (cards != null && heroIndex < cards.Length && cards[heroIndex] != null)
|
||||
{
|
||||
cards[heroIndex].SetPlaced(true);
|
||||
}
|
||||
|
||||
UpdateHint();
|
||||
}
|
||||
|
||||
/// <summary>预览体只做展示:关掉 AI 与寻路,避免它自己跑起来。</summary>
|
||||
private static void PreparePreview(GameObject preview)
|
||||
{
|
||||
var unitBase = preview.GetComponent<UnitBase>();
|
||||
if (unitBase != null) unitBase.enabled = false;
|
||||
|
||||
var agent = preview.GetComponent<UnityEngine.AI.NavMeshAgent>();
|
||||
if (agent != null) agent.enabled = false;
|
||||
|
||||
var status = preview.GetComponent<StatusEffectController>();
|
||||
if (status != null) status.enabled = false;
|
||||
|
||||
var animator = preview.GetComponent<Animator>();
|
||||
if (animator != null) animator.applyRootMotion = false;
|
||||
}
|
||||
|
||||
private bool TryRemoveAtPointer()
|
||||
{
|
||||
if (mainCamera == null)
|
||||
{
|
||||
mainCamera = Camera.main;
|
||||
if (mainCamera == null) return false;
|
||||
}
|
||||
|
||||
Ray ray = mainCamera.ScreenPointToRay(Input.mousePosition);
|
||||
RaycastHit[] hits = Physics.RaycastAll(ray, 200f);
|
||||
|
||||
GameObject closest = null;
|
||||
float closestDistance = float.MaxValue;
|
||||
foreach (RaycastHit hit in hits)
|
||||
{
|
||||
GameObject root = hit.collider.transform.root.gameObject;
|
||||
if (IsPreview(root) && hit.distance < closestDistance)
|
||||
{
|
||||
closestDistance = hit.distance;
|
||||
closest = root;
|
||||
}
|
||||
}
|
||||
|
||||
if (closest == null) return false;
|
||||
|
||||
RemovePreview(closest);
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool IsPreview(GameObject candidate)
|
||||
{
|
||||
foreach (var pair in placed)
|
||||
{
|
||||
if (pair.Value.preview == candidate) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void RemovePreview(GameObject preview)
|
||||
{
|
||||
int cell = -1;
|
||||
foreach (var pair in placed)
|
||||
{
|
||||
if (pair.Value.preview == preview)
|
||||
{
|
||||
cell = pair.Key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (cell < 0) return;
|
||||
|
||||
int heroIndex = placed[cell].heroIndex;
|
||||
placed.Remove(cell);
|
||||
placedHeroes.Remove(heroIndex);
|
||||
|
||||
if (cards != null && heroIndex < cards.Length && cards[heroIndex] != null)
|
||||
{
|
||||
cards[heroIndex].SetPlaced(false);
|
||||
}
|
||||
|
||||
Destroy(preview);
|
||||
UpdateHint();
|
||||
}
|
||||
|
||||
private void ClearAll()
|
||||
{
|
||||
foreach (var pair in placed)
|
||||
{
|
||||
if (pair.Value.preview != null) Destroy(pair.Value.preview);
|
||||
}
|
||||
placed.Clear();
|
||||
placedHeroes.Clear();
|
||||
|
||||
if (cards != null)
|
||||
{
|
||||
foreach (FormationCardDrag card in cards)
|
||||
{
|
||||
if (card != null) card.SetPlaced(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 默认阵型 =====
|
||||
|
||||
public void ApplyDefaultFormation()
|
||||
{
|
||||
ClearAll();
|
||||
|
||||
int meleeTotal = 0, rangedTotal = 0;
|
||||
for (int i = 0; i < HeroCount; i++)
|
||||
{
|
||||
HeroEntry entry = GetHero(i);
|
||||
if (entry == null) continue;
|
||||
if (entry.isMelee) meleeTotal++; else rangedTotal++;
|
||||
}
|
||||
|
||||
int meleeIndex = 0, rangedIndex = 0;
|
||||
for (int i = 0; i < HeroCount; i++)
|
||||
{
|
||||
HeroEntry entry = GetHero(i);
|
||||
if (entry == null) continue;
|
||||
|
||||
int cell = entry.isMelee
|
||||
? DeployGrid.DefaultCell(true, meleeIndex++, meleeTotal)
|
||||
: DeployGrid.DefaultCell(false, rangedIndex++, rangedTotal);
|
||||
PlaceHero(i, cell);
|
||||
}
|
||||
|
||||
UpdateHint();
|
||||
}
|
||||
|
||||
/// <summary>没摆放的英雄补到默认位置,保证开战时全部出战。</summary>
|
||||
private void FillUnplacedWithDefault()
|
||||
{
|
||||
for (int i = 0; i < HeroCount; i++)
|
||||
{
|
||||
HeroEntry entry = GetHero(i);
|
||||
if (entry == null || placedHeroes.Contains(i)) continue;
|
||||
|
||||
int row = entry.isMelee ? DeployGrid.Rows - 1 : 0;
|
||||
int cell = FindFreeCellInRow(row);
|
||||
if (cell < 0) cell = FindFreeCellInRow(DeployGrid.Rows / 2);
|
||||
if (cell >= 0) PlaceHero(i, cell);
|
||||
}
|
||||
}
|
||||
|
||||
private int FindFreeCellInRow(int row)
|
||||
{
|
||||
for (int c = 0; c < DeployGrid.Cols; c++)
|
||||
{
|
||||
int index = DeployGrid.ToIndex(row, c);
|
||||
if (!placed.ContainsKey(index)) return index;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// ===== 进入战斗 =====
|
||||
|
||||
public void StartBattle()
|
||||
{
|
||||
FillUnplacedWithDefault();
|
||||
SaveFormation();
|
||||
|
||||
if (!string.IsNullOrEmpty(battleSceneName))
|
||||
{
|
||||
SceneManager.LoadScene(battleSceneName);
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveFormation()
|
||||
{
|
||||
int count = HeroCount;
|
||||
var prefabs = new GameObject[count];
|
||||
var cells = new int[count];
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
HeroEntry entry = GetHero(i);
|
||||
prefabs[i] = entry != null ? entry.prefab : null;
|
||||
cells[i] = -1;
|
||||
}
|
||||
|
||||
foreach (var pair in placed)
|
||||
{
|
||||
int heroIndex = pair.Value.heroIndex;
|
||||
if (heroIndex >= 0 && heroIndex < count)
|
||||
{
|
||||
cells[heroIndex] = pair.Key;
|
||||
}
|
||||
}
|
||||
|
||||
FormationData.HeroPrefabs = prefabs;
|
||||
FormationData.CellIndices = cells;
|
||||
FormationData.HasCustomFormation = placed.Count > 0;
|
||||
}
|
||||
|
||||
// ===== 落点高亮与坐标换算 =====
|
||||
|
||||
private void CreateDropHighlight()
|
||||
{
|
||||
dropHighlight = GameObject.CreatePrimitive(PrimitiveType.Quad);
|
||||
dropHighlight.name = "DropHighlight";
|
||||
Destroy(dropHighlight.GetComponent<Collider>());
|
||||
dropHighlight.transform.rotation = Quaternion.Euler(90f, 0f, 0f);
|
||||
dropHighlight.transform.localScale = new Vector3(DeployGrid.CellWidth * 0.95f, DeployGrid.CellDepth * 0.95f, 1f);
|
||||
|
||||
highlightMaterial = new Material(Shader.Find("Sprites/Default"));
|
||||
dropHighlight.GetComponent<Renderer>().sharedMaterial = highlightMaterial;
|
||||
dropHighlight.SetActive(false);
|
||||
}
|
||||
|
||||
private void UpdateDropHighlight(Vector2 screenPosition)
|
||||
{
|
||||
if (dropHighlight == null) return;
|
||||
|
||||
int cell = ScreenToCell(screenPosition);
|
||||
if (cell < 0)
|
||||
{
|
||||
dropHighlight.SetActive(false);
|
||||
return;
|
||||
}
|
||||
|
||||
dropHighlight.SetActive(true);
|
||||
Vector3 position = DeployGrid.CellPosition(cell);
|
||||
dropHighlight.transform.position = new Vector3(position.x, DeployGrid.GridY + 0.02f, position.z);
|
||||
highlightMaterial.color = placed.ContainsKey(cell) ? InvalidColor : ValidColor;
|
||||
}
|
||||
|
||||
/// <summary>屏幕坐标 -> 格位索引(用地平面求交,不受场上单位碰撞体干扰)。</summary>
|
||||
private int ScreenToCell(Vector2 screenPosition)
|
||||
{
|
||||
if (mainCamera == null)
|
||||
{
|
||||
mainCamera = Camera.main;
|
||||
if (mainCamera == null) return -1;
|
||||
}
|
||||
|
||||
Ray ray = mainCamera.ScreenPointToRay(screenPosition);
|
||||
var groundPlane = new Plane(Vector3.up, new Vector3(0f, DeployGrid.UnitY, 0f));
|
||||
if (!groundPlane.Raycast(ray, out float distance)) return -1;
|
||||
|
||||
return DeployGrid.SnapToCell(ray.GetPoint(distance));
|
||||
}
|
||||
|
||||
private static bool IsPointerOverUI()
|
||||
{
|
||||
return EventSystem.current != null && EventSystem.current.IsPointerOverGameObject();
|
||||
}
|
||||
|
||||
private void UpdateHint()
|
||||
{
|
||||
if (hintText == null) return;
|
||||
|
||||
if (placed.Count == 0)
|
||||
{
|
||||
hintText.text = "把下面的英雄卡拖到网格上摆放;不摆放也可以,直接开战 = 默认阵型(近战前排 / 远程后排)";
|
||||
}
|
||||
else
|
||||
{
|
||||
hintText.text = $"已摆放 {placed.Count}/{HeroCount} 点击场上的英雄可以收回";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: Cnkc5iKsUHoNF8IxniUVHSg050NRdXwXVA3C45jSWBpu1mOI1TGUcgI=
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,28 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>一名出战英雄的配置。</summary>
|
||||
[System.Serializable]
|
||||
public class HeroEntry
|
||||
{
|
||||
[Tooltip("显示在卡片上的名字")]
|
||||
public string displayName;
|
||||
|
||||
[Tooltip("战斗场景里生成的单位 Prefab")]
|
||||
public GameObject prefab;
|
||||
|
||||
[Tooltip("卡片头像,可为空")]
|
||||
public Sprite icon;
|
||||
|
||||
[Tooltip("近战 = 默认阵型时放在前排")]
|
||||
public bool isMelee;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 出战英雄名单:阵型编排场景和战斗场景共用同一份数据资产。
|
||||
/// 后续接入卡组系统后,由卡组编辑页写入这里。
|
||||
/// </summary>
|
||||
[CreateAssetMenu(fileName = "HeroRoster", menuName = "召唤之战/出战英雄名单")]
|
||||
public class HeroRoster : ScriptableObject
|
||||
{
|
||||
public HeroEntry[] heroes = new HeroEntry[0];
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: WStJtXutVnupM5EaWNmSpJzxL7igHY0WxYx+ztP7uTtMEbVRGFGKhyk=
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -198,11 +198,6 @@ public class MainCharacterCombat : MonoBehaviour
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ManaSystem.Instance == null || !ManaSystem.Instance.TryConsumeMana(characterData.manaCost))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
PlayerCharacterData playerData = PlayerDataManager.Instance != null
|
||||
? PlayerDataManager.Instance.GetDeckCharacter(index)
|
||||
: null;
|
||||
|
||||
@@ -1,14 +1,30 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
[System.Serializable]
|
||||
public class PlayerOwnedCharacterJson
|
||||
{
|
||||
public string characterId;
|
||||
public int level = 1;
|
||||
public int starLevel = 0;
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
public class PlayerSaveJson
|
||||
{
|
||||
public string playerId = "local_player";
|
||||
public PlayerOwnedCharacterJson[] unlockedCharacters = new PlayerOwnedCharacterJson[0];
|
||||
public string[] currentDeck = new string[0];
|
||||
}
|
||||
|
||||
public class PlayerDataManager : MonoBehaviour
|
||||
{
|
||||
public static PlayerDataManager Instance { get; private set; }
|
||||
|
||||
[Header("玩家拥有的角色(临时写死)")]
|
||||
public List<PlayerCharacterData> ownedCharacters = new List<PlayerCharacterData>();
|
||||
[Header("本地玩家 JSON(正式版替换为服务器响应)")]
|
||||
public TextAsset localPlayerJson;
|
||||
|
||||
[Header("当前卡组(临时写死)")]
|
||||
public List<PlayerCharacterData> ownedCharacters = new List<PlayerCharacterData>();
|
||||
public PlayerCharacterData[] currentDeck = new PlayerCharacterData[5];
|
||||
|
||||
private void Awake()
|
||||
@@ -19,6 +35,40 @@ public class PlayerDataManager : MonoBehaviour
|
||||
return;
|
||||
}
|
||||
Instance = this;
|
||||
InitializeFromJsonOrFallback();
|
||||
}
|
||||
|
||||
private void InitializeFromJsonOrFallback()
|
||||
{
|
||||
TextAsset json = localPlayerJson != null
|
||||
? localPlayerJson
|
||||
: Resources.Load<TextAsset>("PlayerData");
|
||||
|
||||
if (json != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var save = JsonUtility.FromJson<PlayerSaveJson>(json.text);
|
||||
if (save != null && save.unlockedCharacters != null)
|
||||
{
|
||||
ownedCharacters.Clear();
|
||||
foreach (var entry in save.unlockedCharacters)
|
||||
{
|
||||
if (entry == null || string.IsNullOrEmpty(entry.characterId)) continue;
|
||||
ownedCharacters.Add(new PlayerCharacterData(entry.characterId, Mathf.Max(1, entry.level), Mathf.Max(0, entry.starLevel)));
|
||||
}
|
||||
|
||||
currentDeck = new PlayerCharacterData[5];
|
||||
for (int i = 0; i < save.currentDeck.Length && i < currentDeck.Length; i++)
|
||||
currentDeck[i] = GetCharacterData(save.currentDeck[i]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Debug.LogError("玩家 JSON 解析失败:" + e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
InitializeTempData();
|
||||
}
|
||||
@@ -26,12 +76,15 @@ public class PlayerDataManager : MonoBehaviour
|
||||
private void InitializeTempData()
|
||||
{
|
||||
ownedCharacters.Clear();
|
||||
|
||||
ownedCharacters.Add(new PlayerCharacterData("human_soldier", 1, 0));
|
||||
ownedCharacters.Add(new PlayerCharacterData("archer", 1, 0));
|
||||
ownedCharacters.Add(new PlayerCharacterData("shield_guard", 1, 0));
|
||||
ownedCharacters.Add(new PlayerCharacterData("mage", 1, 0));
|
||||
ownedCharacters.Add(new PlayerCharacterData("elite_soldier", 1, 0));
|
||||
|
||||
currentDeck[0] = ownedCharacters[0];
|
||||
currentDeck[1] = ownedCharacters[1];
|
||||
currentDeck = new PlayerCharacterData[5];
|
||||
for (int i = 0; i < currentDeck.Length && i < ownedCharacters.Count; i++)
|
||||
currentDeck[i] = ownedCharacters[i];
|
||||
}
|
||||
|
||||
public PlayerCharacterData GetCharacterData(string characterId)
|
||||
@@ -41,20 +94,13 @@ public class PlayerDataManager : MonoBehaviour
|
||||
|
||||
public PlayerCharacterData GetDeckCharacter(int slot)
|
||||
{
|
||||
if (slot < 0 || slot >= currentDeck.Length)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (slot < 0 || slot >= currentDeck.Length) return null;
|
||||
return currentDeck[slot];
|
||||
}
|
||||
|
||||
public void UpgradeCharacter(string characterId)
|
||||
{
|
||||
var character = GetCharacterData(characterId);
|
||||
if (character != null)
|
||||
{
|
||||
character.level++;
|
||||
Debug.Log($"角色 {characterId} 升级到 {character.level} 级");
|
||||
}
|
||||
if (character != null) character.level++;
|
||||
}
|
||||
}
|
||||
@@ -63,7 +63,10 @@ public class StatusEffectController : MonoBehaviour
|
||||
stunnedRemaining = 0f;
|
||||
if (animator != null)
|
||||
{
|
||||
animator.speed = Mathf.Max(0.01f, animatorSpeedBeforeStun);
|
||||
UnitBase unitBase = GetComponent<UnitBase>();
|
||||
animator.speed = unitBase != null
|
||||
? Mathf.Max(0.01f, unitBase.AttackSpeedMultiplier)
|
||||
: 1f;
|
||||
}
|
||||
DestroyIndicator();
|
||||
}
|
||||
|
||||
@@ -1,286 +0,0 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
[RequireComponent(typeof(Button))]
|
||||
public class SummonCardButton : MonoBehaviour
|
||||
{
|
||||
[Header("角色数据")]
|
||||
public CharacterData characterData;
|
||||
|
||||
[Header("卡槽配置")]
|
||||
[Min(1)]
|
||||
public int summonSlot = 1;
|
||||
|
||||
[Header("UI 引用")]
|
||||
public Image cooldownMask;
|
||||
public Text cooldownText;
|
||||
public Image cardIcon;
|
||||
public Text characterNameText;
|
||||
public Text manaCostText;
|
||||
public Image costNumberImage; // 费用数字图片
|
||||
|
||||
[Header("费用数字精灵")]
|
||||
public Sprite[] costNumberSprites; // 数字_0 到 数字_8,对应费用 1-9
|
||||
|
||||
private Button button;
|
||||
private MainCharacterCombat combat;
|
||||
private float cooldownTimer;
|
||||
private bool isOnCooldown;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
button = GetComponent<Button>();
|
||||
combat = FindObjectOfType<MainCharacterCombat>();
|
||||
button.onClick.AddListener(OnClick);
|
||||
|
||||
isOnCooldown = false;
|
||||
cooldownTimer = 0f;
|
||||
|
||||
UpdateCardUI();
|
||||
UpdateCooldownUI();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (button != null)
|
||||
{
|
||||
button.onClick.RemoveListener(OnClick);
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (isOnCooldown)
|
||||
{
|
||||
cooldownTimer -= Time.deltaTime;
|
||||
if (cooldownTimer <= 0f)
|
||||
{
|
||||
isOnCooldown = false;
|
||||
cooldownTimer = 0f;
|
||||
}
|
||||
|
||||
UpdateCooldownUI();
|
||||
}
|
||||
|
||||
UpdateButtonState();
|
||||
}
|
||||
|
||||
public void SetDeckCharacter(CharacterData data)
|
||||
{
|
||||
characterData = data;
|
||||
isOnCooldown = false;
|
||||
cooldownTimer = 0f;
|
||||
ClearCardUI();
|
||||
UpdateCardUI();
|
||||
UpdateCooldownUI();
|
||||
UpdateButtonState();
|
||||
}
|
||||
|
||||
private void ClearCardUI()
|
||||
{
|
||||
if (cardIcon != null) cardIcon.sprite = null;
|
||||
if (characterNameText != null) characterNameText.text = "";
|
||||
if (manaCostText != null) manaCostText.text = "";
|
||||
if (costNumberImage != null)
|
||||
{
|
||||
costNumberImage.sprite = null;
|
||||
costNumberImage.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void OnClick()
|
||||
{
|
||||
if (characterData == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 决战模式:有布阵管理器时走选兵逻辑,不消耗金币。
|
||||
if (BattleSetupManager.Instance != null)
|
||||
{
|
||||
BattleSetupManager.Instance.SelectCharacter(characterData);
|
||||
return;
|
||||
}
|
||||
|
||||
// 拆家版:固定八卡组采用四槽轮转,成功出牌后立即补下一张。
|
||||
if (CastleSpawnManager.Instance != null)
|
||||
{
|
||||
if (BattleDeckCycle.Instance != null)
|
||||
{
|
||||
BattleDeckCycle.Instance.TryPlay(summonSlot);
|
||||
}
|
||||
else if (CastleSpawnManager.Instance.TrySpawn(characterData, 0))
|
||||
{
|
||||
StartCooldown();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 召唤师对决模式:点卡片直接在召唤师身边召唤,消耗能量。
|
||||
if (SummonerDuelManager.Instance != null)
|
||||
{
|
||||
if (SummonerDuelManager.Instance.TrySummon(characterData))
|
||||
{
|
||||
StartCooldown();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 建筑模式:检查建筑费用是否足够
|
||||
if (GoldSystem.Instance == null || GoldSystem.Instance.CurrentGold < characterData.buildingCost)
|
||||
{
|
||||
Debug.Log($"金币不足,需要{characterData.buildingCost}");
|
||||
return;
|
||||
}
|
||||
|
||||
// 触发建筑放置模式
|
||||
if (BuildingPlacementManager.Instance != null)
|
||||
{
|
||||
BuildingPlacementManager.Instance.StartPlacingBuilding(characterData);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("找不到 BuildingPlacementManager");
|
||||
}
|
||||
}
|
||||
|
||||
private void StartCooldown()
|
||||
{
|
||||
if (characterData != null)
|
||||
{
|
||||
isOnCooldown = true;
|
||||
cooldownTimer = characterData.cooldownTime;
|
||||
UpdateCooldownUI();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateCooldownUI()
|
||||
{
|
||||
bool canAfford = CanAfford();
|
||||
|
||||
if (cooldownMask != null)
|
||||
{
|
||||
// CD中显示CD进度,能量不足时显示满遮罩
|
||||
if (isOnCooldown)
|
||||
{
|
||||
cooldownMask.fillAmount = cooldownTimer / GetCooldownTime();
|
||||
cooldownMask.enabled = true;
|
||||
}
|
||||
else if (!canAfford)
|
||||
{
|
||||
// 能量不足:显示满遮罩
|
||||
cooldownMask.fillAmount = 1f;
|
||||
cooldownMask.enabled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 可用状态:隐藏遮罩
|
||||
cooldownMask.fillAmount = 0f;
|
||||
cooldownMask.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (cooldownText != null)
|
||||
{
|
||||
cooldownText.text = isOnCooldown ? Mathf.CeilToInt(cooldownTimer).ToString() : "";
|
||||
cooldownText.enabled = isOnCooldown;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateButtonState()
|
||||
{
|
||||
if (button == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
button.interactable = !isOnCooldown && CanAfford();
|
||||
}
|
||||
|
||||
private bool CanAfford()
|
||||
{
|
||||
if (characterData == null || characterData.unitPrefab == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 决战模式按人口判断。
|
||||
if (BattleSetupManager.Instance != null)
|
||||
{
|
||||
return BattleSetupManager.Instance.CanAfford(characterData);
|
||||
}
|
||||
|
||||
// 拆家版按能量判断。
|
||||
if (CastleSpawnManager.Instance != null)
|
||||
{
|
||||
return CastleSpawnManager.Instance.CanAfford(characterData);
|
||||
}
|
||||
|
||||
// 召唤师对决模式按能量判断。
|
||||
if (SummonerDuelManager.Instance != null)
|
||||
{
|
||||
return SummonerDuelManager.Instance.CanAfford(characterData);
|
||||
}
|
||||
|
||||
// 建筑模式按金币判断。
|
||||
if (GoldSystem.Instance == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return GoldSystem.Instance.CurrentGold >= characterData.buildingCost;
|
||||
}
|
||||
|
||||
private float GetCooldownTime()
|
||||
{
|
||||
return characterData != null ? characterData.cooldownTime : 5f;
|
||||
}
|
||||
|
||||
private void UpdateCardUI()
|
||||
{
|
||||
if (characterData == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (cardIcon != null && characterData.cardIcon != null)
|
||||
{
|
||||
cardIcon.sprite = characterData.cardIcon;
|
||||
}
|
||||
|
||||
if (characterNameText != null)
|
||||
{
|
||||
characterNameText.text = characterData.characterName;
|
||||
}
|
||||
|
||||
if (manaCostText != null)
|
||||
{
|
||||
manaCostText.text = characterData.manaCost.ToString();
|
||||
}
|
||||
|
||||
// 更新费用数字精灵
|
||||
UpdateCostNumberSprite();
|
||||
}
|
||||
|
||||
private void UpdateCostNumberSprite()
|
||||
{
|
||||
if (costNumberImage == null || characterData == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int cost = characterData.manaCost;
|
||||
|
||||
// 费用范围 1-9,对应精灵索引 0-8
|
||||
if (cost >= 1 && cost <= 9 && costNumberSprites != null && costNumberSprites.Length >= cost)
|
||||
{
|
||||
costNumberImage.sprite = costNumberSprites[cost - 1];
|
||||
costNumberImage.enabled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 超出范围或未配置精灵数组,隐藏图片
|
||||
costNumberImage.enabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// 召唤师对决模式:点卡片直接在召唤师身边召唤单位(能量消耗 + 卡片冷却沿用现有系统)。
|
||||
/// 复用 SummonCardButton 的点击入口:无 BattleSetupManager、无 GoldSystem 时走本模式。
|
||||
/// </summary>
|
||||
public class SummonerDuelManager : MonoBehaviour
|
||||
{
|
||||
public static SummonerDuelManager Instance { get; private set; }
|
||||
|
||||
[Header("召唤师")]
|
||||
public MainCharacterCombat summonerCombat;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (summonerCombat == null)
|
||||
{
|
||||
summonerCombat = FindObjectOfType<MainCharacterCombat>();
|
||||
}
|
||||
}
|
||||
|
||||
// 卡片点击入口:能量足够时由召唤师执行召唤动画与生成。
|
||||
public bool TrySummon(CharacterData data)
|
||||
{
|
||||
if (summonerCombat == null)
|
||||
{
|
||||
Debug.LogError("找不到召唤师 MainCharacterCombat");
|
||||
return false;
|
||||
}
|
||||
|
||||
// RequestSummon 内部走能量判断(TryConsumeMana)与召唤动画。
|
||||
return summonerCombat.RequestSummonByData(data);
|
||||
}
|
||||
|
||||
// 卡片可用性:能量是否足够(供 SummonCardButton 判断置灰)。
|
||||
public bool CanAfford(CharacterData data)
|
||||
{
|
||||
return data != null
|
||||
&& ManaSystem.Instance != null
|
||||
&& ManaSystem.Instance.CurrentManaInt >= data.manaCost;
|
||||
}
|
||||
}
|
||||
+81
-49
@@ -9,8 +9,8 @@ public class UnitBase : MonoBehaviour
|
||||
public float speed = 5f;
|
||||
|
||||
[Header("寻敌视野")]
|
||||
[Tooltip("检测范围内优先攻击敌方战斗单位;范围外则搜索敌方建筑物")]
|
||||
public float detectionRange = 25f;
|
||||
[Tooltip("只在检测范围内寻找敌方战斗单位;范围内没有敌人时原地待机")]
|
||||
public float detectionRange = 30f;
|
||||
|
||||
//攻击范围
|
||||
public float attackRange = 5f;
|
||||
@@ -65,18 +65,23 @@ public class UnitBase : MonoBehaviour
|
||||
// 召唤师:不自动追击远处敌人,只在站定后攻击射程内的目标;移动由 SummonerClickMovement 控制。
|
||||
public bool summonerMode = false;
|
||||
|
||||
//召唤师模式下允许自动攻击的开关(移动时由操控脚本关闭,站定时开启)。
|
||||
// 召唤师模式下允许自动攻击的开关(移动时由操控脚本关闭,站定后开启)。
|
||||
private bool summonerAttackEnabled = true;
|
||||
|
||||
[Header("无目标回位")]
|
||||
[Tooltip("开启后,当前没有敌方单位时返回生成时的初始格位;不限制正常追击距离。")]
|
||||
public bool returnToAnchorWhenIdle = false;
|
||||
[Min(0.1f)]
|
||||
public float anchorArrivalDistance = 0.35f;
|
||||
private Vector3 anchorPosition;
|
||||
private bool anchorInitialized;
|
||||
|
||||
//角色状态
|
||||
private State state = State.Alert;
|
||||
|
||||
//攻击目标
|
||||
public GameObject target;
|
||||
|
||||
//出生使命:出生时快照当时己方标志指向的建筑,之后标志变化不影响本单位(支持分兵)。
|
||||
private GameObject missionTarget;
|
||||
|
||||
// NavMesh 寻路更新间隔(优化性能)
|
||||
private float nextPathUpdateTime = 0f;
|
||||
private const float PathUpdateInterval = 0.5f; // 每 0.5 秒更新一次路径
|
||||
@@ -149,6 +154,8 @@ public class UnitBase : MonoBehaviour
|
||||
worldHealthBar = GetComponent<WorldHealthBar>();
|
||||
statusEffectController = EnsureStatusEffectController();
|
||||
unitSkill = GetComponent<UnitSkill>();
|
||||
anchorPosition = transform.position;
|
||||
anchorInitialized = true;
|
||||
|
||||
// 如果有 NavMeshAgent,配置它
|
||||
if (navAgent != null)
|
||||
@@ -165,13 +172,6 @@ public class UnitBase : MonoBehaviour
|
||||
|
||||
// 根运动作用于所有动画:走路摇摆、攻击前冲由 OnAnimatorMove 统一协调
|
||||
animator.applyRootMotion = true;
|
||||
|
||||
//出生使命:快照己方标志当前指向的建筑(之后玩家切换标志不影响本单位)
|
||||
var myTeam = GetComponent<UnitTeam>();
|
||||
if (myTeam != null && AttackTargetController.Instance != null)
|
||||
{
|
||||
missionTarget = AttackTargetController.Instance.GetTeamTarget(myTeam.teamId);
|
||||
}
|
||||
}
|
||||
|
||||
private StatusEffectController EnsureStatusEffectController()
|
||||
@@ -266,6 +266,12 @@ public class UnitBase : MonoBehaviour
|
||||
|
||||
//场上没有敌人时进入待机,不再巡逻移动
|
||||
state = State.Alert;
|
||||
if (returnToAnchorWhenIdle && anchorInitialized && !IsAtAnchor())
|
||||
{
|
||||
ReturnToAnchor();
|
||||
return;
|
||||
}
|
||||
|
||||
// 召唤师模式下动画由 SummonerClickMovement 控制(移动时 speed=1,站定后 speed=0)
|
||||
if (!summonerMode)
|
||||
{
|
||||
@@ -385,36 +391,66 @@ public class UnitBase : MonoBehaviour
|
||||
{
|
||||
}
|
||||
|
||||
//目标决策:周围有敌人打敌人(detectionRange 内),否则直奔出生使命建筑(快照,标志变化不影响)。
|
||||
private GameObject FindTarget()
|
||||
private bool IsAtAnchor()
|
||||
{
|
||||
// Phase 1: detectionRange 内的敌方战斗单位
|
||||
GameObject enemy = FindNearestEnemyUnit();
|
||||
if (enemy != null) return enemy;
|
||||
|
||||
// Phase 2: 出生使命建筑;只有仍属于敌方的使命建筑才继续攻击。
|
||||
if (IsTargetValid(missionTarget))
|
||||
{
|
||||
return missionTarget;
|
||||
}
|
||||
|
||||
// 使命建筑已被己方占领、失活或销毁时,改攻敌方主城堡。
|
||||
var myTeam = GetComponent<UnitTeam>();
|
||||
if (myTeam != null && AttackTargetController.Instance != null)
|
||||
{
|
||||
GameObject castle = AttackTargetController.Instance.GetEnemyCastleOfTeam(myTeam.teamId);
|
||||
missionTarget = castle;
|
||||
return castle;
|
||||
}
|
||||
return null;
|
||||
if (!anchorInitialized) return true;
|
||||
Vector3 current = transform.position;
|
||||
current.y = 0f;
|
||||
Vector3 anchor = anchorPosition;
|
||||
anchor.y = 0f;
|
||||
return Vector3.Distance(current, anchor) <= anchorArrivalDistance;
|
||||
}
|
||||
|
||||
//判断建筑是否我方阵营(含主城堡和已占领资源建筑)
|
||||
private bool IsFriendlyBuilding(GameObject building)
|
||||
private void ReturnToAnchor()
|
||||
{
|
||||
var myTeam = GetComponent<UnitTeam>();
|
||||
var buildingTeam = building.GetComponent<UnitTeam>();
|
||||
return myTeam != null && buildingTeam != null && buildingTeam.teamId == myTeam.teamId;
|
||||
if (navAgent == null || !anchorInitialized)
|
||||
{
|
||||
if (!summonerMode) animator.SetInteger("speed", 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!navAgent.enabled)
|
||||
{
|
||||
navAgent.enabled = true;
|
||||
}
|
||||
|
||||
if (!navAgent.isOnNavMesh)
|
||||
{
|
||||
if (!summonerMode) animator.SetInteger("speed", 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsAtAnchor())
|
||||
{
|
||||
navAgent.isStopped = true;
|
||||
navAgent.ResetPath();
|
||||
transform.position = new Vector3(anchorPosition.x, transform.position.y, anchorPosition.z);
|
||||
navAgent.nextPosition = transform.position;
|
||||
if (!summonerMode) animator.SetInteger("speed", 0);
|
||||
return;
|
||||
}
|
||||
|
||||
navAgent.isStopped = false;
|
||||
if (Time.time >= nextPathUpdateTime)
|
||||
{
|
||||
navAgent.SetDestination(anchorPosition);
|
||||
nextPathUpdateTime = Time.time + PathUpdateInterval;
|
||||
}
|
||||
animator.SetInteger("speed", navAgent.velocity.sqrMagnitude > 0.01f ? 1 : 0);
|
||||
}
|
||||
|
||||
//目标决策:只寻找 detectionRange 内的敌方战斗单位;范围内没有敌人时原地待机或回到锚点。
|
||||
private GameObject FindTarget()
|
||||
{
|
||||
return FindNearestEnemyUnit();
|
||||
}
|
||||
|
||||
// 判断目标是否属于本单位自身的层级树(自身或其子物体)。
|
||||
// 不能用 transform.root 比较:单位和敌人常挂在同一个场景容器下,会被误判成"自己"而全部跳过。
|
||||
private bool IsOwnHierarchy(Transform other)
|
||||
{
|
||||
if (other == null) return true;
|
||||
return other == transform || other.IsChildOf(transform) || transform.IsChildOf(other);
|
||||
}
|
||||
|
||||
//查找 detectionRange 内最近的敌方战斗单位
|
||||
@@ -429,19 +465,15 @@ public class UnitBase : MonoBehaviour
|
||||
|
||||
Vector3 myPos = transform.position;
|
||||
|
||||
// Phase 1: detectionRange 内的敌方战斗单位
|
||||
GameObject nearestUnit = null;
|
||||
float nearestUnitSqr = float.PositiveInfinity;
|
||||
|
||||
UnitTeam[] allTeams = FindObjectsOfType<UnitTeam>();
|
||||
foreach (var unit in allTeams)
|
||||
{
|
||||
if (unit.gameObject == gameObject || !myTeam.IsEnemy(unit.gameObject))
|
||||
if (!myTeam.IsEnemy(unit.gameObject))
|
||||
continue;
|
||||
if (unit.transform.root == transform.root)
|
||||
continue;
|
||||
// 跳过建筑物(它们有 Building Tag),只找战斗单位
|
||||
if (unit.CompareTag("Building"))
|
||||
if (IsOwnHierarchy(unit.transform))
|
||||
continue;
|
||||
|
||||
float sqrDist = (unit.transform.position - myPos).sqrMagnitude;
|
||||
@@ -810,7 +842,7 @@ public class UnitBase : MonoBehaviour
|
||||
{
|
||||
GameObject opponent = targetTeam.gameObject;
|
||||
|
||||
if (opponent == null || opponent == gameObject || opponent.transform.root == transform.root)
|
||||
if (opponent == null || IsOwnHierarchy(opponent.transform))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -896,7 +928,7 @@ public class UnitBase : MonoBehaviour
|
||||
{
|
||||
GameObject opponent = targetTeam.gameObject;
|
||||
|
||||
if (opponent == null || opponent == gameObject || opponent.transform.root == transform.root)
|
||||
if (opponent == null || IsOwnHierarchy(opponent.transform))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -1084,8 +1116,8 @@ public class UnitBase : MonoBehaviour
|
||||
|
||||
private void FinishAttack()
|
||||
{
|
||||
//攻击动画结束后恢复当前攻速倍率。
|
||||
animator.speed = attackSpeedMultiplier;
|
||||
//攻击动画结束后恢复正常 Animator 播放速度,避免攻击动画调速污染走路动画。
|
||||
animator.speed = 1f;
|
||||
|
||||
//攻击结束后只继续攻击当前锁定目标,不在这里重新寻敌。
|
||||
if (IsTargetValid(target) && IsWithinAttackReach(target))
|
||||
|
||||
@@ -9,8 +9,9 @@ public class WorldHealthBar : MonoBehaviour
|
||||
// 当前生命值,范围会自动限制在 0 到 maxHealth 之间。
|
||||
[Min(0f)] public float currentHealth = 100f;
|
||||
|
||||
// 技能能量上限,所有单位默认使用100点。
|
||||
// 技能能量上限,英雄默认使用100点;没有技能的小怪可关闭能量条。
|
||||
[Header("技能能量")]
|
||||
public bool showEnergyBar = true;
|
||||
[Min(1f)] public float maxEnergy = 100f;
|
||||
|
||||
// 当前技能能量,范围会自动限制在 0 到 maxEnergy 之间。
|
||||
@@ -100,7 +101,7 @@ public class WorldHealthBar : MonoBehaviour
|
||||
maxEnergy = Mathf.Max(1f, maxEnergy);
|
||||
_currentEnergy = Mathf.Clamp(_currentEnergy, 0f, maxEnergy);
|
||||
CalculateLayout();
|
||||
CreateBar(); // 创建血条 UI 结构,但颜色在 Start() 中设置
|
||||
CreateBar(); // 创建血条 UI 结构;无技能单位只创建血条
|
||||
UpdateFill();
|
||||
UpdateEnergyFill();
|
||||
}
|
||||
@@ -397,7 +398,7 @@ public class WorldHealthBar : MonoBehaviour
|
||||
canvas.sortingOrder = 100;
|
||||
barObject.GetComponent<CanvasScaler>().dynamicPixelsPerUnit = 10f;
|
||||
barRect = barObject.GetComponent<RectTransform>();
|
||||
barRect.sizeDelta = new Vector2(PixelWidth, TotalPixelHeight);
|
||||
barRect.sizeDelta = new Vector2(PixelWidth, showEnergyBar ? TotalPixelHeight : PixelHeight);
|
||||
barRect.localScale = Vector3.one * (worldWidth / PixelWidth);
|
||||
barRect.localPosition = GetBarRootPosition();
|
||||
ApplyScaleCompensation();
|
||||
@@ -417,6 +418,12 @@ public class WorldHealthBar : MonoBehaviour
|
||||
inner.fillOrigin = 0;
|
||||
fillImage = inner;
|
||||
|
||||
if (!showEnergyBar)
|
||||
{
|
||||
energyFillImage = null;
|
||||
return;
|
||||
}
|
||||
|
||||
RectTransform energyRow = CreateRow(
|
||||
barObject.transform,
|
||||
"EnergyBar",
|
||||
@@ -455,7 +462,8 @@ public class WorldHealthBar : MonoBehaviour
|
||||
private Vector3 GetBarRootPosition()
|
||||
{
|
||||
float pixelToWorld = worldWidth / PixelWidth;
|
||||
float worldEnergyOffset = (TotalPixelHeight - PixelHeight) * 0.5f * pixelToWorld;
|
||||
int barHeight = showEnergyBar ? TotalPixelHeight : PixelHeight;
|
||||
float worldEnergyOffset = (barHeight - PixelHeight) * 0.5f * pixelToWorld;
|
||||
float localEnergyOffset = transform.lossyScale.y != 0f
|
||||
? worldEnergyOffset / transform.lossyScale.y
|
||||
: worldEnergyOffset;
|
||||
@@ -539,27 +547,24 @@ public class WorldHealthBar : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
// 创建并缓存一个带圆角的白色 Sprite,供边框和填充复用。
|
||||
// 创建并缓存一个直角矩形白色 Sprite,供边框和填充复用。
|
||||
private static Sprite RoundedSprite()
|
||||
{
|
||||
if (roundedSprite != null) return roundedSprite;
|
||||
|
||||
const int width = 64;
|
||||
const int height = 16;
|
||||
const float radius = 5f;
|
||||
Texture2D texture = new Texture2D(width, height, TextureFormat.RGBA32, false);
|
||||
texture.wrapMode = TextureWrapMode.Clamp;
|
||||
texture.filterMode = FilterMode.Bilinear;
|
||||
texture.hideFlags = HideFlags.HideAndDontSave;
|
||||
|
||||
// 填充纯白色,无圆角
|
||||
for (int y = 0; y < height; y++)
|
||||
{
|
||||
for (int x = 0; x < width; x++)
|
||||
{
|
||||
float dx = Mathf.Max(Mathf.Max(radius - x, 0f), x - (width - 1 - radius));
|
||||
float dy = Mathf.Max(Mathf.Max(radius - y, 0f), y - (height - 1 - radius));
|
||||
bool inside = dx * dx + dy * dy <= radius * radius;
|
||||
texture.SetPixel(x, y, inside ? Color.white : Color.clear);
|
||||
texture.SetPixel(x, y, Color.white);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user