695 lines
32 KiB
C#
695 lines
32 KiB
C#
#if UNITY_EDITOR
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Text;
|
||
using Coffee.UIExtensions;
|
||
using TMPro;
|
||
using UnityEditor;
|
||
using UnityEngine;
|
||
|
||
/// <summary>
|
||
/// 烈焰风暴「结算面板(Settle)」/「奖励框(AwardFrame)」的 UI 化变体生成器。
|
||
///
|
||
/// 背景:XJMH 的爆奖面板都是「Art 源预制体 → MyGame 变体」两层。Art 源是给 3D 场景用的,
|
||
/// 直接当 UI 用会有三个问题:粒子不在 Canvas 里渲染、卡面数字没接、没有 BJEndUI 写分数。
|
||
/// 同门 Boss(铁拳 / 伽马风暴 / 破坏球 / 幸运轮 …)都在
|
||
/// <c>Assets/MyGame/aa/Game/XJMH/Boss/BJ/BJEnd</c> 下留了做好的变体,本脚本就是照抄它们的做法。
|
||
///
|
||
/// 从已有变体(PW_Effect_Settle_IronFist_Self_XingJiXunHang)反解出来的加工步骤:
|
||
/// 1. 所有 ParticleSystemRenderer:m_Enabled = 0(改由 UIParticle 渲染)
|
||
/// 2. 所有 ParticleSystemRenderer:m_SortingLayer = 0(对齐 Default 层)
|
||
/// 3. 根下新增 <c>UIParticle</c> 子节点:UIParticle 组件 + m_Particles 收全所有粒子
|
||
/// + m_Scale3D = (10,10,10)
|
||
/// ⚠ 这个节点自己的 localScale 在运行时会被 UIParticle 覆盖掉
|
||
/// (AutoScalingMode.Transform → UpdateTransformScale() 把它设成 1/parentLossyScale),
|
||
/// 所以模板里写 0 也只是"随便写";真正决定大小的是「各粒子节点 localScale × m_Scale3D」。
|
||
/// 3b. 各粒子节点 localScale 归一化:模板把源里的 30 / 46.176 / 48.395 统一改成 5
|
||
/// (配 m_Scale3D=10,等效放大 ≈1.67×,是同门统一口径)
|
||
/// 4. 根 RectTransform:anchor/pivot = 0.5,sizeDelta = 100×100,位置归零
|
||
/// 5. 分数文本(<c>Node/Textparent/Node</c>):字体换 SourceHanSans、spriteAsset 换金币图
|
||
/// 6. <c>Textparent</c> 下新增 <c>Node (1)</c> 文本(倍数用),scale 0.7 / 位置 (0,8) / 字号 36
|
||
/// 7. 根上挂 BJEndUI:tMP_score = 源里的分数文本,tMP_beishu = 新增的 Node (1)
|
||
///
|
||
/// 奖励框(AwardFrame)另走一条:不碰粒子,只在根上挂 BJImage(tmp_CoinNum = <c>Num</c> 文本)。
|
||
///
|
||
/// ⚠️ 本脚本不会去改 Art 源预制体(除非你点「就地补齐选中预制体」)。生成的是变体,
|
||
/// 这样以后美术更新 Art 源,MyGame 变体还能跟着走。
|
||
/// </summary>
|
||
public class FlameStrikeVariantBuilder : EditorWindow
|
||
{
|
||
// ---------------------------------------------------------------- 路径常量
|
||
private const string ArtEffectDir = "Assets/Art/XJMH/pw/pw_prefab/game/effect";
|
||
private const string BJEndDir = "Assets/MyGame/aa/Game/XJMH/Boss/BJ/BJEnd";
|
||
private const string BJScoreDir = "Assets/MyGame/aa/Game/XJMH/Boss/BJ/BJScore";
|
||
|
||
/// <summary>变体文件名后缀(与同门 _XingJiXunHang / _XieZi 一致)</summary>
|
||
private const string Suffix = "_LieYanFengBao";
|
||
|
||
// ---------------------------------------------------------------- 资源 guid(与同门变体完全一致)
|
||
private const string GoldSpriteGuid = "b5a3b539bfcb41e46ad594b9b14d7732";
|
||
private const string SilverSpriteGuid = "e034cf5e78470b44f8a1c4d42d5039d6";
|
||
private const string FontGuid = "fdd40eef2e75f6d46b51c289c46fe60b";
|
||
|
||
// ---------------------------------------------------------------- 模板参考值(从 BJEnd 已有变体抄的)
|
||
private const float TemplateParticleNodeScale = 5f; // 粒子节点 localScale(源里是 30 / 46.176 / 48.395)
|
||
private const float TemplateBeishuScale = 0.7f; // 倍数文本 localScale
|
||
private static readonly Vector2 TemplateBeishuPos = new Vector2(0f, 8f);
|
||
private static readonly Vector2 TemplateBeishuSize = new Vector2(200f, 50f);
|
||
private const float TemplateBeishuFontSize = 36f;
|
||
private static readonly Vector3 TemplateUiParticleScale3D = new Vector3(10f, 10f, 10f);
|
||
|
||
// ---------------------------------------------------------------- 选项
|
||
private bool _addUiParticle = true;
|
||
private bool _disableParticleRenderers = true;
|
||
private bool _resetSortingLayer = true;
|
||
private bool _scaleParticleNodes = true; // 模板口径:粒子节点归一化到 5
|
||
private bool _proportionalScale = true; // 按比例归一化(保留节点之间的相对大小)
|
||
private float _particleNodeScale = TemplateParticleNodeScale;
|
||
private bool _fixRootRect = true;
|
||
private bool _applyTextStyle = true;
|
||
private bool _addBjEndUI = true;
|
||
private bool _createBeishuNode = true;
|
||
private bool _addBjImage = true;
|
||
private bool _overwrite;
|
||
private Vector2 _scroll;
|
||
|
||
[MenuItem("Tools/烈焰风暴/结算面板 & 奖励框 变体生成器")]
|
||
private static void Open()
|
||
{
|
||
var w = GetWindow<FlameStrikeVariantBuilder>(false, "烈焰风暴变体", true);
|
||
w.minSize = new Vector2(460f, 520f);
|
||
w.Show();
|
||
}
|
||
|
||
private void OnGUI()
|
||
{
|
||
_scroll = EditorGUILayout.BeginScrollView(_scroll);
|
||
|
||
EditorGUILayout.LabelField("数据源(Art 源预制体)", EditorStyles.boldLabel);
|
||
EditorGUILayout.HelpBox(
|
||
"源都在 Assets/Art/XJMH/pw/pw_prefab/game/effect 下:\n" +
|
||
" · PW_Effect_Settle_FlameStrike_Self → " + BJEndDir + "\n" +
|
||
" · PW_Effect_Settle_FlameStrike_Other → " + BJEndDir + "\n" +
|
||
" · PW_Effect_AwardFrame_FlameStrike → " + BJScoreDir + "\n" +
|
||
"生成的变体名会带后缀 " + Suffix, MessageType.None);
|
||
|
||
EditorGUILayout.Space();
|
||
EditorGUILayout.LabelField("加工步骤(对齐 BJEnd 里已有变体)", EditorStyles.boldLabel);
|
||
|
||
_disableParticleRenderers = EditorGUILayout.ToggleLeft(
|
||
"关闭所有 ParticleSystemRenderer(改由 UIParticle 渲染)", _disableParticleRenderers);
|
||
_resetSortingLayer = EditorGUILayout.ToggleLeft(
|
||
"所有粒子 Renderer 的 SortingLayer 归零(Default)", _resetSortingLayer);
|
||
_addUiParticle = EditorGUILayout.ToggleLeft(
|
||
"添加 / 复用 UIParticle 节点,并自动收全所有粒子(Scale3D = 10,10,10)", _addUiParticle);
|
||
_fixRootRect = EditorGUILayout.ToggleLeft(
|
||
"修正根 RectTransform(anchor/pivot 0.5、sizeDelta 100×100、位置归零)", _fixRootRect);
|
||
_applyTextStyle = EditorGUILayout.ToggleLeft(
|
||
"分数文本套用模板样式(SourceHanSans + 金币 spriteAsset)", _applyTextStyle);
|
||
|
||
using (new EditorGUI.IndentLevelScope())
|
||
{
|
||
_scaleParticleNodes = EditorGUILayout.ToggleLeft(
|
||
$"把各粒子节点的 localScale 归一化到 {TemplateParticleNodeScale}(模板口径)", _scaleParticleNodes);
|
||
using (new EditorGUI.DisabledScope(!_scaleParticleNodes))
|
||
{
|
||
_proportionalScale = EditorGUILayout.ToggleLeft(
|
||
"按比例归一化(保留节点之间的相对大小,推荐)", _proportionalScale);
|
||
_particleNodeScale = EditorGUILayout.FloatField(
|
||
_proportionalScale ? "主缩放归一化到" : "所有节点统一改成", _particleNodeScale);
|
||
}
|
||
EditorGUILayout.HelpBox(
|
||
"源里各粒子节点的 localScale 是 30(12 个)+ 1(2 个)。模板把 30 归一化成 5," +
|
||
"再配 m_Scale3D = 10 → 等效 50,与其它 Boss 面板同一口径。\n" +
|
||
"「按比例」= 30→5、1→0.1667(推荐,不会把原本小的节点放大 5 倍);\n" +
|
||
"关掉则所有节点都写 " + _particleNodeScale + "。\n" +
|
||
"⚠ UIParticle 节点自己的 scale 运行时会被强制改成 1/parentLossyScale(AutoScalingMode.Transform)," +
|
||
"在预制体里调它是没用的,别白费功夫。", MessageType.Info);
|
||
}
|
||
|
||
EditorGUILayout.Space();
|
||
EditorGUILayout.LabelField("挂脚本", EditorStyles.boldLabel);
|
||
_addBjEndUI = EditorGUILayout.ToggleLeft(
|
||
"BJEndUI(结算面板:写倍数 + 最终分数)", _addBjEndUI);
|
||
using (new EditorGUI.IndentLevelScope())
|
||
{
|
||
_createBeishuNode = EditorGUILayout.ToggleLeft(
|
||
"在 Textparent 下新建 Node (1) 作为倍数文本", _createBeishuNode);
|
||
}
|
||
_addBjImage = EditorGUILayout.ToggleLeft(
|
||
"BJImage(奖励框:写金币数)", _addBjImage);
|
||
|
||
EditorGUILayout.Space();
|
||
_overwrite = EditorGUILayout.ToggleLeft("允许覆盖已存在的变体文件", _overwrite);
|
||
|
||
EditorGUILayout.Space();
|
||
if (GUILayout.Button("生成:结算面板 Self + Other", GUILayout.Height(30f)))
|
||
{
|
||
BuildSettlePanel(true);
|
||
BuildSettlePanel(false);
|
||
}
|
||
if (GUILayout.Button("生成:结算面板 Self", GUILayout.Height(24f))) BuildSettlePanel(true);
|
||
if (GUILayout.Button("生成:结算面板 Other", GUILayout.Height(24f))) BuildSettlePanel(false);
|
||
EditorGUILayout.Space();
|
||
if (GUILayout.Button("生成:奖励框 AwardFrame", GUILayout.Height(24f))) BuildAwardFrame();
|
||
|
||
EditorGUILayout.Space();
|
||
EditorGUILayout.LabelField("备用", EditorStyles.boldLabel);
|
||
if (GUILayout.Button("就地补齐选中的预制体(不生成变体,直接改它自己)", GUILayout.Height(24f)))
|
||
{
|
||
PatchSelectedInPlace();
|
||
}
|
||
|
||
EditorGUILayout.Space();
|
||
if (GUILayout.Button("检查资源到位情况", GUILayout.Height(24f))) ReportAssets();
|
||
|
||
EditorGUILayout.EndScrollView();
|
||
}
|
||
|
||
// ================================================================ 生成入口
|
||
|
||
private void BuildSettlePanel(bool isSelf)
|
||
{
|
||
string srcName = isSelf ? "PW_Effect_Settle_FlameStrike_Self" : "PW_Effect_Settle_FlameStrike_Other";
|
||
string srcPath = ArtEffectDir + "/" + srcName + ".prefab";
|
||
string dstPath = BJEndDir + "/" + srcName + Suffix + ".prefab";
|
||
|
||
CreateVariant(srcPath, dstPath, root => ModifySettlePanel(root, isSelf));
|
||
}
|
||
|
||
private void BuildAwardFrame()
|
||
{
|
||
const string srcName = "PW_Effect_AwardFrame_FlameStrike";
|
||
string srcPath = ArtEffectDir + "/" + srcName + ".prefab";
|
||
string dstPath = BJScoreDir + "/" + srcName + Suffix + ".prefab";
|
||
|
||
CreateVariant(srcPath, dstPath, ModifyAwardFrame);
|
||
}
|
||
|
||
// ================================================================ 变体创建
|
||
|
||
/// <summary>把 Art 源实例化成变体并保存到目标路径(SaveAsPrefabAsset 作用在预制体实例上 → 生成变体)。</summary>
|
||
private void CreateVariant(string srcPath, string dstPath, Action<GameObject> modify)
|
||
{
|
||
var src = AssetDatabase.LoadAssetAtPath<GameObject>(srcPath);
|
||
if (src == null)
|
||
{
|
||
Debug.LogError($"[烈焰风暴变体] 找不到源预制体:{srcPath}");
|
||
return;
|
||
}
|
||
|
||
EnsureFolder(Path.GetDirectoryName(dstPath).Replace('\\', '/'));
|
||
|
||
if (File.Exists(dstPath) && !_overwrite)
|
||
{
|
||
Debug.LogError($"[烈焰风暴变体] {dstPath} 已存在。要覆盖请先勾选「允许覆盖已存在的变体文件」," +
|
||
"或直接在 Project 里改名后再生成。");
|
||
return;
|
||
}
|
||
|
||
var inst = PrefabUtility.InstantiatePrefab(src) as GameObject;
|
||
if (inst == null)
|
||
{
|
||
Debug.LogError($"[烈焰风暴变体] 实例化失败:{srcPath}");
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
inst.name = Path.GetFileNameWithoutExtension(dstPath);
|
||
modify?.Invoke(inst);
|
||
|
||
PrefabUtility.SaveAsPrefabAsset(inst, dstPath, out bool success);
|
||
if (!success)
|
||
{
|
||
Debug.LogError($"[烈焰风暴变体] 保存失败:{dstPath}");
|
||
return;
|
||
}
|
||
AssetDatabase.SaveAssets();
|
||
|
||
var saved = AssetDatabase.LoadAssetAtPath<GameObject>(dstPath);
|
||
var type = PrefabUtility.GetPrefabAssetType(saved);
|
||
if (type == PrefabAssetType.Variant)
|
||
Debug.Log($"[烈焰风暴变体] ✔ 已生成变体:{dstPath}");
|
||
else
|
||
Debug.LogWarning($"[烈焰风暴变体] {dstPath} 已生成,但类型是 {type} 而不是 Variant " +
|
||
"(说明它没继承 Art 源,请检查)");
|
||
|
||
if (saved != null) EditorGUIUtility.PingObject(saved);
|
||
}
|
||
finally
|
||
{
|
||
DestroyImmediate(inst);
|
||
}
|
||
}
|
||
|
||
// ================================================================ 结算面板加工
|
||
|
||
/// <summary>结算面板(Settle_FlameStrike_Self / _Other)的 UI 化 + BJEndUI。</summary>
|
||
private void ModifySettlePanel(GameObject root, bool isSelf)
|
||
{
|
||
var sb = new StringBuilder();
|
||
sb.AppendLine($"[烈焰风暴变体] 处理 {(isSelf ? "Self" : "Other")} 结算面板:{root.name}");
|
||
|
||
// ---- 1/2. 粒子 Renderer:关掉自带渲染 + 层归零
|
||
var psList = root.GetComponentsInChildren<ParticleSystem>(true);
|
||
int rendererCount = 0;
|
||
foreach (var ps in psList)
|
||
{
|
||
var psr = ps.GetComponent<ParticleSystemRenderer>();
|
||
if (psr == null) continue;
|
||
if (_disableParticleRenderers) psr.enabled = false;
|
||
if (_resetSortingLayer) psr.sortingLayerID = 0;
|
||
rendererCount++;
|
||
}
|
||
sb.AppendLine($" 粒子系统 {psList.Length} 个,Renderer 处理 {rendererCount} 个" +
|
||
$"(enabled={!_disableParticleRenderers}、sortingLayer 归零={_resetSortingLayer})");
|
||
|
||
// ---- 3. UIParticle 节点
|
||
if (_addUiParticle)
|
||
{
|
||
bool created = EnsureUiParticle(root, psList, out int particleCount);
|
||
sb.AppendLine(created
|
||
? $" 新建 UIParticle 节点,收粒子 {particleCount} 个,Scale3D={TemplateUiParticleScale3D}"
|
||
: $" 复用已有 UIParticle 节点,刷新粒子列表 {particleCount} 个,Scale3D={TemplateUiParticleScale3D}");
|
||
|
||
// 粒子节点缩放归一化:模板把源里的 30 / 46.176 / 48.395 统一改成 5,配 m_Scale3D=10 用。
|
||
// ⚠ 这一步与 UIParticle 是新建还是复用无关 —— 真正决定大小的是「粒子节点 scale × m_Scale3D」,
|
||
// UIParticle 节点自己的 scale 运行时会被 AutoScalingMode.Transform 覆盖,两种情况都要归一化。
|
||
if (_scaleParticleNodes)
|
||
sb.AppendLine(NormalizeParticleNodeScales(psList));
|
||
else
|
||
sb.AppendLine(" 粒子节点缩放:保持源里的原值(未归一化 —— 与其它 Boss 面板口径不一致,请预览确认)");
|
||
}
|
||
|
||
// ---- 4. 根 RectTransform
|
||
if (_fixRootRect)
|
||
{
|
||
var rt = root.GetComponent<RectTransform>();
|
||
if (rt != null)
|
||
{
|
||
rt.anchorMin = new Vector2(0.5f, 0.5f);
|
||
rt.anchorMax = new Vector2(0.5f, 0.5f);
|
||
rt.pivot = new Vector2(0.5f, 0.5f);
|
||
rt.sizeDelta = new Vector2(100f, 100f);
|
||
rt.anchoredPosition = Vector2.zero;
|
||
rt.localPosition = Vector3.zero;
|
||
rt.localRotation = Quaternion.identity;
|
||
sb.AppendLine(" 根 RectTransform:anchor/pivot 0.5、sizeDelta 100×100、位置归零");
|
||
}
|
||
}
|
||
|
||
// ---- 5. 分数文本样式
|
||
var scoreTmp = FindScoreTmp(root);
|
||
if (scoreTmp == null)
|
||
{
|
||
Debug.LogError($"[烈焰风暴变体] {root.name} 里找不到分数文本(预期路径 Node/Textparent/Node),BJEndUI 未挂");
|
||
}
|
||
else if (_applyTextStyle)
|
||
{
|
||
var font = LoadAsset<TMP_FontAsset>(FontGuid);
|
||
var gold = LoadAsset<TMP_SpriteAsset>(GoldSpriteGuid);
|
||
if (font != null) scoreTmp.font = font;
|
||
if (gold != null) scoreTmp.spriteAsset = gold;
|
||
sb.AppendLine($" 分数文本 {GetPath(scoreTmp.transform)}:字体=" +
|
||
$"{(font != null ? font.name : "未改")}、spriteAsset={(gold != null ? gold.name : "未改")}");
|
||
}
|
||
|
||
// ---- 6/7. 倍数文本 + BJEndUI
|
||
if (_addBjEndUI)
|
||
{
|
||
if (scoreTmp == null)
|
||
{
|
||
sb.AppendLine(" 跳过 BJEndUI(没有分数文本可接)");
|
||
}
|
||
else
|
||
{
|
||
TextMeshProUGUI beishuTmp = null;
|
||
if (_createBeishuNode)
|
||
{
|
||
beishuTmp = EnsureBeishuNode(scoreTmp);
|
||
sb.AppendLine($" 倍数文本:{GetPath(beishuTmp.transform)}(scale {TemplateBeishuScale}," +
|
||
$"pos {TemplateBeishuPos},字号 {TemplateBeishuFontSize})");
|
||
}
|
||
|
||
var endUI = root.GetComponent<BJEndUI>();
|
||
if (endUI == null) endUI = root.AddComponent<BJEndUI>();
|
||
endUI.tMP_score = scoreTmp;
|
||
endUI.tMP_beishu = beishuTmp;
|
||
endUI.goldSprite = LoadAsset<TMP_SpriteAsset>(GoldSpriteGuid);
|
||
endUI.SilverSprite = LoadAsset<TMP_SpriteAsset>(SilverSpriteGuid);
|
||
endUI.img_BaoJiangZhong = null;
|
||
endUI.useScoreAnim = false;
|
||
endUI.autoDestroyDelay = 5f;
|
||
|
||
sb.AppendLine($" 已挂 BJEndUI(tMP_score={scoreTmp.name}、" +
|
||
$"tMP_beishu={(beishuTmp != null ? beishuTmp.name : "空")}," +
|
||
$"gold/SilverSprite 已接,autoDestroyDelay=5)");
|
||
}
|
||
}
|
||
|
||
Debug.Log(sb.ToString());
|
||
}
|
||
|
||
// ================================================================ 奖励框加工
|
||
|
||
/// <summary>奖励框(AwardFrame_FlameStrike):不碰粒子,只挂 BJImage(写金币数)。</summary>
|
||
private void ModifyAwardFrame(GameObject root)
|
||
{
|
||
var sb = new StringBuilder();
|
||
sb.AppendLine($"[烈焰风暴变体] 处理奖励框:{root.name}");
|
||
|
||
if (_fixRootRect)
|
||
{
|
||
var rt = root.GetComponent<RectTransform>();
|
||
if (rt != null)
|
||
{
|
||
rt.anchorMin = new Vector2(0.5f, 0.5f);
|
||
rt.anchorMax = new Vector2(0.5f, 0.5f);
|
||
rt.pivot = new Vector2(0.5f, 0.5f);
|
||
rt.sizeDelta = new Vector2(100f, 100f);
|
||
rt.anchoredPosition = Vector2.zero;
|
||
rt.localPosition = Vector3.zero;
|
||
rt.localRotation = Quaternion.identity;
|
||
sb.AppendLine(" 根 RectTransform:anchor/pivot 0.5、sizeDelta 100×100、位置归零");
|
||
}
|
||
}
|
||
|
||
if (_addBjImage)
|
||
{
|
||
var coinTmp = FindCoinNumTmp(root);
|
||
if (coinTmp == null)
|
||
{
|
||
Debug.LogError($"[烈焰风暴变体] {root.name} 里找不到金币数字文本(预期在 Num 节点下),BJImage 未挂");
|
||
}
|
||
else
|
||
{
|
||
var bjImage = root.GetComponent<BJImage>();
|
||
if (bjImage == null) bjImage = root.AddComponent<BJImage>();
|
||
bjImage.tmp_CoinNum = coinTmp;
|
||
bjImage.goldSprite = LoadAsset<TMP_SpriteAsset>(GoldSpriteGuid);
|
||
bjImage.silverSprite = LoadAsset<TMP_SpriteAsset>(SilverSpriteGuid);
|
||
sb.AppendLine($" 已挂 BJImage(tmp_CoinNum={GetPath(coinTmp.transform)},gold/silverSprite 已接)");
|
||
}
|
||
}
|
||
|
||
Debug.Log(sb.ToString());
|
||
}
|
||
|
||
// ================================================================ 就地补齐(改选中预制体本身)
|
||
|
||
private void PatchSelectedInPlace()
|
||
{
|
||
var path = AssetDatabase.GetAssetPath(Selection.activeObject);
|
||
if (string.IsNullOrEmpty(path) || !path.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
Debug.LogError("[烈焰风暴变体] 请先在 Project 窗口里选中一个 .prefab 再点这个按钮");
|
||
return;
|
||
}
|
||
if (PrefabUtility.GetPrefabAssetType(Selection.activeObject) == PrefabAssetType.Variant)
|
||
{
|
||
Debug.LogError($"[烈焰风暴变体] {path} 是变体,请直接在变体上改;就地补齐只支持普通预制体");
|
||
return;
|
||
}
|
||
|
||
var contents = PrefabUtility.LoadPrefabContents(path);
|
||
if (contents == null) return;
|
||
try
|
||
{
|
||
bool isAwardFrame = contents.name.IndexOf("AwardFrame", StringComparison.OrdinalIgnoreCase) >= 0;
|
||
if (isAwardFrame) ModifyAwardFrame(contents);
|
||
else ModifySettlePanel(contents, contents.name.IndexOf("Self", StringComparison.OrdinalIgnoreCase) >= 0);
|
||
|
||
PrefabUtility.SaveAsPrefabAsset(contents, path, out bool ok);
|
||
if (ok) Debug.Log($"[烈焰风暴变体] ✔ 已就地补齐:{path}");
|
||
else Debug.LogError($"[烈焰风暴变体] 就地保存失败:{path}");
|
||
}
|
||
finally
|
||
{
|
||
PrefabUtility.UnloadPrefabContents(contents);
|
||
}
|
||
AssetDatabase.SaveAssets();
|
||
}
|
||
|
||
// ================================================================ 资源检查
|
||
|
||
private void ReportAssets()
|
||
{
|
||
var sb = new StringBuilder("[烈焰风暴] 资源到位检查\n");
|
||
|
||
CheckPrefab(sb, ArtEffectDir + "/PW_Effect_Settle_FlameStrike_Self.prefab");
|
||
CheckPrefab(sb, ArtEffectDir + "/PW_Effect_Settle_FlameStrike_Other.prefab");
|
||
CheckPrefab(sb, ArtEffectDir + "/PW_Effect_AwardFrame_FlameStrike.prefab");
|
||
CheckPrefab(sb, ArtEffectDir + "/PW_Effect_Wheel.prefab");
|
||
|
||
CheckPrefab(sb, BJEndDir + "/PW_Effect_Wheel_LieYanFengBao.prefab");
|
||
CheckPrefab(sb, BJEndDir + "/PW_Effect_Settle_FlameStrike_Self" + Suffix + ".prefab");
|
||
CheckPrefab(sb, BJEndDir + "/PW_Effect_Settle_FlameStrike_Other" + Suffix + ".prefab");
|
||
CheckPrefab(sb, BJScoreDir + "/PW_Effect_AwardFrame_FlameStrike" + Suffix + ".prefab");
|
||
|
||
CheckPrefab(sb, "Assets/MyGame/aa/Game/XJMH/Boss/LieYanFengBao/PW_Enemy_115.prefab");
|
||
CheckPrefab(sb, "Assets/MyGame/aa/Game/XJMH/Prefabs/Bullit/PW_Bullet_FlameRound.prefab");
|
||
CheckPrefab(sb, "Assets/Art/XJMH/pw/pw_spine/pw_spine_plane/pw_spine_plane_lyfb/PW_Spine_Plane_LYFB_SkeletonData.asset");
|
||
|
||
sb.AppendLine();
|
||
sb.AppendLine("音效(tbaudio.json 里应为 69009-69012):");
|
||
foreach (var n in new[] { "PW_FlameStrike_Explosion", "PW_FlameStrike_Die",
|
||
"PW_FlameStrike_WheelExplosion", "PW_FlameStrike_WheelFly" })
|
||
{
|
||
var p = "Assets/Audios/XJMH/" + n + ".ogg";
|
||
sb.AppendLine(" " + (File.Exists(p) ? "✔" : "✘") + " " + p);
|
||
}
|
||
|
||
sb.AppendLine();
|
||
sb.AppendLine("挂脚本可用性:");
|
||
sb.AppendLine(" " + (LoadAsset<TMP_SpriteAsset>(GoldSpriteGuid) != null ? "✔" : "✘") + " 金币 spriteAsset");
|
||
sb.AppendLine(" " + (LoadAsset<TMP_SpriteAsset>(SilverSpriteGuid) != null ? "✔" : "✘") + " 银币 spriteAsset");
|
||
sb.AppendLine(" " + (LoadAsset<TMP_FontAsset>(FontGuid) != null ? "✔" : "✘") + " SourceHanSans 字体");
|
||
|
||
Debug.Log(sb.ToString());
|
||
}
|
||
|
||
private static void CheckPrefab(StringBuilder sb, string path)
|
||
{
|
||
bool exists = File.Exists(path);
|
||
sb.AppendLine((exists ? "✔ " : "✘ ") + path + (exists ? "" : " ← 缺失"));
|
||
}
|
||
|
||
// ================================================================ 工具方法
|
||
|
||
/// <summary>确保根下有一个 UIParticle 节点,并把所有粒子收进 m_Particles。</summary>
|
||
private static bool EnsureUiParticle(GameObject root, ParticleSystem[] psList, out int particleCount)
|
||
{
|
||
var ui = root.GetComponentInChildren<UIParticle>(true);
|
||
bool created = false;
|
||
|
||
if (ui == null)
|
||
{
|
||
var go = new GameObject("UIParticle", typeof(RectTransform), typeof(CanvasRenderer), typeof(UIParticle));
|
||
var rt = (RectTransform)go.transform;
|
||
rt.SetParent(root.transform, false);
|
||
rt.anchorMin = new Vector2(0.5f, 0.5f);
|
||
rt.anchorMax = new Vector2(0.5f, 0.5f);
|
||
rt.pivot = new Vector2(0.5f, 0.5f);
|
||
rt.anchoredPosition = Vector2.zero;
|
||
rt.sizeDelta = Vector2.zero;
|
||
rt.localScale = Vector3.one; // 运行时会被 UIParticle 覆盖成 1/parentLossyScale,这里给 1 只是编辑器里好看
|
||
ui = go.GetComponent<UIParticle>();
|
||
created = true;
|
||
}
|
||
|
||
// UIParticle 的序列化字段都是 private → 用 SerializedObject 写
|
||
var so = new SerializedObject(ui);
|
||
var arr = so.FindProperty("m_Particles");
|
||
if (arr != null)
|
||
{
|
||
arr.arraySize = psList.Length;
|
||
for (int i = 0; i < psList.Length; i++)
|
||
arr.GetArrayElementAtIndex(i).objectReferenceValue = psList[i];
|
||
}
|
||
var scale3D = so.FindProperty("m_Scale3D");
|
||
if (scale3D != null) scale3D.vector3Value = TemplateUiParticleScale3D;
|
||
so.ApplyModifiedPropertiesWithoutUndo();
|
||
|
||
particleCount = psList.Length;
|
||
if (arr == null)
|
||
Debug.LogWarning("[烈焰风暴变体] 没找到 UIParticle.m_Particles 字段(插件版本可能不同),请手动拖粒子列表");
|
||
return created;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 把各粒子节点的 localScale 归一化到模板口径(源里的 30 → 5,配 m_Scale3D=10 使用)。
|
||
///
|
||
/// 按比例模式:取出现次数最多的缩放当「主缩放」,factor = 目标 / 主缩放,所有节点乘同一倍率
|
||
/// → 30→5、1→0.1667,不会把原本就小的节点放大 5 倍(烈焰风暴源里正好是 12 个 30 + 2 个 1)。
|
||
/// 非按比例:所有节点直接写死目标值(模板做法的字面复刻)。
|
||
/// </summary>
|
||
private string NormalizeParticleNodeScales(ParticleSystem[] psList)
|
||
{
|
||
if (psList == null || psList.Length == 0) return " 粒子节点缩放:没有粒子系统,跳过";
|
||
|
||
var counts = new Dictionary<float, int>();
|
||
foreach (var ps in psList)
|
||
{
|
||
float v = ps.transform.localScale.x;
|
||
counts.TryGetValue(v, out int c);
|
||
counts[v] = c + 1;
|
||
}
|
||
float dominant = 1f;
|
||
int best = -1;
|
||
foreach (var kv in counts)
|
||
{
|
||
if (kv.Value > best) { best = kv.Value; dominant = kv.Key; }
|
||
}
|
||
if (Mathf.Approximately(dominant, 0f)) dominant = 1f;
|
||
|
||
float factor = _proportionalScale ? _particleNodeScale / dominant : 0f;
|
||
|
||
var sb = new StringBuilder();
|
||
sb.Append(_proportionalScale
|
||
? $" 粒子节点归一化:主缩放 {dominant}({best}/{psList.Length} 个)→ {_particleNodeScale},倍率 {factor:0.####}"
|
||
: $" 粒子节点归一化:全部统一写成 {_particleNodeScale}");
|
||
|
||
int shown = 0;
|
||
foreach (var ps in psList)
|
||
{
|
||
var old = ps.transform.localScale;
|
||
var nv = _proportionalScale ? old * factor : Vector3.one * _particleNodeScale;
|
||
ps.transform.localScale = nv;
|
||
SetConstrainProportionsScale(ps.transform, true);
|
||
if (shown++ < 16)
|
||
sb.Append($"\n {GetPath(ps.transform)}: {old.x:0.####} → {nv.x:0.####}");
|
||
}
|
||
return sb.ToString();
|
||
}
|
||
|
||
/// <summary>在 Textparent 下确保存在倍数文本节点(模板里叫 Node (1))。</summary>
|
||
private static TextMeshProUGUI EnsureBeishuNode(TextMeshProUGUI scoreTmp)
|
||
{
|
||
var parent = scoreTmp.transform.parent;
|
||
if (parent == null)
|
||
{
|
||
Debug.LogWarning("[烈焰风暴变体] 分数文本没有父节点,倍数文本挂在它自己下面");
|
||
parent = scoreTmp.transform;
|
||
}
|
||
|
||
var exist = parent.Find("Node (1)");
|
||
if (exist != null)
|
||
{
|
||
var t = exist.GetComponent<TextMeshProUGUI>();
|
||
if (t != null) return t;
|
||
}
|
||
|
||
var go = new GameObject("Node (1)", typeof(RectTransform));
|
||
var rt = (RectTransform)go.transform;
|
||
rt.SetParent(parent, false);
|
||
rt.anchorMin = new Vector2(0.5f, 0.5f);
|
||
rt.anchorMax = new Vector2(0.5f, 0.5f);
|
||
rt.pivot = new Vector2(0.5f, 0.5f);
|
||
rt.anchoredPosition = TemplateBeishuPos;
|
||
rt.sizeDelta = TemplateBeishuSize;
|
||
rt.localScale = Vector3.one * TemplateBeishuScale;
|
||
|
||
var tmp = go.AddComponent<TextMeshProUGUI>();
|
||
tmp.font = scoreTmp.font; // 运行时 AddComponent 的文本必须显式给字体,否则不显示
|
||
tmp.spriteAsset = scoreTmp.spriteAsset;
|
||
tmp.fontSize = TemplateBeishuFontSize;
|
||
tmp.alignment = TextAlignmentOptions.Center;
|
||
tmp.color = Color.white;
|
||
tmp.raycastTarget = false;
|
||
tmp.text = "225"; // 占位,运行时由 BJEndUI.Init 覆写
|
||
return tmp;
|
||
}
|
||
|
||
/// <summary>分数文本:优先 Node/Textparent/Node,再退到 Textparent 下的第一个 TMP。</summary>
|
||
private static TextMeshProUGUI FindScoreTmp(GameObject root)
|
||
{
|
||
var t = root.transform.Find("Node/Textparent/Node") ?? root.transform.Find("Textparent/Node");
|
||
if (t != null)
|
||
{
|
||
var tmp = t.GetComponent<TextMeshProUGUI>();
|
||
if (tmp != null) return tmp;
|
||
}
|
||
|
||
var tp = FindDeep(root.transform, "Textparent");
|
||
if (tp != null)
|
||
{
|
||
var tmp = tp.GetComponentInChildren<TextMeshProUGUI>(true);
|
||
if (tmp != null) return tmp;
|
||
}
|
||
|
||
Debug.LogWarning($"[烈焰风暴变体] {root.name}:没找到 Textparent,退用整棵树里第一个 TMP");
|
||
return root.GetComponentInChildren<TextMeshProUGUI>(true);
|
||
}
|
||
|
||
/// <summary>奖励框的金币数字文本(Num 节点下的 TMP)。</summary>
|
||
private static TextMeshProUGUI FindCoinNumTmp(GameObject root)
|
||
{
|
||
var num = FindDeep(root.transform, "Num");
|
||
if (num != null)
|
||
{
|
||
var tmp = num.GetComponentInChildren<TextMeshProUGUI>(true);
|
||
if (tmp != null) return tmp;
|
||
}
|
||
return root.GetComponentInChildren<TextMeshProUGUI>(true);
|
||
}
|
||
|
||
private static Transform FindDeep(Transform root, string name)
|
||
{
|
||
if (root == null) return null;
|
||
if (string.Equals(root.name, name, StringComparison.OrdinalIgnoreCase)) return root;
|
||
for (int i = 0; i < root.childCount; i++)
|
||
{
|
||
var r = FindDeep(root.GetChild(i), name);
|
||
if (r != null) return r;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
private static string GetPath(Transform t)
|
||
{
|
||
if (t == null) return "?";
|
||
var s = t.name;
|
||
var p = t.parent;
|
||
while (p != null) { s = p.name + "/" + s; p = p.parent; }
|
||
return s;
|
||
}
|
||
|
||
private static void SetConstrainProportionsScale(Transform t, bool value)
|
||
{
|
||
var so = new SerializedObject(t);
|
||
var p = so.FindProperty("m_ConstrainProportionsScale");
|
||
if (p != null)
|
||
{
|
||
p.boolValue = value;
|
||
so.ApplyModifiedPropertiesWithoutUndo();
|
||
}
|
||
}
|
||
|
||
private static T LoadAsset<T>(string guid) where T : UnityEngine.Object
|
||
{
|
||
var path = AssetDatabase.GUIDToAssetPath(guid);
|
||
if (string.IsNullOrEmpty(path))
|
||
{
|
||
Debug.LogWarning($"[烈焰风暴变体] guid 解析不到资源:{guid}");
|
||
return null;
|
||
}
|
||
return AssetDatabase.LoadAssetAtPath<T>(path);
|
||
}
|
||
|
||
private static void EnsureFolder(string folder)
|
||
{
|
||
if (AssetDatabase.IsValidFolder(folder)) return;
|
||
var parts = folder.Split('/');
|
||
var cur = parts[0];
|
||
for (int i = 1; i < parts.Length; i++)
|
||
{
|
||
var next = cur + "/" + parts[i];
|
||
if (!AssetDatabase.IsValidFolder(next)) AssetDatabase.CreateFolder(cur, parts[i]);
|
||
cur = next;
|
||
}
|
||
}
|
||
}
|
||
#endif
|