This commit is contained in:
2026-08-16 21:40:22 +08:00
parent ae4505ccfb
commit 533f0d37d7
17 changed files with 6417 additions and 705 deletions
@@ -86,6 +86,7 @@ namespace Assets.MyGame.Scripts.Net.WebResponses
/// <summary>
/// 多次死亡 param:星际巡航(最多3次)/ 烈焰风暴(最多2次)。
/// JSON: { "isKill": true, "beishu": 155, "fishId": "13009-3-3" }
/// 机械蝎(12028/12029)另带 animId:1=不死只播BJEndUI,2/3/4=带死亡的爆奖
/// </summary>
[Serializable]
public class BossMultiDeathParam
@@ -99,6 +100,17 @@ namespace Assets.MyGame.Scripts.Net.WebResponses
/// <summary>鱼ID,格式:原ID-总次数-当前次数(如 13009-3-2)</summary>
public string fishId = "";
/// <summary>
/// 机械蝎专用:死亡/状态动画 ID。
/// 1 = 不死(只显示 BJEndUI,Boss 保留继续打)
/// 2/3/4 = 带死亡的爆奖(三种表现,Boss 消失)
/// 其他 Boss 无此字段(默认 0)。
/// </summary>
public int animId;
/// <summary>是否带死亡的爆奖(animId >= 2;机械蝎判定用,优先于 isKill)</summary>
public bool IsDeathAward => animId >= 2;
/// <summary>从 fishId 解析总死亡次数(解析失败返回 0)</summary>
public int TotalCount
{
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 99a2ec58fc6f71746aa937e9a2f67a23
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,144 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEngine;
/// <summary>
/// 机械蝎八连抽配置生成器(编辑器工具)。
///
/// 用法:菜单 Tools → 机械蝎 → 生成八连抽配置资产
/// 1. 自动查找项目中 PW_Effect_FreeDropChip / PW_Effect_Settle_Scorpion 预制体
/// 2. 自动查找 PW_FreeItem_DropGemstone* / DropCard* / DropProp* Sprite,按文件名数字排序填入
/// 3. 生成 JiXieXieConfig.asset 到 JiXieXie 目录(已存在则覆盖并刷新)
/// 4. 选中生成的资产,拖到爆奖预制体的 JiXieXieEvent.config 字段即可
/// </summary>
public static class JiXieXieConfigAutoFill
{
private const string ConfigDir = "Assets/MyGame/aa/Game/XJMH/Boss/JiXieXie";
private const string ConfigPath = ConfigDir + "/JiXieXieConfig.asset";
[MenuItem("Tools/机械蝎/生成八连抽配置资产")]
public static void GenerateConfig()
{
// 1. 查找预制体
GameObject chipPrefab = FindAsset<GameObject>("PW_Effect_FreeDropChip");
GameObject settlePrefab = FindAsset<GameObject>("PW_Effect_Settle_Scorpion");
// 2. 查找图片(按名字数字排序)
Sprite[] gemstones = FindSpritesSorted("PW_FreeItem_DropGemstone");
Sprite[] cards = FindSpritesSorted("PW_FreeItem_DropCard");
Sprite[] goods = FindSpritesSorted("PW_FreeItem_DropProp");
if (chipPrefab == null)
Debug.LogWarning("[JiXieXie] 未找到 PW_Effect_FreeDropChip 预制体,config.freeDropChipPrefab 为空");
if (settlePrefab == null)
Debug.LogWarning("[JiXieXie] 未找到 PW_Effect_Settle_Scorpion 预制体,config.settleScorpionPrefab 为空");
if (gemstones.Length == 0 || cards.Length == 0 || goods.Length == 0)
Debug.LogWarning($"[JiXieXie] 部分图片没找到:宝石={gemstones.Length} 卡牌={cards.Length} 道具={goods.Length}");
// 3. 生成/覆盖配置资产
JiXieXieConfig cfg = AssetDatabase.LoadAssetAtPath<JiXieXieConfig>(ConfigPath);
bool isNew = cfg == null;
if (isNew)
{
cfg = ScriptableObject.CreateInstance<JiXieXieConfig>();
EnsureDir();
AssetDatabase.CreateAsset(cfg, ConfigPath);
}
cfg.freeDropChipPrefab = chipPrefab;
cfg.settleScorpionPrefab = settlePrefab;
cfg.gemstoneSprites = gemstones;
cfg.cardSprites = cards;
cfg.goodsSprites = goods;
cfg.propNames = BuildPropNames(goods);
EditorUtility.SetDirty(cfg);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
// 4. 选中并提示
Selection.activeObject = cfg;
EditorGUIUtility.PingObject(cfg);
Debug.Log($"[JiXieXie] 配置资产{(isNew ? "已生成" : "已更新")}:{ConfigPath}\n" +
$" 宝石={cfg.gemstoneSprites.Length} 卡牌={cfg.cardSprites.Length} 道具={cfg.goodsSprites.Length}\n" +
" 请把该资产拖到爆奖预制体 JiXieXieEvent 的 config 字段");
}
// ==================== 工具方法 ====================
private static void EnsureDir()
{
if (!AssetDatabase.IsValidFolder(ConfigDir))
{
string parent = "Assets/MyGame/aa/Game/XJMH/Boss";
if (!AssetDatabase.IsValidFolder(parent))
{
Debug.LogError($"[JiXieXie] 目录不存在:{parent}");
return;
}
AssetDatabase.CreateFolder(parent, "JiXieXie");
}
}
/// <summary>按名称模糊查找第一个指定类型资产</summary>
private static T FindAsset<T>(string nameFilter) where T : UnityEngine.Object
{
string[] guids = AssetDatabase.FindAssets(nameFilter + " t:" + typeof(T).Name);
foreach (string guid in guids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
T asset = AssetDatabase.LoadAssetAtPath<T>(path);
if (asset != null) return asset;
}
return null;
}
/// <summary>查找名称带前缀的所有 Sprite,并按文件名末尾数字排序</summary>
private static Sprite[] FindSpritesSorted(string namePrefix)
{
string[] guids = AssetDatabase.FindAssets(namePrefix + " t:Sprite");
List<Sprite> list = new List<Sprite>();
foreach (string guid in guids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
Sprite sprite = AssetDatabase.LoadAssetAtPath<Sprite>(path);
if (sprite != null)
list.Add(sprite);
}
// 按文件名末尾数字排序:DropGemstone1 < DropGemstone2 ... < DropGemstone10
list.Sort((a, b) => ExtractTrailingNumber(a.name).CompareTo(ExtractTrailingNumber(b.name)));
return list.ToArray();
}
/// <summary>提取文件名末尾的数字(如 "PW_FreeItem_DropProp1000" → 1000)</summary>
private static int ExtractTrailingNumber(string name)
{
Match m = Regex.Match(name, @"(\d+)$");
return m.Success ? int.Parse(m.Value) : 0;
}
/// <summary>按 goods 数量生成默认道具名(1000+ 猜测为火箭炮碎片,其余叫 道具{ID})</summary>
private static string[] BuildPropNames(Sprite[] goods)
{
if (goods == null || goods.Length == 0) return new string[0];
string[] names = new string[goods.Length];
for (int i = 0; i < goods.Length; i++)
{
if (goods[i] == null) { names[i] = ""; continue; }
int id = ExtractTrailingNumber(goods[i].name);
if (id >= 1000 && id <= 1003)
names[i] = "火箭炮碎片";
else if (id == 0)
names[i] = "金币";
else if (id == 1)
names[i] = "钻石";
else
names[i] = $"道具{id}";
}
return names;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 48db2b478c92fa540800e4b020aae0a8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,130 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: a2d6ec45ade76e745a6d9968ae64cf7d, type: 3}
m_Name: JiXieXieConfig
m_EditorClassIdentifier:
freeDropChipPrefab: {fileID: 1590595001857623, guid: 97d0f70681ad02b4cac0becfe91493bc,
type: 3}
settleScorpionPrefab: {fileID: 1163031175862678, guid: 25cdb6f7f14f7db42920da145d6bef52,
type: 3}
gemstoneSprites:
- {fileID: 21300000, guid: 868f82a2dd136c04da8e7e45fc8a6c6e, type: 3}
- {fileID: 21300000, guid: a65fa4d44f55c9247960c881444b61bc, type: 3}
- {fileID: 21300000, guid: cc0fcb0add32c2248a492b67fe09b8f6, type: 3}
- {fileID: 21300000, guid: 1570bb1c7ca44fd47a7444e1880ef229, type: 3}
- {fileID: 21300000, guid: 066cca573daf75747a26fe5ca3863044, type: 2}
- {fileID: 21300000, guid: 3e9bba24a8e3d014495c511f46de7d9d, type: 3}
- {fileID: 21300000, guid: d544e3e70b0033c48a9c7441afe43eb1, type: 2}
- {fileID: 21300000, guid: 70d2129bedf75324687de76ad250565b, type: 2}
- {fileID: 21300000, guid: 75ced8e0a1f225c469d14e68ee2b6101, type: 3}
- {fileID: 21300000, guid: 7af48f548f150cc40abe33678a193767, type: 2}
- {fileID: 21300000, guid: a011b974531fd264d8aa43f6692e986a, type: 2}
- {fileID: 21300000, guid: 857189b4242e25349ae006d897dca886, type: 3}
- {fileID: 21300000, guid: c72808bde77924645a1514489e7ed08a, type: 2}
- {fileID: 21300000, guid: 39db69c70c806e948a3a57e040885592, type: 2}
- {fileID: 21300000, guid: c9a3e0e956e616f40b9b4e5e1c0846cf, type: 3}
- {fileID: 21300000, guid: 4af8251b0012b8941a702b573e93eb41, type: 2}
cardSprites:
- {fileID: 21300000, guid: be2a5ae06ece9864aa275124a43909fa, type: 2}
- {fileID: 21300000, guid: 2770e075c6c965c4195b0ac231af14b7, type: 3}
- {fileID: 21300000, guid: 7686cfd9492ed674a9f00d1394dc1d14, type: 3}
- {fileID: 21300000, guid: aa1ae18b88f6c1f4b9ecbb391598d729, type: 3}
- {fileID: 21300000, guid: 2a375c86039b7834fa0c6f71b5103519, type: 3}
- {fileID: 21300000, guid: 96efd2e67a939dd4299387c50cee7f81, type: 3}
- {fileID: 21300000, guid: 9757d6dc8a986b642b232ab48ce1a496, type: 2}
- {fileID: 21300000, guid: 9389cbf1ec55df04eb8e3f80d33d49e3, type: 3}
- {fileID: 21300000, guid: 3ca1dab2457a37b4cbab75643a53d559, type: 3}
- {fileID: 21300000, guid: 164975a33d92b7642838f7f7d3421cdd, type: 2}
- {fileID: 21300000, guid: 59f3691a8bee69b4cb15076334e934e1, type: 2}
- {fileID: 21300000, guid: 434fa5275dfac224eb9d01552543c37b, type: 2}
- {fileID: 21300000, guid: 0e3ec333aac4849438a6a853efe0f7e8, type: 2}
- {fileID: 21300000, guid: 85e826c203cdb094ab80e1b0910fa49a, type: 3}
- {fileID: 21300000, guid: fb85359958daba44482d29146b966d7b, type: 2}
- {fileID: 21300000, guid: ea3d46b8339c0524da4b0f1605ef1740, type: 3}
- {fileID: 21300000, guid: d1360d4c0158da7448cf307985a1fcfc, type: 2}
- {fileID: 21300000, guid: b72a87db6356ce74c82088037184008a, type: 2}
- {fileID: 21300000, guid: 08045d585aafdb44193a389f958c8758, type: 2}
- {fileID: 21300000, guid: 37431ec46f3e2ae43a07807382bc3ed6, type: 3}
goodsSprites:
- {fileID: 21300000, guid: 1d110e0ad258ca54aa93e2c67ce3899a, type: 2}
- {fileID: 21300000, guid: 77816851598769b45a94a17b1ba4b451, type: 3}
- {fileID: 21300000, guid: 5eeb77c79438df648a6ee596f6f1fc57, type: 3}
- {fileID: 21300000, guid: 3edd7884686662c4aadeee78d4a509eb, type: 3}
- {fileID: 21300000, guid: 1e42892beb898b8488014d742a800543, type: 3}
- {fileID: 21300000, guid: 6568d93439840e04f8a2a19b5da4b745, type: 3}
- {fileID: 21300000, guid: 2157d3c04f46e9e49a0b142e7d9b5d9e, type: 3}
- {fileID: 21300000, guid: c3e47328e1b853043b567f5253bf2c01, type: 3}
- {fileID: 21300000, guid: 59ab4df37fec2d44aac41da3cd8182ea, type: 3}
- {fileID: 21300000, guid: 87eec89dcc6d4c743ae362f52211c707, type: 2}
- {fileID: 21300000, guid: d7bcc5f862a223141bd9b9e152fcc9b5, type: 3}
- {fileID: 21300000, guid: 6ba36d5836348d945baeee493510680c, type: 3}
- {fileID: 21300000, guid: e495fb530e5f1b14583f72cd5070c407, type: 2}
- {fileID: 21300000, guid: 47d681577cd9d9143b3d9693aee1f478, type: 3}
- {fileID: 21300000, guid: 4afde32a750f7664386da86e412ed9f6, type: 2}
- {fileID: 21300000, guid: d22e64b9ffc73c14f895161b3e267cd0, type: 2}
- {fileID: 21300000, guid: 8c73e3e838617864f8a9e7ab2c22315c, type: 3}
- {fileID: 21300000, guid: 02cb9983d722e2647805137415613325, type: 2}
- {fileID: 21300000, guid: 33de53e8049594a40ab45a0dc23a0cf3, type: 2}
- {fileID: 21300000, guid: 3afba2eeb8beca84a8a17b084b565468, type: 3}
- {fileID: 21300000, guid: 3948d8cf3ceede248965a429349cf979, type: 2}
- {fileID: 21300000, guid: 6615632243573aa40838936f5c5484df, type: 3}
- {fileID: 21300000, guid: d5f773c04375e47438be84443c93668e, type: 2}
- {fileID: 21300000, guid: 32fa9bc20939f354c97acafd96493d0e, type: 2}
- {fileID: 21300000, guid: 7fb1ca20d9ab46f4283abd89d2723896, type: 2}
- {fileID: 21300000, guid: 8ee387e982aff3a44a49845fad212950, type: 2}
- {fileID: 21300000, guid: c20a3932beeb4fc44a0956bbd43fd878, type: 3}
- {fileID: 21300000, guid: 3b10cc1775548c748af6ab906ceb35d4, type: 2}
- {fileID: 21300000, guid: eaa882c43bfdf7e4f994a309ad1dbb4b, type: 3}
- {fileID: 21300000, guid: c792a0fb4b1db094287bb535189fbfa8, type: 2}
- {fileID: 21300000, guid: ebdf18dcff2b7dd40b0a009d7475db98, type: 2}
- {fileID: 21300000, guid: f679323489c932e4dadb2585ad5e9d67, type: 3}
- {fileID: 21300000, guid: ef49b80f7470a9945ab4cd51d60ad71e, type: 2}
- {fileID: 21300000, guid: a1da9962c55bd3641994604435af772d, type: 2}
- {fileID: 21300000, guid: 9418909a747892f40b4c3776b94d53bb, type: 2}
- {fileID: 21300000, guid: a93a624c62447f44ebf1bb373efd5fd2, type: 3}
propNames:
- "\u91D1\u5E01"
- "\u91D1\u5E01"
- "\u91D1\u5E01"
- "\u91D1\u5E01"
- "\u91D1\u5E01"
- "\u91D1\u5E01"
- "\u91D1\u5E01"
- "\u91D1\u5E01"
- "\u91D1\u5E01"
- "\u91D1\u5E01"
- "\u91D1\u5E01"
- "\u91D1\u5E01"
- "\u94BB\u77F3"
- "\u94BB\u77F3"
- "\u94BB\u77F3"
- "\u9053\u51772"
- "\u9053\u51772"
- "\u9053\u51772"
- "\u9053\u51773"
- "\u9053\u51773"
- "\u9053\u51773"
- "\u9053\u51774"
- "\u9053\u51774"
- "\u9053\u51774"
- "\u706B\u7BAD\u70AE\u788E\u7247"
- "\u706B\u7BAD\u70AE\u788E\u7247"
- "\u706B\u7BAD\u70AE\u788E\u7247"
- "\u706B\u7BAD\u70AE\u788E\u7247"
- "\u706B\u7BAD\u70AE\u788E\u7247"
- "\u706B\u7BAD\u70AE\u788E\u7247"
- "\u706B\u7BAD\u70AE\u788E\u7247"
- "\u706B\u7BAD\u70AE\u788E\u7247"
- "\u706B\u7BAD\u70AE\u788E\u7247"
- "\u706B\u7BAD\u70AE\u788E\u7247"
- "\u706B\u7BAD\u70AE\u788E\u7247"
- "\u706B\u7BAD\u70AE\u788E\u7247"
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4adb4d6d53ad6b74cabd5722c72c2d25
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,40 @@
using UnityEngine;
/// <summary>
/// 机械蝎八连抽配置资产(ScriptableObject)。
///
/// ⭐ 把所有散落的资源引用(预制体 + 图片数组 + 道具名称)集中到一个资产里,
/// 爆奖预制体的 JiXieXieEvent 只拖这一个 config 字段即可,不用逐个拖十几张图。
///
/// 生成方式(推荐):
/// 菜单 Tools → 机械蝎 → 生成八连抽配置资产
/// 自动查找项目中 PW_FreeItem_DropGemstone/DropCard/DropProp 资源并填入,
/// 生成 JiXieXieConfig.asset 到 JiXieXie 目录,然后拖到爆奖预制体的 config 字段。
///
/// 手动方式:
/// Assets → Create → XJMH → 机械蝎八连抽配置,再手动拖入(不推荐,图太多)。
/// </summary>
[CreateAssetMenu(fileName = "JiXieXieConfig", menuName = "XJMH/机械蝎八连抽配置", order = 0)]
public class JiXieXieConfig : ScriptableObject
{
[Header("预制体")]
[Tooltip("宝石/卡牌单片预制体(PW_Effect_FreeDropChip)")]
public GameObject freeDropChipPrefab;
[Tooltip("特殊奖励结算特效(PW_Effect_Settle_Scorpion)")]
public GameObject settleScorpionPrefab;
[Header("图片(数组顺序 = 按数字排序自动填充)")]
[Tooltip("宝石图片数组(PW_FreeItem_DropGemstone1~4),代码按 type 1~4 取,顺序别乱")]
public Sprite[] gemstoneSprites;
[Tooltip("卡牌底色图片数组(PW_FreeItem_DropCard0~4),代码按 type 取,顺序别乱")]
public Sprite[] cardSprites;
[Tooltip("道具图片数组(PW_FreeItem_DropProp0~4、1000~1003),代码按 rewardID 取")]
public Sprite[] goodsSprites;
[Header("道具名称(顺序与 goodsSprites 一一对应)")]
[Tooltip("显示在卡牌 Name 上的道具名,如 金币/钻石/火箭炮碎片...")]
public string[] propNames;
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a2d6ec45ade76e745a6d9968ae64cf7d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,92 +1,814 @@
using Assets.MyGame.Scripts.Net.APIResponses.Enum;
using DG.Tweening;
using System;
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using Random = UnityEngine.Random;
/// <summary>
/// 机械蝎 Boss 爆奖事件(通用爆炸模式)。
/// 机械蝎 Boss 爆奖事件(八连抽模式)。
///
/// 流程:
/// Boss 死亡 → Init → 等待 explosionDelay → 范围杀鱼 → 加分滚动
/// 爆奖预制体本身就是爆炸特效,在死亡位置创建。
/// 对应美术规格(12028 机械蝎)+ Lua 参考(PW_Effect.txt DropTenCardEffect / FreeDropCardEffect):
/// 1. 创建 N 个 PW_Effect_FreeDropChip 实例(美术 8 个),宝石图片在 Gemstone 的 Image(Source Image) 上替换
/// 2. 翻牌:隐藏 Gemstone 相关子物体,显示卡牌相关子物体 Card,
/// Card 和 Goods 的 Image(Source Image) 替换图片,Name/Num 文本填充
/// 3. 显示 BJEndUI 面板收尾
///
/// 奖励数据(本版只发金币):
/// 总金币(RoomKillNotice.gold → maxScore)用拆红包随机分成 chipCount 份,
/// 8 份之和 == maxScore。宝石颜色与卡牌颜色一一对应。
/// 卡牌颜色分级【金币阈值 = 炮倍 × 阈值倍数】:
/// num < 炮倍×100 绿,< 炮倍×200 蓝,< 炮倍×500 紫,≥ 炮倍×500 红(金色不用),
/// 每张卡下方显示本份金币数量。
///
/// 流程(用户确认版,全程 UI 层):
/// 死亡位置一次出现 8 颗宝石 → 停留1秒 → 飞到屏幕中心两排(每排4个)排好
/// → 一张一张翻牌(全是金币)→ 卡牌飞到座位上方 → 显示 BJEndUI + 加分 + 回收
///
/// ⭐ 资源配置:所有预制体/图片/名称集中在 JiXieXieConfig 资产(ScriptableObject),
/// 只需拖一个 config 字段。资产用菜单 Tools → 机械蝎 → 生成八连抽配置资产 一键生成。
/// </summary>
public class JiXieXieEvent : BJEventBase
{
[Header("爆炸配置")]
[Tooltip("爆炸检测半径(世界单位)")]
public float explosionRadius = 5f;
[Tooltip("鱼层(Inspector 设 Fish)")]
public LayerMask fishLayer;
[Tooltip("爆炸延迟(Boss 死亡后到爆炸的等待)")]
public float explosionDelay = 0.5f;
[Tooltip("爆炸特效持续时间(播完后才进入加分并回收)")]
public float explosionDuration = 2f;
// ==================== Inspector 字段 ====================
/// <summary>爆炸中心(世界坐标,由 Boss 传入=死亡位置)</summary>
[Header("八连抽 - 配置资产")]
[Tooltip("拖入 JiXieXieConfig 资产(Tools→机械蝎→生成八连抽配置资产 一键生成)。\n包含:FreeDropChip 预制体 + 宝石/卡牌/道具图片 + 道具名称")]
public JiXieXieConfig config;
[Header("八连抽 - 配置")]
[Tooltip("宝石/卡牌数量(美术 10 个)")]
public int chipCount = 10;
[Tooltip("拆红包每份最少金币(默认 1,保证每张卡都有钱)")]
public long minPartAmount = 1;
[Tooltip("宝石在屏幕靠上位置出现的间隔(秒,越小越像同时出现)")]
public float gemAppearInterval = 0.03f;
[Tooltip("宝石出生点 Y = 屏幕高度 × 该比例(1=顶部,0.5=正中,默认靠上)")]
public float spawnYRatio = 0.78f;
[Tooltip("宝石飞向屏幕中心的时长(秒)")]
public float flyToCenterDuration = 0.8f;
[Tooltip("卡牌放大倍率(chip 根节点最终缩放,1=原始大小)")]
public float cardScale = 1.5f;
[Tooltip("两排整体中心 Y = 屏幕高度 × 该比例(0.5=正中央,调大靠上,调小靠下)")]
public float gridCenterYRatio = 0.5f;
[Tooltip("两排布局:每颗卡水平间距基础值(UI 像素,实际间距 = 该值 × cardScale)")]
public float gridSpacingX = 120f;
[Tooltip("两排布局:上下排间距基础值(UI 像素,实际间距 = 该值 × cardScale)")]
public float gridRowGap = 160f;
[Tooltip("每张卡翻牌间隔(秒)")]
public float flipInterval = 0.5f;
[Tooltip("全部翻完后停留时间(秒),让玩家看清再飞向炮台")]
public float flipEndDelay = 1f;
[Tooltip("卡片飞到座位上方的时长(秒)")]
public float flyToSeatDuration = 0.6f;
[Header("八连抽 - 宝石颜色(与卡牌颜色一一对应)")]
[Tooltip("绿色宝石在 gemstoneSprites 数组中的下标(0 = DropGemstone1 绿色)")]
public int gemGreenIndex = 0;
[Tooltip("蓝色宝石在 gemstoneSprites 数组中的下标(1 = DropGemstone2 蓝色)")]
public int gemBlueIndex = 1;
[Tooltip("紫色宝石在 gemstoneSprites 数组中的下标(2 = DropGemstone3 紫色)")]
public int gemPurpleIndex = 2;
[Tooltip("红色宝石在 gemstoneSprites 数组中的下标(3 = DropGemstone4 红色)")]
public int gemRedIndex = 3;
[Header("八连抽 - 卡牌颜色分级(金币阈值 = 炮倍 × 阈值倍数)")]
[Tooltip("卡牌背面图在 cardSprites 数组中的下标(PW_FreeItem_DropCard0 是背面)")]
public int cardBackIndex = 0;
[Tooltip("绿卡上限倍数:金币 < 炮倍×该值 → 绿色(100倍)")]
public int greenLimitBeishu = 100;
[Tooltip("蓝卡上限倍数:金币 < 炮倍×该值 → 蓝色(200倍)")]
public int blueLimitBeishu = 200;
[Tooltip("红卡下限倍数:金币 ≥ 炮倍×该值 → 红色(500倍);< 该值 → 紫色(300倍档)")]
public int redLimitBeishu = 500;
[Tooltip("绿色卡在 cardSprites 数组中的下标(1 = DropCard1 绿色)")]
public int greenCardIndex = 1;
[Tooltip("蓝色卡在 cardSprites 数组中的下标(2 = DropCard2 蓝色)")]
public int blueCardIndex = 2;
[Tooltip("紫色卡在 cardSprites 数组中的下标(3 = DropCard3 紫色)")]
public int purpleCardIndex = 3;
[Tooltip("红色卡在 cardSprites 数组中的下标(4 = DropCard4 红色)")]
public int redCardIndex = 4;
[Header("八连抽 - 子物体名称(与预制体保持一致)")]
public string gemstoneName = "Gemstone";
public string cardObjName = "Card";
public string cardImagePath = "Card/Card";
public string goodsPath = "Card/Goods";
public string namePath = "Card/Name";
public string numPath = "Card/Num";
public string mulPath = "Card/Num/Mul";
public string starEffectName = "Effect_Gemstone_star";
public string bombEffectName = "Effect_Gemstone_Bomb";
public string orangeEffectName = "Effect_Card_Orange";
public string purpleEffectName = "Effect_Card_Purple";
public string createEffectName = "Effect_Create";
public string tailEffectName = "Effect_Tail";
public string arriveEffectName = "Effect_Arrive";
/// <summary>爆炸/散落中心(世界坐标,由 Boss 传入=死亡位置)</summary>
[HideInInspector]
public Vector3 explosionCenter = Vector3.zero;
// ==================== 运行时数据 ====================
/// <summary>单片掉落数据</summary>
private class DropItemInfo
{
public int rewardType; // 奖励类型(本版恒为 1 金币)
public int rewardID; // 道具ID(本版恒为 0 金币图)
public long num; // 本份金币数量
public int cardColor; // 颜色分级 1=绿 2=蓝 3=紫 4=红(宝石和卡牌共用,一一对应)
}
/// <summary>当前局掉落数据(长度=chipCount)</summary>
private List<DropItemInfo> _dropInfos = new List<DropItemInfo>();
/// <summary>创建出来的 chip 实例列表(die 时统一销毁)</summary>
private readonly List<GameObject> _chipInstances = new List<GameObject>();
/// <summary>UI 层父节点(chip 都挂这里,随 UI 卸载自动清理)</summary>
private Transform _uiLayer;
/// <summary>UI Canvas(世界坐标 → UI 坐标转换用)</summary>
private Canvas _uiCanvas;
/// <summary>累计金币(rewardType==1 的 num 之和,显示在翻牌完成处)</summary>
private long _earnTotal = 0;
// ==================== 初始化 ====================
public override void Init(long goldCount, int posID, long paobei, long fishScore, int beishu,
Action<int> action, Action action1 = null)
{
base.Init(goldCount, posID, paobei, fishScore, beishu, action, action1);
// ApplySeatOffset 会把位置移到屏幕中心,这里恢复到死亡位置
transform.position = explosionCenter;
StartCoroutine(ExplodeThenScore());
// 解析服务端数据(失败则随机生成)
_dropInfos = ParseDropInfos();
// 启动八连抽流程
StartCoroutine(TenDropFlow());
}
/// <summary>重写:不移动到屏幕中心,但缓存座位引用(供 isSelf 判断使用)</summary>
protected override void ApplySeatOffset()
{
// 不调用 base.ApplySeatOffset(),避免位置被移到屏幕中心 (0,0,-100)
// 但需要设置 _targetSit,否则 Init 里 isSelf 判断会失败
GetSeatPosition();
}
/// <summary>爆炸 → 杀鱼 → 加分滚动</summary>
private IEnumerator ExplodeThenScore()
// ============================================================
// 数据解析
// ============================================================
/// <summary>
/// 八连抽只发金币:用【拆红包算法】把总金币(maxScore)随机拆成 chipCount 份,
/// 每份数量随机(二倍均值法),但 8 份之和恒等于 maxScore。
/// 卡牌颜色分级【金币阈值 = 炮倍(paoBei) × 阈值倍数】:
/// num < 炮倍×100 → 绿;< 炮倍×200 → 蓝;< 炮倍×500 → 紫;≥ 炮倍×500 → 红(金色不用)
/// 每张卡 num 不同 → 颜色可能不同。
/// </summary>
private List<DropItemInfo> ParseDropInfos()
{
// 等待 Boss 死亡动画结束
yield return new WaitForSeconds(explosionDelay);
List<DropItemInfo> result = new List<DropItemInfo>();
// 爆炸中心 = Boss 传入的死亡位置
Vector3 center = explosionCenter;
Debug.Log($"[JiXieXie] 爆炸 位置={center}");
// ⭐ 拆红包:随机拆成 chipCount 份,总和 == maxScore
long[] amounts = SplitRedPacket(maxScore, chipCount, minPartAmount);
// 爆奖预制体本身就是爆炸特效,已在死亡位置创建并激活,这里不再 Instantiate
// ⭐ 金币阈值 = 炮倍 × 阈值倍数(用户口径:100/200/300/500 倍档)
long greenMax = paoBei * (long)greenLimitBeishu; // 炮倍×100
long blueMax = paoBei * (long)blueLimitBeishu; // 炮倍×200
long redMin = paoBei * (long)redLimitBeishu; // 炮倍×500
// 物理圈检测,范围内的普通鱼走死鱼方法
Vector3 sitPos = GetSeatPosition();
var hits = Physics2D.OverlapCircleAll(center, explosionRadius, fishLayer);
int killCount = 0;
foreach (var hit in hits)
for (int i = 0; i < amounts.Length; i++)
{
if (!hit.CompareTag("Fish")) continue;
var monster = hit.GetComponentInParent<MonsterContral>();
if (monster == null) continue;
if (monster.fishenum != FishTypeEnum.普通鱼) continue;
var info = new DropItemInfo();
info.rewardType = 1; // 金币
info.rewardID = 0; // 金币图(goodsSprites 里 ID=0 那张)
info.num = amounts[i];
// 每条鱼的金币 = 炮倍 × 鱼底分
var coinNum = new RedPacketRecord
{
Amount = paoBei * monster.monsterData.fishTypeConfig.fishScore
};
CreateFishManager.Instance.Id_DieFish_FX(monster.guid, coinNum, sitPos, isSelf);
killCount++;
// ⭐ 每张卡按自己的金币数量 × 阈值判定颜色(num 不同 → 颜色可不同)
info.cardColor = info.num < greenMax ? 1
: info.num < blueMax ? 2
: info.num < redMin ? 3
: 4;
result.Add(info);
}
Debug.Log($"[JiXieXie] 爆炸炸死 {killCount} 条鱼");
// 等爆炸特效播完再进入加分滚动 + 回收
yield return new WaitForSeconds(explosionDuration);
// 如果 chipCount 被改成比 amounts 多(防御),补齐
while (result.Count < chipCount)
{
var info = new DropItemInfo();
info.rewardType = 1;
info.rewardID = 0;
info.num = 0;
info.cardColor = 1;
result.Add(info);
}
// 进入加分滚动 → MoveToSit → OnScoreEnd → JiaFen → die(回池)
StartAddScore();
_earnTotal = maxScore;
// ⭐ 日志打印每份金额 + 颜色,方便确认
Debug.Log($"[JiXieXie] 金币拆红包 {result.Count} 份: [{string.Join(",", amounts)}] 总={_earnTotal} paoBei={paoBei} 阈值={greenMax}/{blueMax}/{redMin}");
return result;
}
/// <summary>回收爆奖预制体到对象池(先清理分数面板,防止泄漏)</summary>
/// <summary>
/// 拆红包算法(二倍均值法):把 total 随机拆成 count 份,
/// 每份 ≥ min,所有份之和恒等于 total。
/// </summary>
private long[] SplitRedPacket(long total, int count, long min)
{
if (count <= 0)
{
return new long[0];
}
if (count == 1)
{
return new long[] { total };
}
long[] result = new long[count];
// 不够分:每份至少 min,直接平均兜底(避免出现 0)
if (total < count * min)
{
long per = count > 0 ? total / count : 0;
long rem = count > 0 ? total % count : 0;
for (int i = 0; i < count; i++)
{
result[i] = per + (i == 0 ? rem : 0);
}
return result;
}
long remain = total;
for (int i = 0; i < count - 1; i++)
{
int left = count - i; // 剩余份数
long max = (remain - (left - 1) * min) * 2 / left; // 二倍均值上限
if (max < min) max = min;
long span = max - min + 1;
// ⭐ 用 double 计算避免大金额时 float 精度不足导致数值相同/重复
long amt = min + (long)((double)Random.value * span); // [min, max] 随机
if (amt > remain) amt = remain;
if (amt < min) amt = min;
result[i] = amt;
remain -= amt;
}
result[count - 1] = remain; // 最后一份拿剩余
if (result[count - 1] < min) result[count - 1] = min; // 防御:最后一份不足 min 时保底(总和会略超,可接受)
return result;
}
// ============================================================
// 八连抽流程
// ============================================================
/// <summary>
/// 完整流程(用户确认版):
/// 屏幕靠上位置一次出现 8 颗宝石 → 直接飞到排开两排(每排4个)
/// → 一张一张翻牌(全是金币)→ 卡牌飞到炮台座位 → 显示 BJEndUI + 加分 + 回收
/// </summary>
private IEnumerator TenDropFlow()
{
// ---------- 阶段1:屏幕靠上位置一次出现 8 颗宝石 ----------
yield return StartCoroutine(SpawnGemsAtDiePos());
// ---------- 阶段2:生成完直接飞到排开位置,两排每排 4 个 ----------
yield return StartCoroutine(FlyToCenterGrid());
// ---------- 阶段3:一张一张翻牌(全是金币) ----------
yield return StartCoroutine(FlipCards());
// ---------- 阶段3.5:翻完停留,让玩家看清全部卡牌再走 ----------
yield return new WaitForSeconds(flipEndDelay);
// ---------- 阶段4:卡牌移动到炮台座位 ----------
yield return StartCoroutine(FlyCardsToSeat());
// ---------- 阶段5:显示 BJEndUI + 加分 + 回收 ----------
ShowEndAndFinish();
}
/// <summary>阶段1:8 颗宝石在屏幕靠上位置一次出现(微小偏移 + 逐个快速放大)</summary>
private IEnumerator SpawnGemsAtDiePos()
{
GameObject chipPrefab = config != null ? config.freeDropChipPrefab : null;
if (chipPrefab == null)
{
Debug.LogError("[JiXieXie] config 或 config.freeDropChipPrefab 未赋值(PW_Effect_FreeDropChip)!跳过宝石阶段");
yield break;
}
EnsureUiLayer();
if (_uiLayer == null || _uiCanvas == null)
{
Debug.LogError("[JiXieXie] 未找到 UI 层/Canvas(UIManager.uiRoot 或 currGameType.subPanelParent),跳过宝石阶段");
yield break;
}
// ⭐ 出生点 = 屏幕靠上位置(y = 屏幕高度 × spawnYRatio),不再用死亡位置
Vector2 baseUi = CoordinateTransformation.ScreenPoint_ToCanvas(
new Vector2(Screen.width / 2f, Screen.height * spawnYRatio), _uiCanvas);
_chipInstances.Clear();
for (int i = 0; i < _dropInfos.Count; i++)
{
GameObject chip = Instantiate(chipPrefab, _uiLayer);
chip.name = $"Chip_{i}";
_chipInstances.Add(chip);
// ⭐ 全部出现在同一点:出生点 + 微小偏移(避免完全重叠,观感像一堆宝石)
Vector2 tiny = Random.insideUnitCircle * 8f;
var rt = chip.GetComponent<RectTransform>();
if (rt != null)
rt.anchoredPosition = baseUi + tiny;
else
chip.transform.position = explosionCenter;
chip.transform.localScale = Vector3.zero;
// ⭐ 宝石颜色 = 卡牌颜色(cardColor 分级,一一对应)
Transform gemstone = chip.transform.Find(gemstoneName);
if (gemstone != null)
{
var img = gemstone.GetComponent<Image>();
if (img != null)
{
Sprite sp = GetGemstoneColorSprite(_dropInfos[i].cardColor);
if (sp != null) img.sprite = sp;
}
gemstone.gameObject.SetActive(true);
}
// 卡牌默认隐藏
Transform cardObj = chip.transform.Find(cardObjName);
if (cardObj != null) cardObj.gameObject.SetActive(false);
// 关闭所有特效子物体
SetChildActive(chip.transform, starEffectName, false);
SetChildActive(chip.transform, bombEffectName, false);
SetChildActive(chip.transform, orangeEffectName, false);
SetChildActive(chip.transform, purpleEffectName, false);
SetChildActive(chip.transform, tailEffectName, false);
SetChildActive(chip.transform, arriveEffectName, false);
// 第一颗带出场特效
if (i == 0)
SetChildActive(chip.transform, createEffectName, true);
chip.SetActive(true);
// 放大出现(目标 = cardScale,间隔很小 → 观感一次全部出现)
Transform star = chip.transform.Find(starEffectName);
chip.transform.DOScale(Vector3.one * cardScale, 0.4f).SetEase(Ease.OutElastic)
.OnComplete(() =>
{
if (star != null) star.gameObject.SetActive(true);
});
Debug.Log($"[JiXieXie] 宝石[{i}] 屏幕靠上位置出现 颜色={GetColorName(_dropInfos[i].cardColor)}");
yield return new WaitForSeconds(gemAppearInterval);
}
}
/// <summary>阶段3:宝石飞到排开位置(两排每排 4 个,中心 Y 由 gridCenterYRatio 控制)</summary>
private IEnumerator FlyToCenterGrid()
{
if (_uiLayer == null || _uiCanvas == null) yield break;
// ⭐ 排开位置基准 = 屏幕 (gridCenterYRatio) 处(默认 0.5 屏幕中央,可配上下)
Vector2 centerUi = CoordinateTransformation.ScreenPoint_ToCanvas(
new Vector2(Screen.width / 2f, Screen.height * gridCenterYRatio), _uiCanvas);
// 两排布局(动态列数):10 张 = 每排 5 个;8 张 = 每排 4 个
// ⭐ 间距跟随卡牌放大:实际间距 = 基础值 × cardScale(卡牌放大了间距也相应放大,比例不挤)
int perRow = (_chipInstances.Count + 1) / 2; // 10 → 5,8 → 4
float spacingX = gridSpacingX * cardScale;
float rowGap = gridRowGap * cardScale;
for (int i = 0; i < _chipInstances.Count; i++)
{
GameObject chip = _chipInstances[i];
if (chip == null) continue;
int row = i / perRow; // 0=上排,1=下排
int col = i % perRow;
float x = (col - (perRow - 1) / 2f) * spacingX; // 居中:-2,-1,0,1,2 倍间距
float y = (row == 0 ? 1 : -1) * rowGap * 0.5f;
Vector2 target = centerUi + new Vector2(x, y);
var rt = chip.GetComponent<RectTransform>();
if (rt != null)
rt.DOAnchorPos(target, flyToCenterDuration).SetEase(Ease.OutCubic);
else
chip.transform.DOMove(explosionCenter + (Vector3)target, flyToCenterDuration).SetEase(Ease.OutCubic);
Debug.Log($"[JiXieXie] 宝石[{i}] 飞到 排{row + 1}列{col + 1} 目标={target}");
}
yield return new WaitForSeconds(flyToCenterDuration + 0.1f);
}
/// <summary>阶段5:所有卡牌移动到炮台座位(回收点)</summary>
private IEnumerator FlyCardsToSeat()
{
if (_uiCanvas == null) yield break;
// ⭐ 回收点 = 击杀者炮台座位位置(GetSeatPosition 返回 currentTurret 位置)
Vector3 seatWorld = GetSeatPosition();
Vector2 seatUi = CoordinateTransformation.WorldPos_ToCanvas(seatWorld, _uiCanvas);
for (int i = 0; i < _chipInstances.Count; i++)
{
GameObject chip = _chipInstances[i];
if (chip == null) continue;
// 轻微错开,观感像飞向同一个点
Vector2 target = seatUi + new Vector2((i % 3 - 1) * 12f, 0);
var rt = chip.GetComponent<RectTransform>();
if (rt != null)
rt.DOAnchorPos(target, flyToSeatDuration).SetEase(Ease.InCubic);
else
chip.transform.DOMove(seatWorld, flyToSeatDuration).SetEase(Ease.InCubic);
Debug.Log($"[JiXieXie] 卡牌[{i}] 飞向炮台座位 {target}");
yield return new WaitForSeconds(0.05f);
}
yield return new WaitForSeconds(flyToSeatDuration + 0.2f);
}
/// <summary>阶段6:显示 BJEndUI 面板 + 加分 + 回收</summary>
private void ShowEndAndFinish()
{
Debug.Log($"[JiXieXie] 八连抽完成,显示 BJEndUI 倍数={bJbeishu} 总分={maxScore} isSelf={isSelf}");
Vector3 uiPos = GetSeatPosition() + Vector3.up * bjEndUIOffsetY;
if (bjEndUI != null)
{
var endUI = Instantiate(bjEndUI, uiPos, Quaternion.identity,
CreateFishManager.Instance != null ? CreateFishManager.Instance.bjEffectRoot : null);
endUI.Init(bJbeishu, maxScore, isSelf);
}
else
{
Debug.LogWarning("[JiXieXie] bjEndUI 未赋值(BJEventBase 基类字段),跳过结束面板");
}
// 回加金币到玩家
JiaFen?.Invoke();
// 回收爆奖预制体 + 销毁 chip
die();
}
/// <summary>阶段2:逐颗翻牌(隐藏 Gemstone,显示 Card,替换 Card/Goods 图片)</summary>
private IEnumerator FlipCards()
{
for (int i = 0; i < _chipInstances.Count; i++)
{
GameObject chip = _chipInstances[i];
if (chip == null) continue;
DropItemInfo info = _dropInfos[i];
if (info == null) continue;
FlipOneCard(chip, info, i);
yield return new WaitForSeconds(flipInterval);
}
}
/// <summary>翻单张卡:隐藏宝石 → 显示卡牌背面 → 翻转显示正面(分级色)→ 特效</summary>
private void FlipOneCard(GameObject chip, DropItemInfo info, int index)
{
// 隐藏宝石相关子物体
Transform gemstone = chip.transform.Find(gemstoneName);
if (gemstone != null) gemstone.gameObject.SetActive(false);
SetChildActive(chip.transform, starEffectName, false);
SetChildActive(chip.transform, bombEffectName, false);
// 显示卡牌容器,缩放 0 → 1
Transform cardObj = chip.transform.Find(cardObjName);
if (cardObj == null)
{
Debug.LogWarning($"[JiXieXie] chip 里找不到 {cardObjName},跳过翻牌");
return;
}
cardObj.gameObject.SetActive(true);
cardObj.localScale = Vector3.zero;
// ⭐ 翻牌素材:背面 = cardSprites[cardBackIndex](DropCard0),正面 = 按金币分级的颜色图
Sprite backSprite = GetCardBackSprite();
Sprite frontSprite = GetCardColorSprite(info.cardColor);
// Card / Goods 图片(Source Image)
Transform cardImage = chip.transform.Find(cardImagePath);
Transform goods = chip.transform.Find(goodsPath);
if (cardImage != null)
{
var img = cardImage.GetComponent<Image>();
if (img != null)
{
// 先显示背面,翻转后换成正面
img.sprite = backSprite != null ? backSprite : frontSprite;
}
}
if (goods != null)
{
var img = goods.GetComponent<Image>();
if (img != null)
{
Sprite sp = GetGoodsSprite(info.rewardID);
if (sp != null) img.sprite = sp;
}
}
// 文本:名称 / 数量 / 倍数(正面内容先设好,翻过来就能看到)
Transform nameTf = chip.transform.Find(namePath);
if (nameTf != null)
{
var tmp = nameTf.GetComponent<TextMeshProUGUI>();
if (tmp != null)
tmp.text = GetPropName(info.rewardID);
}
Transform numTf = chip.transform.Find(numPath);
if (numTf != null)
{
var tmp = numTf.GetComponent<TextMeshProUGUI>();
if (tmp != null)
tmp.text = info.num > 0 ? FormatGold(info.num) : "已达上限";
}
Transform mulTf = chip.transform.Find(mulPath);
if (mulTf != null)
{
var tmp = mulTf.GetComponent<TextMeshProUGUI>();
if (tmp != null)
tmp.gameObject.SetActive(false); // 倍数:本版不拆多倍数,隐藏
}
// ⭐ 翻牌动画:背面放大出现 → 停顿 → 压扁(scaleX→0) → 换正面 → 弹开(scaleX→1)
Sequence flipSeq = DOTween.Sequence();
flipSeq.Append(cardObj.DOScale(Vector3.one, 0.12f).SetEase(Ease.OutBack)); // 背面出现
flipSeq.AppendInterval(0.1f); // 让玩家看清背面
flipSeq.Append(cardObj.DOScaleX(0f, 0.1f).SetEase(Ease.InOutQuad)); // 压扁成一条线
flipSeq.AppendCallback(() =>
{
// 翻到一半换正面(分级色)
if (cardImage != null)
{
var img = cardImage.GetComponent<Image>();
if (img != null && frontSprite != null)
img.sprite = frontSprite;
}
});
flipSeq.Append(cardObj.DOScaleX(1f, 0.15f).SetEase(Ease.OutBack)); // 弹开露出正面
// 紫/红卡附加:弹开后放大一下 + 对应特效(紫=Effect_Card_Purple,红=Effect_Card_Orange)
if (info.cardColor >= 3)
{
Transform purple = chip.transform.Find(purpleEffectName);
Transform orange = chip.transform.Find(orangeEffectName);
flipSeq.Append(cardObj.DOScale(Vector3.one * 1.2f, 0.25f).SetEase(Ease.OutElastic));
flipSeq.AppendCallback(() =>
{
if (info.cardColor == 3)
{
if (purple != null) purple.gameObject.SetActive(true);
}
else
{
if (orange != null) orange.gameObject.SetActive(true);
}
});
flipSeq.Append(cardObj.DOScale(Vector3.one, 0.15f));
}
Debug.Log($"[JiXieXie] 翻牌[{index}] 背面→正面({GetColorName(info.cardColor)}) num={info.num}");
}
// ============================================================
// 工具方法(全部从 config 读取资源)
// ============================================================
/// <summary>
/// 缓存 UI 层父节点 + Canvas:
/// 父节点 = currGameType.subPanelParent.parent(与爆奖预制体同层,随 UI 卸载自动清理)
/// Canvas = UIManager.uiRoot 上的 Canvas(世界坐标 → UI 坐标转换用)
/// </summary>
private void EnsureUiLayer()
{
if (_uiLayer != null && _uiCanvas != null) return;
if (CreateFishManager.Instance != null && CreateFishManager.Instance.currGameType != null)
{
var subPanel = CreateFishManager.Instance.currGameType.subPanelParent;
if (subPanel != null)
_uiLayer = subPanel.parent;
}
if (UIManager.Instance != null && UIManager.Instance.uiRoot != null)
_uiCanvas = UIManager.Instance.uiRoot.GetComponent<Canvas>();
if (_uiCanvas == null && _uiLayer != null)
_uiCanvas = _uiLayer.GetComponentInParent<Canvas>();
}
/// <summary>
/// 按颜色分级取宝石图(与卡牌颜色一一对应)。
/// ⭐ 优先按文件名末尾数字精确匹配:DropGemstone1=绿 2=蓝 3=紫 4=红,
/// 不受资产数组顺序影响;匹配不到再回退 gemGreen/Blue/Purple/RedIndex 下标。
/// </summary>
private Sprite GetGemstoneColorSprite(int cardColor)
{
if (config == null || config.gemstoneSprites == null || config.gemstoneSprites.Length == 0) return null;
// 精确匹配:文件名末尾数字 == cardColor(DropGemstone1~4)
Sprite hit = FindSpriteByNumber(config.gemstoneSprites, cardColor);
if (hit != null) return hit;
// 兜底:按下标
int idx = cardColor == 1 ? gemGreenIndex
: cardColor == 2 ? gemBlueIndex
: cardColor == 3 ? gemPurpleIndex
: gemRedIndex;
idx = Mathf.Clamp(idx, 0, config.gemstoneSprites.Length - 1);
return config.gemstoneSprites[idx];
}
/// <summary>取卡牌背面图。⭐ 优先精确匹配 DropCard0,回退 cardBackIndex 下标</summary>
private Sprite GetCardBackSprite()
{
if (config == null || config.cardSprites == null || config.cardSprites.Length == 0) return null;
Sprite hit = FindSpriteByNumber(config.cardSprites, 0); // DropCard0 = 背面
if (hit != null) return hit;
int idx = Mathf.Clamp(cardBackIndex, 0, config.cardSprites.Length - 1);
return config.cardSprites[idx];
}
/// <summary>
/// 按卡牌颜色分级取卡牌图。
/// ⭐ 优先按文件名末尾数字精确匹配:DropCard1=绿 2=蓝 3=紫 4=红,
/// 不受资产数组顺序影响;匹配不到再回退 green/Blue/Purple/RedCardIndex 下标。
/// </summary>
private Sprite GetCardColorSprite(int cardColor)
{
if (config == null || config.cardSprites == null || config.cardSprites.Length == 0) return null;
// 精确匹配:文件名末尾数字 == cardColor(DropCard1~4)
Sprite hit = FindSpriteByNumber(config.cardSprites, cardColor);
if (hit != null) return hit;
// 兜底:按下标
int idx = cardColor == 1 ? greenCardIndex
: cardColor == 2 ? blueCardIndex
: cardColor == 3 ? purpleCardIndex
: redCardIndex;
idx = Mathf.Clamp(idx, 0, config.cardSprites.Length - 1);
return config.cardSprites[idx];
}
/// <summary>在 sprite 数组中查找文件名末尾数字 == number 的图(找不到返回 null)</summary>
private static Sprite FindSpriteByNumber(Sprite[] sprites, int number)
{
if (sprites == null || sprites.Length == 0) return null;
for (int i = 0; i < sprites.Length; i++)
{
if (sprites[i] != null && ExtractTrailingNumber(sprites[i].name) == number)
return sprites[i];
}
return null;
}
/// <summary>卡牌颜色分级 → 中文名(日志用)</summary>
private string GetColorName(int cardColor)
{
switch (cardColor)
{
case 1: return "绿";
case 2: return "蓝";
case 3: return "紫";
case 4: return "红";
default: return $"未知({cardColor})";
}
}
/// <summary>
/// 在 goodsSprites 中定位 rewardID 对应下标。
/// 优先精确匹配(文件名末尾数字 == rewardID,支持 1000+ 火箭炮),
/// 匹配不到再按取模兜底(避免数组长度取模导致错位)。
/// </summary>
private int FindGoodsIndex(int rewardID)
{
if (config == null || config.goodsSprites == null || config.goodsSprites.Length == 0) return -1;
for (int i = 0; i < config.goodsSprites.Length; i++)
{
if (config.goodsSprites[i] != null
&& ExtractTrailingNumber(config.goodsSprites[i].name) == rewardID)
{
return i;
}
}
return Mathf.Abs(rewardID) % config.goodsSprites.Length;
}
/// <summary>按 rewardID 取道具图,越界回退 0</summary>
private Sprite GetGoodsSprite(int rewardID)
{
int idx = FindGoodsIndex(rewardID);
if (idx < 0 || config.goodsSprites == null) return null;
return config.goodsSprites[idx];
}
/// <summary>按 rewardID 取道具名称,未配置则返回默认</summary>
private string GetPropName(int rewardID)
{
int idx = FindGoodsIndex(rewardID);
if (config != null && config.propNames != null && config.propNames.Length > 0
&& idx >= 0 && idx < config.propNames.Length
&& !string.IsNullOrEmpty(config.propNames[idx]))
{
return config.propNames[idx];
}
return "奖励";
}
/// <summary>提取文件名末尾数字(如 "PW_FreeItem_DropProp1000" → 1000),无数字返回 0</summary>
private static int ExtractTrailingNumber(string name)
{
if (string.IsNullOrEmpty(name)) return 0;
int i = name.Length - 1;
while (i >= 0 && char.IsDigit(name[i])) i--;
string numStr = name.Substring(i + 1);
return int.TryParse(numStr, out int v) ? v : 0;
}
/// <summary>金币千分位格式化</summary>
private string FormatGold(long num)
{
return num.ToString("N0");
}
/// <summary>按名字设置子物体显隐(找不到跳过)</summary>
private void SetChildActive(Transform root, string childName, bool active)
{
if (root == null || string.IsNullOrEmpty(childName)) return;
Transform child = root.Find(childName);
if (child != null) child.gameObject.SetActive(active);
}
// ============================================================
// 收尾
// ============================================================
/// <summary>回收爆奖预制体到对象池(先清理 chip 实例 + 分数面板)</summary>
public override void die()
{
CleanupBeforeDie();
// 销毁八连抽 chip 实例
for (int i = 0; i < _chipInstances.Count; i++)
{
if (_chipInstances[i] != null)
Destroy(_chipInstances[i]);
}
_chipInstances.Clear();
EffectPool.Instance.Return(gameObject);
}
@@ -300,6 +300,7 @@ MonoBehaviour:
bjEndUIOffsetY: 150
coinFlyDuration: 0.5
coinArrivalInterval: 0.1
showAddScoreFX: 1
isSelf: 0
autoFinishOnScoreFull: 1
totalBJCount: 0
@@ -308,10 +309,42 @@ MonoBehaviour:
isScoreStart: 0
isScoreEnd: 0
isGoldEffectStart: 0
explosionRadius: 5
fishLayer:
serializedVersion: 2
m_Bits: 0
explosionDelay: 0.5
explosionDuration: 2
config: {fileID: 11400000, guid: 4adb4d6d53ad6b74cabd5722c72c2d25, type: 2}
chipCount: 10
minPartAmount: 1
gemAppearInterval: 0.03
spawnYRatio: 0.78
flyToCenterDuration: 0.8
cardScale: 1.5
gridCenterYRatio: 0.5
gridSpacingX: 120
gridRowGap: 160
flipInterval: 0.4
flyToSeatDuration: 0.6
gemGreenIndex: 0
gemBlueIndex: 1
gemPurpleIndex: 2
gemRedIndex: 3
cardBackIndex: 0
greenLimitBeishu: 100
blueLimitBeishu: 200
redLimitBeishu: 500
greenCardIndex: 1
blueCardIndex: 2
purpleCardIndex: 3
redCardIndex: 4
gemstoneName: Gemstone
cardObjName: Card
cardImagePath: Card/Card
goodsPath: Card/Goods
namePath: Card/Name
numPath: Card/Num
mulPath: Card/Num/Mul
starEffectName: Effect_Gemstone_star
bombEffectName: Effect_Gemstone_Bomb
orangeEffectName: Effect_Card_Orange
purpleEffectName: Effect_Card_Purple
createEffectName: Effect_Create
tailEffectName: Effect_Tail
arriveEffectName: Effect_Arrive
explosionCenter: {x: 0, y: 0, z: 0}
@@ -59878,6 +59878,7 @@ GameObject:
m_Component:
- component: {fileID: 224855352063733292}
- component: {fileID: 222892102804854659}
- component: {fileID: 5608389863781542425}
m_Layer: 0
m_Name: Num
m_TagString: Untagged
@@ -59913,6 +59914,95 @@ CanvasRenderer:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1810759860515733}
m_CullTransparentMesh: 0
--- !u!114 &5608389863781542425
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1810759860515733}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_text:
m_isRightToLeft: 0
m_fontAsset: {fileID: 0}
m_sharedMaterial: {fileID: 0}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 4294967295
m_fontColor: {r: 1, g: 1, b: 1, a: 1}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:
topLeft: {r: 1, g: 1, b: 1, a: 1}
topRight: {r: 1, g: 1, b: 1, a: 1}
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
bottomRight: {r: 1, g: 1, b: 1, a: 1}
m_fontColorGradientPreset: {fileID: 0}
m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0
m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: 0
m_overrideHtmlColors: 0
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 28
m_fontSizeBase: 28
m_fontWeight: 400
m_enableAutoSizing: 0
m_fontSizeMin: 0
m_fontSizeMax: 0
m_fontStyle: 0
m_HorizontalAlignment: 2
m_VerticalAlignment: 512
m_textAlignment: 65535
m_characterSpacing: 0
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_enableWordWrapping: 0
m_wordWrappingRatios: 0.4
m_overflowMode: 0
m_linkedTextComponent: {fileID: 0}
parentLinkedComponent: {fileID: 0}
m_enableKerning: 0
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 0
m_isCullingEnabled: 0
m_horizontalMapping: 0
m_verticalMapping: 0
m_uvLineOffset: 0
m_geometrySortingOrder: 0
m_IsTextObjectScaleStatic: 0
m_VertexBufferAutoSizeReduction: 0
m_useMaxVisibleDescender: 1
m_pageToDisplay: 1
m_margin: {x: 0, y: 0, z: 0, w: 0}
m_isUsingLegacyAnimationComponent: 0
m_isVolumetricText: 0
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
--- !u!1 &1834793653561820
GameObject:
m_ObjectHideFlags: 0
@@ -12,7 +12,7 @@ using UnityEngine;
/// Settle: PW_Effect_Settle_FuJiaTianXia (Node子物体指认Source Image为tj_fcboss_0)
///
/// 形态二(铁拳 IronFist, weaponID=6):
/// 1. FullScreen(PW_Effect_FullScreen_IronFist): 依次激活 tap_tip → knock_tip(Source Image=Prop_Poster_IronFist) → 隐藏Tip
/// 1. FullScreen(PW_Effect_FullScreen_Symmetra): 依次激活 tap_tip → knock_tip(Source Image=Prop_Poster_IronFist) → 隐藏Tip
/// 2. Prop(PW_Prop_IronFist): 激活Hole子对象 → Hole从死亡位置移动到屏幕左下角 → 隐藏Hole → 激活Hand
/// 3. TextCrit: PW_Effect_TextCrit_Other / PW_Effect_TextCrit_Self (满屏暴击)
/// 4. AwardFrame: PW_Effect_AwardFrame_IronFist
@@ -32,9 +32,8 @@ using UnityEngine;
/// 5. AwardFrame: PW_Effect_AwardFrame_GammaStorm
/// 6. Settle: PW_Effect_Settle_GammaStorm_Other
///
/// ⭐ 注意: 美术表格中铁拳写的是"PW_Effect_FullScreen_Symmetra",但根据Lua逻辑
/// (PW_EffectMgr.CreateEffect("FullScreen", "PW_Effect_FullScreen_" .. flag))
/// 应该是 PW_Effect_FullScreen_IronFist,此处按Lua逻辑处理。
/// ⭐ 注意: 铁拳(形态二)的全屏 UI 框按美术规格用 PW_Effect_FullScreen_Symmetra,
/// 海报(Source Image) 仍设置 Prop_Poster_IronFist。
///
/// ==================== 完整流程 ====================
///
@@ -260,6 +259,23 @@ public class XingJiXunHangEvent : BJEventBase
?? root["weapon_id"]?.Value<int>()
?? 0;
// ⭐ 服务端现在用 animId 区分三种带死亡的爆奖(weaponID 可能不再下发):
// animId=2 → 形态二 铁拳 IronFist (weaponID=6)
// animId=3 → 形态三 光子 Symmetra (weaponID=8)
// animId=4 → 形态四 伽马风暴 GammaStorm (weaponID=7)
if (_weaponId == 0)
{
int animId = root["animId"]?.Value<int>() ?? 0;
switch (animId)
{
case 2: _weaponId = 6; break; // 铁拳
case 3: _weaponId = 8; break; // 光子
case 4: _weaponId = 7; break; // 伽马风暴
}
if (animId >= 2)
Debug.Log($"[XingJiXunHang] 无 weaponID,按 animId={animId} 映射为 weaponID={_weaponId}");
}
Debug.Log($"[XingJiXunHang] 解析武器 ID: weaponID={_weaponId}");
}
catch (Exception e)
@@ -288,7 +304,8 @@ public class XingJiXunHangEvent : BJEventBase
_weaponTag = "IronFist";
_settlePrefab = isSelf ? settleIronFistSelf : settleIronFistOther;
_awardFramePrefab = awardFrameIronFist;
_fullScreenPrefab = fullScreenIronFist;
// ⭐ 美术规格:铁拳的全屏 UI 框用 PW_Effect_FullScreen_Symmetra(海报仍为 Prop_Poster_IronFist)
_fullScreenPrefab = fullScreenSymmetra;
_propPrefab = propIronFist;
_posterSprite = posterIronFist;
break;
@@ -103,6 +103,16 @@ public class XingJiXunHang_BOSS : MonsterControl_BossBase
beishu = p.beishu;
if (!string.IsNullOrEmpty(p.fishId))
_cruiseFishId = p.fishId;
// ⭐ animId 优先于 isKill(服务端实际以 animId 为准):
// animId==1 → 不死(只播 BJEndUI,Boss 保留)
// animId>=2 → 带死亡的爆奖(2/3/4 三种表现)
// animId==0 → 老消息无 animId,回退用 isKill
if (p.animId >= 1)
{
isKill = p.animId >= 2;
Debug.Log($"[XingJiXunHang] animId={p.animId} → isKill={isKill}");
}
}
else
{