根据提供的code differences信息,由于没有具体的代码变更内容,我将生成一个通用的commit message模板:

```
docs(readme): 更新文档说明

- 添加项目使用指南
- 完善API接口说明
- 修正错误的配置示例
```

注意:由于您提供的code differences部分为空,以上是一个示例模板。当您有具体的代码变更时,请提供详细的变更内容以便生成更准确的commit message。
This commit is contained in:
huadexuan committed 2026-10-02 10:04:12 +08:00
1 parent 8d58c2223c
commit 11f1af357d
191 files changed
+2315 -1539

No files matched your search

+137
View File
@@ -0,0 +1,137 @@
using UnityEngine;
/// <summary>
/// AI 召唤师控制器:自动召唤单位攻击玩家
/// </summary>
public class AISummoner : MonoBehaviour
{
[Header("AI 配置")]
[Tooltip("召唤间隔(秒)")]
[Min(1f)]
public float summonInterval = 3f;
[Tooltip("随机偏移范围(秒),避免太规律")]
[Min(0f)]
public float summonRandomOffset = 1f;
[Tooltip("AI 难度:召唤CD缩放(越小越快)")]
[Range(0.5f, 2f)]
public float difficultyScale = 1f;
[Header("能量管理")]
[Tooltip("AI 初始能量")]
[Min(0)]
public int startingMana = 150;
[Tooltip("AI 是否使用无限能量")]
public bool unlimitedMana = false;
private MainCharacterCombat combat;
private ManaSystem manaSystem;
private float summonCooldown;
private bool isActive;
private void Start()
{
combat = GetComponent<MainCharacterCombat>();
if (combat == null)
{
Debug.LogError($"[AISummoner] {name} 缺少 MainCharacterCombat 组件!");
enabled = false;
return;
}
manaSystem = FindObjectOfType<ManaSystem>();
if (manaSystem == null)
{
Debug.LogWarning($"[AISummoner] 场景中没有 ManaSystem,AI 将无法检查能量");
}
// 初始延迟,避免开局立刻召唤
summonCooldown = summonInterval * difficultyScale;
isActive = true;
Debug.Log($"[AISummoner] {name} AI 初始化完成,召唤间隔: {summonInterval}s");
}
private void Update()
{
if (!isActive) return;
// AI 被眩晕时停止召唤
if (combat.IsStunned) return;
summonCooldown -= Time.deltaTime;
if (summonCooldown <= 0f)
{
TrySummon();
ResetCooldown();
}
}
private void TrySummon()
{
// 随机选择一个可召唤的角色槽位(1-5)
var characters = combat.summonableCharacters;
if (characters == null || characters.Length == 0)
{
Debug.LogWarning($"[AISummoner] {name} 没有可召唤的角色");
return;
}
// 过滤出有效的角色数据
int[] validSlots = new int[characters.Length];
int validCount = 0;
for (int i = 0; i < characters.Length; i++)
{
if (characters[i] != null && characters[i].unitPrefab != null)
{
validSlots[validCount++] = i + 1;
}
}
if (validCount == 0)
{
// 没有可召唤的角色(能量不足或没有有效角色)
return;
}
// 随机选择一个有效槽位
int randomIndex = Random.Range(0, validCount);
int selectedSlot = validSlots[randomIndex];
// 执行召唤(使用 MainCharacterCombat.RequestSummon)
bool success = combat.RequestSummon(selectedSlot);
if (success)
{
var selectedCharacter = characters[selectedSlot - 1];
Debug.Log($"[AISummoner] {name} 召唤了 {selectedCharacter.characterName} (槽位: {selectedSlot})");
}
}
private void ResetCooldown()
{
float randomOffset = Random.Range(-summonRandomOffset, summonRandomOffset);
summonCooldown = summonInterval * difficultyScale + randomOffset;
}
/// <summary>
/// 启用/禁用 AI
/// </summary>
public void SetActive(bool active)
{
isActive = active;
Debug.Log($"[AISummoner] {name} AI {(active ? "启用" : "禁用")}");
}
/// <summary>
/// 调整难度(实时修改召唤速度)
/// </summary>
public void SetDifficulty(float scale)
{
difficultyScale = Mathf.Clamp(scale, 0.5f, 2f);
Debug.Log($"[AISummoner] {name} 难度调整为 {difficultyScale}");
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: DC8WtyqpBy35KStQpLkCge01+kowo40bznmAMX3C7ZNwPI0r5/h6PKA=
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,305 @@
using UnityEngine;
using UnityEngine.EventSystems;
/// <summary>
/// 建筑攻击标志控制器(双阵营):每个阵营各自一个攻击标志,默认指向敌方主城堡。
/// 本地玩家点击建筑切换自己阵营的标志;两方标志同时显示,指向同一建筑时并排错开。
/// 单位寻敌:周围有敌人打敌人,否则直奔己方标志建筑;到达后是我方阵营(增援完成)→ 己方标志切回主城堡。
/// </summary>
public class AttackTargetController : MonoBehaviour
{
public static AttackTargetController Instance { get; private set; }
[Header("蓝方(teamId=0)攻击目标")]
public GameObject blueTarget;
[Header("红方(teamId=1)攻击目标")]
public GameObject redTarget;
[Header("标志视觉(头顶箭头)")]
[Tooltip("标志 Prefab:拖入自定义图标替换默认箭头;留空时代码生成默认箭头")]
[SerializeField] private GameObject markerPrefab;
[Tooltip("标志相对建筑包围盒顶部的额外高度")]
[SerializeField] private float markerHeightOffset = 0.8f;
[Tooltip("双方标志指向同一建筑时的并排错开距离")]
[SerializeField] private float markerSideOffset = 1.1f;
[Tooltip("建筑子级下该名字的空物体 = 标志锚点(可选,编辑器可调位置)")]
[SerializeField] private string anchorName = "标志锚点";
private Transform blueMarker;
private Transform redMarker;
private Renderer[] blueMarkerRenderers;
private Renderer[] redMarkerRenderers;
private GameObject cachedBlueEnemyCastle; // teamId 0 的敌方主城堡(红色城堡)
private GameObject cachedRedEnemyCastle; // teamId 1 的敌方主城堡(蓝色城堡)
private float nextCastleRefreshTime;
private const float CastleRefreshInterval = 0.5f;
private Camera mainCamera;
private static readonly int TintColorId = Shader.PropertyToID("_TintColor");
private static readonly int ColorId = Shader.PropertyToID("_Color");
private static readonly Color BlueColor = new Color(0.2f, 0.55f, 1f, 0.95f);
private static readonly Color RedColor = new Color(1f, 0.2f, 0.15f, 0.95f);
private MaterialPropertyBlock propertyBlock;
private void Awake()
{
if (Instance != null && Instance != this) { Destroy(gameObject); return; }
Instance = this;
}
private void Start()
{
mainCamera = Camera.main;
CreateMarkers();
blueTarget = GetEnemyCastleOfTeam(0);
redTarget = GetEnemyCastleOfTeam(1);
Debug.Log($"攻击标志初始: 蓝方→{blueTarget?.name ?? "未找到"} 红方→{redTarget?.name ?? "未找到"}");
}
private void Update()
{
// 主城堡引用失效时定期重查(场景加载时序容错)
if ((cachedBlueEnemyCastle == null || cachedRedEnemyCastle == null) && Time.time >= nextCastleRefreshTime)
{
nextCastleRefreshTime = Time.time + CastleRefreshInterval;
cachedBlueEnemyCastle = FindBuilding("红色城堡");
cachedRedEnemyCastle = FindBuilding("蓝色城堡");
if (blueTarget == null && cachedBlueEnemyCastle != null) blueTarget = cachedBlueEnemyCastle;
if (redTarget == null && cachedRedEnemyCastle != null) redTarget = cachedRedEnemyCastle;
}
HandleClick();
}
// 点击建筑:只切换本地玩家阵营的攻击标志
private void HandleClick()
{
if (mainCamera == null) mainCamera = Camera.main;
if (mainCamera == null) return;
bool clicked = Input.GetMouseButtonDown(0)
|| (Input.touchCount > 0 && Input.GetTouch(0).phase == TouchPhase.Began);
if (!clicked) return;
// 点击落在 UI 上不处理
if (EventSystem.current != null)
{
if (Input.touchCount > 0)
{
if (EventSystem.current.IsPointerOverGameObject(Input.GetTouch(0).fingerId)) return;
}
else if (EventSystem.current.IsPointerOverGameObject()) return;
}
Vector2 screenPos = Input.touchCount > 0 ? (Vector2)Input.GetTouch(0).position : Input.mousePosition;
Ray ray = mainCamera.ScreenPointToRay(screenPos);
if (Physics.Raycast(ray, out RaycastHit hit, 500f))
{
Transform building = FindBuildingInParents(hit.collider.transform);
if (building == null) return;
int localTeam = GameManager.Instance != null ? GameManager.Instance.LocalPlayerTeamId : 0;
// 点自己方主城堡无效
if (building.name == GetTeamCastleName(localTeam)) return;
if (localTeam == 0)
{
blueTarget = building.gameObject;
}
else
{
redTarget = building.gameObject;
}
Debug.Log($"{(localTeam == 0 ? "蓝方" : "红方")}攻击标志 → {building.name}");
}
}
/// <summary>获取阵营当前的攻击目标(无效时兜底敌方主城堡)。单位出生时调用一次做使命快照。</summary>
public GameObject GetTeamTarget(int teamId)
{
GameObject t = teamId == 0 ? blueTarget : redTarget;
if (t != null && t.activeInHierarchy) return t;
return GetEnemyCastleOfTeam(teamId);
}
/// <summary>获取阵营的敌方主城堡(teamId 0→红色城堡,1→蓝色城堡),带缓存。</summary>
public GameObject GetEnemyCastleOfTeam(int teamId)
{
if (teamId == 0)
{
if (cachedBlueEnemyCastle == null || !cachedBlueEnemyCastle.activeInHierarchy)
{
cachedBlueEnemyCastle = FindBuilding("红色城堡");
}
return cachedBlueEnemyCastle;
}
if (cachedRedEnemyCastle == null || !cachedRedEnemyCastle.activeInHierarchy)
{
cachedRedEnemyCastle = FindBuilding("蓝色城堡");
}
return cachedRedEnemyCastle;
}
private static GameObject FindBuilding(string name)
{
var found = GameObject.Find(name);
return found != null && found.CompareTag("Building") ? found : null;
}
private static string GetTeamCastleName(int teamId)
{
return teamId == 0 ? "蓝色城堡" : "红色城堡";
}
private static Transform FindBuildingInParents(Transform t)
{
while (t != null)
{
if (t.CompareTag("Building")) return t;
t = t.parent;
}
return null;
}
// 双标志:位置/朝向/颜色(指向同一建筑时并排)
private void LateUpdate()
{
if (mainCamera == null) mainCamera = Camera.main;
bool sameBuilding = blueTarget != null && blueTarget == redTarget;
float blueSide = sameBuilding ? markerSideOffset : 0f;
float redSide = sameBuilding ? -markerSideOffset : 0f;
UpdateMarker(blueMarker, blueMarkerRenderers, blueTarget, BlueColor, blueSide);
UpdateMarker(redMarker, redMarkerRenderers, redTarget, RedColor, redSide);
}
private void UpdateMarker(Transform marker, Renderer[] renderers, GameObject target, Color color, float sideOffset)
{
if (marker == null) return;
if (target == null)
{
marker.gameObject.SetActive(false);
return;
}
marker.gameObject.SetActive(true);
// 位置:锚点优先(可选),否则建筑包围盒顶部 + 高度偏移
Vector3 pos;
Transform anchor = target.transform.Find(anchorName);
if (anchor != null)
{
pos = anchor.position;
}
else
{
Bounds bounds = CalculateTargetBounds(target);
pos = new Vector3(target.transform.position.x, bounds.max.y + markerHeightOffset, target.transform.position.z);
}
// 指向同一建筑时沿相机右方向并排错开
if (sideOffset != 0f && mainCamera != null)
{
pos += mainCamera.transform.right * sideOffset;
}
marker.position = pos;
// billboard:箭头尖朝下指着建筑
if (mainCamera != null)
{
Vector3 toCamera = mainCamera.transform.position - marker.position;
marker.rotation = Quaternion.LookRotation(toCamera) * Quaternion.Euler(90f, 0f, 0f);
}
// 颜色按标志所属阵营
if (renderers == null) return;
propertyBlock ??= new MaterialPropertyBlock();
foreach (Renderer rend in renderers)
{
if (rend == null) continue;
rend.GetPropertyBlock(propertyBlock);
propertyBlock.SetColor(TintColorId, color);
propertyBlock.SetColor(ColorId, color);
rend.SetPropertyBlock(propertyBlock);
}
}
private static Bounds CalculateTargetBounds(GameObject target)
{
var renderers = target.GetComponentsInChildren<Renderer>();
if (renderers.Length == 0) return new Bounds(target.transform.position, Vector3.one);
Bounds b = renderers[0].bounds;
for (int i = 1; i < renderers.Length; i++) b.Encapsulate(renderers[i].bounds);
return b;
}
private void CreateMarkers()
{
if (blueMarker != null && redMarker != null) return;
blueMarker = CreateMarker("蓝方攻击标志", BlueColor);
blueMarkerRenderers = blueMarker.GetComponentsInChildren<Renderer>();
redMarker = CreateMarker("红方攻击标志", RedColor);
redMarkerRenderers = redMarker.GetComponentsInChildren<Renderer>();
}
private Transform CreateMarker(string name, Color color)
{
GameObject markerGo;
// 优先用可替换的标志 Prefab(可在 Inspector 拖入自定义图标)
if (markerPrefab != null)
{
markerGo = Instantiate(markerPrefab, transform);
markerGo.name = name;
}
else
{
// 兜底:代码生成默认倒锥箭头(尖朝下指着建筑)
markerGo = new GameObject(name, typeof(MeshFilter), typeof(MeshRenderer));
markerGo.transform.SetParent(transform, false);
markerGo.GetComponent<MeshFilter>().sharedMesh = CreateArrowMesh();
var mat = new Material(Shader.Find("Legacy Shaders/Particles/Additive"));
mat.SetColor(TintColorId, color);
markerGo.GetComponent<Renderer>().sharedMaterial = mat;
}
return markerGo.transform;
}
// 4面锥体:尖朝下,高2,底半径0.7
private static Mesh CreateArrowMesh()
{
var mesh = new Mesh();
mesh.name = "AttackMarkerArrow";
Vector3 tip = new Vector3(0f, -1.2f, 0f);
Vector3[] baseQuad = {
new Vector3(-0.7f, 0.8f, -0.7f), new Vector3(0.7f, 0.8f, -0.7f),
new Vector3(0.7f, 0.8f, 0.7f), new Vector3(-0.7f, 0.8f, 0.7f)
};
var vertices = new System.Collections.Generic.List<Vector3>();
var normals = new System.Collections.Generic.List<Vector3>();
void AddFace(Vector3 a, Vector3 b, Vector3 c, Vector3 n)
{
vertices.AddRange(new[] { a, b, c });
normals.AddRange(new[] { n, n, n });
}
AddFace(baseQuad[0], baseQuad[1], tip, new Vector3(0f, 0f, -1f)); // 后
AddFace(baseQuad[1], baseQuad[2], tip, new Vector3(1f, 0f, 0f)); // 右
AddFace(baseQuad[2], baseQuad[3], tip, new Vector3(0f, 0f, 1f)); // 前
AddFace(baseQuad[3], baseQuad[0], tip, new Vector3(-1f, 0f, 0f)); // 左
mesh.SetVertices(vertices);
mesh.SetNormals(normals);
mesh.triangles = new int[] { 0,1,2, 3,4,5, 6,7,8, 9,10,11 };
mesh.RecalculateBounds();
return mesh;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: DH0f5COpWy/XS+dNIQqjD+gef0E+5tOypqT0ghE/5oh3Kpon5JW5YkY=
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,102 @@
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// 战斗管理器:监听双方召唤师死亡,触发胜负判定并显示结果 UI。
/// </summary>
public class BattleManager : MonoBehaviour
{
[Header("召唤师引用")]
public GameObject playerSummoner;
public GameObject enemySummoner;
[Header("胜负 UI")]
public GameObject victoryPanel;
public GameObject defeatPanel;
private WorldHealthBar playerHealthBar;
private WorldHealthBar enemyHealthBar;
private bool battleEnded = false;
private void Start()
{
// 获取双方召唤师的血条组件
if (playerSummoner != null)
{
playerHealthBar = playerSummoner.GetComponent<WorldHealthBar>();
}
if (enemySummoner != null)
{
enemyHealthBar = enemySummoner.GetComponent<WorldHealthBar>();
}
// 隐藏胜负 UI
if (victoryPanel != null) victoryPanel.SetActive(false);
if (defeatPanel != null) defeatPanel.SetActive(false);
}
private void Update()
{
if (battleEnded) return;
// 检测己方召唤师死亡
if (playerHealthBar != null && playerHealthBar.IsDead)
{
OnBattleEnd(false);
return;
}
// 检测敌方召唤师死亡
if (enemyHealthBar != null && enemyHealthBar.IsDead)
{
OnBattleEnd(true);
return;
}
}
private void OnBattleEnd(bool victory)
{
battleEnded = true;
// 暂停游戏
Time.timeScale = 0f;
// 显示对应 UI
if (victory)
{
Debug.Log("胜利!");
if (victoryPanel != null)
{
victoryPanel.SetActive(true);
}
}
else
{
Debug.Log("失败!");
if (defeatPanel != null)
{
defeatPanel.SetActive(true);
}
}
}
// 重新开始按钮回调
public void OnRestartButtonClicked()
{
Time.timeScale = 1f;
UnityEngine.SceneManagement.SceneManager.LoadScene(
UnityEngine.SceneManagement.SceneManager.GetActiveScene().name);
}
// 退出按钮回调
public void OnQuitButtonClicked()
{
Time.timeScale = 1f;
#if UNITY_EDITOR
UnityEditor.EditorApplication.isPlaying = false;
#else
Application.Quit();
#endif
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: CX4W5H+pAi6y7ekeQOC/HN3hMDdmdGBmOHSBBZ23k0yBSbBQ0lyufCA=
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+128
View File
@@ -0,0 +1,128 @@
using UnityEngine;
using System.Collections.Generic;
/// <summary>
/// 增益系统管理器(单例)
/// 管理全局增益效果的应用
/// </summary>
public class BuffSystem : MonoBehaviour
{
private static BuffSystem instance;
public static BuffSystem Instance
{
get
{
if (instance == null)
{
var go = new GameObject("BuffSystem");
instance = go.AddComponent<BuffSystem>();
DontDestroyOnLoad(go);
}
return instance;
}
}
private void Awake()
{
if (instance != null && instance != this)
{
Destroy(gameObject);
return;
}
instance = this;
}
/// <summary>
/// 应用群体治疗增益
/// </summary>
public void ApplyGroupHeal(float healAmount)
{
var allies = FindObjectsOfType<UnitBase>();
int healedCount = 0;
foreach (var unit in allies)
{
var team = unit.GetComponent<UnitTeam>();
if (team != null && team.teamId == 0) // 友方
{
var healthBar = unit.GetComponent<WorldHealthBar>();
if (healthBar != null)
{
healthBar.Heal(healAmount);
healedCount++;
}
}
}
Debug.Log($"群体治疗:治疗了 {healedCount} 个友方单位,每个恢复 {healAmount} 生命");
}
/// <summary>
/// 应用全局攻速增益
/// </summary>
public void ApplyGlobalAttackSpeedBuff(float speedMultiplier)
{
var allies = FindObjectsOfType<UnitBase>();
int buffedCount = 0;
foreach (var unit in allies)
{
var team = unit.GetComponent<UnitTeam>();
if (team != null && team.teamId == 0) // 友方
{
// 使用 UnitBase 的攻速倍增系统
unit.MultiplyAttackSpeed(speedMultiplier, 3f); // 最高3倍攻速
buffedCount++;
}
}
Debug.Log($"全局攻速增益:{buffedCount} 个友方单位攻速提升 {(speedMultiplier - 1) * 100}%");
}
/// <summary>
/// 应用全局攻击力增益
/// </summary>
public void ApplyGlobalAttackBuff(float attackMultiplier)
{
var allies = FindObjectsOfType<UnitBase>();
int buffedCount = 0;
foreach (var unit in allies)
{
var team = unit.GetComponent<UnitTeam>();
if (team != null && team.teamId == 0) // 友方
{
unit.attackPower *= attackMultiplier;
buffedCount++;
}
}
Debug.Log($"全局攻击增益:{buffedCount} 个友方单位攻击力提升 {(attackMultiplier - 1) * 100}%");
}
/// <summary>
/// 应用全局生命增益
/// </summary>
public void ApplyGlobalHealthBuff(float healthMultiplier)
{
var allies = FindObjectsOfType<UnitBase>();
int buffedCount = 0;
foreach (var unit in allies)
{
var team = unit.GetComponent<UnitTeam>();
if (team != null && team.teamId == 0) // 友方
{
var healthBar = unit.GetComponent<WorldHealthBar>();
if (healthBar != null)
{
healthBar.maxHealth *= healthMultiplier;
healthBar.currentHealth *= healthMultiplier;
buffedCount++;
}
}
}
Debug.Log($"全局生命增益:{buffedCount} 个友方单位生命值提升 {(healthMultiplier - 1) * 100}%");
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: CS8f5CmqUnrbYBZA6cN6qw0k/aTgXi53/u8Zomku7cDHnoDDves9rxI=
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,37 @@
using UnityEngine;
/// <summary>
/// 游戏管理器:管理全局游戏状态,包括本地玩家的阵营 ID。
/// PVE:LocalPlayerTeamId = 0(固定)
/// PVP:服务器分配后设置 LocalPlayerTeamId(比如 0 或 1)
/// </summary>
public class GameManager : MonoBehaviour
{
public static GameManager Instance { get; private set; }
[Header("本地玩家阵营")]
[Tooltip("PVE: 固定为0; PVP: 服务器分配")]
public int LocalPlayerTeamId = 0;
private void Awake()
{
// 单例模式
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
}
/// <summary>
/// 设置本地玩家阵营 ID(PVP 时由服务器调用)
/// </summary>
public void SetLocalPlayerTeam(int teamId)
{
LocalPlayerTeamId = teamId;
Debug.Log($"本地玩家阵营设置为: {teamId}");
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: CXsY5y6qB3m8jy9B8G9TGvKZDGiLWx+kljaHa6xdphe8IExxiF8gS90=
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,84 @@
using UnityEngine;
using System;
/// <summary>
/// 金币管理器,全局单例
/// </summary>
public class GoldManager : MonoBehaviour
{
public static GoldManager Instance { get; private set; }
[Header("金币设置")]
[SerializeField] private int initialGold = 500;
[SerializeField] private int currentGold = 500;
/// <summary>
/// 当前金币数量(只读)
/// </summary>
public int CurrentGold => currentGold;
/// <summary>
/// 金币变化事件(参数:新的金币数量)
/// </summary>
public event Action<int> OnGoldChanged;
private void Awake()
{
// 单例模式
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
}
private void Start()
{
currentGold = initialGold;
OnGoldChanged?.Invoke(currentGold);
}
/// <summary>
/// 增加金币
/// </summary>
public void AddGold(int amount)
{
if (amount <= 0) return;
currentGold += amount;
OnGoldChanged?.Invoke(currentGold);
Debug.Log($"获得金币 +{amount},当前金币: {currentGold}");
}
/// <summary>
/// 尝试消费金币,如果金币足够则扣除并返回 true,否则返回 false
/// </summary>
public bool TrySpendGold(int amount)
{
if (amount <= 0)
{
Debug.LogWarning("消费金币数量必须大于0");
return false;
}
if (currentGold < amount)
{
Debug.LogWarning($"金币不足!需要 {amount},当前只有 {currentGold}");
return false;
}
currentGold -= amount;
OnGoldChanged?.Invoke(currentGold);
Debug.Log($"消费金币 -{amount},当前金币: {currentGold}");
return true;
}
/// <summary>
/// 检查是否有足够的金币
/// </summary>
public bool HasEnoughGold(int amount)
{
return currentGold >= amount;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: WyhL5CykAihbfEqP3wWb6UBmEruxa4+cSMr8jlsjQfxkQcT5+AouqHg=
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+60
View File
@@ -0,0 +1,60 @@
using UnityEngine;
using UnityEngine.UI;
public class GoldSystem : MonoBehaviour
{
public static GoldSystem Instance { get; private set; }
[Header("金币配置")]
[Min(0)] public int startingGold = 200;
[Min(0)] public int maxGold = 9999;
[Header("UI(可选)")]
public Text goldText;
public int CurrentGold { get; private set; }
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
CurrentGold = startingGold;
UpdateUI();
}
public bool TrySpend(int amount)
{
if (amount < 0 || CurrentGold < amount)
{
return false;
}
CurrentGold -= amount;
UpdateUI();
return true;
}
public void AddGold(int amount)
{
if (amount <= 0)
{
return;
}
CurrentGold = Mathf.Min(CurrentGold + amount, maxGold);
UpdateUI();
}
private void UpdateUI()
{
if (goldText != null)
{
goldText.text = CurrentGold.ToString();
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: WX8YsHivBi942BoKU7cgLdeehhcrD10jK7kN5ksiyEdHUsqm9i72MxI=
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+93
View File
@@ -0,0 +1,93 @@
using UnityEngine;
using UnityEngine.UI;
public class ManaSystem : MonoBehaviour
{
public static ManaSystem Instance { get; private set; }
[Header("能量配置")]
[Min(1)]
public int maxMana = 10;
[Min(0f)]
public float manaRegenRate = 1f;
[Header("UI 引用")]
public Image[] manaBeads;
private float currentMana;
private float manaRegenTimer;
public float CurrentMana => currentMana;
public int CurrentManaInt => Mathf.FloorToInt(currentMana);
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
}
private void Start()
{
currentMana = maxMana;
UpdateManaUI();
}
private void Update()
{
if (currentMana < maxMana)
{
manaRegenTimer += Time.deltaTime;
if (manaRegenTimer >= manaRegenRate)
{
manaRegenTimer = 0f;
currentMana = Mathf.Min(currentMana + 1f, maxMana);
UpdateManaUI();
}
}
}
public bool TryConsumeMana(int cost)
{
if (CurrentManaInt < cost)
{
return false;
}
currentMana -= cost;
if (currentMana < 0f)
{
currentMana = 0f;
}
UpdateManaUI();
return true;
}
public void AddMana(int amount)
{
currentMana = Mathf.Min(currentMana + amount, maxMana);
UpdateManaUI();
}
private void UpdateManaUI()
{
if (manaBeads == null || manaBeads.Length == 0)
{
return;
}
int fullBeads = CurrentManaInt;
for (int i = 0; i < manaBeads.Length; i++)
{
if (manaBeads[i] != null)
{
manaBeads[i].enabled = i < fullBeads;
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: XnMc4S2sBy8qcU1oamvWm5En4Q1vl0fXBe/cr/TVqswIGCNCmFl3B8g=
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,176 @@
using System.Collections.Generic;
using UnityEngine;
using System.IO;
[System.Serializable]
public class PlayerOwnedCharacterJson
{
public string characterId;
public int level = 1;
public int starLevel = 0;
}
[System.Serializable]
public class SavedFormationData
{
public string[] characterIds = new string[0];
public int[] cellIndices = new int[0];
}
[System.Serializable]
public class PlayerSaveJson
{
public string playerId = "local_player";
public PlayerOwnedCharacterJson[] unlockedCharacters = new PlayerOwnedCharacterJson[0];
public string[] currentDeck = new string[0];
public SavedFormationData savedFormation = new SavedFormationData();
}
public class PlayerDataManager : MonoBehaviour
{
public static PlayerDataManager Instance { get; private set; }
[Header("本地玩家 JSON(正式版替换为服务器响应)")]
public TextAsset localPlayerJson;
public List<PlayerCharacterData> ownedCharacters = new List<PlayerCharacterData>();
public PlayerCharacterData[] currentDeck = new PlayerCharacterData[5];
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
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();
}
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 = new PlayerCharacterData[5];
for (int i = 0; i < currentDeck.Length && i < ownedCharacters.Count; i++)
currentDeck[i] = ownedCharacters[i];
}
public PlayerCharacterData GetCharacterData(string characterId)
{
return ownedCharacters.Find(c => c.characterId == characterId);
}
public PlayerCharacterData GetDeckCharacter(int slot)
{
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++;
}
/// <summary>
/// 保存阵型到 PlayerData.json
/// </summary>
public void SaveFormation(string[] characterIds, int[] cellIndices)
{
string path = Application.dataPath + "/Resources/PlayerData.json";
try
{
// 读取现有数据
string json = File.ReadAllText(path);
var saveData = JsonUtility.FromJson<PlayerSaveJson>(json);
// 更新阵型数据
saveData.savedFormation = new SavedFormationData
{
characterIds = characterIds,
cellIndices = cellIndices
};
// 写回文件
string updatedJson = JsonUtility.ToJson(saveData, true);
File.WriteAllText(path, updatedJson);
Debug.Log($"<color=green>✅ 阵型已保存到 PlayerData.json</color>");
Debug.Log($"保存了 {characterIds.Length} 个英雄的阵型数据");
}
catch (System.Exception e)
{
Debug.LogError($"保存阵型失败: {e.Message}");
}
}
/// <summary>
/// 从 PlayerData.json 加载阵型
/// </summary>
public SavedFormationData LoadFormation()
{
TextAsset json = localPlayerJson != null
? localPlayerJson
: Resources.Load<TextAsset>("PlayerData");
if (json != null)
{
try
{
var save = JsonUtility.FromJson<PlayerSaveJson>(json.text);
if (save != null && save.savedFormation != null)
{
return save.savedFormation;
}
}
catch (System.Exception e)
{
Debug.LogError("加载阵型失败:" + e.Message);
}
}
return new SavedFormationData();
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: B3kety+vUirkqF/JCKtcAbO1EzdK3P+w/NCAM+fzZEfjZiKS0aAKu2k=
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,132 @@
using UnityEngine;
public class StatusEffectController : MonoBehaviour
{
[Header("通用眩晕显示")]
[SerializeField] private bool showStunIndicator = true;
[SerializeField] private float indicatorHeight = 0.35f;
[SerializeField] private GameObject stunIndicatorPrefab;
private Animator animator;
private float stunnedRemaining;
private float animatorSpeedBeforeStun = 1f;
private GameObject stunIndicator;
private Transform indicatorTransform;
public bool IsStunned => stunnedRemaining > 0f;
public float StunnedRemaining => Mathf.Max(0f, stunnedRemaining);
private void Awake()
{
animator = GetComponent<Animator>();
}
public void ApplyStun(float duration)
{
if (duration <= 0f)
{
return;
}
if (!IsStunned && animator != null)
{
animatorSpeedBeforeStun = animator.speed;
}
stunnedRemaining = Mathf.Max(stunnedRemaining, duration);
if (animator != null)
{
animator.speed = 0f;
}
if (showStunIndicator)
{
EnsureIndicator();
}
}
private void Update()
{
if (!IsStunned)
{
return;
}
stunnedRemaining -= Time.deltaTime;
if (animator != null)
{
animator.speed = 0f;
}
if (stunnedRemaining <= 0f)
{
stunnedRemaining = 0f;
if (animator != null)
{
UnitBase unitBase = GetComponent<UnitBase>();
animator.speed = unitBase != null
? Mathf.Max(0.01f, unitBase.AttackSpeedMultiplier)
: 1f;
}
DestroyIndicator();
}
}
private void LateUpdate()
{
if (stunIndicator == null || !IsStunned)
{
return;
}
Bounds bounds = new Bounds(transform.position, Vector3.zero);
Renderer[] renderers = GetComponentsInChildren<Renderer>(true);
if (renderers.Length > 0)
{
bounds = renderers[0].bounds;
for (int i = 1; i < renderers.Length; i++)
{
bounds.Encapsulate(renderers[i].bounds);
}
}
indicatorTransform.position = new Vector3(
bounds.center.x,
bounds.max.y + indicatorHeight,
bounds.center.z);
}
private void EnsureIndicator()
{
if (stunIndicator != null || stunIndicatorPrefab == null)
{
return;
}
stunIndicator = Instantiate(stunIndicatorPrefab, transform);
stunIndicator.name = "StunIndicator_Particle";
indicatorTransform = stunIndicator.transform;
}
private void DestroyIndicator()
{
if (stunIndicator != null)
{
ParticleSystem[] particleSystems = stunIndicator.GetComponentsInChildren<ParticleSystem>(true);
foreach (ParticleSystem particleSystem in particleSystems)
{
particleSystem.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear);
}
Destroy(stunIndicator);
}
stunIndicator = null;
indicatorTransform = null;
}
private void OnDestroy()
{
DestroyIndicator();
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: CXxJ5i2tWng6gliTb4vc+EzvyuKbQYGasKmqCCMYXvn0s0NTPXvZPEM=
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: