feat(player): 添加玩家阵型保存功能 新增savedFormation字段用于存储玩家的阵型配置, 包括角色ID列表和对应的格子索引位置信息。 ```
470 lines
13 KiB
C#
470 lines
13 KiB
C#
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();
|
|
SaveFormationToJson();
|
|
}
|
|
|
|
private void SaveFormationToJson()
|
|
{
|
|
int count = HeroCount;
|
|
var characterIds = new string[count];
|
|
var cells = new int[count];
|
|
|
|
// 初始化数组
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
HeroEntry entry = GetHero(i);
|
|
characterIds[i] = entry != null ? GetCharacterId(entry.prefab) : "";
|
|
cells[i] = -1;
|
|
}
|
|
|
|
// 填充已放置的格位
|
|
foreach (var pair in placed)
|
|
{
|
|
int heroIndex = pair.Value.heroIndex;
|
|
if (heroIndex >= 0 && heroIndex < count)
|
|
{
|
|
cells[heroIndex] = pair.Key;
|
|
}
|
|
}
|
|
|
|
// 保存到 PlayerData.json
|
|
if (PlayerDataManager.Instance != null)
|
|
{
|
|
PlayerDataManager.Instance.SaveFormation(characterIds, cells);
|
|
}
|
|
else
|
|
{
|
|
Debug.LogError("PlayerDataManager.Instance 未找到,无法保存阵型");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 根据 Prefab 获取 characterId
|
|
/// </summary>
|
|
private string GetCharacterId(GameObject prefab)
|
|
{
|
|
if (prefab == null) return "";
|
|
|
|
// 查找对应的 CharacterData
|
|
var allData = Resources.LoadAll<CharacterData>("Data");
|
|
foreach (var data in allData)
|
|
{
|
|
if (data.unitPrefab == prefab)
|
|
{
|
|
return data.characterId;
|
|
}
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
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} 点击场上的英雄可以收回";
|
|
}
|
|
}
|
|
}
|