diff --git a/.codely/clipboard/clipboard-1789377053304.png b/.codely/clipboard/clipboard-1789377053304.png deleted file mode 100644 index 5b807c6..0000000 Binary files a/.codely/clipboard/clipboard-1789377053304.png and /dev/null differ diff --git a/.codely/clipboard/clipboard-1789535987301.png b/.codely/clipboard/clipboard-1789535987301.png new file mode 100644 index 0000000..dacbf3a Binary files /dev/null and b/.codely/clipboard/clipboard-1789535987301.png differ diff --git a/Assets/Prefabs/主角.meta b/Assets/Prefabs/主角.meta new file mode 100644 index 0000000..bc57174 --- /dev/null +++ b/Assets/Prefabs/主角.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: XXMf4Sr4WiqPxdnTrjn9FdzKz7CqmKicQSn0NFVDnCyDO6WMWFJD2dg= +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Prefabs/主角/召唤师主角.prefab b/Assets/Prefabs/主角/召唤师主角.prefab new file mode 100644 index 0000000..b39dcc9 --- /dev/null +++ b/Assets/Prefabs/主角/召唤师主角.prefab @@ -0,0 +1,220 @@ +%YAML 1.1 +%TAG !u! tag:yousandi.cn,2023: +--- !u!1 &7776896532109503703 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 7 + m_Component: + - component: {fileID: 286193082999517309} + m_Layer: 0 + m_HasEditorInfo: 1 + m_Name: attackPos + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &286193082999517309 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7776896532109503703} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: -0.019, y: 0.283, z: 0.069} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1444656758206389486} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1001 &989067199113700134 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + serializedVersion: 3 + m_TransformParent: {fileID: 0} + m_Modifications: + - target: {fileID: -8679921383154817045, guid: 6b859b567d8ba4a7a87c03aa4b87c18c, + type: 3} + propertyPath: m_LocalPosition.x + value: -0.8 + objectReference: {fileID: 0} + - target: {fileID: -8679921383154817045, guid: 6b859b567d8ba4a7a87c03aa4b87c18c, + type: 3} + propertyPath: m_LocalPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: -8679921383154817045, guid: 6b859b567d8ba4a7a87c03aa4b87c18c, + type: 3} + propertyPath: m_LocalPosition.z + value: 0.9 + objectReference: {fileID: 0} + - target: {fileID: -8679921383154817045, guid: 6b859b567d8ba4a7a87c03aa4b87c18c, + type: 3} + propertyPath: m_LocalRotation.w + value: 1 + objectReference: {fileID: 0} + - target: {fileID: -8679921383154817045, guid: 6b859b567d8ba4a7a87c03aa4b87c18c, + type: 3} + propertyPath: m_LocalRotation.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: -8679921383154817045, guid: 6b859b567d8ba4a7a87c03aa4b87c18c, + type: 3} + propertyPath: m_LocalRotation.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: -8679921383154817045, guid: 6b859b567d8ba4a7a87c03aa4b87c18c, + type: 3} + propertyPath: m_LocalRotation.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: -8679921383154817045, guid: 6b859b567d8ba4a7a87c03aa4b87c18c, + type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: -8679921383154817045, guid: 6b859b567d8ba4a7a87c03aa4b87c18c, + type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: -8679921383154817045, guid: 6b859b567d8ba4a7a87c03aa4b87c18c, + type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 919132149155446097, guid: 6b859b567d8ba4a7a87c03aa4b87c18c, + type: 3} + propertyPath: m_Name + value: "\u53EC\u5524\u5E08\u4E3B\u89D2" + objectReference: {fileID: 0} + - target: {fileID: 919132149155446097, guid: 6b859b567d8ba4a7a87c03aa4b87c18c, + type: 3} + propertyPath: m_TagString + value: Player + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_RemovedGameObjects: [] + m_AddedGameObjects: + - targetCorrespondingSourceObject: {fileID: 1852576806548013000, guid: 6b859b567d8ba4a7a87c03aa4b87c18c, + type: 3} + insertIndex: -1 + addedObject: {fileID: 286193082999517309} + m_AddedComponents: + - targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 6b859b567d8ba4a7a87c03aa4b87c18c, + type: 3} + insertIndex: -1 + addedObject: {fileID: 5899466420499838505} + - targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 6b859b567d8ba4a7a87c03aa4b87c18c, + type: 3} + insertIndex: -1 + addedObject: {fileID: 8874205787751956438} + - targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 6b859b567d8ba4a7a87c03aa4b87c18c, + type: 3} + insertIndex: -1 + addedObject: {fileID: 6516315820976732023} + m_SourcePrefab: {fileID: 100100000, guid: 6b859b567d8ba4a7a87c03aa4b87c18c, type: 3} +--- !u!1 &106035464483675767 stripped +GameObject: + m_CorrespondingSourceObject: {fileID: 919132149155446097, guid: 6b859b567d8ba4a7a87c03aa4b87c18c, + type: 3} + m_PrefabInstance: {fileID: 989067199113700134} + m_PrefabAsset: {fileID: 0} +--- !u!95 &5899466420499838505 +Animator: + serializedVersion: 6 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 106035464483675767} + m_Enabled: 1 + m_Avatar: {fileID: 9000000, guid: 6b859b567d8ba4a7a87c03aa4b87c18c, type: 3} + m_Controller: {fileID: 22100000, guid: ca1bd6051433541cfa22bac24d4a1103, type: 2} + m_CullingMode: 0 + m_UpdateMode: 0 + m_UpdateFrequencyMode: 0 + m_UpdateFrequency: 0 + m_ApplyRootMotion: 0 + m_LinearVelocityBlending: 0 + m_StabilizeFeet: 0 + m_WarningMessage: + m_HasTransformHierarchy: 1 + m_AllowConstantClipSamplingOptimization: 1 + m_KeepAnimatorStateOnDisable: 0 + m_WriteDefaultValuesOnDisable: 0 +--- !u!114 &8874205787751956438 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 106035464483675767} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: e370e6a2ddc454f8c8837028614c86d7, type: 3} + m_Name: + m_EditorClassIdentifier: + attackRange: 8 + detectRange: 10 + attackPower: 50 + attackInterval: 2 + modelFacingOffset: 90 + useRootMotion: 1 + projectilePrefab: {fileID: 6786356955586915818, guid: e491f7d23e3cd4069823405c487be19f, + type: 3} + attackPos: {fileID: 286193082999517309} + summonableCharacters: + - {fileID: 11400000, guid: 6f138ec1db2b741aab394542890c7400, type: 2} + - {fileID: 11400000, guid: 5a726a224ff5b46718f8f4fc75299bca, type: 2} + - {fileID: 11400000, guid: 51d1dacb02fc441fa9bbea4443e50967, type: 2} + - {fileID: 11400000, guid: 6a793da90a16b4d0c97ab9c3af691849, type: 2} + - {fileID: 11400000, guid: 49f7e9a7946834177a8dce1ef1c5edb9, type: 2} + summonDistance: 1.5 + summonSideOffset: 0.8 + summonSearchStep: 0.8 + summonSearchCount: 5 + summonDepthSearchStep: 1 + summonDepthSearchCount: 2 + summonAnimationTimeout: 2.5 + projectileSpeed: 12 + projectileLifetime: 5 +--- !u!114 &6516315820976732023 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 106035464483675767} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9355c53f591d4459ea620fc8191efa17, type: 3} + m_Name: + m_EditorClassIdentifier: + maxHealth: 300 + currentHealth: 300 + maxEnergy: 100 + _currentEnergy: 0 + heightOffset: 0.25 + barWidth: 1.2 + borderColor: {r: 0.03, g: 0.03, b: 0.03, a: 0.95} + deathEffectDuration: 1.1 + deathEffectColor: {r: 0.55, g: 0.9, b: 1, a: 1} + showDamageNumbers: 1 + damageNumberDuration: 0.8 + damageNumberRiseHeight: 1.2 + damageNumberSpawnHeight: 1.5 + damageNumberSize: 0.09 +--- !u!4 &1444656758206389486 stripped +Transform: + m_CorrespondingSourceObject: {fileID: 1852576806548013000, guid: 6b859b567d8ba4a7a87c03aa4b87c18c, + type: 3} + m_PrefabInstance: {fileID: 989067199113700134} + m_PrefabAsset: {fileID: 0} diff --git a/Assets/Prefabs/主角/召唤师主角.prefab.meta b/Assets/Prefabs/主角/召唤师主角.prefab.meta new file mode 100644 index 0000000..0a35fa8 --- /dev/null +++ b/Assets/Prefabs/主角/召唤师主角.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: CH9NsSqqVnqFjFbGA8nYsMTtB1V8QYYj+f2/XGK8gFxQUJ1gLpYP0cw= +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scenes/Chapter1/SummonerDuelTest.unity b/Assets/Scenes/Chapter1/SummonerDuelTest.unity new file mode 100644 index 0000000..b54356b Binary files /dev/null and b/Assets/Scenes/Chapter1/SummonerDuelTest.unity differ diff --git a/Assets/Scenes/Chapter1/SummonerDuelTest.unity.meta b/Assets/Scenes/Chapter1/SummonerDuelTest.unity.meta new file mode 100644 index 0000000..4487c91 --- /dev/null +++ b/Assets/Scenes/Chapter1/SummonerDuelTest.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: Xn0f5ir8VS6B22vrUeDxX6zyjPoPyJCwnujSqZKSP5RfDlB/o9Ks2sI= +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Script/BattleManager.cs b/Assets/Script/BattleManager.cs new file mode 100644 index 0000000..008604a --- /dev/null +++ b/Assets/Script/BattleManager.cs @@ -0,0 +1,102 @@ +using UnityEngine; +using UnityEngine.UI; + +/// +/// 战斗管理器:监听双方召唤师死亡,触发胜负判定并显示结果 UI。 +/// +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(); + } + + if (enemySummoner != null) + { + enemyHealthBar = enemySummoner.GetComponent(); + } + + // 隐藏胜负 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 + } +} diff --git a/Assets/Script/BattleManager.cs.meta b/Assets/Script/BattleManager.cs.meta new file mode 100644 index 0000000..2e1c5a7 --- /dev/null +++ b/Assets/Script/BattleManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: Di4etymsUXvNaAtAYCGTjqWlsQBo5CkpSQWOamGKNe31K6x/sOO8dpY= +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Script/MainCharacterCombat.cs b/Assets/Script/MainCharacterCombat.cs index 0eb423b..628d681 100644 --- a/Assets/Script/MainCharacterCombat.cs +++ b/Assets/Script/MainCharacterCombat.cs @@ -206,6 +206,23 @@ public class MainCharacterCombat : MonoBehaviour return true; } + // 召唤师对决模式:点卡片按 CharacterData 直接召唤(能量消耗在 RequestSummon 同路径处理)。 + public bool RequestSummonByData(CharacterData characterData) + { + if (summoning || animator == null || characterData == null || characterData.unitPrefab == null) + { + return false; + } + + int index = System.Array.IndexOf(summonableCharacters, characterData); + if (index < 0) + { + return false; + } + + return RequestSummon(index + 1); + } + private void BeginSummon(GameObject prefab, CharacterData characterData, PlayerCharacterData playerData) { if (prefab == null) diff --git a/Assets/Script/SummonCardButton.cs b/Assets/Script/SummonCardButton.cs index fe2da6c..59a338d 100644 --- a/Assets/Script/SummonCardButton.cs +++ b/Assets/Script/SummonCardButton.cs @@ -79,6 +79,16 @@ public class SummonCardButton : MonoBehaviour return; } + // 召唤师对决模式:点卡片直接在召唤师身边召唤,消耗能量。 + if (SummonerDuelManager.Instance != null) + { + if (SummonerDuelManager.Instance.TrySummon(characterData)) + { + StartCooldown(); + } + return; + } + // 建筑模式:检查建筑费用是否足够 if (GoldSystem.Instance == null || GoldSystem.Instance.CurrentGold < characterData.buildingCost) { @@ -163,6 +173,12 @@ public class SummonCardButton : MonoBehaviour return BattleSetupManager.Instance.CanAfford(characterData); } + // 召唤师对决模式按能量判断。 + if (SummonerDuelManager.Instance != null) + { + return SummonerDuelManager.Instance.CanAfford(characterData); + } + // 建筑模式按金币判断。 if (GoldSystem.Instance == null) { diff --git a/Assets/Script/SummonerClickMovement.cs b/Assets/Script/SummonerClickMovement.cs new file mode 100644 index 0000000..90826b8 --- /dev/null +++ b/Assets/Script/SummonerClickMovement.cs @@ -0,0 +1,157 @@ +using UnityEngine; +using UnityEngine.EventSystems; + +/// +/// 召唤师对决:召唤师点地移动操控(直线移动,无寻路)。 +/// 点击己方区域地面 → 直线走到目标点(clamp 在己方带内)。 +/// 移动时不攻击;站定后自动攻击射程内目标。 +/// +public class SummonerClickMovement : MonoBehaviour +{ + [Header("己方半场 clamp(世界坐标)")] + public float minX = -9f; + public float maxX = 9f; + public float minZ = -29f; + public float maxZ = -11f; + + [Header("移动速度")] + public float moveSpeed = 5f; + public float stopDistance = 0.2f; + + [Header("地面射线")] + public LayerMask groundMask = ~0; + + private Camera mainCamera; + private MainCharacterCombat combat; + private Animator animator; + private Vector3 targetPosition; + private bool hasTarget; + + private void Awake() + { + combat = GetComponent(); + animator = GetComponent(); + } + + private void Start() + { + mainCamera = Camera.main; + } + + private void Update() + { + HandleClickToMove(); + HandleMovement(); + } + + // 点地移动:点击地面即设置目标点(EventSystem 判定不在 UI 上时才生效)。 + private void HandleClickToMove() + { + if (Input.GetMouseButtonDown(0) && !IsPointerOverUI()) + { + if (RaycastGround(out Vector3 groundPoint)) + { + Vector3 clamped = ClampToOwnHalf(groundPoint); + MoveTo(clamped); + } + } + } + + // 执行直线移动 + private void HandleMovement() + { + if (!hasTarget) return; + + Vector3 currentPos = transform.position; + Vector3 direction = targetPosition - currentPos; + direction.y = 0f; + float distance = direction.magnitude; + + // 到达目标 + if (distance <= stopDistance) + { + ClearTarget(); + return; + } + + // 移动 + direction.Normalize(); + transform.position += direction * moveSpeed * Time.deltaTime; + + // 朝向移动方向 + if (direction.sqrMagnitude > 0.001f) + { + transform.rotation = Quaternion.LookRotation(direction, Vector3.up); + } + + // 播放走路动画 + if (animator != null) + { + animator.SetInteger("speed", 1); + } + } + + // 外部入口:把召唤师移动到某点(clamp 后设置目标)。 + public void MoveTo(Vector3 worldPosition) + { + Vector3 clamped = ClampToOwnHalf(worldPosition); + targetPosition = clamped; + targetPosition.y = transform.position.y; // 保持 Y 不变 + hasTarget = true; + + // 移动中暂停攻击(防风筝规则) + if (combat != null) + { + combat.enabled = false; + } + } + + public void ClearTarget() + { + hasTarget = false; + + // 停止动画 + if (animator != null) + { + animator.SetInteger("speed", 0); + } + + // 站定后恢复自动攻击 + if (combat != null) + { + combat.enabled = true; + } + } + + // 半场 clamp:X 与 Z 均限制在己方带内。 + private Vector3 ClampToOwnHalf(Vector3 position) + { + return new Vector3( + Mathf.Clamp(position.x, minX, maxX), + position.y, + Mathf.Clamp(position.z, minZ, maxZ)); + } + + private bool RaycastGround(out Vector3 point) + { + point = Vector3.zero; + if (mainCamera == null) + { + mainCamera = Camera.main; + if (mainCamera == null) return false; + } + + Ray ray = mainCamera.ScreenPointToRay(Input.mousePosition); + if (Physics.Raycast(ray, out RaycastHit hit, 200f, groundMask.value)) + { + point = hit.point; + return true; + } + return false; + } + + private bool IsPointerOverUI() + { + return EventSystem.current != null && EventSystem.current.IsPointerOverGameObject(); + } +} diff --git a/Assets/Script/SummonerClickMovement.cs.meta b/Assets/Script/SummonerClickMovement.cs.meta new file mode 100644 index 0000000..25010e8 --- /dev/null +++ b/Assets/Script/SummonerClickMovement.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: D3MZ53/+Bi5T65J7i/sGjy4OrNmkAPexctr/dMT82/ffKFb1P6oQ0Co= +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Script/SummonerDuelManager.cs b/Assets/Script/SummonerDuelManager.cs new file mode 100644 index 0000000..b9521ee --- /dev/null +++ b/Assets/Script/SummonerDuelManager.cs @@ -0,0 +1,52 @@ +using UnityEngine; + +/// +/// 召唤师对决模式:点卡片直接在召唤师身边召唤单位(能量消耗 + 卡片冷却沿用现有系统)。 +/// 复用 SummonCardButton 的点击入口:无 BattleSetupManager、无 GoldSystem 时走本模式。 +/// +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(); + } + } + + // 卡片点击入口:能量足够时由召唤师执行召唤动画与生成。 + 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; + } +} \ No newline at end of file diff --git a/Assets/Script/SummonerDuelManager.cs.meta b/Assets/Script/SummonerDuelManager.cs.meta new file mode 100644 index 0000000..84b5b50 --- /dev/null +++ b/Assets/Script/SummonerDuelManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: DCtO4X/+USoIwkGoI7+2kj00a9R1yMRAYBXPILcLhDHrIqmIIORGtp8= +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Script/SummonerHealthBarUI.cs b/Assets/Script/SummonerHealthBarUI.cs new file mode 100644 index 0000000..f5636a8 --- /dev/null +++ b/Assets/Script/SummonerHealthBarUI.cs @@ -0,0 +1,54 @@ +using UnityEngine; +using UnityEngine.UI; + +/// +/// 召唤师专属血条 UI:顶部 HUD 显示双方召唤师血量,区别于单位头顶血条。 +/// +public class SummonerHealthBarUI : MonoBehaviour +{ + [Header("UI 引用")] + public Image playerHealthFill; + public Image enemyHealthFill; + public Text playerHealthText; + public Text enemyHealthText; + + [Header("召唤师引用")] + public GameObject playerSummoner; + public GameObject enemySummoner; + + private WorldHealthBar playerHealthBar; + private WorldHealthBar enemyHealthBar; + + private void Start() + { + // 获取双方召唤师的血条组件 + if (playerSummoner != null) + { + playerHealthBar = playerSummoner.GetComponent(); + } + + if (enemySummoner != null) + { + enemyHealthBar = enemySummoner.GetComponent(); + } + } + + private void Update() + { + UpdateHealthBar(playerHealthBar, playerHealthFill, playerHealthText); + UpdateHealthBar(enemyHealthBar, enemyHealthFill, enemyHealthText); + } + + private void UpdateHealthBar(WorldHealthBar healthBar, Image fillImage, Text healthText) + { + if (healthBar == null || fillImage == null) return; + + float ratio = healthBar.currentHealth / healthBar.maxHealth; + fillImage.fillAmount = ratio; + + if (healthText != null) + { + healthText.text = $"{Mathf.CeilToInt(healthBar.currentHealth)}/{Mathf.CeilToInt(healthBar.maxHealth)}"; + } + } +} diff --git a/Assets/Script/SummonerHealthBarUI.cs.meta b/Assets/Script/SummonerHealthBarUI.cs.meta new file mode 100644 index 0000000..a2a6c10 --- /dev/null +++ b/Assets/Script/SummonerHealthBarUI.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: CilJ4H+sVH2HTBsacd3p++uNBi+u613d+RUcMwWW0wsoqCmwgU2DNms= +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Script/UnitBase.cs b/Assets/Script/UnitBase.cs index 7e4a097..32227d5 100644 --- a/Assets/Script/UnitBase.cs +++ b/Assets/Script/UnitBase.cs @@ -72,6 +72,13 @@ public class UnitBase : MonoBehaviour [Min(0f)] public float separationWeight = 3f; [Min(0f)] public float targetSpreadBias = 6f; + [Header("召唤师模式")] + // 召唤师:不自动追击远处敌人,只在站定后攻击射程内的目标;移动由 SummonerClickMovement 控制。 + public bool summonerMode = false; + + //召唤师模式下允许自动攻击的开关(移动时由操控脚本关闭,站定时开启)。 + private bool summonerAttackEnabled = true; + private static Collider[] s_separationBuffer = new Collider[30]; //角色状态 @@ -243,6 +250,20 @@ public class UnitBase : MonoBehaviour target = FindNearestOpponent(); if (target != null) { + // 召唤师模式:不主动追击,站定且攻击开关开启时才攻击射程内目标。 + if (summonerMode) + { + if (!IsWithinAttackReach(target) || !summonerAttackEnabled) + { + state = State.Alert; + // 动画由 SummonerClickMovement 控制,这里不设置 + applyGravity(); + return; + } + attack(target); + return; + } + state = State.Track; //目标在攻击范围外则朝目标移动,否则开始攻击 @@ -260,7 +281,11 @@ public class UnitBase : MonoBehaviour //场上没有敌人时进入待机,不再巡逻移动 state = State.Alert; - animator.SetInteger("speed", 0); + // 召唤师模式下动画由 SummonerClickMovement 控制(移动时 speed=1,站定后 speed=0) + if (!summonerMode) + { + animator.SetInteger("speed", 0); + } applyGravity(); } @@ -268,6 +293,16 @@ public class UnitBase : MonoBehaviour public string OpponentTag => GetOpponentTag(); + // 召唤师模式下控制"移动中不攻击":移动时 false,站定后 true。 + public void SetSummonerAttackEnabled(bool enabled) + { + summonerAttackEnabled = enabled; + if (!enabled && state == State.Track) + { + state = State.Alert; + } + } + public float AttackSpeedMultiplier => attackSpeedMultiplier; public bool IsStunned => statusEffectController != null && statusEffectController.IsStunned; diff --git a/Assets/Script/WorldHealthBar.cs b/Assets/Script/WorldHealthBar.cs index f9812e7..19fbfeb 100644 --- a/Assets/Script/WorldHealthBar.cs +++ b/Assets/Script/WorldHealthBar.cs @@ -89,6 +89,9 @@ public class WorldHealthBar : MonoBehaviour // 防止死亡逻辑重复执行。 private bool isDead; + // 公开死亡状态供外部查询(用于胜负判定) + public bool IsDead => isDead; + // 初始化血量和血条 UI。 private void Awake() { diff --git a/CODELY.md b/CODELY.md index 2159d29..cbec3bc 100644 --- a/CODELY.md +++ b/CODELY.md @@ -42,7 +42,8 @@ - [2026-09-10 18:38:13] 第一关地图(Level1-1.scene)已采用"皇室战争"风格简化设计:移除所有边界矮墙(俯视角只能看到墙顶),改用旗帜+石柱交替排列标识边界(X=±5.5,Z轴8个位置交替);保留中间战道掩体(木箱×6、碎石堆×4);天空改为纯色蓝灰色+线性雾效(起始40m,结束100m);摄像机添加边界Clamp限制(X: -7~7, Z: -32~93,宽松模式),主角靠近边界时摄像机停止跟随;光照调整为从后上方照射(50°, 0°, 0°),角色正面清晰可见。**Why:** 正交俯视视角下垂直墙面几乎看不见(只能看到墙顶),旗帜+石柱有高度和轮廓,视觉识别强。**How to apply:** 后续关卡延续此风格,避免使用低矮平面装饰物。 - [2026-09-10 18:38:32] 召唤逻辑已优化碰撞处理:当目标位置被占用时,自动螺旋搜索周围空位(左右搜索8次×前后3层×2方向,约102个候选点);如果所有位置都被占用,自动退还能量豆并取消召唤,避免浪费能量。参数:summonSearchCount=8, summonDepthSearchCount=3, summonSearchStep=0.8f, summonDepthSearchStep=1f。 - [2026-09-11 14:55:27] 敌人血条颜色问题已修复:WorldHealthBar 在 Awake() 时根据 GameObject.tag 判断颜色,WaveSpawner 在 Instantiate() 后设置标签会导致时序错误(血条已创建成绿色)。解决方案:直接在敌人 Prefab 上设置 Enemy 标签,确保 Awake() 时标签已正确。已修改狼人/蓝色怪物/弓箭猎人/石巨人四个 Prefab。**Why:** 血条颜色在对象创建时确定,后设置标签无法自动刷新。**How to apply:** 后续新增敌人 Prefab 时直接在 Prefab 上设置 Enemy 标签,不依赖运行时代码设置。 -- [2026-09-14 16:00:19] 2026-09-13 玩法大转向:从闯关推图改为"建筑生产+自动作战+主角辅助"的对攻玩法(见《召唤之战-核心玩法.md》)。旧闯关玩法由用户在另一台电脑的分支保存,当前 main 按新玩法开发。提交 beab4cf 已实现:GoldSystem/GoldMine 金币经济、Barracks 兵营产兵、BuildingPlacementManager 建筑摆放、CharacterData 新增 productionInterval/buildingCost、UnitBase/Projectile 支持攻击建筑、CoreBattleTest.scene 验证场景、CameraFollow 重写(WASD/鼠标/触摸拖动+缩放)。胜负目标改为摧毁对方主建筑。**How to apply:** 后续开发以建筑/经济/产兵系统为主线,能量豆召唤相关旧逻辑仅作为历史参考。 +- [2026-09-16 14:22:13] 2026-09-13 玩法大转向:从闯关推图改为"建筑生产+自动作战+主角辅助"的对攻玩法(见《召唤之战-核心玩法.md》)。旧闯关玩法由用户在另一台电脑的分支保存,当前 main 按新玩法开发。提交 beab4cf 已实现:GoldSystem/GoldMine 金币经济、Barracks 兵营产兵、BuildingPlacementManager 建筑摆放、CharacterData 新增 productionInterval/buildingCost、UnitBase/Projectile 支持攻击建筑、CoreBattleTest.scene 验证场景、CameraFollow 重写(WASD/鼠标/触摸拖动+缩放)。胜负目标改为摧毁对方主建筑。**How to apply:** 2026-09-16 起玩法进一步改为"召唤师对决"(见同日新条目),建筑/经济/产兵系统已封存、不再作为主线,相关脚本留作素材。 + - [2026-09-14 18:24:46] 2026-09-14 性能实测(9 个角色 Prefab):大多数 5~8k 三角面/22 骨骼/512 贴图,性能没问题;两个待处理异常——蓝色怪物 34408 面(是量产低价单位,建议减面到 8k 左右)、法师贴图 2048x2048(建议缩到 512)。另建议设计层限制每方场上单位上限(约15)保性能与 2.5D 可读性,并确认方向光关实时阴影,最终以低端安卓真机小游戏包为准。**How to apply:** 后续做性能优化或新增量产型敌人时按此基准控制面数。 - [2026-09-14 21:19:53] 2026-09-13 新增建筑系统核心脚本:BuildingHealth(建筑血量+生产进度接口)、GoldSystem/GoldMine(金币经济与产出)、Barracks(兵营产兵,读取 CharacterData.productionInterval)。WorldHealthBar 增加 SetEnergy() 通用下方条接口,建筑复用该条显示生产进度,单位仍用于技能能量。BuildingPlacementManager 支持虚拟网格吸附、占位检测、金币扣除,新放置建筑自动挂 BuildingHealth。UnitBase/Projectile 的 ApplyDirectDamage/ApplySkillHit/ApplyAreaSkillDamage/HitTarget 均已接入 BuildingHealth,建筑可被近战/技能/范围/飞行物伤害。兵营 Prefab(Assets/Prefabs/建筑/兵营.prefab)已写入 Barracks+WorldHealthBar+BuildingHealth,默认阵营 Friend,场景实例保留 Enemy 覆盖。**Why:** 建筑作为可攻击目标是新对攻玩法的核心,生产进度条复用血条下方条避免额外 UI。**How to apply:** 后续新增建筑类型(防御塔/资源建筑)参照 BuildingHealth 接口;修改血条下方条逻辑时注意区分单位和建筑语义。 - [2026-09-14 21:34:17] 2026-09-14 发现:团结引擎 1.10.2 (macOS) 原生 NavMesh 烘焙失效——空场景放一个静态 Cube,用 NavMeshBuilder.BuildNavMeshData / UpdateNavMeshDataAsync / 经典全局 BuildNavMesh 烘焙全部产出 0 三角形,无报错;收集源(CollectSources)、Job 系统、AI 模块包均正常;换构建目标/域重载/重启编辑器均无效。**Why:** 原生导航烘焙在此安装上深层失效,继续用 NavMesh 会浪费时间。**How to apply:** 本项目寻路不要用 NavMesh/NavMeshAgent/NavMeshSurface,改用纯 C# 网格 A*(见 GridPathfinder.cs);若未来升级团结版本可先重跑最小 Cube 烘焙测试再决定。 @@ -52,6 +53,8 @@ - [2026-09-15 17:33:54] NavMesh 寻路攻击漂移问题修复方案:单位攻击时会因 NavMeshAgent 继续工作而漂移。解决方法是在 attack() 方法和 Update() 的 State.Attack 分支中直接禁用 NavMeshAgent (navAgent.enabled = false),在 move() 方法中重新启用。同时修复了 applyGravity() 调用已禁用 CharacterController 导致的警告(添加 controller.enabled 检查)。性能优化:寻路路径更新频率限制为 0.5 秒一次(nextPathUpdateTime + PathUpdateInterval),减少 93% 路径计算开销。 - [2026-09-15 18:42:26] 伤害数字显示规则:友方单位(Friend tag)受伤显示红色数字向下浮动,敌方单位(Enemy tag)受伤显示白色数字向上浮动,治疗显示绿色向上。技术实现:8方向黑色描边(DamageNumberOutlineWidth = 0.025f 常量)+ 1个彩色主字,共9个TextMesh。通过 gameObject.CompareTag("Friend") 判断受击者阵营。已更新到《召唤之战-核心玩法.md》文档 2.3 章节。 - [2026-09-15 19:33:31] 金币系统已完成:GoldManager 单例管理,初始500金币,右上角UI实时更新。金矿建筑产出50金币/5秒,主角产出30金币/8秒。WorldHealthBar 的 currentEnergy 改为属性(带 setter 自动调用 UpdateEnergyFill),能量条尺寸从8像素增加到12像素(EnergyPixelHeight=12,EnergyPixelWidth=100)。能量条显示金币产出进度,满了产出并重置。已更新核心玩法文档。 +- [2026-09-16 14:22:13] 2026-09-16 玩法定稿为"召唤师对决"并已写入《召唤之战-核心玩法.md》:无基地/金矿/兵营/防御塔,玩家手动点地操控召唤师(移动不攻击、站定攻击),限己方半场(三分屏:己方/交战/敌方区),能量直购出兵(单位在召唤师身边生成),击杀敌方召唤师获胜,超时毒圈收缩并解除半场限制,中期混沌裂缝事件,召唤师技能 3~4 种,摇杆留作实测后升级项。**Why:** 用户多轮迭代后的最终方向,旧"建筑生产+自动作战"对攻玩法封存不删。**How to apply:** 新战斗开发一律基于召唤师对决方案;不要在新玩法场景引入金矿/兵营/建筑摆放;BuildingPlacementManager/Barracks/GoldMine 等封存留作后续建筑卡或 PVE 特殊关素材。 +- [2026-09-16 14:48:52] 召唤师对决首版场景已完成(SummonerDuelTest.unity,2026-09-16):新场景地面20×80、固定正交相机43°/orthoSize28、召唤师z=0(敌方z=35)均避开底部UI;新增 SummonerClickMovement(点地走位+clamp X[-9,9] Z[-19,6],移动中关攻击、到点恢复)、SummonerDuelManager(点卡片按能量直召,单位在召唤师身边生成)、UnitBase 新增 summonerMode 开关(不追击、站定攻击,默认关闭不影响旧单位);战斗界面UI从 CoreBattleTest 整树搬运复用,SummonCardButton 增加对决模式分支(无布阵/金币管理器时走能量直召+冷却)。主角已存为 Assets/Prefabs/主角/召唤师主角.prefab。**How to apply:** 后续对决玩法迭代(毒圈/胜负判定/召唤师技能)基于该场景与脚本扩展;Play Mode 验证等用户手动执行。 ### Reference - [2026-09-09 12:23:17] 飞行物脚本 Projectile.cs 负责锁敌追踪、碰撞检测和伤害结算。箭矢预制体 ArrowProjectile_White.prefab 已配置 Projectile 组件和 CapsuleCollider(Is Trigger)。远程单位需在攻击动画松弦帧添加 OnAttackEffect Event,并在 UnitBase 中设置 attackType=RangedProjectile、projectilePrefab、projectileSpawnPoint。 diff --git a/召唤之战-核心玩法.md b/召唤之战-核心玩法.md index bdbda34..bb96f3e 100644 --- a/召唤之战-核心玩法.md +++ b/召唤之战-核心玩法.md @@ -1,288 +1,213 @@ # 召唤之战-核心玩法 -> 当前版本:核心玩法草案 +> 当前版本:召唤师对决玩法(2026-09-16 定稿) > -> 目标:围绕“建筑生产 + 自动作战 + 主角辅助”的对攻玩法,先确定单局战斗的基本规则,再逐步扩展角色、装备、养成和对战模式。 +> 目标:两个召唤师在三分屏战场上通过召唤单位进行对决,摧毁敌方召唤师获胜。玩家手动操控召唤师,单位自动作战。 +> +> 旧版"建筑生产 + 自动作战 + 主角辅助"方案已由 git 提交存档(e847318 及更早),相关脚本封存不删除,见"八、与现有系统的衔接"。 ## 一、核心定位 -《召唤之战》是一款以**召唤和建筑生产**为核心的实时策略战斗游戏。 +《召唤之战》是一款**召唤师对决**实时策略游戏。 -玩家通过金币建造金矿和兵营,兵营持续生产角色单位。己方单位自动寻找目标并向敌方推进,玩家主要通过经济投入、建筑选择、兵种组合和主角技能影响战局。 +一句话规则:手动操控召唤师 + 消耗能量召唤单位 + 击杀敌方召唤师获胜。 -核心决策是: - -- 先建造金矿,发展经济; -- 还是先建造兵营,尽快形成战斗压力; -- 选择哪种角色建筑; -- 建筑放在哪里,如何避免堵塞和被快速摧毁; -- 什么时候操作主角移动或释放技能。 +- 单位由 AI 自动作战,玩家不微操单位; +- 玩家的全部操作只有三类:**点地移动召唤师、点卡片召唤单位、释放召唤师技能**; +- 与同类游戏(皇室战争)的核心差异:召唤师可手动操控且是胜负目标本身、召唤师半场限制、混沌裂缝随机事件。 ## 二、单局战斗结构 -### 2.1 地图 +### 2.1 三分屏战场 -- 地图长度比当前第一版长地图明显缩短,避免玩家长时间赶路。 -- 每个小关使用一个独立场景,同时复用通用建筑、角色、UI 和特效资源。 -- 地图保留主题变化,例如城堡、荒野、沼泽、矿坑、雪山等。 -- 地图中间设置少量障碍物,用来分割进攻路径。 -- 进攻路线根据关卡设计分为 1 路、2 路或 3 路。 -- 单位从己方区域向敌方区域推进,并在路线上寻找最近的有效目标。 +竖屏横向三条带,整场尽收眼底: -### 2.2 寻路(已实现 ✅) +| 区域 | 位置 | 规则 | +|---|---|---| +| 己方区 | 下 1/3 | 召唤师唯一活动区;出兵落点(召唤师身边);己方单位可自由出入 | +| 交战区 | 中 1/3 | 双方单位交战主战场;后期混沌裂缝的刷新舞台 | +| 敌方区 | 上 1/3 | 敌方召唤师活动区 | -**当前状态:** -- 已接入 Unity NavMesh 自动寻路系统 -- 单位可以自动绕过障碍物和地形 -- 支持左右两路地图布局 -- 寻路性能已优化(路径更新频率限制为 0.5 秒/次) +**区域规则:** -**寻路规则:** -- 单位自动寻找距离最近的敌方单位或建筑 -- 遇到障碍物时自动绕行 -- 攻击时停止移动,避免边打边漂移 -- 单位之间可以轻微重叠(无物理碰撞),避免密集战斗时卡位 -- NavMeshAgent 提供基础避让,但不保证完全不重叠 +- 只限制召唤师:双方单位全场自由移动(单位必须能进入敌方区攻击敌方召唤师); +- 召唤师位置 clamp 在己方区内(实现同摄像机边界 clamp 思路); +- 边境线有战术价值:召唤师站己方区前沿可以用射程够到交战区的敌人——压线 = 高风险高输出,缩后排 = 安全,站位深度自动产生; +- 地图总长 45~60 单位(双方召唤师间距约 40),比旧版 120 单位长图明显缩短。 -**技术细节:** -- 使用 NavMeshAgent 替代 CharacterController 移动 -- 攻击时禁用 NavMeshAgent,防止漂移 -- 移动时重新启用 NavMeshAgent -- 攻击距离判定增加 1.5 米缓冲,避免单位互相顶到才攻击 +### 2.2 相机 -### 2.3 伤害数字反馈(已实现 ✅) +- 固定正交 2.5D 视角俯瞰全场(沿用现有机位思路,约 43°); +- 不拖动、不跟随,整场可见是点地移动精确性的前提; +- 输入只有三层:点地(移动召唤师)、卡片(召唤单位)、技能按钮,互不冲突(地面点击用射线过滤 UI)。 -**当前状态:** -- 单位受击时头顶弹出伤害数字 -- 8 方向黑色描边 + 彩色主字,清晰可见 -- 根据受击者阵营显示不同颜色和浮动方向 +### 2.3 寻路(已实现 ✅,保留) -**显示规则:** +- 已接入 Unity NavMesh 自动寻路,单位自动绕过障碍物; +- 单位自动寻找距离最近的敌方单位或敌方召唤师; +- 攻击时禁用 NavMeshAgent 防止漂移,移动时重新启用; +- 路径更新频率 0.5 秒/次; +- 单位之间可轻微重叠,攻击距离判定增加 1.5 米缓冲。 + +### 2.4 伤害数字(已实现 ✅,保留) | 情况 | 数字颜色 | 浮动方向 | 说明 | |------|---------|---------|------| -| 友方单位受伤 | 红色 | 向下 | 玩家的士兵被敌人攻击 | -| 敌方单位受伤 | 白色 | 向上 | 敌人被玩家的士兵攻击 | +| 友方单位受伤 | 红色 | 向下 | 召唤师或己方单位被敌人攻击 | +| 敌方单位受伤 | 白色 | 向上 | 敌人被己方攻击 | | 任何单位治疗 | 绿色 | 向上 | 单位被治疗恢复生命值 | -**技术细节:** -- 每个伤害数字由 9 个 TextMesh 组成(1 个彩色主字 + 8 个黑色描边) -- 描边宽度统一为 0.025,所有角色一致 -- 伤害数字持续 0.8 秒,带缩放和淡出动画 -- 同时最多显示 60 个伤害数字,防止极端情况刷屏 +技术细节不变:8 方向黑描边 + 彩色主字,描边宽 0.025,持续 0.8 秒,全场并发上限 60。 -## 三、主角定位 +## 三、召唤师 -主角保留,并作为己方单位加入战场,而不是单纯的 UI 指挥官。 +### 3.1 操控 -### 3.1 主角职责 +- **手动操控**(这是整个玩法的灵魂,参与感来源); +- **点地移动**,不用摇杆:点一下走一段,可连续点更新目标; +- **移动时不攻击,站定时自动攻击射程内最近敌人**——防风筝设计,召唤师是"重型单位",走位决策 = 什么时候敢站定输出; +- 摇杆留作后续升级项(如"按住拖动连续转向"),等实测确认需要躲弹幕级微操再加。 -- 主角可以普通攻击; -- 主角全程由 AI 自动控制,不支持点击地面手动移动(避免与拖动屏幕、建筑放置的输入冲突); -- 没有敌人时,主角在己方主建筑周围小范围来回巡逻; -- 索敌范围较小:只有敌人进入主角附近一定范围内才会被发现并攻击,不主动远离基地寻找敌人; -- 巡逻和追击都被限制在基地附近区域,主角不能完全离开基地; -- 主角可以手动释放技能(通过 UI 技能按钮,不占用地面点击操作); -- 主角技能冷却较长,主要用于辅助、补刀和改变局部战况; -- 主角不是主要战斗力,不能替代建筑生产的军队; -- 主角的价值是提升玩家参与感,并在关键时刻提供战术干预。 +### 3.2 生存性(防一波带走) -### 3.2 主角装备 +召唤师比塔脆,需要底线手段防止死亡螺旋: -主角可以装备不同武器或装备。装备可改变主角的普通攻击方式和技能效果。 +- 基础血量约为普通单位的 3~4 倍; +- 脱战回血:数秒未受伤害后缓慢回复; +- 召唤的单位天然优先攻击最近敌人,自动充当召唤师护卫; +- 开局短暂保护(如 3 秒保护期),防止开局被冲脸秒杀。 -初步装备方向: +### 3.3 召唤师技能 -| 装备 | 普攻方向 | 技能方向 | 定位 | -|---|---|---|---| -| 法杖/魔杖 | 远程魔法攻击 | 范围伤害、控制或增益 | 远程辅助 | -| 弓箭 | 远程物理攻击 | 多重射击、点杀或穿透 | 远程输出辅助 | -| 大剑 | 近战攻击 | 范围斩击、击退或短时强化 | 近战辅助 | +- 每位召唤师装备 1 个召唤师技能,冷却较长,不占用点地操作; +- 首发做 3~4 种,至少一个保命向: + - **闪现**:向指定方向瞬移一段距离(保命/进场); + - **治疗**:恢复自身或附近血量(续航); + - **护盾**:短时减伤或格挡(抗压); + - 预留一个功能向(如加速/嘲讽),以测试手感定; +- 技能释放为点击目标式(点敌人/点地面),不用摇杆瞄准。 -装备技能冷却较长,主要是锦上添花,不应让主角单独决定整场胜负。 +## 四、出兵系统 -装备获取、稀有度和具体数值暂不实现,后续可与抽卡系统关联。 +### 4.1 能量 -## 四、建筑系统 +- 战斗内唯一资源为能量,自动回复(沿用 ManaSystem,约 3 秒 1 点,具体以测试平衡为准); +- 能量上限沿用现有 10 点方案; +- 单位费用分 1~6 档,高强度角色费用更高; +- **金币不进入战斗内**,只用于账户养成,避免双货币混淆。 -### 4.1 建筑类型 +### 4.2 卡片 -首版至少包含以下建筑: +- 出战卡组 5 张(沿用现有 5 卡底栏 UI 和 SummonCardButton); +- 点卡片消耗能量,**单位在召唤师身边生成**("召唤师施法召出单位",主题自洽,天然落在己方区); +- 场上单位上限:每方约 15(性能与 2.5D 可读性)。 -| 建筑 | 主要作用 | -|---|---| -| 主建筑 | 己方核心建筑,负责承受攻击并作为重要胜负目标 | -| 金矿 | 持续产生金币,支持后续建造 | -| 兵营 | 按固定间隔持续生产指定角色单位 | +### 4.3 单位规则 -除了主建筑外,金矿和兵营都需要消耗金币建造。 +- 单位出生后自动寻找最近有效目标(敌方单位或敌方召唤师); +- 单位可自由进入任意区域,包括敌方区; +- 单位既是进攻手段,也是召唤师的护卫。 -后续可以增加防御塔、特殊功能建筑或不同生产类型的建筑,但不作为首版核心要求。 +## 五、胜负规则 -### 4.2 建筑摆放 +### 5.1 主要胜负 -采用“隐藏粗网格 + 视觉上不显示网格线 + 建筑自动吸附”的方案。玩家看到的是自然地面,不需要操作可见的密集网格;只有建筑放置位置受到隐藏网格约束。 +- 敌方召唤师死亡 → 立即胜利; +- 己方召唤师死亡 → 立即失败。 -首版不制作复杂的六边形网格编辑器,也不要求玩家精确调整每个建筑的连续坐标。建筑放置系统负责将建筑吸附到最近的合法位置,同时保留一定的布局自由度。 +### 5.2 毒圈强制终局 -#### 基本规则 +- 正常对局约 3 分钟; +- 超时后毒圈从全场范围开始收缩(约 90 秒),圈外伤害递增(示意 5% → 20% 最大生命/秒,以测试为准); +- **毒圈吞掉己方区后,"召唤师半场限制"自然解除**——前期分区限制是保险,毒圈负责拆掉保险,后期召唤师被逼进交战区正面相遇,比赛必有结局; +- 毒圈完全闭合仍分不出胜负时,比较双方召唤师血量百分比(兜底)。 -- 建筑只能放置在允许的己方区域内; -- 建筑移动预览时自动吸附到最近的粗网格位置; -- 建筑之间不能重叠; -- 建筑之间需要保持最小安全间距; -- 建筑不能超出地图边界; -- 建筑不能堵住主建筑、主角或己方单位的主要出口; -- 建筑不能完全封死己方主要进攻路线; -- 非法位置不能确认建造。 +## 六、混沌裂缝事件(差异点) -#### 建筑占格建议 +- 单局中期随机在交战区刷出混沌裂缝; +- 裂缝出现 1 只超级英雄:同档位 3~4 种、互有克制、强度可控; +- 争夺归属而非纯随机赠送(PVE 阶段可先用简单交替归属验证手感:第一只归甲方、第二只归乙方); +- PVE 先上验证手感;PVP 阶段改为双方同时获得同档英雄,消除运气差; +- 裂缝是单次事件,运气被限制在单局内的一次波动,不影响底盘公平性。 -- 金矿:暂定占用 2×2 格; -- 兵营:暂定占用 2×2 格; -- 主建筑:暂定占用 3×3 或 4×4 格; -- 建筑之间至少保留一格缓冲距离,避免建筑虽然没有重叠,但互相贴近导致单位无法通过。 +## 七、养成系统 -具体网格尺寸和建筑占格数量以实际地图比例和单位体型测试结果为准。 +### 7.1 资源分层 -#### 放置预览 +| 层 | 资源 | 用途 | +|---|---|---| +| 战斗内 | 能量(自动回复) | 只影响单局,召唤单位 | +| 账户外 | 金币 | 角色升级 | +| 账户外 | 抽奖券(对战奖励) | 免费抽卡渠道 | +| 账户外 | 钻石(付费) | 抽卡加速、外观 | -玩家选择金矿或兵营后,进入建筑放置状态: +### 7.2 角色收集与升级 -1. 显示半透明的建筑预览; -2. 预览自动吸附到最近的粗网格位置; -3. 建筑底部显示放置边框; -4. 合法位置显示绿色边框,并允许确认建造; -5. 非法位置显示红色边框,并禁止确认建造; -6. 点击确认后扣除金币并生成正式建筑; -7. 点击取消或右键退出放置状态。 +- 稀有度 4 档:N / R / SR / SSR; +- 获取渠道:关卡首通奖励 + 抽奖券抽卡 + 商店; +- 升级消耗:金币 + 同名卡片; +- 等级提升基础属性(沿用 CharacterData 基础属性 + growth 成长值架构); +- 关键等级节点(Lv5 / Lv10)解锁技能强化。 -绿色/红色边框需要让玩家明确知道当前位置是否能够放置,而不显示完整网格线。 +### 7.3 卡组 -#### 合法性检查 +- 出战卡组 5 张,从已收集角色池中配置; +- 卡池随收集扩大。 -建筑位置校验至少需要考虑: +### 7.4 召唤师养成 -1. 建筑自身占用范围; -2. 与其他建筑的安全间距; -3. 建筑与地图边界的距离; -4. 主角和生产单位的出生点; -5. 己方主要进攻通道是否仍然可通行; -6. 建筑被摧毁后是否仍会留下阻挡; -7. 建筑是否处于可建造地面。 +- 召唤师等级单独养成,影响召唤师基础属性与召唤师技能强度; +- 召唤师技能在配置界面选择装配(3~4 选 1); +- 武器/装备方向(法杖/弓/大剑)作为后续扩展,可对应不同召唤师技能形态。 -实现上可以组合使用隐藏网格占用判断、碰撞范围检测和进攻路线通行检查,而不只依赖单个 Collider。 +### 7.5 进度与模式 -#### 视觉与资源复用 +- PVE 章节:AI 召唤师当 BOSS,关卡差异 = 敌方召唤师技能 + 初始兵种 + 裂缝事件开关; +- 混沌裂缝在第 2~3 章作为机制教学登场; +- PVP 竞技场段位后置,依赖后端接口(Java 服务)。 -- 兵营和金矿使用统一的正方形底座规格; -- 兵营通过顶部站立的对应角色模型表现生产兵种; -- 金矿通过明显的金色矿脉、矿井入口和采矿收集装置表现功能; -- 主建筑使用更大的独立底座和召唤核心突出重要性; -- 运行时角色展示、生产进度条和放置边框不烘焙进建筑贴图。 +### 7.6 数值红线 -如果隐藏粗网格仍无法稳定解决卡位问题,再考虑使用预设建筑槽位。主角和战斗单位仍使用真实世界坐标移动,只有建筑放置使用隐藏网格。 +- 稀有度影响强度上限与获取成本,但同级数值压制必须有策略解法; +- 付费点 = 抽卡、外观,不直接卖数值,避免稀有度造成无法通过策略弥补的碾压。 -## 五、兵营生产 +## 八、与现有系统的衔接 -### 5.1 基本规则 +### 8.1 直接复用 -- 角色对应的兵营建造完成后,开始生产该角色; -- 兵营每隔若干秒自动产出一个单位; -- 生产出的单位从兵营附近的出生位置出现; -- 单位出现后自动寻找目标并参与战斗; -- 兵营被摧毁后停止生产; -- 生产间隔、建筑血量和角色费用由角色与建筑配置决定。 +- UnitBase 全套战斗/寻路(NavMeshAgent、移动/攻击状态机——"移动不攻击/站定攻击"与现有状态机同构); +- MainCharacterCombat(召唤师战斗逻辑基础); +- ManaSystem 能量、SummonCardButton 卡片 UI、伤害数字、WorldHealthBar; +- CameraFollow 的边界 clamp 思路用于召唤师半场限制。 -### 5.2 建筑表现 +### 8.2 新增 -首版尽量复用同一个兵营模型: +- 召唤师点地移动输入与半场 clamp; +- 召唤师专属血条 / HUD; +- 毒圈(收缩圆环 + 距离判定伤害); +- 胜负判定改为"召唤师死亡"; +- 召唤师技能 3~4 种。 -- 所有角色兵营使用统一的兵营建筑模型; -- 兵营顶部或正面显示对应角色的小头像; -- 头像外显示绿色生产进度条; -- 进度条从空逐渐增长,完成后生产一个单位并重新开始; -- 金矿可以暂时复用统一的建筑表现; -- 建筑外观差异主要通过头像、颜色和 UI 标识体现。 +### 8.3 封存不删 -这样可以先验证玩法,不把开发成本集中在大量建筑模型上。后续玩家数量和内容规模确定后,再增加建筑皮肤或主题外观。 +- BuildingPlacementManager / Barracks / GoldMine / GoldSystem / 主基地相关逻辑; +- 留作后续"限时建筑卡"内容或 PVE 特殊关卡素材。 -## 六、经济系统 +## 九、落地优先级 -- 金币是建造金矿和兵营的主要资源; -- 金矿持续产生金币; -- 兵营和其他功能建筑消耗金币; -- 玩家需要在经济发展和即时战力之间做选择; -- 高稀有度或高强度角色可以设置更高的建筑/生产成本,但不能只依靠数值碾压普通角色; -- 金币初始值、金矿产出速度、建筑费用和单位生产间隔需要通过单局测试平衡。 +1. 召唤师点地移动 + 半场 clamp; +2. 胜负判定改为召唤师死亡 + 召唤师专属血条; +3. 能量直购出兵(召唤师身边生成); +4. 毒圈强制终局; +5. 召唤师技能 3~4 种; +6. PVE AI 召唤师 + 混沌裂缝事件; +7. 养成系统 UI(收集 / 升级 / 卡组配置); +8. PVP + 后端接口。 -## 七、自动战斗规则 +## 十、当前明确不做 -- 己方和敌方单位均由 AI 控制; -- 单位优先寻找距离最近的有效目标; -- 有效目标包括敌方单位和敌方建筑; -- 单位进入攻击范围后停止移动并进行普通攻击; -- 角色技能按各自规则自动释放; -- 主角与普通单位一样属于战场单位,但拥有玩家手动移动和手动技能操作; -- 主角没有主动指令时,按照自动巡逻和自动攻击规则行动。 - -## 八、胜负规则 - -### 8.1 当前确定规则 - -- 摧毁对方主建筑作为主要胜利目标; -- 己方主建筑被摧毁时,立即失败; -- 对方主建筑被摧毁时,立即胜利; -- 摧毁金矿和兵营可以削弱对方经济与持续生产能力,但暂不直接作为立即胜负条件。 - -### 8.2 限时结束 - -如果时间结束时双方主建筑都没有被摧毁,具体判定方式暂未最终确定。 - -待评估方案: - -1. 比较双方主建筑剩余生命值百分比; -2. 比较双方被摧毁建筑价值或建筑得分; -3. 比较双方场上剩余战力; -4. 平局后进入短时间加时,先摧毁建筑的一方获胜。 - -当前倾向优先测试“建筑摧毁得分”或“主建筑剩余生命值”方案,最终以实际对战体验为准。 - -## 九、角色与养成方向 - -角色是玩家收集和配置的战斗单位。每个角色可以对应一种兵营生产方案。 - -后续养成方向: - -- 角色通过抽卡或其他收集方式获得; -- 每次对战后获得抽奖券,作为免费获取渠道; -- 角色卡片可以升级; -- 普通等级提升基础属性; -- 在关键等级节点改变或强化技能; -- 角色具有稀有度; -- 稀有角色可以拥有更强或更特殊的能力,同时承担更高的金币消耗或生产成本; -- 需要避免稀有度直接造成无法通过策略弥补的数值碾压。 - -抽卡概率、碎片来源、金币消耗、升级节点和付费方案属于长期系统,暂不影响首版核心战斗验证。 - -## 十、首版验证优先级 - -1. 缩短地图并制作 1~2 条进攻路线; -2. 实现少量障碍物和基本自动寻路; -3. 保留主角的普通攻击、手动移动和一个手动技能; -4. 实现金矿产出金币; -5. 实现统一兵营模型和角色头像; -6. 实现兵营生产单位和绿色生产进度条; -7. 实现建筑不能重叠、不能过近和不能堵住出生点的摆放校验; -8. 实现攻击建筑和摧毁主建筑的胜负逻辑; -9. 通过单机对战或简单 AI 验证“发展经济还是快速建兵”的选择是否有趣; -10. 核心战斗成立后,再加入更多角色、装备、养成和对战模式。 - -## 十一、当前明确不做的内容 - -- 首版不制作复杂的完整 RTS 网格编辑系统; -- 首版不制作大量不同兵营模型; -- 首版不让主角成为主要输出核心; -- 首版不先实现完整抽卡、充值和联网对战; -- 首版不先扩展大量角色和装备; -- 首版不在限时胜负规则未验证前绑定复杂的排名和奖励系统。 +- 摇杆操控(留作实测后的升级项); +- 防御塔、永久建筑、限时建筑卡(首版一律不做); +- 金矿、兵营等战斗内经济建筑(封存,不参与新玩法); +- 抽卡概率与付费数值设计(先验证核心战斗); +- 排名、段位等复杂外圈系统(依赖 PVP 后置)。 \ No newline at end of file