角色能力,存档

This commit is contained in:
2026-05-19 11:50:21 +08:00
parent d25f237e76
commit 2dcb7a961a
136 changed files with 36035 additions and 27551 deletions

View File

@@ -197,11 +197,10 @@ namespace BaseGames.Editor.Combat
_detailRoot.Add(btnRow);
}
/// <summary>attack1 → attack2 → attack3 连击链数值横排预览。</summary>
/// <summary>连击序列数值横排预览。</summary>
private void BuildComboPreview(WeaponSO weapon)
{
// 只在有至少一个连击数据时显示
if (weapon.attack1Source == null && weapon.attack2Source == null && weapon.attack3Source == null)
if (weapon.groundComboSteps == null || weapon.groundComboSteps.Length == 0)
return;
var section = new Label("连击链预览") { style = { unityFontStyleAndWeight = FontStyle.Bold, marginBottom = 4 } };
@@ -210,8 +209,11 @@ namespace BaseGames.Editor.Combat
var chain = new VisualElement();
chain.AddToClassList("stats-preview");
void AddSegment(string label, ClipTransition clip, DamageSourceSO src, bool addArrow)
for (int i = 0; i < weapon.groundComboSteps.Length; i++)
{
var step = weapon.groundComboSteps[i];
bool addArrow = i < weapon.groundComboSteps.Length - 1;
var cell = new VisualElement
{
style =
@@ -230,24 +232,21 @@ namespace BaseGames.Editor.Combat
}
};
// 段名
cell.Add(new Label(label)
cell.Add(new Label($"攻击{i + 1}")
{
style = { fontSize = 10, color = new Color(0.65f, 0.65f, 0.65f) }
});
// Clip 名称
string clipName = clip?.Clip != null ? clip.Clip.name : "<无动画>";
string clipName = step.clip?.Clip != null ? step.clip.Clip.name : "<无动画>";
cell.Add(new Label(clipName)
{
style = { fontSize = 11, unityFontStyleAndWeight = FontStyle.Bold }
});
// 伤害数值
if (src != null)
if (step.damageSource != null)
{
int dmg = Mathf.RoundToInt(src.BaseDamage * src.DamageMultiplier);
cell.Add(new Label($"伤害 {dmg} [{src.BreakLevel}]")
int dmg = Mathf.RoundToInt(step.damageSource.BaseDamage * step.damageSource.DamageMultiplier);
cell.Add(new Label($"伤害 {dmg} [{step.damageSource.BreakLevel}]")
{
style = { fontSize = 10, color = new Color(1f, 0.7f, 0.3f) }
});
@@ -266,10 +265,6 @@ namespace BaseGames.Editor.Combat
chain.Add(new Label("→") { style = { alignSelf = Align.Center, marginLeft = 2, marginRight = 2 } });
}
AddSegment("攻击1", weapon.attack1Clip, weapon.attack1Source, true);
AddSegment("攻击2", weapon.attack2Clip, weapon.attack2Source, true);
AddSegment("攻击3", weapon.attack3Clip, weapon.attack3Source, false);
_detailRoot.Add(chain);
// 追加空中/上/下攻击的简要行
@@ -288,9 +283,9 @@ namespace BaseGames.Editor.Combat
});
}
ExtraStat("空中", weapon.airAttackSource);
ExtraStat("上", weapon.upAttackSource);
ExtraStat("下", weapon.downAttackSource);
ExtraStat("空中", weapon.airComboSteps?[0].damageSource);
ExtraStat("上", weapon.upStep.damageSource);
ExtraStat("下", weapon.downStep.damageSource);
if (extraRow.childCount > 0)
_detailRoot.Add(extraRow);

View File

@@ -0,0 +1,118 @@
using UnityEditor;
using UnityEngine;
using BaseGames.Combat;
using BaseGames.Player;
namespace BaseGames.Editor.Combat
{
/// <summary>
/// WeaponSO 自定义 Inspector在 hitBoxPrefab 下方提供一键生成按钮。
/// 等同于菜单 BaseGames / Create / Weapon HitBox Prefab但已预填 weaponId 并自动赋值。
/// </summary>
[CustomEditor(typeof(WeaponSO))]
public class WeaponSOEditor : UnityEditor.Editor
{
private const string OutputFolder = "Assets/_Game/Prefabs/Weapons";
public override void OnInspectorGUI()
{
DrawDefaultInspector();
var weapon = (WeaponSO)target;
EditorGUILayout.Space(6);
EditorGUILayout.LabelField("HitBox Prefab 工具", EditorStyles.boldLabel);
bool hasId = !string.IsNullOrWhiteSpace(weapon.weaponId);
bool hasPrefab = weapon.hitBoxPrefab != null;
if (!hasId)
{
EditorGUILayout.HelpBox("请先填写 weaponId再生成 HitBox Prefab。", MessageType.Info);
return;
}
string prefabPath = $"{OutputFolder}/WPN_{weapon.weaponId}_HitBox.prefab";
EditorGUILayout.HelpBox($"输出路径:{prefabPath}", MessageType.None);
string btnLabel = hasPrefab ? "重新生成 HitBox Prefab" : "一键生成 HitBox Prefab";
if (GUILayout.Button(btnLabel))
{
if (hasPrefab && !EditorUtility.DisplayDialog(
"确认重新生成",
$"hitBoxPrefab 已有引用,是否覆盖并重新赋值?\n\n{prefabPath}",
"覆盖", "取消"))
return;
var prefab = CreateHitBoxPrefab(weapon.weaponId, prefabPath);
if (prefab != null)
{
Undo.RecordObject(weapon, "Assign HitBox Prefab");
weapon.hitBoxPrefab = prefab;
EditorUtility.SetDirty(weapon);
AssetDatabase.SaveAssets();
}
}
}
// ── 创建逻辑 ──────────────────────────────────────────────────────────
private static GameObject CreateHitBoxPrefab(string weaponId, string assetPath)
{
EditorScaffoldUtils.EnsureFolder(OutputFolder);
int layer = LayerMask.NameToLayer("PlayerHitBox");
if (layer < 0)
{
Debug.LogWarning("[WeaponSOEditor] 未找到 Physics Layer 'PlayerHitBox',子节点 Layer 将设为 Default。");
layer = 0;
}
string prefabName = $"WPN_{weaponId}_HitBox";
var root = new GameObject(prefabName);
var instance = root.AddComponent<WeaponHitBoxInstance>();
var so = new SerializedObject(instance);
AddHitBoxChild(root, so, "HitBox_Ground", "_hitBoxGround", layer, new Vector2(1f, 0.5f));
AddHitBoxChild(root, so, "HitBox_Up", "_hitBoxUp", layer, new Vector2(0.5f, 1f ));
AddHitBoxChild(root, so, "HitBox_Down", "_hitBoxDown", layer, new Vector2(1f, 0.5f));
AddHitBoxChild(root, so, "HitBox_Air", "_hitBoxAir", layer, new Vector2(0.5f, 1f ));
so.ApplyModifiedPropertiesWithoutUndo();
var prefab = PrefabUtility.SaveAsPrefabAsset(root, assetPath);
Object.DestroyImmediate(root);
AssetDatabase.Refresh();
if (prefab != null)
{
EditorScaffoldUtils.PingAndSelect(prefab);
Debug.Log($"[WeaponSOEditor] 已创建:{assetPath}");
}
else
{
Debug.LogError($"[WeaponSOEditor] Prefab 保存失败:{assetPath}");
}
return prefab;
}
private static void AddHitBoxChild(GameObject root, SerializedObject so,
string nodeName, string fieldName,
int layer, Vector2 boxSize)
{
var child = new GameObject(nodeName);
child.transform.SetParent(root.transform, false);
child.layer = layer;
var col = child.AddComponent<BoxCollider2D>();
col.isTrigger = true;
col.size = boxSize;
var hb = child.AddComponent<HitBox>();
var prop = so.FindProperty(fieldName);
if (prop != null)
prop.objectReferenceValue = hb;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: df9abfb2b89aa244bbcc1f4e62694dd6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: