Files
5.24BuYu/Assets/MyGame/aa/UI/Game/XJMH/GameMainPanel_XJMH.cs
T
2026-10-08 05:01:13 +08:00

2827 lines
127 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Assets.Framework.Tool;
using Assets.MyGame.Scripts.Net;
using Assets.MyGame.Scripts.Net.API;
using Assets.MyGame.Scripts.Net.API.GameConfig_SubClass;
using Assets.MyGame.Scripts.Net.APIResponses.Enum;
using DG.Tweening;
using Spine.Unity;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using TMPro;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.UI;
/// <summary>
/// 星际迷航(XJMH)主面板:只放 XJMH 专属逻辑(炮台切换面板、金币柱、核弹道具)。
/// 通用逻辑都在父类 GameMainPanel_FishBase(三个游戏共用,不动具体实现)。
/// XJMH 专属表现移植自原版 Lua:
/// 切换面板 ← PW_SelectBatteryView(EnterAction / ExitAction / UpdateBattery / SelectBattery)
/// 金币柱 ← PW_GoldColumnMgr / PW_GoldColumn(见 GoldColumnController)
/// 道具 ← 原版只开放核弹头:按钮 + 缩放面板(面板同"道具二层"样式,条目按后端核弹数据自动生成)
/// 设置 ← 按钮 + 缩放面板(覆写父类的宽度展开动画,只影响 XJMH)
/// </summary>
public class GameMainPanel_XJMH : GameMainPanel_FishBase
{
// ==================== 炮台切换面板(按钮式) ====================
[Header("炮台切换面板")]
[Tooltip("长按 img_OpenPT 弹出的切换面板(预制体里嵌的是原版 PW_SelectBatteryView 界面)")]
public GameObject ChangePTPanel;
public Button btn_GreenPT; // 对应 xjmhTurretList[0]
public Button btn_BluePT; // 对应 xjmhTurretList[1]
public Button btn_GoldPT; // 对应 xjmhTurretList[2]
public Button btn_VIPPT; // 对应 xjmhTurretList[3]
// 按钮与列表索引的映射(顺序固定:Green=0, Blue=1, Gold=2, VIP=3)
private Button[] _ptButtons;
// ⭐ 原版 PW_SelectBatteryView 的三个滑动节点(预制体里就是原版界面结构):
// mGOContent(内容)从下方滑入滑出;mBtnUserInfo(头像)/ mGOGirl(看板娘)从左侧滑入滑出
private RectTransform _ptContent;
private RectTransform _ptUserInfo;
private RectTransform _ptGirl;
private Vector2 _ptContentHome;
private Vector2 _ptUserInfoHome;
private Vector2 _ptGirlHome;
[Header("切换面板动画(原版 EnterAction/ExitAction 参数,可在面板上实时调)")]
[Tooltip("内容/头像/看板娘的滑动距离(像素),原版 500")]
public float ptSlideDistance = 500f;
[Tooltip("滑入/滑出时长(秒),原版 0.3")]
public float ptSlideTime = 0.3f;
// ==================== 金币柱 ====================
[Tooltip("金币柱控制器:可在预制体上手动挂 GoldColumnController 并拖进来(参数可存盘、Inspector 实时可调);留空则运行时自动挂")]
public GoldColumnController goldColumnController;
private GoldColumnController _goldColumn;
[Header("金币柱位置(面板上实时调,每次出柱生效;左右座位各一套)")]
[Tooltip("左座位:首柱相对座位的 X 偏移(以此处为准,控制器组件上的同名字段会被覆盖)")]
public float goldColumnOffsetX = 150f;
[Tooltip("左座位:相邻金币柱的 X 间距")]
public float goldColumnDeltaX = 40f;
[Tooltip("左座位:金币柱 Y(相对面板底边,0=贴着底边往上长)")]
public float goldColumnOffsetY = 0f;
[Tooltip("右座位:首柱相对座位的 X 偏移(左/右按座位在面板中线的左/右判定)")]
public float goldColumnOffsetXRight = 150f;
[Tooltip("右座位:相邻金币柱的 X 间距")]
public float goldColumnDeltaXRight = 40f;
[Tooltip("右座位:金币柱 Y(相对面板底边,0=贴着底边往上长)")]
public float goldColumnOffsetYRight = 0f;
// ==================== 核弹列表(按钮 + 缩放面板) ====================
[Header("核弹列表(按钮 + 缩放面板)")]
[Tooltip("打开核弹列表的按钮(原版道具入口,与通用道具按钮 btn_Prop 分离);拖你要用的那个按钮本身即可。留空则核弹列表没有入口")]
public Button btn_NukeList;
[Tooltip("核弹列表面板:点按钮后缩放弹出,条目自动生成在它下面(做成道具二层同款:Image + HorizontalLayoutGroup + ContentSizeFitter)")]
public GameObject nukeListPanel;
[Tooltip("核弹条目模板:不拖则用父类道具二层的模板 tmp_PropTwoItem")]
public GameObject nukeItemTemplate;
// ==================== 设置列表(按钮 + 缩放面板) ====================
[Header("设置列表(按钮 + 缩放面板)")]
[Tooltip("打开设置列表的按钮;留空则沿用父类 btn_SettingList(两个按钮都可用)")]
public Button btn_SettingPopup;
[Tooltip("设置列表备用面板(一般不用拖:默认用父类的 SittingListParent 通用设置列表;仅当 SittingListParent 没挂时才用这个)")]
public GameObject settingListPanel;
// ==================== 帮助面板(原版 PW_Help:图鉴 / 操作 / 玩法) ====================
[Header("帮助面板(原版 PW_Help:图鉴/操作/玩法)")]
[Tooltip("打开帮助面板的按钮(如设置列表里的帮助项);留空则该面板没有入口")]
public Button btn_HelpPopup;
[Tooltip("帮助面板:拖 XJMH 预制体里已嵌好的 PW_Help 物体(初始会自动隐藏)")]
public GameObject helpPanel;
[Tooltip("图鉴条目模板:拖当前图鉴的 TuJianItem 条目;留空则图鉴页签只切换、不生成条目")]
public TuJianItem helpTuJianItemTemplate;
[Tooltip("操作/玩法条目模板:拖 Assets/Art/XJMH/pw/pw_prefab/item/PW_HelpItem2.prefab(内含 _textName / _textDes1~11 / _imgIcon1~5);留空则这两个页签只切换、不生成条目")]
public GameObject helpItem2Template;
// ==================== 锁定设置面板(长按锁定键打开,原版 PW_SetLock) ====================
[Header("锁定设置面板(长按锁定键打开,原版 PW_SetLock)")]
[Tooltip("原版 PW_SetLock 面板:拖 Assets/Art/XJMH/pw/pw_prefab/msgbox/PW_SetLock.prefab 的实例(初始自动隐藏);留空则长按锁定退回通用 SuoDing 面板")]
public GameObject lockSetPanel;
[Tooltip("锁定条目模板:拖 Assets/Art/XJMH/pw/pw_prefab/item/PW_SetLockItem.prefab;留空则只开关面板、不生成条目")]
public GameObject lockSetItemTemplate;
// ==================== 设置面板(原版 PW_Set:音乐/音效/震动 + 画质三档) ====================
[Header("设置面板(原版 PW_Set:音乐/音效/震动 + 画质)")]
[Tooltip("打开设置面板的按钮(如设置列表里的设置项);留空则该面板没有入口")]
public Button btn_SetPopup;
[Tooltip("设置面板:拖 XJMH 预制体里已嵌好的 PW_Set 物体(初始会自动隐藏)。\n开关与通用设置(UIHome_BY_SettingPenel)同一套存储:音乐/音效=slider 0/1、画质=GraphicsQuality")]
public GameObject setPanel;
// ==================== 退出倒计时(原版 PW_ExitView) ====================
[Header("退出倒计时(原版 PW_ExitView)")]
[Tooltip("原版 PW_ExitView 面板:拖 Assets/Art/XJMH/pw/pw_prefab/view/PW_ExitView.prefab 的实例(初始自动隐藏);留空则点退出走父类确认框流程")]
public GameObject exitViewPanel;
[Tooltip("倒计时起始秒数(原版 3:显示 3→2→1→0 后进入退出加载界面)")]
public int exitViewSeconds = 3;
[Tooltip("数字跳动放大倍数(原版 1.6)")]
public float exitBounceScale = 1.6f;
[Tooltip("跳动放大时长(秒,原版 0.05)")]
public float exitBounceUpTime = 0.05f;
[Tooltip("跳动回弹时长(秒,原版 0.3)")]
public float exitBounceBackTime = 0.3f;
[Tooltip("每拍跳动音效 id(PW_Button,原版 SoundMgr:PlaySound(Button))")]
public int exitBounceAudioID = 60009;
[Tooltip("副标题文案(原版 PW_Config.Text.SyncServerData)")]
public string exitViewTipText = "同步服务器数据中...";
// ==================== 断线重连等待提示(Hl_WaitTip) ====================
[Header("断线重连等待提示(Hl_WaitTip)")]
[Tooltip("Hl_WaitTip 面板:拖 Assets/hall/hallprefab/view/Hl_WaitTip.prefab 的实例(建议放进面板预制体最上层;自带全屏 Canvas + 转圈粒子,初始自动隐藏);留空则不提示")]
public GameObject waitTipPanel;
[Tooltip("提示文案(预制体默认「正在连接服务器...」)")]
public string waitTipMessage = "正在连接服务器...";
// ==================== 免费模式框(原版 PW_Battery_FreeFrame_L/R) ====================
[Header("免费模式框(原版 PW_Battery_FreeFrame_L/R)")]
[Tooltip("左侧免费模式框:拖 Assets/Art/XJMH/pw/pw_prefab/game/battery/PW_Battery_FreeFrame_L.prefab 的实例(建议摆在左侧座位炮台下方,初始自动隐藏)")]
public GameObject freeFrameL;
[Tooltip("右侧免费模式框:拖 PW_Battery_FreeFrame_R.prefab 实例(建议摆在右侧座位炮台下方)")]
public GameObject freeFrameR;
[Tooltip("自己进入免费模式时的滑入距离(像素,原版 200)")]
public float freeFrameSlideDistance = 200f;
[Tooltip("滑入时长(秒,原版 0.4)")]
public float freeFrameSlideInTime = 0.4f;
[Tooltip("滑出时长(秒,原版 0.3)")]
public float freeFrameSlideOutTime = 0.3f;
[Tooltip("触发免费模式的鱼号列表(默认 12025 弹药箱):击杀后按配置里该鱼的 times 秒显示免费框;\n需要其它鱼(女龙王等)往这里加鱼号即可")]
public List<string> freeModeFishNumbers = new List<string> { "12025" };
[Tooltip("配置里查不到 times 时的兜底时长(秒)")]
public float freeModeFallbackSeconds = 10f;
[Tooltip("免费模式框总开关:关闭时功能停用(不响应击杀广播、不显示);面板勾上即可恢复")]
public bool freeFrameEnabled = false;
[Header("核弹列表动画(照原版 Rocket 逐项弹出)")]
[Tooltip("相邻条目的弹出/收缩间隔(秒),原版 0.1")]
public float nukeItemStagger = 0.1f;
[Tooltip("单个条目单步缩放时长(秒),原版 0.1;展开共 4 步:0→1→压扁→拉长→回正")]
public float nukeItemPopTime = 0.1f;
[Header("设置弹窗缩放时长(照原版 BoxBase)")]
[Tooltip("设置面板展开时长(秒),原版 0.3")]
public float popupOpenTime = 0.3f;
[Tooltip("设置面板收起时长(秒),原版 0.3")]
public float popupCloseTime = 0.3f;
public override void Awake()
{
base.Awake();
InitPTPanel();
InitNukeProp();
InitSettingPopup();
InitHelpPanel();
InitLockSetPanel();
InitSetPanel();
InitExitView();
InitWaitTip();
InitFreeFrame();
}
public override void Start()
{
base.Start();
// ⭐ 关掉锁定开关 → 退出"锁定面板锁定"模式(与父类通用版语义一致):
// 下次再开锁定开关 = 正常锁定,不受锁定面板勾选约束
if (toggle_Lock != null)
{
toggle_Lock.onValueChanged.AddListener(on =>
{
if (!on) _xjLockWhiteOn = false;
});
}
}
protected override void OnShow(object args = null)
{
base.OnShow(args);
AudioManager.Instance.ChangeBG(61109);
}
// ==================== 切换面板:初始化 / 显示 / 隐藏 ====================
/// <summary>初始化切换面板:绑定按钮点击、抓原版滑动节点、默认隐藏</summary>
private void InitPTPanel()
{
_ptButtons = new Button[] { btn_GreenPT, btn_BluePT, btn_GoldPT, btn_VIPPT };
for (int i = 0; i < _ptButtons.Length; i++)
{
int idx = i; // 闭包捕获
if (_ptButtons[i] != null)
_ptButtons[i].onClick.AddListener(() => OnPTButtonClicked(idx));
}
if (ChangePTPanel == null) return;
// 原版 PW_SelectBatteryView 的滑动节点 + 原位(进出场动画用)
_ptContent = FindChildByName(ChangePTPanel.transform, "mGOContent") as RectTransform;
_ptUserInfo = FindChildByName(ChangePTPanel.transform, "mBtnUserInfo") as RectTransform;
_ptGirl = FindChildByName(ChangePTPanel.transform, "mGOGirl") as RectTransform;
if (_ptContent != null) _ptContentHome = _ptContent.anchoredPosition;
if (_ptUserInfo != null) _ptUserInfoHome = _ptUserInfo.anchoredPosition;
if (_ptGirl != null) _ptGirlHome = _ptGirl.anchoredPosition;
// ⭐ 原版整屏根节点自带"点空白关闭"按钮(新版预制体里接的是 SetActive(false) 硬关),
// 这里改接原版 ExitAction 的滑动收场,关得和原版一样
var bgBtn = ChangePTPanel.GetComponent<Button>();
if (bgBtn != null)
{
bgBtn.onClick.RemoveAllListeners();
bgBtn.onClick.AddListener(HidePTPanel);
}
ChangePTPanel.SetActive(false);
}
/// <summary>长按 img_OpenPT → 开关切换面板</summary>
protected override void OnOpenPTLongPressTriggered()
{
if (ChangePTPanel == null)
{
Debug.LogWarning("[XJMH] ChangePTPanel 未配置");
return;
}
if (ChangePTPanel.activeSelf) HidePTPanel();
else ShowPTPanel();
}
/// <summary>面板隐藏时清理(父类 OnHide 调用)</summary>
protected override void CleanupTurretPanel()
{
base.CleanupTurretPanel();
// 立即收起(不走动画,面板整体隐藏后动画也看不到),并把滑动节点复位
if (ChangePTPanel != null)
{
_ptContent?.DOKill();
_ptUserInfo?.DOKill();
_ptGirl?.DOKill();
if (_ptContent != null) _ptContent.anchoredPosition = _ptContentHome;
if (_ptUserInfo != null) _ptUserInfo.anchoredPosition = _ptUserInfoHome;
if (_ptGirl != null) _ptGirl.anchoredPosition = _ptGirlHome;
ChangePTPanel.SetActive(false);
}
_goldColumn?.Cleanup();
// 核弹列表:离场时立即收起并清动画锁(下次进场是干净状态)
_nukeAnimating = false;
for (int i = 0; i < _nukeItems.Count; i++)
if (_nukeItems[i] != null) _nukeItems[i].DOKill();
if (nukeListPanel != null)
{
nukeListPanel.transform.DOKill();
nukeListPanel.transform.localScale = Vector3.one;
nukeListPanel.SetActive(false);
}
// 帮助面板:离场立即收起
if (helpPanel != null)
{
helpPanel.transform.DOKill();
helpPanel.transform.localScale = Vector3.one;
helpPanel.SetActive(false);
}
// 锁定设置面板:离场立即收起
if (lockSetPanel != null)
{
lockSetPanel.transform.DOKill();
lockSetPanel.transform.localScale = Vector3.one;
lockSetPanel.SetActive(false);
}
// 设置面板:离场立即收起
if (setPanel != null)
{
setPanel.transform.DOKill();
setPanel.transform.localScale = Vector3.one;
setPanel.SetActive(false);
}
// 退出倒计时:离场立即终止协程并收起
if (_exitRoutine != null)
{
StopCoroutine(_exitRoutine);
_exitRoutine = null;
}
_exitViewRunning = false;
if (exitViewPanel != null)
{
exitViewPanel.transform.DOKill();
exitViewPanel.transform.localScale = Vector3.one;
exitViewPanel.SetActive(false);
}
// 重连等待提示:离场立即收起
if (waitTipPanel != null) waitTipPanel.SetActive(false);
// 免费模式框:离场立即停止倒计时、收起并清动画
if (_freeFrameTicker != null)
{
StopCoroutine(_freeFrameTicker);
_freeFrameTicker = null;
}
ResetFreeFrameView(_freeFrameL);
ResetFreeFrameView(_freeFrameR);
}
/// <summary>
/// 打开切换面板:移植原版 PW_SelectBatteryView:EnterAction ——
/// 内容从下方滑入、头像/看板娘从左侧滑入,并刷新当前炮台标记。
/// </summary>
private void ShowPTPanel()
{
// 按钮可用性:超出 xjmhTurretList 数量的按钮隐藏
var list = currSitData != null ? currSitData.xjmhTurretList : null;
int count = (list != null) ? list.Count : 0;
for (int i = 0; i < _ptButtons.Length; i++)
{
if (_ptButtons[i] == null) continue;
bool available = i < count && list[i] != null;
_ptButtons[i].gameObject.SetActive(available);
}
ChangePTPanel.SetActive(true);
ChangePTPanel.transform.localScale = Vector3.one; // 清掉旧版缩放入场可能留下的残留
// 原版 EnterAction:先摆到"屏外",再滑回原位
_ptContent?.DOKill();
_ptUserInfo?.DOKill();
_ptGirl?.DOKill();
if (_ptContent != null)
{
_ptContent.anchoredPosition = _ptContentHome + Vector2.down * ptSlideDistance;
_ptContent.DOAnchorPos(_ptContentHome, ptSlideTime).SetEase(Ease.InSine)
.OnComplete(() => { if (_ptContent != null) _ptContent.anchoredPosition = _ptContentHome; });
}
if (_ptUserInfo != null)
{
_ptUserInfo.anchoredPosition = _ptUserInfoHome + Vector2.left * ptSlideDistance;
_ptUserInfo.DOAnchorPos(_ptUserInfoHome, ptSlideTime).SetEase(Ease.InSine);
}
if (_ptGirl != null)
{
_ptGirl.anchoredPosition = _ptGirlHome + Vector2.left * ptSlideDistance;
_ptGirl.DOAnchorPos(_ptGirlHome, ptSlideTime).SetEase(Ease.InSine);
}
UpdatePTSelection();
}
/// <summary>
/// 关闭切换面板:移植原版 PW_SelectBatteryView:ExitAction ——
/// 内容向下滑出、头像/看板娘向左滑出,滑完隐藏并复位。
/// </summary>
private void HidePTPanel()
{
if (ChangePTPanel == null || !ChangePTPanel.activeSelf) return;
_ptContent?.DOKill();
_ptUserInfo?.DOKill();
_ptGirl?.DOKill();
if (_ptContent != null)
{
_ptContent.DOAnchorPos(_ptContentHome + Vector2.down * ptSlideDistance, ptSlideTime).SetEase(Ease.OutSine)
.OnComplete(() =>
{
if (_ptContent != null) _ptContent.anchoredPosition = _ptContentHome; // 复位,下次入场干净
if (ChangePTPanel != null) ChangePTPanel.SetActive(false);
});
}
else
{
ChangePTPanel.SetActive(false); // 找不到滑动节点时兜底直接关
}
if (_ptUserInfo != null)
_ptUserInfo.DOAnchorPos(_ptUserInfoHome + Vector2.left * ptSlideDistance, ptSlideTime).SetEase(Ease.InSine)
.OnComplete(() => { if (_ptUserInfo != null) _ptUserInfo.anchoredPosition = _ptUserInfoHome; });
if (_ptGirl != null)
_ptGirl.DOAnchorPos(_ptGirlHome + Vector2.left * ptSlideDistance, ptSlideTime).SetEase(Ease.OutSine)
.OnComplete(() => { if (_ptGirl != null) _ptGirl.anchoredPosition = _ptGirlHome; });
}
/// <summary>
/// 刷新"当前炮台"标记:移植原版 PW_SelectBatteryView:UpdateBattery ——
/// 选中的打勾(Tick)、骨架播 "stand01" 且取消 freeze、显示 Effect;未选中的骨架定格。
/// </summary>
private void UpdatePTSelection()
{
int cur = currSitData != null ? currSitData.currentTurretIndex : -1;
for (int i = 0; i < _ptButtons.Length; i++)
{
var btn = _ptButtons[i];
if (btn == null) continue;
bool selected = i == cur;
var tick = FindChildByName(btn.transform, "Tick");
if (tick != null) tick.gameObject.SetActive(selected);
var plane = FindChildByName(btn.transform, "Plane");
if (plane == null) continue;
var skeleton = plane.GetComponent<SkeletonGraphic>();
if (skeleton == null) skeleton = plane.GetComponentInChildren<SkeletonGraphic>(true);
if (skeleton != null && skeleton.AnimationState != null)
{
skeleton.AnimationState.SetAnimation(0, "stand01", true);
skeleton.freeze = !selected; // 原版:未选中的骨架定格
}
var effect = plane.Find("Effect");
if (effect == null) effect = FindChildByName(plane, "Effect");
if (effect != null) effect.gameObject.SetActive(selected);
}
}
/// <summary>
/// 按钮点击:按索引切换炮台。移植原版 PW_SelectBatteryView:SelectBattery ——
/// 点当前炮台只关闭面板;切换失败(越界/不可用)保持面板打开。
/// </summary>
private void OnPTButtonClicked(int index)
{
if (currSitData == null)
{
HidePTPanel();
return;
}
// 点的是当前炮台 → 只关闭
if (index == currSitData.currentTurretIndex)
{
HidePTPanel();
return;
}
int prevIndex = currSitData.currentTurretIndex;
bool success = currSitData.SwitchTurretByIndex(index);
if (!success)
{
Debug.LogWarning($"[XJMH] 切换炮台失败,索引越界或列表为空: {index}");
return;
}
// 实际发生切换才同步(XJMH 炮台没有编号,帧同步同样传列表索引)
if (currSitData.currentTurretIndex != prevIndex)
{
SyncChangeTurret(currSitData.currentTurretIndex);
UpdatePTSelection();
}
HidePTPanel();
}
// ==================== 金币柱 ====================
/// <summary>
/// 座位加金币时播金币柱(基类钩子覆写)。
/// 所有杀鱼/爆奖加分都会经此;自己座位用 Self 预制体、其他座位用 Other。
/// </summary>
public override void ShowGoldColumn(SitPosData sit, long earn)
{
if (sit == null || earn <= 0) return;
if (_goldColumn == null)
{
_goldColumn = goldColumnController != null ? goldColumnController : gameObject.AddComponent<GoldColumnController>();
_goldColumn.Init(transform as RectTransform);
}
// ⭐ 位置参数以面板上的字段为准(每次出柱前同步,Play 模式下改完立即生效)
// 左右座位各一套:控制器内按座位在面板中线的左/右(sign)自动选对应那组
_goldColumn.offsetX = goldColumnOffsetX;
_goldColumn.deltaX = goldColumnDeltaX;
_goldColumn.offsetY = goldColumnOffsetY;
_goldColumn.offsetXRight = goldColumnOffsetXRight;
_goldColumn.deltaXRight = goldColumnDeltaXRight;
_goldColumn.offsetYRight = goldColumnOffsetYRight;
bool isSelf = currSitData != null && sit == currSitData;
_goldColumn.ShowOne(sit, earn, isSelf);
}
// ==================== 道具:原版核弹列表(与通用道具分离,互不影响) ====================
/// <summary>核弹使用冷却截止时刻(Time.time),与通用道具的二层冷却一致,防连点连发</summary>
private float _nukeCooldownEnd;
/// <summary>核弹列表当前条目(与 _nukeItemCfgs 一一对应:逐项动画、刷新数量用)</summary>
private readonly List<RectTransform> _nukeItems = new List<RectTransform>();
private readonly List<PropInfConfig> _nukeItemCfgs = new List<PropInfConfig>();
/// <summary>核弹列表弹出/收起的动画锁(原版 rocketAction,动画期间不响应再点)</summary>
private bool _nukeAnimating;
/// <summary>
/// 道具入口(通用与原版分离,互不影响;不改父类):
/// - 原版核弹列表:只走面板上单独拖的 btn_NukeList(数据来自后端道具表,几个显示几个,多了自动加);
/// - 通用道具按钮 btn_Prop:保持父类 Awake 绑的通用两级道具菜单(本类不再抢绑、不清监听);
/// - 刮刮卡快捷按钮:恢复通用行为(不再屏蔽)。
/// </summary>
private void InitNukeProp()
{
if (btn_NukeList != null)
{
btn_NukeList.onClick.RemoveAllListeners();
btn_NukeList.onClick.AddListener(ToggleNukeList);
}
if (nukeListPanel != null)
nukeListPanel.SetActive(false); // 弹窗初始收起
}
/// <summary>开关核弹列表:展开/收起照原版 Rocket(逐项缩放弹跳 + 反序收缩),动画期间加锁防连点</summary>
private void ToggleNukeList()
{
if (nukeListPanel == null)
{
Debug.LogWarning("[XJMH] nukeListPanel 未配置,核弹列表无法打开");
return;
}
if (_nukeAnimating) return; // 原版:rocketAction 动画锁
if (nukeListPanel.activeSelf)
{
PlayNukeClose();
return;
}
BuildNukeList();
nukeListPanel.transform.localScale = Vector3.one; // 面板本身不做缩放,条目逐项弹出(原版 Rocket)
nukeListPanel.SetActive(true);
PlayNukeOpen();
APIRequestList.GetUser((user) =>
{
if (user != null) GameManager.Instance.UpdataUserData(user);
// 只刷新数量,不重建条目(重建会打断弹出动画)
if (nukeListPanel != null && nukeListPanel.activeSelf) RefreshNukeCounts();
});
}
/// <summary>
/// 展开动画:照原版 Rocket —— 第 i 项延迟 i×0.1s 出现,
/// 然后 0.1s 缩到 1 → 0.1s 压扁(0.9,1.1) → 0.1s 拉长(1.1,0.9) → 0.1s 回正(1,1)。
/// </summary>
private void PlayNukeOpen()
{
_nukeAnimating = true;
int pending = 0;
for (int i = 0; i < _nukeItems.Count; i++)
{
var item = _nukeItems[i];
if (item == null) continue;
pending++;
item.DOKill();
item.gameObject.SetActive(false);
var seq = DOTween.Sequence();
seq.AppendInterval(i * nukeItemStagger);
seq.AppendCallback(() =>
{
if (item == null) return;
item.gameObject.SetActive(true);
item.localScale = Vector3.zero;
});
seq.Append(item.DOScale(Vector3.one, nukeItemPopTime));
seq.Append(item.DOScale(new Vector3(0.9f, 1.1f, 1f), nukeItemPopTime));
seq.Append(item.DOScale(new Vector3(1.1f, 0.9f, 1f), nukeItemPopTime));
seq.Append(item.DOScale(Vector3.one, nukeItemPopTime));
seq.OnComplete(() =>
{
pending--;
if (pending <= 0) _nukeAnimating = false;
});
}
if (pending == 0) _nukeAnimating = false;
}
/// <summary>
/// 收起动画:照原版 Rocket 反序 —— 第 i 项延迟 (总数-1-i)×0.1s,然后 0.1s 缩到 0;
/// 全部收完隐藏整个面板。
/// </summary>
private void PlayNukeClose()
{
_nukeAnimating = true;
int total = _nukeItems.Count;
int pending = 0;
for (int i = 0; i < total; i++)
{
var item = _nukeItems[i];
if (item == null || !item.gameObject.activeSelf) continue;
pending++;
item.DOKill();
var seq = DOTween.Sequence();
seq.AppendInterval((total - 1 - i) * nukeItemStagger);
seq.Append(item.DOScale(Vector3.zero, nukeItemPopTime));
seq.OnComplete(() =>
{
if (item != null) item.gameObject.SetActive(false);
pending--;
if (pending <= 0) FinishNukeClose();
});
}
if (pending == 0) FinishNukeClose();
}
private void FinishNukeClose()
{
_nukeAnimating = false;
if (nukeListPanel != null) nukeListPanel.SetActive(false);
}
/// <summary>只刷新已有条目的数量显示(使用核弹后拉最新背包;不重建、不打断动画)</summary>
private void RefreshNukeCounts()
{
for (int i = 0; i < _nukeItems.Count; i++)
{
var item = _nukeItems[i];
var cfg = i < _nukeItemCfgs.Count ? _nukeItemCfgs[i] : null;
if (item != null && cfg != null)
SetNukeItemVisual(item.gameObject, cfg, GetOwnedCount(cfg.number));
}
}
/// <summary>
/// 重建核弹列表:条目克隆进核弹面板(做成"道具二层"同款:child0=图标+名字、child1=数量、child2=冷却圈默认隐藏),
/// 每条 = 后端道具表里 type=13(鱼雷/核弹)的启用项 —— 后端几个就显示几个(现 4 个,多了自动加)。
/// </summary>
private void BuildNukeList()
{
if (nukeListPanel == null) return;
var template = nukeItemTemplate != null ? nukeItemTemplate : tmp_PropTwoItem;
if (template == null) return;
var parent = nukeListPanel.transform;
for (int i = parent.childCount - 1; i >= 0; i--)
{
var child = parent.GetChild(i).gameObject;
// 模板自身跳过(包括"复制道具二层面板"时里面自带的那个模板),并把自带模板隐藏
if (child == template || child.name == template.name)
{
child.SetActive(false);
continue;
}
Destroy(child);
}
_nukeItems.Clear();
_nukeItemCfgs.Clear();
var nukes = GetNukeProps();
foreach (var cfg in nukes)
{
var item = Instantiate(template, parent);
item.SetActive(true);
SetNukeItemVisual(item, cfg, GetOwnedCount(cfg.number));
_nukeItems.Add(item.transform as RectTransform);
_nukeItemCfgs.Add(cfg);
var btn = item.GetComponent<Button>();
if (btn == null) btn = item.GetComponentInChildren<Button>();
if (btn != null)
{
var data = cfg; // 闭包捕获
btn.onClick.AddListener(() => UseNuke(data));
}
}
Debug.Log($"[XJMH] 核弹列表: {nukes.Count} 条");
}
/// <summary>点击核弹:冷却中拦截、数量不足提示,否则发房间协议使用(爆奖表现走 TorpedoResultNotice 广播)</summary>
private void UseNuke(PropInfConfig t)
{
if (t == null) return;
if (Time.time < _nukeCooldownEnd)
{
UIManager.Instance.ShowBoardError("道具冷却中,请稍候");
return;
}
if (GetOwnedCount(t.number) <= 0)
{
var n = string.IsNullOrEmpty(t.name) ? "核弹" : t.name;
UIManager.Instance.ShowBoardError($"[{n}] 数量不足,暂无法使用");
return;
}
_nukeCooldownEnd = Time.time + 5f;
WebSocketSendList.UseTorpedo(
GameManager.Instance.gameData.currRoomData.roomId,
GameManager.Instance.user.id,
t.number);
}
/// <summary>后端道具表里 type=13(鱼雷/核弹)的启用项(不读背包,数量为 0 的也显示)</summary>
private static List<PropInfConfig> GetNukeProps()
{
var result = new List<PropInfConfig>();
var propConfig = GameManager.Instance?.gameConfig?.propConfig;
if (propConfig == null) return result;
foreach (var t in propConfig)
{
if (t == null || t.isEnable == false) continue;
if (t.type != (int)PropEnum.鱼雷) continue;
result.Add(t);
}
return result;
}
/// <summary>背包里该编号道具的数量(未拥有返回 0)</summary>
private static int GetOwnedCount(string number)
{
var props = GameManager.Instance.user?.props;
if (props == null || string.IsNullOrEmpty(number)) return 0;
foreach (var p in props)
{
if (p != null && p.number == number) return Mathf.Max(0, p.count);
}
return 0;
}
/// <summary>
/// 核弹条目显示("道具二层"同款):child0 = 图标 + 名字,child1 = 数量,child2 = 冷却圈默认隐藏。
/// 兼容一级模板(child0 = 数量):名字跳过,数量落在 child0。
/// </summary>
private static void SetNukeItemVisual(GameObject item, PropInfConfig t, int count)
{
// child0 图标
var icon = item.transform.childCount > 0 ? item.transform.GetChild(0).GetComponent<Image>() : null;
if (icon == null) icon = item.GetComponent<Image>();
if (icon == null) icon = item.GetComponentInChildren<Image>(true);
if (icon != null) SetNukeIconSprite(icon, t);
// child0 名字(有 child1 才写名字,避免盖住一级模板的数量)
var nameTmp = item.transform.childCount > 0 ? item.transform.GetChild(0).GetComponent<TextMeshProUGUI>() : null;
if (nameTmp == null) nameTmp = item.GetComponent<TextMeshProUGUI>();
if (nameTmp == null) nameTmp = item.GetComponentInChildren<TextMeshProUGUI>(true);
bool hasCountChild = item.transform.childCount > 1;
if (nameTmp != null && hasCountChild && !string.IsNullOrEmpty(t.name))
nameTmp.text = t.name;
// child1 数量(没有 child1 时退回 child0)
var countTmp = hasCountChild ? item.transform.GetChild(1).GetComponent<TextMeshProUGUI>() : null;
if (countTmp == null) countTmp = nameTmp;
if (countTmp != null) countTmp.text = Mathf.Max(0, count).ToString();
// child2 冷却圈:默认隐藏(本代码自带 5 秒冷却拦截,不用圈)
if (item.transform.childCount > 2)
item.transform.GetChild(2).gameObject.SetActive(false);
}
/// <summary>核弹条目图标:优先用后端缓存 sprite_Icon,没有则按 imageUrl 下载回填(同通用道具二层惯例)</summary>
private static void SetNukeIconSprite(Image img, PropInfConfig t)
{
if (img == null || t == null) return;
if (t.sprite_Icon != null)
{
img.sprite = t.sprite_Icon;
return;
}
if (string.IsNullOrEmpty(t.imageUrl)) return;
WebTool.DownImage(APIManager.baseURL + t.imageUrl, (texture) =>
{
if (img == null || texture == null) return;
t.sprite_Icon = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), Vector2.one * 0.5f);
img.sprite = t.sprite_Icon;
});
}
// ==================== 设置列表(按钮 + 缩放面板,覆写父类虚方法) ====================
/// <summary>设置面板展开态尺寸(父类原来靠宽度动画展开,改缩放前先补到展开态)</summary>
private Vector2 _settingPanelOpenSize = new Vector2(319f, 161f);
/// <summary>设置列表入口:拖了 btn_SettingPopup 就额外绑一个开关(父类 btn_SettingList 的开关仍然有效)</summary>
private void InitSettingPopup()
{
if (btn_SettingPopup != null && btn_SettingPopup != btn_SettingList)
{
// ⭐ 与通用设置按钮(btn_Setting)是同一个时不绑:父类在该按钮上有"打开通用设置界面"的监听,
// 清掉监听会让通用设置界面被覆盖打不开——两者分离,互不影响
if (btn_SettingPopup == btn_Setting)
{
Debug.LogWarning("[XJMH] btn_SettingPopup 配成了通用设置按钮,已跳过绑定(保留通用设置界面入口);请换一个独立按钮");
}
else
{
btn_SettingPopup.onClick.RemoveAllListeners();
btn_SettingPopup.onClick.AddListener(ToggleSettingPopup);
}
}
if (settingListPanel != null)
settingListPanel.SetActive(false); // 弹窗初始收起
// 记录设置面板的"展开态尺寸"(预制体里可能存的是收起态宽度 0)
var panel = GetSettingPanel();
var rt = panel != null ? panel.transform as RectTransform : null;
if (rt != null)
{
_settingPanelOpenSize = rt.sizeDelta;
if (_settingPanelOpenSize.x < 1f) _settingPanelOpenSize = new Vector2(319f, 161f);
}
}
/// <summary>开关设置列表(与父类 btn_SettingList 同逻辑)</summary>
private void ToggleSettingPopup()
{
if (isOpenSetting) StartCoroutine(CloseSettingList());
else StartCoroutine(OpenSettingList());
}
/// <summary>是否是"有既有职责"的设置类按钮(通用设置 / 设置列表 / 列表备用入口 / 帮助入口)——PW_Set 不能抢绑这些</summary>
private bool IsSharedSettingButton(Button btn)
{
return btn == btn_Setting || btn == btn_SettingList ||
btn == btn_SettingPopup || btn == btn_HelpPopup;
}
/// <summary>设置列表面板:优先用父类的 SittingListParent(通用设置列表,宽度 0↔319 那个);
/// 只有它没挂时才退到单独拖的 settingListPanel(备用)</summary>
private GameObject GetSettingPanel()
{
if (SittingListParent != null) return SittingListParent.gameObject;
if (settingListPanel != null) return settingListPanel;
return null;
}
/// <summary>打开设置列表:XJMH 用缩放弹出(覆写父类的宽度展开,父类实现不动)</summary>
public override IEnumerator OpenSettingList()
{
var panel = GetSettingPanel();
if (panel != null)
{
var rt = panel.transform as RectTransform;
if (rt != null && rt.sizeDelta.x < 1f && _settingPanelOpenSize.x >= 1f)
rt.sizeDelta = _settingPanelOpenSize; // 展开态宽度兜底(父类原先用宽度动画)
OpenScalePanel(panel);
}
isOpenSetting = true;
FlipSettingButton(true);
yield break;
}
/// <summary>关闭设置列表:缩放收起(覆写父类的宽度收起)</summary>
public override IEnumerator CloseSettingList()
{
CloseScalePanel(GetSettingPanel());
isOpenSetting = false;
FlipSettingButton(false);
yield break;
}
/// <summary>
/// 设置按钮的镜像指示(与父类一致的规则):打开时 X 缩放 = |x| × -1(即 -1),收起时回到 |x|(即 +1)。
/// 父类的 btn_SettingList 和自己拖的 btn_SettingPopup 都会翻(同一个对象只翻一次)。
/// </summary>
private void FlipSettingButton(bool open)
{
FlipButtonX(btn_SettingList, open);
if (btn_SettingPopup != null && btn_SettingPopup != btn_SettingList)
FlipButtonX(btn_SettingPopup, open);
}
private static void FlipButtonX(Button btn, bool open)
{
if (btn == null) return;
var v3 = btn.transform.localScale;
v3.x = Mathf.Abs(v3.x) * (open ? -1f : 1f);
btn.transform.localScale = v3;
}
// ==================== 帮助面板(原版 PW_Help:图鉴 / 操作 / 玩法) ====================
/// <summary>帮助面板内部节点(按原版预制体名字找)</summary>
private GameObject[] _helpTabs; // mScrollView1~3(1=图鉴 2=操作 3=玩法)
private Transform _helpTuJianCol1; // mScrollView1/Viewport/Content/1
private Transform _helpTuJianCol2; // mScrollView1/Viewport/Content/2
private Transform _helpOpContent; // mScrollView2/Viewport/Content(操作条目)
private Transform _helpPlayContent; // mScrollView3/Viewport/Content(玩法条目)
private bool _helpOpBuilt; // 操作页条目已生成(静态数据,只建一次)
private bool _helpPlayBuilt; // 玩法页条目已生成
private int _helpCurTab = -1;
/// <summary>
/// 帮助面板入口:拖了 btn_HelpPopup 就能打开;面板内部节点按原版名字找(mBtnBack / mBtnItem1~3 / mScrollView1~3),
/// 预制体里"点了直接把 PW_Help 隐藏"的按钮会改接成缩放收场(同切换面板的做法)。
/// </summary>
private void InitHelpPanel()
{
if (btn_HelpPopup != null)
{
btn_HelpPopup.onClick.RemoveAllListeners();
btn_HelpPopup.onClick.AddListener(ToggleHelpPanel);
}
if (helpPanel == null) return;
helpPanel.SetActive(false); // 初始收起
_helpTabs = new GameObject[3];
for (int i = 0; i < 3; i++)
{
var tab = FindChildByName(helpPanel.transform, "mScrollView" + (i + 1));
_helpTabs[i] = tab != null ? tab.gameObject : null;
var tabNode = FindChildByName(helpPanel.transform, "mBtnItem" + (i + 1));
if (tabNode != null)
{
int idx = i + 1; // 闭包捕获
var b = tabNode.GetComponent<Button>();
if (b == null) b = tabNode.GetComponentInChildren<Button>();
if (b != null)
{
b.onClick.RemoveAllListeners();
b.onClick.AddListener(() => ChangeHelpTab(idx));
}
}
}
// 图鉴页签(view1)双列容器:mScrollView1/Viewport/Content 下的 1 / 2
var content = _helpTabs[0] != null ? FindChildByName(_helpTabs[0].transform, "Content") : null;
if (content != null)
{
_helpTuJianCol1 = content.Find("1");
_helpTuJianCol2 = content.Find("2");
}
// 操作页签(view2)/ 玩法页签(view3):mScrollView2|3/Viewport/Content
_helpOpContent = _helpTabs[1] != null ? FindChildByName(_helpTabs[1].transform, "Content") : null;
_helpPlayContent = _helpTabs[2] != null ? FindChildByName(_helpTabs[2].transform, "Content") : null;
// 返回按钮
var back = FindChildByName(helpPanel.transform, "mBtnBack");
if (back != null)
{
var b = back.GetComponent<Button>();
if (b == null) b = back.GetComponentInChildren<Button>();
if (b != null)
{
b.onClick.RemoveAllListeners();
b.onClick.AddListener(CloseHelpPanel);
}
}
// 预制体里可能还接了别的按钮"直接把 PW_Help SetActive(false)"(点背景关闭),改接缩放收场
var all = helpPanel.GetComponentsInChildren<Button>(true);
for (int i = 0; i < all.Length; i++)
{
var b = all[i];
if (b == null) continue;
for (int e = 0; e < b.onClick.GetPersistentEventCount(); e++)
{
if (b.onClick.GetPersistentTarget(e) == helpPanel)
{
b.onClick.RemoveAllListeners();
b.onClick.AddListener(CloseHelpPanel);
break;
}
}
}
}
/// <summary>开关帮助面板(缩放弹出/收起,同设置弹窗)</summary>
private void ToggleHelpPanel()
{
if (helpPanel == null)
{
Debug.LogWarning("[XJMH] helpPanel 未配置,帮助面板无法打开");
return;
}
if (helpPanel.activeSelf)
{
CloseHelpPanel();
return;
}
BuildHelpTuJian();
ChangeHelpTab(1);
OpenScalePanel(helpPanel);
}
private void CloseHelpPanel()
{
if (helpPanel == null || !helpPanel.activeSelf) return;
CloseScalePanel(helpPanel);
}
/// <summary>切换页签:1=图鉴 2=操作 3=玩法(操作/玩法首次进入时按原版生成条目)</summary>
private void ChangeHelpTab(int index)
{
if (_helpTabs == null) return;
if (_helpCurTab == index) return;
_helpCurTab = index;
for (int i = 0; i < _helpTabs.Length; i++)
{
if (_helpTabs[i] != null) _helpTabs[i].SetActive(i == index - 1);
}
// 操作(2)/ 玩法(3):照原版 PW_Help:InitItem2 / InitItem3,把 PW_Config 的数据填进 PW_HelpItem2 条目
if (index == 2 && !_helpOpBuilt)
{
_helpOpBuilt = true;
BuildHelpItem2List(_helpOpContent, HelpOperationCfg);
}
else if (index == 3 && !_helpPlayBuilt)
{
_helpPlayBuilt = true;
BuildHelpItem2List(_helpPlayContent, HelpPlayMethodCfg);
}
}
/// <summary>
/// 图鉴页签:借用当前图鉴功能的数据(CreateFishManager.spawnList 后端下发的房间鱼配置
/// + GameManager.dic_fishTypeConfigs + 图鉴条目 TuJianItem),
/// 按类别【普通→黄金→特殊→Boss→特殊Boss】排序(同类内分值从小到大)后双列交替排布(原版 PW_Help 的 1/2 两列)。
/// </summary>
private void BuildHelpTuJian()
{
if (_helpTuJianCol1 == null || _helpTuJianCol2 == null || helpTuJianItemTemplate == null) return;
ClearHelpColumn(_helpTuJianCol1);
ClearHelpColumn(_helpTuJianCol2);
var mgr = CreateFishManager.Instance;
var spawnList = mgr != null ? mgr.spawnList : null;
if (spawnList == null) return;
// 先收集(按鱼号去重),再排序,最后摆放
var added = new HashSet<string>();
var fishList = new List<HelpTuJianEntry>();
for (int i = 0; i < spawnList.Count; i++)
{
var item = spawnList[i];
if (item == null) continue;
if (item.type == 2)
{
// 鱼阵条目:解析出阵内实际生成的鱼再加入图鉴(与当前图鉴功能同逻辑)
if (mgr.FishArrayDatas != null && mgr.FishArrayDatas.TryGetValue(item.fishNumber, out var yz))
CollectHelpTuJianFish(yz.fishNumber, added, null, fishList);
}
else
{
CollectHelpTuJianFish(item.fishNumber, added, item, fishList);
}
}
// 排序:先按类别 普通→黄金→特殊→Boss→特殊Boss,同类内再按分值从小到大;OrderBy 稳定
var sorted = fishList.OrderBy(e => e.rank).ThenBy(e => e.score).ToList();
for (int i = 0; i < sorted.Count; i++)
{
var e = sorted[i];
var col = (i % 2 == 0) ? _helpTuJianCol1 : _helpTuJianCol2;
var go = Instantiate(helpTuJianItemTemplate.gameObject, col);
go.SetActive(true);
var comp = go.GetComponent<TuJianItem>();
if (comp != null) comp.InItItem(e.detail, e.isGuDingBeiShu);
}
Debug.Log($"[XJMH] 帮助-图鉴条目: {sorted.Count} 条");
}
/// <summary>图鉴排序用的一条数据</summary>
private struct HelpTuJianEntry
{
public RoomFishDetailConfig detail;
public bool isGuDingBeiShu;
public int score; // 排序用:鱼的分值(倍数)
public int rank; // 排序用:类别 普通0 → 黄金1 → 特殊2 → Boss3 → 特殊Boss4
}
/// <summary>收集一条图鉴数据(按鱼号去重、类型过滤;普通/黄金鱼=固定倍数,其余显示"最高X倍")</summary>
private static void CollectHelpTuJianFish(string fishNumber, HashSet<string> added,
RoomFishDetailConfig detail, List<HelpTuJianEntry> outList)
{
if (string.IsNullOrEmpty(fishNumber) || !added.Add(fishNumber)) return;
if (!GameManager.Instance.dic_fishTypeConfigs.TryGetValue(fishNumber, out var cfg)) return;
bool isGuDingBeiShu;
int rank;
switch ((FishTypeEnum)cfg.fishType)
{
case FishTypeEnum.普通鱼:
isGuDingBeiShu = true;
rank = 0;
break;
case FishTypeEnum.黄金鱼:
isGuDingBeiShu = true;
rank = 1;
break;
case FishTypeEnum.特殊鱼:
isGuDingBeiShu = false;
rank = 2;
break;
case FishTypeEnum.Boss鱼:
isGuDingBeiShu = false;
rank = 3;
break;
case FishTypeEnum.特殊Boss:
isGuDingBeiShu = false;
rank = 4;
break;
default:
return; // 其他类型不进图鉴(与当前图鉴功能一致)
}
if (detail == null) detail = new RoomFishDetailConfig { fishNumber = fishNumber };
outList.Add(new HelpTuJianEntry
{
detail = detail,
isGuDingBeiShu = isGuDingBeiShu,
score = (int)cfg.fishScore,
rank = rank,
});
}
/// <summary>清空图鉴列(模板自身/同名模板跳过,避免把模板删掉)</summary>
private void ClearHelpColumn(Transform col)
{
for (int i = col.childCount - 1; i >= 0; i--)
{
var child = col.GetChild(i).gameObject;
if (helpTuJianItemTemplate != null
&& (child == helpTuJianItemTemplate.gameObject || child.name == helpTuJianItemTemplate.name)) continue;
Destroy(child);
}
}
// ==================== 帮助面板:操作 / 玩法(原版 PW_HelpItem2,无独立 lua,逻辑在 PW_Help.txt:InitItem2/InitItem3) ====================
/// <summary>条目里文本的最大条数(原版 _textDes1~11)与图片的最大条数(原版 _imgIcon1~5)</summary>
private const int HelpDesMax = 11;
private const int HelpImgMax = 5;
/// <summary>原版 lua 图片根路径 → 本工程美术根路径(目录名小写:Common/Handbook/FreeModel)</summary>
private const string LuaHelpSpriteRoot = "Assets/Bundles/PW/PW_Sprite/";
private const string ArtHelpSpriteRoot = "Assets/Art/XJMH/pw/pw_sprite/";
/// <summary>已加载过的帮助图缓存(Addressables 常驻,避免切页签重复请求)</summary>
private static readonly Dictionary<string, Sprite> HelpSpriteCache = new Dictionary<string, Sprite>();
/// <summary>PW_HelpItem2 一条配置(对应原版 PW_Config 的一条 w/h/name/des/image)</summary>
private sealed class HelpItem2Cfg
{
public float w = 300f;
public float h = 398f;
public string name;
public float nameX;
public float nameY;
/// <summary>>0 时 _imgIcon1 直接读"图鉴"里这条鱼的图(FishResourcesData.fishResourcesSpritesDic,与图鉴页同一份),取不到再按 imgs 里的路径加载</summary>
public int fishNumber;
public string[] des; // 第 i 项 = _textDes{i},格式 "x|y|文本";null/越界 = 隐藏该 _textDes
public string[] imgs; // 第 i 项 = _imgIcon{i},格式 "x|y|scale|原版图片路径";null/越界 = 隐藏该 _imgIcon
}
/// <summary>生成操作/玩法条目:清空旧条目(跳过模板本身),逐条克隆模板并填充(原版 PW_Help:InitItem2/3)</summary>
private void BuildHelpItem2List(Transform content, HelpItem2Cfg[] cfgs)
{
if (content == null)
{
Debug.LogWarning("[XJMH] 帮助面板找不到 mScrollView2/3 的 Content,操作/玩法条目无法生成");
return;
}
if (helpItem2Template == null)
{
Debug.LogWarning("[XJMH] helpItem2Template 未配置(拖 PW_HelpItem2.prefab 到面板上),操作/玩法条目无法生成");
return;
}
if (cfgs == null) return;
for (int i = content.childCount - 1; i >= 0; i--)
{
var child = content.GetChild(i).gameObject;
if (child == helpItem2Template || child.name == helpItem2Template.name) continue;
Destroy(child);
}
for (int i = 0; i < cfgs.Length; i++)
{
var go = Instantiate(helpItem2Template, content);
go.SetActive(true);
ApplyHelpItem2(go, cfgs[i]);
}
Debug.Log($"[XJMH] 帮助-{(content == _helpOpContent ? "操作" : "玩法")}条目: {cfgs.Length} 条");
}
/// <summary>把一条配置填进条目:LayoutElement 尺寸、_textName、_textDes1~11、_imgIcon1~5(缺项隐藏)</summary>
private void ApplyHelpItem2(GameObject item, HelpItem2Cfg cfg)
{
if (item == null || cfg == null) return;
var le = item.GetComponent<LayoutElement>();
if (le == null) le = item.AddComponent<LayoutElement>();
le.minWidth = cfg.w;
le.minHeight = cfg.h;
var nameTf = FindChildByName(item.transform, "_textName");
if (nameTf != null) SetHelpText(nameTf, cfg.nameX, cfg.nameY, cfg.name);
for (int i = 1; i <= HelpDesMax; i++)
{
var tf = FindChildByName(item.transform, "_textDes" + i);
if (tf == null) continue;
string raw = (cfg.des != null && i <= cfg.des.Length) ? cfg.des[i - 1] : null;
if (!string.IsNullOrEmpty(raw) && TrySplitText(raw, out float x, out float y, out string text))
{
tf.gameObject.SetActive(true);
SetHelpText(tf, x, y, text);
}
else
{
tf.gameObject.SetActive(false);
}
}
for (int i = 1; i <= HelpImgMax; i++)
{
var tf = FindChildByName(item.transform, "_imgIcon" + i);
if (tf == null) continue;
string raw = (cfg.imgs != null && i <= cfg.imgs.Length) ? cfg.imgs[i - 1] : null;
if (!string.IsNullOrEmpty(raw) && TrySplitImage(raw, out float x, out float y, out float scale, out string path))
{
tf.gameObject.SetActive(true);
tf.localPosition = new Vector3(x, y, 0f);
tf.localScale = new Vector3(scale, scale, scale);
var img = tf.GetComponent<Image>();
if (img == null) img = tf.GetComponentInChildren<Image>(true);
if (img == null) continue;
// ⭐ _imgIcon1:优先读"图鉴"——直接取图鉴页同一份鱼图(进房时已按 TuJian 标签加载好),取不到再走路径加载
var tuJian = (i == 1 && cfg.fishNumber > 0) ? GetTuJianFishSprite(cfg.fishNumber) : null;
if (tuJian != null)
{
img.sprite = tuJian;
img.enabled = true;
}
else
{
LoadHelpSprite(img, path);
}
}
else
{
tf.gameObject.SetActive(false);
}
}
}
/// <summary>取"图鉴"里的鱼图(与帮助面板图鉴页同源:FishResourcesData.fishResourcesSpritesDic,按鱼号索引,进房时已加载好)</summary>
private static Sprite GetTuJianFishSprite(int fishNumber)
{
if (fishNumber <= 0) return null;
var dic = FishResourcesData.fishResourcesSpritesDic;
if (dic == null) return null;
return dic.TryGetValue(fishNumber, out var sprite) ? sprite : null;
}
/// <summary>设置条目里某个文本节点的位置与内容(帮助/锁定条目通用:先摆位置再写文本)</summary>
private static void SetHelpText(Transform tf, float x, float y, string text)
{
if (tf == null) return;
tf.localPosition = new Vector3(x, y, 0f);
SetTextContent(tf, text);
}
/// <summary>
/// 写文本内容(不动位置)。
/// ⭐ 原版 pw 预制体(PW_HelpItem2 / PW_SetLockItem)里的文本挂的是 UGUI 老版 Text
/// (原版组件,PW_CN_Font 中文字体),不是 TMP → 两种都要兼容(TMP 优先,找不到再写老版 Text)。
/// </summary>
private static void SetTextContent(Transform tf, string text)
{
if (tf == null) return;
var tmp = tf.GetComponent<TextMeshProUGUI>();
if (tmp == null) tmp = tf.GetComponentInChildren<TextMeshProUGUI>(true);
if (tmp != null)
{
tmp.text = text ?? string.Empty;
return;
}
var legacy = tf.GetComponent<Text>();
if (legacy == null) legacy = tf.GetComponentInChildren<Text>(true);
if (legacy != null) legacy.text = text ?? string.Empty;
}
/// <summary>拆 "x|y|文本"(原版 des 的 x/y 就是条目内的 localPosition)</summary>
private static bool TrySplitText(string raw, out float x, out float y, out string text)
{
x = y = 0f;
text = null;
int p1 = raw.IndexOf('|');
if (p1 < 0) return false;
int p2 = raw.IndexOf('|', p1 + 1);
if (p2 < 0) return false;
if (!float.TryParse(raw.Substring(0, p1), NumberStyles.Float, CultureInfo.InvariantCulture, out x)) return false;
if (!float.TryParse(raw.Substring(p1 + 1, p2 - p1 - 1), NumberStyles.Float, CultureInfo.InvariantCulture, out y)) return false;
text = raw.Substring(p2 + 1);
return true;
}
/// <summary>拆 "x|y|scale|原版图片路径"</summary>
private static bool TrySplitImage(string raw, out float x, out float y, out float scale, out string path)
{
x = y = 0f;
scale = 1f;
path = null;
var parts = raw.Split('|');
if (parts.Length < 4) return false;
if (!float.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out x)) return false;
if (!float.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out y)) return false;
if (!float.TryParse(parts[2], NumberStyles.Float, CultureInfo.InvariantCulture, out scale)) return false;
path = parts[3];
return true;
}
/// <summary>
/// 原版图片路径 → 本工程 Addressables 地址(不带扩展名)。
/// Assets/Bundles/PW/PW_Sprite/Common/zd_jn_sd.png → Assets/Art/XJMH/pw/pw_sprite/common/zd_jn_sd
/// (只把目录名小写,文件名保留原样;扩展名由加载时按 .png / .asset 两种尝试)
/// </summary>
private static string MapHelpSpritePath(string luaPath)
{
if (string.IsNullOrEmpty(luaPath)) return null;
string p = luaPath.Replace("\\", "/").Trim();
if (p.StartsWith(LuaHelpSpriteRoot, StringComparison.OrdinalIgnoreCase))
p = ArtHelpSpriteRoot + p.Substring(LuaHelpSpriteRoot.Length);
int slash = p.LastIndexOf('/');
int dot = p.LastIndexOf('.');
if (dot > slash) p = p.Substring(0, dot); // 去扩展名
slash = p.LastIndexOf('/');
if (slash > 0 && p.StartsWith(ArtHelpSpriteRoot, StringComparison.OrdinalIgnoreCase))
{
string dir = p.Substring(0, slash);
string file = p.Substring(slash + 1);
p = ArtHelpSpriteRoot + dir.Substring(ArtHelpSpriteRoot.Length).ToLowerInvariant() + "/" + file;
}
return p;
}
/// <summary>给 Image 加载帮助图:命中缓存直接用;否则异步加载(模板自带图先隐藏,避免闪图)</summary>
private void LoadHelpSprite(Image img, string luaPath)
{
if (img == null) return;
string path = MapHelpSpritePath(luaPath);
if (string.IsNullOrEmpty(path))
{
img.enabled = false;
return;
}
if (HelpSpriteCache.TryGetValue(path, out var cached) && cached != null)
{
img.sprite = cached;
img.enabled = true;
return;
}
img.enabled = false;
LoadHelpSpriteAsync(img, path);
}
/// <summary>异步加载帮助图(Addressables:先 .png 再 .asset),都失败则保持隐藏</summary>
private async void LoadHelpSpriteAsync(Image img, string path)
{
string[] exts = { ".png", ".asset" };
for (int i = 0; i < exts.Length; i++)
{
AsyncOperationHandle<Sprite> handle = default;
Sprite sprite = null;
try
{
handle = Addressables.LoadAssetAsync<Sprite>(path + exts[i]);
sprite = await handle.Task;
}
catch (Exception e)
{
if (handle.IsValid()) Addressables.Release(handle);
Debug.LogWarning($"[XJMH] 帮助图加载失败:{path + exts[i]} → {e.Message}");
continue;
}
if (this == null || img == null)
{
if (handle.IsValid()) Addressables.Release(handle);
return;
}
if (handle.Status == AsyncOperationStatus.Succeeded && sprite != null)
{
HelpSpriteCache[path] = sprite;
img.sprite = sprite;
img.enabled = true;
return;
}
if (handle.IsValid()) Addressables.Release(handle);
}
Debug.LogWarning($"[XJMH] 帮助图不存在(需把该图加入 Addressables 组):{path}");
}
/// <summary>操作说明(原版 PW_Config.Operation,4 条:锁定射击/自动技能/散射技能/起飞技能)</summary>
private static readonly HelpItem2Cfg[] HelpOperationCfg =
{
new HelpItem2Cfg
{
w = 270, h = 398, name = "锁定射击", nameX = -20, nameY = 185,
des = new[]
{
"5|80|<size=19>点击“ ”</size>",
"0|37|<size=19>图标, 可开启锁定</size>",
"-5|7|<size=19>功能; 开启该功能</size>",
"-8|-23|<size=19>后, 可以锁定并持</size>",
"-13|-53|<size=19>续攻击目标<color=#EEB548>长按</color></size>",
"-16|-83|<size=19><color=#EEB548>图标, 可打开锁定</color></size>",
"-51|-113|<size=19><color=#EEB548>设置界面</color></size>",
},
imgs = new[] { "26|83|0.8|Assets/Bundles/PW/PW_Sprite/Common/zd_jn_sd.png" },
},
new HelpItem2Cfg
{
w = 270, h = 398, name = "自动技能", nameX = -20, nameY = 185,
des = new[]
{
"0|80|<size=19>点击“ ”</size>",
"-5|37|<size=19>图标, 可以自动发</size>",
"-10|7|<size=19>射子弹, 但需要玩</size>",
"-15|-23|<size=19>家自己控制方向</size>",
"-20|-53|<size=19><color=#EEB548>自动操作, 射击帮</color></size>",
"-80|-83|<size=19><color=#EEB548>手</color></size>",
},
imgs = new[] { "22|83|0.8|Assets/Bundles/PW/PW_Sprite/Common/zd_jn_zd.png" },
},
new HelpItem2Cfg
{
w = 270, h = 398, name = "散射技能", nameX = -20, nameY = 185,
des = new[]
{
"5|80|<size=19>点击“ ”</size>",
"0|37|<size=19>图标, 可开启散射</size>",
"-5|7|<size=19>技能, 可以同时发</size>",
"-10|-23|<size=19>射三发子弹<color=#EEB548>三弹</color></size>",
"-20|-53|<size=19><color=#EEB548>齐发,威力无比</color></size>",
},
imgs = new[] { "27|83|0.8|Assets/Bundles/PW/PW_Sprite/Common/zd_jn_ss.png" },
},
//new HelpItem2Cfg
//{
// w = 270, h = 398, name = "起飞技能", nameX = -20, nameY = 185,
// des = new[]
// {
// "16|80|<size=19>点击炮台右侧<color=#EEB548>“起飞”</color></size>",
// "-7|37|<size=19>按钮, 玩家可以自</size>",
// "-11|7|<size=19>由切换飞机和炮台</size>",
// "-15|-23|<size=19>这两种形态<color=#EEB548>自由</color></size>",
// "-20|-53|<size=19><color=#EEB548>切形, 趣味无穷</color></size>",
// },
//},
};
/// <summary>玩法说明(原版 PW_Config.PlayMethod,12 条)</summary>
private static readonly HelpItem2Cfg[] HelpPlayMethodCfg =
{
new HelpItem2Cfg
{
w = 320, h = 398, name = "烈焰风暴", nameX = -30, nameY = 185,
fishNumber = 12034, // 图鉴:烈焰风暴
des = new[]
{
"5|80|<size=19>击毁“ ”,有概率</size>",
"-5|50|<size=19>触发<color=#EEB548>烈焰火轮攻击</color></size>",
null, // 原版无 des3
"-10|20|<size=19>共有4个火轮协助战斗</size>",
"-15|-10|<size=19>有机会获得<color=#EEB548>巨额财富</color></size>",
},
imgs = new[] { "-15|90|0.4|Assets/Bundles/PW/PW_Sprite/Handbook/tj_ts_lyfb.png" },
},
new HelpItem2Cfg
{
w = 320, h = 398, name = "超级幸运轮", nameX = -30, nameY = 185,
fishNumber = 12031, // 图鉴:幸运轮
des = new[]
{
"5|80|<size=19>击毁“ ”,有概率</size>",
"0|50|<size=19>触发特殊玩法-<color=#EEB548>幸运连连连</color>,</size>",
"-5|20|<size=19>抽中带“ ”的倍率,</size>",
"-10|-10|<size=19>则可以继续<color=#EEB548>下一轮</color>游戏,</size>",
"-15|-50|<size=19>每轮<color=#EEB548>8</color>个倍率,每个倍率</size>",
"-15|-85|<size=19>抽中概率为<color=#EEB548>12.5%</color></size>",
},
imgs = new[]
{
"-10|90|0.4|Assets/Bundles/PW/PW_Sprite/Handbook/tj_xylboss.png",
"-15|20|0.14|Assets/Bundles/PW/PW_Sprite/Handbook/ts_xyl.png",
},
},
new HelpItem2Cfg
{
w = 320, h = 398, name = "破坏球玩法", nameX = -30, nameY = 185,
fishNumber = 12030, // 图鉴:破坏球
des = new[]
{
"5|80|<size=19>击毁“ ”,可触发</size>",
"0|30|<size=19><color=#EEB548>仓鼠火炮支援</color>,全自动冲锋火</size>",
"0|0|<size=19>炮可粉碎一切威胁,火炮击毁</size>",
"-20|-30|<size=19>敌机,有概率获得</size>",
"-22|-60|<size=19><color=#EEB548>2-10倍金币奖励</color></size>",
},
imgs = new[] { "-10|90|0.4|Assets/Bundles/PW/PW_Sprite/Handbook/tj_phq.png" },
},
new HelpItem2Cfg
{
w = 320, h = 398, name = "星际巡航玩法", nameX = -30, nameY = 185,
fishNumber = 12029, // 图鉴:星际巡航
des = new[]
{
"5|80|<size=19>击毁“ ”,有概率</size>",
"0|50|<size=19>触发普通模式或高级模式</size>",
"-5|20|<size=19>普通模式:可获得<color=#EEB548>金币奖励</color></size>",
"-10|-10|<size=19>高级模式:可获得<color=#EEB548>特殊武器</color></size>",
"-15|-50|<size=19>“ ”有概率获得<color=#EEB548>双倍奖励</color></size>",
"-20|-85|<size=19>“ ”最高<color=#EEB548>5倍暴击奖励</color></size>",
"-25|-120|<size=19>“ ”高爆发大范围攻击</size>",
},
imgs = new[]
{
"-10|90|0.4|Assets/Bundles/PW/PW_Sprite/Handbook/tj_fcboss.png",
"-98|-50|0.22|Assets/Bundles/PW/PW_Sprite/Handbook/tj_ironfist.png",
"-90|-85.6|0.22|Assets/Bundles/PW/PW_Sprite/Handbook/tj_gamma.png",
"-98|-120|0.22|Assets/Bundles/PW/PW_Sprite/Handbook/tj_symmetra.png",
},
},
new HelpItem2Cfg
{
w = 320, h = 398, name = "机械蝎玩法", nameX = -30, nameY = 185,
fishNumber = 12028, // 图鉴:机械蝎
des = new[]
{
"-5|80|<size=19>击毁“ ”, 可触发</size>",
"0|50|<size=19>神秘十连抽,十连抽可获得</size>",
"-5|20|<size=19><color=#EEB548>金币、专家级武器碎片、</color></size>",
"-10|-10|<size=19><color=#EEB548>核弹头</color>奖励</size>",
"-15|-35|<size=19>卡牌等级由低到高分别为</size>",
"-20|-60|<size=19><color=#EEB548>绿、蓝、紫、红</color></size>",
"-25|-85|<size=19>级别越高, 奖励越丰厚</size>",
"-15|-110|<size=19>概率: 绿卡30%、蓝卡40%、</size>",
"-25|-135|<size=19>紫卡20%、红卡10%</size>",
},
imgs = new[] { "-24|90|0.4|Assets/Bundles/PW/PW_Sprite/Handbook/tj_xzboss.png" },
},
new HelpItem2Cfg
{
w = 320, h = 398, name = "小丑扑克玩法", nameX = -30, nameY = 185,
fishNumber = 12023, // 图鉴:小丑扑克
des = new[]
{
"-8|80|<size=19>击毁“ ”, 可触发</size>",
"-28|50|<size=19>特殊玩法-翻牌大奖</size>",
"-19.4|20|<size=19><color=#EEB548>最高可获得1,600倍奖励</color></size>",
"-14.5|-10|<size=19><color=#EEB548>牌型: </color>散牌10倍,对子30倍</size>",
"-47|-35|<size=19>顺子50倍,同花80倍</size>",
"-28.85|-60|<size=19><color=#EEB548>同花顺100倍,豹子200倍</color></size>",
"-16|-85|<size=19><color=#EEB548>金牌:1张2倍,2张4倍,3张8倍</color></size>",
"-66.7|-110|<size=19>最终的金币计算: </size>",
"-21|-135|<size=19><color=#EEB548>炮台倍数x牌型奖励x金牌奖励</color></size>",
},
imgs = new[] { "-24|90|0.4|Assets/Bundles/PW/PW_Sprite/Handbook/tj_ts_xcqq.png" },
},
new HelpItem2Cfg
{
w = 320, h = 398, name = "幸运转盘玩法", nameX = -30, nameY = 185,
fishNumber = 12022, // 图鉴:幸运飞碟
des = new[]
{
"-4.85|80|<size=19>击毁“ ”, 可触发</size>",
"-26|50|<size=19>特殊玩法-幸运转盘</size>",
"-18.75|20|<size=19><color=#EEB548>最高可获得1,500倍奖励</color></size>",
"-32.7|-10|<size=19>内圈最高倍数: <color=#EEB548>500倍</color></size>",
"-49|-40|<size=19>外圈最高倍数: <color=#EEB548>3倍</color></size>",
"-63.9|-70|<size=19>最终的金币计算:</size>",
"-12.8|-100|<size=19><color=#EEB548>炮台倍数x内圈倍数x外圈倍数</color></size>",
},
imgs = new[] { "-21|90|0.4|Assets/Bundles/PW/PW_Sprite/Handbook/tj_ts_zp.png" },
},
//new HelpItem2Cfg
//{
// w = 310, h = 398, name = "免费模式玩法", nameX = -30, nameY = 185,
// fishNumber = 12025, // 图鉴:弹药箱
// des = new[]
// {
// "0|80|<size=19>击毁“ ”, 可获得</size>",
// "-10.2|55|<size=19><color=#EEB548>8~12秒</color>免费模式时间; </size>",
// "-7.8|1.9|<size=19>击毁“ ”, 可获得</size>",
// "-18.3825|-24.4|<size=19>3组免费子弹, 共<color=#EEB548>24~36</color></size>",
// "-49.3|-50.9|<size=19><color=#EEB548>秒</color>免费模式时间; </size>",
// "-63.7|-78.2|<size=19>免费模式期间: </size>",
// "-13.2|-104.56|<size=19>1.发射速度比普通模式高1倍</size>",
// "-47.7|-129|<size=19>2.无法调整炮台倍数</size>",
// },
// imgs = new[]
// {
// "-13|92|0.4|Assets/Bundles/PW/PW_Sprite/Handbook/tj_ts_jzx.png",
// "-22.2|13.3|0.4|Assets/Bundles/PW/PW_Sprite/Handbook/tj_flboss.png",
// },
//},
//new HelpItem2Cfg
//{
// w = 300, h = 398, name = "分裂属性: ", nameX = -30, nameY = 185,
// des = new[]
// {
// "6.7|89.29|<size=19>分裂: 击中目标后, <color=#EEB548>有概率</color></size>",
// "4.720001|61.7|<size=19><color=#EEB548>分裂出多发子弹</color>, 攻击附近</size>",
// "-80.7|32.2|<size=19>敌机</size>",
// "-5.8|-2.92|<size=19>1级分裂: <color=#EEB548>8%概率</color>触发分裂</size>",
// "-10.8|-32.13|<size=19>2级分裂(大对子): <color=#EEB548>20%</color></size>",
// "-62.8|-57.64|<size=19><color=#EEB548>概率</color>触发分裂</size>",
// "-15.19|-83.6|<size=19>3级分裂(大三元): <color=#EEB548>50%</color></size>",
// "-68.1|-108.7|<size=19><color=#EEB548>概率</color>触发分裂</size>",
// },
// imgs = new[] { "93.53|187.5|0.6|Assets/Bundles/PW/PW_Sprite/FreeModel/PW_FreeItem_1.png" },
//},
//new HelpItem2Cfg
//{
// w = 300, h = 398, name = "连电属性: ", nameX = -30, nameY = 185,
// des = new[]
// {
// "6.7|89.29|<size=19>连电: 击中目标后, <color=#EEB548>有概率</color></size>",
// "4.720001|61.7|<size=19><color=#EEB548>触发连电效果</color>, 攻击屏幕内</size>",
// "-80.7|32.2|<size=19>敌机</size>",
// "-5.8|-2.92|<size=19>1级连电: <color=#EEB548>8%概率</color>触发连电</size>",
// "-10.8|-32.13|<size=19>2级连电(大对子): <color=#EEB548>20%</color></size>",
// "-62.8|-57.64|<size=19><color=#EEB548>概率</color>触发连电</size>",
// "-15.19|-83.6|<size=19>3级连电(大三元): <color=#EEB548>50%</color></size>",
// "-68.1|-108.7|<size=19><color=#EEB548>概率</color>触发连电</size>",
// },
// imgs = new[] { "93.53|187.5|0.6|Assets/Bundles/PW/PW_Sprite/FreeModel/PW_FreeItem_2.png" },
//},
//new HelpItem2Cfg
//{
// w = 300, h = 398, name = "暴击属性: ", nameX = -30, nameY = 185,
// des = new[]
// {
// "6.699997|89.29|<size=19>暴击: 击毁目标, 有概率</size>",
// "-18.6|61.7|<size=19>触发<color=#EEB548>多倍奖励暴击</color></size>",
// "3.8|31.2|<size=19>1级暴击: <color=#EEB548>20%概率</color>触发<color=#EEB548>2倍</color></size>",
// "-69.16|7.1|<size=19><color=#EEB548>奖励暴击</color></size>",
// "-4.4|-21.84|<size=19>2级暴击(大对子): <color=#EEB548>40%</color></size>",
// "-17.1|-46.6|<size=19><color=#EEB548>概率</color>触发<color=#EEB548>2~3倍奖励暴击</color></size>",
// "-11.2|-78.6|<size=19>3级暴击(大三元): <color=#EEB548>80%</color></size>",
// "-23.7|-105.1|<size=19>概率触发<color=#EEB548>3~5倍奖励暴击</color></size>",
// },
// imgs = new[] { "93.53|187.5|0.6|Assets/Bundles/PW/PW_Sprite/FreeModel/PW_FreeItem_3.png" },
//},
//new HelpItem2Cfg
//{
// w = 300, h = 398, name = "加速属性: ", nameX = -30, nameY = 185,
// des = new[]
// {
// "6.699997|89.29|<size=19>加速: <color=#EEB548>提升免费模式期间</color></size>",
// "-22.89|61.7|<size=19>发射子弹的速度</size>",
// "-7.4|27.11|<size=19>1级加速: 攻速<color=#EEB548>提升20%</color></size>",
// "-2.94|-3.8|<size=19>2级加速(大对子): 攻速</size>",
// "-72.68|-30.1|<size=19><color=#EEB548>提升80%</color></size>",
// "-6.68|-56.7|<size=19>3级加速(大三元): 攻速</size>",
// "-70.52|-80.08|<size=19><color=#EEB548>提升300%</color></size>",
// },
// imgs = new[] { "93.53|187.5|0.6|Assets/Bundles/PW/PW_Sprite/FreeModel/PW_FreeItem_4.png" },
//},
};
// ==================== 免费模式框(原版 PW_Battery_FreeFrame_L/R) ====================
// 移植自原版 PW_Battery.txt(ShowFreeFrame / HideFreeFrame / FreeModeUpdate):
// - 组成:FreeImage(免费图标)+ FreeTime/Time(倒计时,一位小数)+ Frame(自己才有:buff 位 + Num)
// - ⭐ 当前状态:功能已关(freeFrameEnabled=false)——不响应击杀广播、不显示;面板勾上开关即可恢复
// - 触发(口径已定):击杀 freeModeFishNumbers 里的鱼(默认 12025 弹药箱)→ 该座位显示免费框,
// 时长 = 配置里该鱼的 times(出鱼表的"持续事件/生命周期"秒数,直接从 spawnDic 读;查不到用兜底值)
// - 自己(isSelf):先收起 FreeImage/FreeTime,框从屏外侧滑入(原版 0.4s),落位后才显示;
// 倒计时走完滑出(原版 0.3s)后隐藏;其它玩家:立即显示/直接隐藏,不显示 Frame
// - 左右两侧各一份(两人场):左边座位用 L、右边用 R,可同时显示互不影响
// - 倒计时每帧走表(原版 FreeModeUpdate 的 "%.1f"),走完自动收起
// - 原版其它玩家的 FreeImage/FreeTime 会吸附到炮台 bottom/FreeImagePos、FreeTimePos 锚点;
// 新炮台没有这两个锚点,这里直接用预制体摆放位置(拖框时摆到对应座位炮台下方即可)
// - 击杀广播来自 CreateFishManager.OnFishKilled(新增的静态事件,监听方自行过滤鱼号)
/// <summary>一侧免费模式框的节点与初始位置(左右各一份)</summary>
private sealed class XjmhFreeFrameView
{
public GameObject root; // PW_Battery_FreeFrame_L / R
public RectTransform rootRt;
public Vector2 home; // 预制体摆放位置(滑入落点 / 滑出起点)
public GameObject freeImage; // FreeImage
public GameObject freeTime; // FreeTime
public Transform timeText; // FreeTime/Time(倒计时文本)
public GameObject frame; // Frame(自己才有:buff 图标列表 + Num)
public bool selfShown; // 本次显示是否走的"自己"(决定收起时是否滑出)
public bool counting; // 是否在倒计时
public float remaining; // 剩余秒数
}
private XjmhFreeFrameView _freeFrameL;
private XjmhFreeFrameView _freeFrameR;
private Coroutine _freeFrameTicker; // 倒计时协程(任一侧在计时时运行)
/// <summary>免费模式框初始化:抓节点、记录原位、初始隐藏、订阅击杀广播</summary>
private void InitFreeFrame()
{
_freeFrameL = BuildFreeFrameView(freeFrameL);
_freeFrameR = BuildFreeFrameView(freeFrameR);
CreateFishManager.OnFishKilled -= OnFishKilledHandler; // 防重复订阅
CreateFishManager.OnFishKilled += OnFishKilledHandler;
}
private XjmhFreeFrameView BuildFreeFrameView(GameObject root)
{
if (root == null) return null;
var v = new XjmhFreeFrameView
{
root = root,
rootRt = root.transform as RectTransform,
freeImage = FindChildByName(root.transform, "FreeImage")?.gameObject,
freeTime = FindChildByName(root.transform, "FreeTime")?.gameObject,
timeText = FindChildByName(root.transform, "Time"),
frame = FindChildByName(root.transform, "Frame")?.gameObject,
};
if (v.rootRt != null) v.home = v.rootRt.anchoredPosition;
root.SetActive(false); // 初始隐藏(原版 freeFrame:SetActive(false))
return v;
}
/// <summary>
/// 显示免费模式框(原版 Battery:ShowFreeFrame)并启动倒计时:
/// 自己 → Frame 显示 + 从屏外滑入、落位后才显示 FreeImage/FreeTime;其它玩家 → 立即显示、不显示 Frame。
/// seconds &lt;= 0 时用 freeModeFallbackSeconds。
/// </summary>
public void ShowFreeFrame(bool isLeft, bool isSelf, float seconds = 0f)
{
if (!freeFrameEnabled) return; // ⭐ 功能已关(freeFrameEnabled=false)
var v = isLeft ? _freeFrameL : _freeFrameR;
if (v == null || v.root == null) return;
if (seconds <= 0f) seconds = freeModeFallbackSeconds;
bool alreadyShowing = v.root.activeSelf;
v.selfShown = isSelf;
v.counting = true;
v.remaining = seconds;
v.root.SetActive(true);
if (v.frame != null) v.frame.SetActive(isSelf); // 原版:只有自己显示 Frame
if (isSelf && v.frame != null)
SetTextContent(FindChildByName(v.frame.transform, "Num"), "1"); // 原版=#buffList;新设计无 buff 组,占位 1
if (v.timeText != null) SetTextContent(v.timeText, seconds.ToString("F1")); // 初值(原版 %.1f)
if (isSelf && v.rootRt != null && !alreadyShowing)
{
// 原版:先收起 FreeImage/FreeTime,从屏外滑入,onEnd 才 ActiveFreeTimeAndImagePos
SetFreeFrameInfoActive(v, false);
var rt = v.rootRt;
rt.DOKill();
rt.anchoredPosition = new Vector2(v.home.x + (isLeft ? -freeFrameSlideDistance : freeFrameSlideDistance), v.home.y);
rt.DOAnchorPos(v.home, freeFrameSlideInTime)
.OnComplete(() => SetFreeFrameInfoActive(v, true));
}
else
{
SetFreeFrameInfoActive(v, true);
}
// 启动倒计时(任一侧在计时时运行同一协程)
if (_freeFrameTicker == null) _freeFrameTicker = StartCoroutine(FreeFrameTickRoutine());
}
/// <summary>隐藏免费模式框(两侧都收起;原版 Battery:HideFreeFrame:自己先滑出再隐藏,其它玩家直接隐藏)</summary>
public void HideFreeFrame()
{
HideFreeFrameSide(_freeFrameL);
HideFreeFrameSide(_freeFrameR);
}
/// <summary>收起指定侧的框(自己滑出后隐藏、其它侧直接隐藏)</summary>
private void HideFreeFrameSide(XjmhFreeFrameView v)
{
if (v == null || v.root == null || !v.root.activeSelf) return;
v.counting = false;
SetFreeFrameInfoActive(v, false);
if (v.frame != null) v.frame.SetActive(false);
if (v.selfShown && v.rootRt != null)
{
var rt = v.rootRt;
rt.DOKill();
float outX = v.home.x + ((v == _freeFrameL) ? -freeFrameSlideDistance : freeFrameSlideDistance);
rt.DOAnchorPos(new Vector2(outX, v.home.y), freeFrameSlideOutTime)
.OnComplete(() => { if (v.root != null) v.root.SetActive(false); });
}
else
{
if (v.rootRt != null) v.rootRt.DOKill();
v.root.SetActive(false);
}
v.selfShown = false;
}
/// <summary>击杀广播:命中 freeModeFishNumbers 的鱼 → 该座位按鱼的 times 秒显示免费框</summary>
private void OnFishKilledHandler(MonsterContral monster, SitPosData sit)
{
if (!freeFrameEnabled) return; // ⭐ 功能已关(freeFrameEnabled=false)
if (monster == null || sit == null) return;
if (freeModeFishNumbers == null || freeModeFishNumbers.Count == 0) return;
string fishNumber = monster.monsterData?.fishTypeConfig?.fishNumber;
if (string.IsNullOrEmpty(fishNumber) || !freeModeFishNumbers.Contains(fishNumber)) return;
int times = FindFreeModeFishTimes(fishNumber); // 配置里该鱼的 times(持续事件/生命周期,秒)
float seconds = times > 0 ? times : freeModeFallbackSeconds;
ShowFreeFrame(IsSeatLeft(sit), sit == currSitData, seconds);
}
/// <summary>从当前出鱼配置里查该鱼的 times("持续事件"时长);查不到返回 0</summary>
private static int FindFreeModeFishTimes(string fishNumber)
{
var mgr = CreateFishManager.Instance;
var dic = mgr != null ? mgr.spawnDic : null;
if (dic != null && dic.TryGetValue(fishNumber, out var cfg) && cfg != null && cfg.times > 0)
return cfg.times;
return 0;
}
/// <summary>判断座位在屏幕哪一侧(两人场:比较两个座位的 X,更靠左的算左;单人场按座号兜底)</summary>
private bool IsSeatLeft(SitPosData sit)
{
if (sit == null) return true;
float x = sit.transform.position.x;
for (int i = 0; i < list_Sits.Count; i++)
{
var other = list_Sits[i];
if (other == null || other == sit) continue;
if (other.user == null || !other.gameObject.activeSelf) continue;
return x <= other.transform.position.x;
}
return sit.posID <= 0;
}
/// <summary>倒计时:每帧走表,减完自动收起该侧(原版 FreeModeUpdate)</summary>
private IEnumerator FreeFrameTickRoutine()
{
while (true)
{
yield return null;
bool anyCounting = false;
if (TickFreeFrameSide(_freeFrameL, Time.deltaTime)) anyCounting = true;
if (TickFreeFrameSide(_freeFrameR, Time.deltaTime)) anyCounting = true;
if (!anyCounting) break;
}
_freeFrameTicker = null;
}
private bool TickFreeFrameSide(XjmhFreeFrameView v, float dt)
{
if (v == null || !v.counting) return false;
v.remaining -= dt;
if (v.remaining <= 0f)
{
v.remaining = 0f;
HideFreeFrameSide(v); // 倒计时结束 → 收起
return false;
}
if (v.timeText != null) SetTextContent(v.timeText, v.remaining.ToString("F1"));
return true;
}
/// <summary>buff 图标数量(原版 Frame/Num = #freeBuffList;两侧都写)</summary>
public void SetFreeFrameBuffCount(int count)
{
string str = count.ToString();
if (_freeFrameL != null && _freeFrameL.frame != null)
SetTextContent(FindChildByName(_freeFrameL.frame.transform, "Num"), str);
if (_freeFrameR != null && _freeFrameR.frame != null)
SetTextContent(FindChildByName(_freeFrameR.frame.transform, "Num"), str);
}
/// <summary>FreeImage/FreeTime 显隐(原版 ActiveFreeTimeAndImagePos / Deactive)</summary>
private static void SetFreeFrameInfoActive(XjmhFreeFrameView v, bool active)
{
if (v.freeImage != null) v.freeImage.SetActive(active);
if (v.freeTime != null) v.freeTime.SetActive(active);
}
/// <summary>立即复位(离场清理:停动画、回原位、全部隐藏)</summary>
private static void ResetFreeFrameView(XjmhFreeFrameView v)
{
if (v == null || v.root == null) return;
if (v.rootRt != null)
{
v.rootRt.DOKill();
v.rootRt.anchoredPosition = v.home;
}
SetFreeFrameInfoActive(v, false);
if (v.frame != null) v.frame.SetActive(false);
v.selfShown = false;
v.counting = false;
v.remaining = 0f;
v.root.SetActive(false);
}
// ==================== 断线重连等待提示(Hl_WaitTip) ====================
// socket 断线自动重连期间显示"正在连接服务器..."全屏等待提示(原版重连等待的等价物):
// - 重连状态来自 WebSocketManager.OnReconnectingChanged(事件在重连协程的开始/结束点统一广播,
// 成功、取消(登出/主动关闭)都会收到 false,不会卡提示)
// - 任意 socket 在重连 → 显示;全部恢复 → 收起
// - Hl_WaitTip 预制体自带全屏 Canvas(遮罩挡点击)+ 粒子转圈 + message 文案,直接 SetActive 显隐
private readonly HashSet<SocketType> _reconnectingTypes = new HashSet<SocketType>(); // 当前在重连的 socket
/// <summary>
/// 重连提示初始化:写文案、初始隐藏、订阅 socket 重连事件。
/// 未拖 waitTipPanel 时也保持订阅(只维护集合不显示),不影响其它功能。
/// </summary>
private void InitWaitTip()
{
if (waitTipPanel != null)
{
waitTipPanel.SetActive(false); // 初始隐藏
SetTextContent(FindChildByName(waitTipPanel.transform, "message"), waitTipMessage);
}
WebSocketManager.OnReconnectingChanged -= OnSocketReconnectingChanged; // 防重复订阅
WebSocketManager.OnReconnectingChanged += OnSocketReconnectingChanged;
}
/// <summary>socket 重连状态变化:有任意 socket 在重连 → 显示等待提示;全部恢复 → 收起</summary>
private void OnSocketReconnectingChanged(SocketType type, bool reconnecting)
{
if (reconnecting) _reconnectingTypes.Add(type);
else _reconnectingTypes.Remove(type);
bool show = _reconnectingTypes.Count > 0;
if (waitTipPanel != null && waitTipPanel.activeSelf != show)
waitTipPanel.SetActive(show);
}
/// <summary>注销事件(父类 OnDestroy 里有自己的清理,照常链式调用)</summary>
public override void OnDestroy()
{
WebSocketManager.OnReconnectingChanged -= OnSocketReconnectingChanged;
CreateFishManager.OnFishKilled -= OnFishKilledHandler;
base.OnDestroy();
}
// ==================== 退出倒计时(原版 PW_ExitView) ====================
// 移植自原版 PW_ExitView.txt + PW_GameView:Click_Back(原版点退出无确认框,直接进倒计时):
// - 点退出 → 显示 PW_ExitView 全屏倒计时:3 → 2 → 1 → 0,每拍数字跳动(1.6→1)+ 按钮音效,
// 副标题"同步服务器数据中..."(PW_Config.Text.SyncServerData)
// - 数字走完(原版 time < 0 才回调)→ 收起倒计时,进入父类现有退出加载流程(RequestLeaveRoom:
// 进度条卸载 → 回大厅)
// - 原版还有"系统返还金币"展示(GainNotify source=2 + mGORemainValue 淡入);
// 本项目协议暂无该数据源,返还区保持隐藏(文案已按原版写好,后续有数据再接)
// - ⭐ 父类给退出按钮绑的是确认框流程(StartLeaveCountdown);这里仅从 XJMH 侧重绑成原版倒计时,
// 没拖 exitViewPanel 时保持父类确认框流程不变(父类代码零改动)
// - ⭐ 新旧退出入口(只改子类):
// ① 父类退出按钮 btn_TuiChu(拖了才生效)→ 直接进原版倒计时;
// ② 通用设置列表里的"离开"项(SittingListParent 下按名字找,无需拖引用)
// → 先弹退出选择面板(obj_DJSPanel:退出 / 返回游戏),
// 点"退出"再按正常退出流程走原版倒计时,点"返回游戏"只收起面板
private Transform _exitTimeText; // mTextTime(倒计时数字)
private GameObject _exitRemainRoot; // mGORemainValue(返还区,原版默认隐藏)
private Coroutine _exitRoutine;
private bool _exitViewRunning; // 倒计时中(防重复点击)
/// <summary>返还区说明文案(照原版 PW_Config.Text.RemainValue)</summary>
private const string ExitRemainLabel = "系统返还特殊事件以及特殊怪折算的金币";
/// <summary>
/// 退出倒计时初始化(照原版 PW_ExitView:InitUI/InitView 的节点与文案),
/// 并把"新旧退出入口"统一改绑到倒计时(只改子类):
/// ① 父类退出按钮 btn_TuiChu(未拖时为null,跳过);② 通用设置列表里的"离开"项(按名字查找)。
/// </summary>
private void InitExitView()
{
if (exitViewPanel == null) return;
if (btn_TuiChu != null)
{
btn_TuiChu.onClick.RemoveAllListeners();
btn_TuiChu.onClick.AddListener(OnXjmhExitClicked);
}
// ⭐ 通用设置列表里的"离开"项(Setting .prefab 子物体,名字叫"离开"):
// 先弹退出选择面板(退出 / 返回游戏),不直接进倒计时
var leaveEntry = SittingListParent != null ? FindButton(SittingListParent, "离开") : null;
if (leaveEntry != null && leaveEntry != btn_TuiChu)
{
leaveEntry.onClick.RemoveAllListeners();
leaveEntry.onClick.AddListener(OpenXjmhExitChoose);
}
exitViewPanel.SetActive(false); // 初始收起
// 背景遮挡:倒计时期间挡住后面的打鱼输入(同其它全屏面板)
var rootImg = exitViewPanel.GetComponent<Image>();
if (rootImg != null) rootImg.raycastTarget = true;
_exitTimeText = FindChildByName(exitViewPanel.transform, "mTextTime");
_exitRemainRoot = FindChildByName(exitViewPanel.transform, "mGORemainValue")?.gameObject;
// 固定文案(照原版 InitView)
SetTextContent(_exitTimeText, exitViewSeconds.ToString());
SetTextContent(FindChildByName(exitViewPanel.transform, "mTextTips"), exitViewTipText);
SetTextContent(FindChildByName(_exitRemainRoot != null ? _exitRemainRoot.transform : null, "mTextRemain"), ExitRemainLabel);
if (_exitRemainRoot != null) _exitRemainRoot.SetActive(false); // 本项目暂无返还数据源
}
/// <summary>退出按钮(原版 PW_GameView:Click_Back:直接进倒计时)</summary>
private void OnXjmhExitClicked()
{
if (_exitViewRunning || exitViewPanel == null) return; // 倒计时中防重复
_exitViewRunning = true;
exitViewPanel.SetActive(true);
_exitRoutine = StartCoroutine(ExitViewRoutine());
}
/// <summary>
/// 离开(设置列表项):先弹出退出选择面板(obj_DJSPanel),
/// 点"退出"(btn_QueRen)→ 收起选择面板并按正常退出流程走原版倒计时(OnXjmhExitClicked);
/// 点"返回游戏"(btn_QuXiao)→ 只收起选择面板,继续游戏。未拖选择面板时退化为直接倒计时。
/// </summary>
private void OpenXjmhExitChoose()
{
if (_exitViewRunning) return;
if (obj_DJSPanel == null)
{
OnXjmhExitClicked(); // 兜底:没有选择面板就直接走倒计时
return;
}
if (obj_DJSPanel.activeSelf) return; // 选择面板已弹出
obj_DJSPanel.SetActive(true);
// "退出":收起选择面板 → 正常退出流程(原版倒计时 → 退出加载界面)
if (btn_QueRen != null)
{
btn_QueRen.onClick.RemoveAllListeners();
btn_QueRen.onClick.AddListener(() =>
{
if (obj_DJSPanel != null) obj_DJSPanel.SetActive(false);
OnXjmhExitClicked();
});
}
// "返回游戏":只收起选择面板
if (btn_QuXiao != null)
{
btn_QuXiao.onClick.RemoveAllListeners();
btn_QuXiao.onClick.AddListener(() =>
{
if (obj_DJSPanel != null) obj_DJSPanel.SetActive(false);
});
}
}
/// <summary>
/// 倒计时(照原版 InitView + StartTimer):先显示 3,之后每 1 秒 -1(2、1、0),每拍跳动;
/// 数字走完(原版 time &lt; 0)→ 收起面板并进入退出加载界面(父类 RequestLeaveRoom)。
/// </summary>
private IEnumerator ExitViewRoutine()
{
int t = exitViewSeconds; // 原版 3
SetExitTimeText(t);
BounceExitTime();
while (true)
{
yield return new WaitForSeconds(1f);
t--;
if (t < 0) break; // 原版:time < 0 才回调
SetExitTimeText(t);
BounceExitTime();
}
_exitRoutine = null;
_exitViewRunning = false;
if (exitViewPanel != null) exitViewPanel.SetActive(false);
RequestLeaveRoom(); // 父类现有离房流程(进度条卸载 = 退出加载界面 → 回大厅)
}
private void SetExitTimeText(int value)
{
SetTextContent(_exitTimeText, value.ToString());
}
/// <summary>数字跳动(照原版 BounceNum:放大 1.6 → 回弹到 1,带按钮音效)</summary>
private void BounceExitTime()
{
if (_exitTimeText != null)
{
var tr = _exitTimeText;
tr.DOKill();
tr.localScale = Vector3.one;
tr.DOScale(Vector3.one * exitBounceScale, exitBounceUpTime)
.OnComplete(() => tr.DOScale(Vector3.one, exitBounceBackTime));
}
AudioManager.Instance.PlayerFX(exitBounceAudioID);
}
// ==================== 设置面板(原版 PW_Set,与通用设置 UIHome_BY_SettingPenel 同一套存储) ====================
// 移植自原版 PW_Set.txt(面板 UI/开关逻辑照原版):
// - 音乐开关 ↔ 通用设置音乐滑条:PlayerPrefs "slider_AudioBG" 0/1 + 混音器 BGVolume(同款落地)
// - 音效开关 ↔ 通用设置音效滑条:PlayerPrefs "slider_FX" 0/1 + 混音器 FXVolume
// - 震动开关:原版 key "PW_OpenShake"(项目暂无震动接口,先只记状态)
// - 画质三档(低帧/流畅/华丽)↔ 通用设置画质:PlayerPrefs "GraphicsQuality" 0/1/2 + QualitySettings
// - 开/关两个按钮互斥显示(原版 SetActive(not open)/(open)),返回键缩放收起
/// <summary>画质三档文字(照原版 PW_Config.Text.LowPower1~3)</summary>
private static readonly string[] XjmhLowPowerTexts = { "低帧", "流畅", "华丽" };
private Button _setBtnCloseMusic, _setBtnOpenMusic;
private Button _setBtnCloseEffect, _setBtnOpenEffect;
private Button _setBtnCloseShake, _setBtnOpenShake;
private readonly Transform[] _setPowerTexts = new Transform[3]; // RightSwitch/1~3/Text
private readonly GameObject[] _setPowerOpens = new GameObject[3]; // RightSwitch/1~3/mBtnOpenPower(选中指示)
/// <summary>
/// 设置面板初始化:绑入口按钮、抓内部节点(照原版 PW_Set:InitUI)、初始隐藏。
/// 未拖面板时全部跳过(不影响其它功能)。
/// </summary>
private void InitSetPanel()
{
// ⭐ 入口按钮:与既有设置类入口(通用设置/设置列表/帮助)是同一个按钮时不绑——
// 这些按钮上有父类的监听(如 btn_Setting → 打开通用设置界面),清掉监听会互相覆盖;
// PW_Set 需要独立入口(建议用设置列表里的"设置"项按钮)
if (btn_SetPopup != null)
{
if (IsSharedSettingButton(btn_SetPopup))
{
Debug.LogWarning("[XJMH] btn_SetPopup 配成了通用设置/设置列表等既有按钮,已跳过 PW_Set 绑定(两者互不影响);请拖一个独立按钮");
}
else
{
btn_SetPopup.onClick.RemoveAllListeners();
btn_SetPopup.onClick.AddListener(ToggleSetPanel);
}
}
if (setPanel == null) return;
setPanel.SetActive(false); // 初始收起
// 背景遮挡(同锁定面板:原版打开时 SetCanShoot(false) 的等价做法,避免点到后面的打鱼输入)
var rootImg = setPanel.GetComponent<Image>();
if (rootImg != null) rootImg.raycastTarget = true;
// 返回(照原版 mBtnBack:收起;原版同时恢复射击,本项目由面板整体收起等价)
var back = FindButton(setPanel.transform, "mBtnBack");
if (back != null)
{
back.onClick.RemoveAllListeners();
back.onClick.AddListener(() => CloseScalePanel(setPanel));
}
// 音乐 / 音效 / 震动(LeftSwitch 下开/关两个按钮,照原版 InitUI 的 AddClick)
_setBtnCloseMusic = FindButton(setPanel.transform, "mBtnCloseMusic");
_setBtnOpenMusic = FindButton(setPanel.transform, "mBtnOpenMusic");
_setBtnCloseEffect = FindButton(setPanel.transform, "mBtnCloseEffect");
_setBtnOpenEffect = FindButton(setPanel.transform, "mBtnOpenEffect");
_setBtnCloseShake = FindButton(setPanel.transform, "mBtnCloseShake");
_setBtnOpenShake = FindButton(setPanel.transform, "mBtnOpenShake");
BindSetToggle(_setBtnCloseMusic, ToggleMusic);
BindSetToggle(_setBtnOpenMusic, ToggleMusic);
BindSetToggle(_setBtnCloseEffect, ToggleEffect);
BindSetToggle(_setBtnOpenEffect, ToggleEffect);
BindSetToggle(_setBtnCloseShake, ToggleShake);
BindSetToggle(_setBtnOpenShake, ToggleShake);
// 按钮文字(照原版 InitView:Close="关"、Open="开")
SetButtonLabel(_setBtnCloseMusic, "关");
SetButtonLabel(_setBtnOpenMusic, "开");
SetButtonLabel(_setBtnCloseEffect, "关");
SetButtonLabel(_setBtnOpenEffect, "开");
SetButtonLabel(_setBtnCloseShake, "关");
SetButtonLabel(_setBtnOpenShake, "开");
// 画质三档(RightSwitch/1~3,照原版只做 1~3;节点 4 预制体里本来就是关的)
var rightSwitch = FindChildByName(setPanel.transform, "RightSwitch");
for (int i = 0; i < 3; i++)
{
var node = rightSwitch != null ? rightSwitch.Find((i + 1).ToString()) : null;
_setPowerTexts[i] = FindChildByName(node, "Text");
_setPowerOpens[i] = FindChildByName(node, "mBtnOpenPower")?.gameObject;
var btn = FindButton(node, "mBtnClosePower"); // 原版点击球就叫 mBtnClosePower
if (btn != null)
{
int level = i + 1; // 原版档位 1-based
btn.onClick.RemoveAllListeners();
btn.onClick.AddListener(() => ChangeLowPower(level));
}
}
}
/// <summary>开关设置面板(入口在设置列表里)</summary>
private void ToggleSetPanel()
{
if (setPanel == null)
{
Debug.LogWarning("[XJMH] setPanel 未配置,设置面板无法打开");
return;
}
if (setPanel.activeSelf)
{
CloseScalePanel(setPanel);
}
else
{
RefreshSetPanelState(); // 照原版 InitView:打开时按存储刷新开关状态
OpenScalePanel(setPanel);
}
}
/// <summary>
/// 按当前存储刷新开关显示(照原版 InitView)——读写与通用设置面板同一套:
/// 音乐/音效 = PlayerPrefs float "slider_AudioBG"/"slider_FX"(0=关 其余=开),画质 = int "GraphicsQuality"(0/1/2)。
/// </summary>
private void RefreshSetPanelState()
{
bool musicOpen = PlayerPrefs.GetFloat("slider_AudioBG", 1f) != 0f;
bool effectOpen = PlayerPrefs.GetFloat("slider_FX", 1f) != 0f;
bool shakeOpen = PlayerPrefs.GetInt("PW_OpenShake", 0) == 1;
SwapSetSwitch(_setBtnCloseMusic, _setBtnOpenMusic, musicOpen);
SwapSetSwitch(_setBtnCloseEffect, _setBtnOpenEffect, effectOpen);
SwapSetSwitch(_setBtnCloseShake, _setBtnOpenShake, shakeOpen);
int quality = PlayerPrefs.GetInt("GraphicsQuality", 1);
for (int i = 0; i < 3; i++)
{
SetTextContent(_setPowerTexts[i], XjmhLowPowerTexts[i]);
if (_setPowerOpens[i] != null) _setPowerOpens[i].SetActive(i == quality);
}
}
/// <summary>开/关按钮互斥显示(照原版 SetActive(not open)/(open))</summary>
private static void SwapSetSwitch(Button closeBtn, Button openBtn, bool open)
{
if (closeBtn != null) closeBtn.gameObject.SetActive(!open);
if (openBtn != null) openBtn.gameObject.SetActive(open);
}
/// <summary>音乐开关(照原版 ChangeMusic:存 0/1 + 混音器落地,与通用设置滑条同款)</summary>
private void ToggleMusic()
{
bool open = PlayerPrefs.GetFloat("slider_AudioBG", 1f) == 0f; // 当前关 → 点后开
PlayerPrefs.SetFloat("slider_AudioBG", open ? 1f : 0f);
PlayerPrefs.Save();
ApplyMixerVolume("BGVolume", open ? 1f : 0f);
RefreshSetPanelState();
}
/// <summary>音效开关(照原版 ChangeEffect)</summary>
private void ToggleEffect()
{
bool open = PlayerPrefs.GetFloat("slider_FX", 1f) == 0f;
PlayerPrefs.SetFloat("slider_FX", open ? 1f : 0f);
PlayerPrefs.Save();
ApplyMixerVolume("FXVolume", open ? 1f : 0f);
RefreshSetPanelState();
}
/// <summary>震动开关(照原版 ChangeShake:存 "PW_OpenShake";项目暂无震动接口,先只记状态)</summary>
private void ToggleShake()
{
bool open = PlayerPrefs.GetInt("PW_OpenShake", 0) != 1;
PlayerPrefs.SetInt("PW_OpenShake", open ? 1 : 0);
PlayerPrefs.Save();
RefreshSetPanelState();
}
/// <summary>切画质档(照原版 ChangeLowPower):原版档 1/2/3 ↔ 通用画质 0/1/2</summary>
private void ChangeLowPower(int level)
{
int quality = Mathf.Clamp(level - 1, 0, 2);
if (PlayerPrefs.GetInt("GraphicsQuality", 1) != quality)
{
PlayerPrefs.SetInt("GraphicsQuality", quality);
PlayerPrefs.Save();
// 与通用设置面板同款落地:applyExpensiveChanges 必须为 false(URP 下立即切换会重建管线,可能闪退)
QualitySettings.SetQualityLevel(quality, false);
}
RefreshSetPanelState();
}
/// <summary>与通用设置面板同款落地:0/1 线性值转分贝写进 AudioMixer</summary>
private static void ApplyMixerVolume(string mixerParam, float value)
{
float dB = 20f * Mathf.Log10(Mathf.Max(value, 0.0001f));
AudioManager.Instance.audioMixer.SetFloat(mixerParam, dB);
}
/// <summary>绑开关按钮的点击(照原版 AddClick)</summary>
private static void BindSetToggle(Button btn, UnityEngine.Events.UnityAction action)
{
if (btn == null) return;
btn.onClick.RemoveAllListeners();
btn.onClick.AddListener(action);
}
/// <summary>写按钮上的 "Text" 子节点文字</summary>
private static void SetButtonLabel(Button btn, string text)
{
if (btn == null) return;
SetTextContent(FindChildByName(btn.transform, "Text"), text);
}
// ==================== 锁定设置面板(原版 PW_SetLock,长按锁定键打开) ====================
// 面板 UI/勾选/全选/返回应用逻辑照原版 PW_SetLock.txt;
// 列表数据改为与"帮助-图鉴"同一份数据源动态读取:
// - 房间出鱼(spawnList + 鱼阵解析 + 去重)→ 图鉴分类过滤 → 去掉普通鱼(保留黄金/特殊/Boss/特殊Boss)
// - 奇数进"1"行、偶数进"2"行,两行横向排列
// - 点条目切换勾选(_choose 标记);mBtnAll 全选 / mBtnNone 全不选(mGOAllFlag/mGONoneFlag 指示)
// - 关闭(mBtnBack)时应用:有勾选 → 开锁定 + 激活自动锁定,且之后的自动锁定只锁勾选的鱼
// - 白名单通过覆写父类 IsFishAllowedBySuoDing 提供,父类通用流程不动
// - 鱼图与图鉴页同源:直接取 FishResourcesData.fishResourcesSpritesDic(不需要额外注册)
/// <summary>锁定条目配置(从图鉴数据源动态收集,每次开面板重建)</summary>
private sealed class XjmhLockCfg
{
public string name; // 仅日志用
public string fishNumber;
public string score; // 展示文本(服务端 fishMultipleDesc,缺省按"最高X倍"拼接)
}
/// <summary>原版 PW_Config.Text.SetLockDes(面板底部说明)</summary>
private const string LockSetDesText = "<color=#EEB548>勾选锁定敌机, 设定锁定范围</color>勾选敌机将会自动锁定勾选的敌机进行射击; 若不勾选, 则需手动点击敌机进行预订射击自动开启后, <color=#EEB548>点击敌机切换锁定目标</color>";
/// <summary>锁定面板内部节点(按原版 PW_SetLock 预制体名字找)</summary>
private Button _lockBtnBack; // mBtnBack(关闭并应用)
private Button _lockBtnAll; // mBtnAll(全选)
private Button _lockBtnNone; // mBtnNone(全不选)
private GameObject _lockAllFlag; // mGOAllFlag(全选指示)
private GameObject _lockNoneFlag; // mGONoneFlag(全不选指示)
private Transform _lockRow1; // ScrollView/Viewport/Content/1(横向第一行)
private Transform _lockRow2; // ScrollView/Viewport/Content/2(横向第二行)
private RectTransform _lockContent; // ScrollView/Viewport/Content(宽度按实际内容贴合,不留可滑动空白)
/// <summary>已生成条目的勾选标记(全选/全不选/单点后统一刷新用)</summary>
private sealed class LockSetItemRef
{
public string key; // fishNumber 字符串(与锁定白名单判断一致)
public GameObject choose; // _choose 标记
}
private readonly List<LockSetItemRef> _lockSetItems = new List<LockSetItemRef>();
/// <summary>勾选的锁定鱼号(对应原版 LockEnemyList,跨开关面板保留;空 = 不限制)</summary>
private readonly HashSet<string> _xjLockSelected = new HashSet<string>();
/// <summary>是否处于"锁定面板白名单"模式(面板勾选后开启;关掉锁定开关即退出,与通用版语义一致)</summary>
private bool _xjLockWhiteOn;
/// <summary>
/// 锁定面板初始化:抓内部节点、绑按钮(照原版 PW_SetLock:InitUI)、写固定文案。
/// 未拖面板时全部跳过(长按退回父类通用 SuoDing 面板,玩法不受影响)。
/// </summary>
private void InitLockSetPanel()
{
if (lockSetPanel == null) return;
lockSetPanel.SetActive(false); // 初始收起
// 背景遮挡:原版打开时禁用射击(SetCanShoot(false)),本项目等价做法 = 让面板根图挡射线,
// 避免调锁定时点到后面的打鱼输入
var rootImg = lockSetPanel.GetComponent<Image>();
if (rootImg != null) rootImg.raycastTarget = true;
// 返回(照原版:关闭即保存并应用勾选)
_lockBtnBack = FindButton(lockSetPanel.transform, "mBtnBack");
if (_lockBtnBack != null)
{
_lockBtnBack.onClick.RemoveAllListeners();
_lockBtnBack.onClick.AddListener(OnLockSetBackClicked);
}
// 全选 / 全不选
_lockBtnAll = FindButton(lockSetPanel.transform, "mBtnAll");
if (_lockBtnAll != null)
{
_lockBtnAll.onClick.RemoveAllListeners();
_lockBtnAll.onClick.AddListener(SelectAllLockItems);
}
_lockBtnNone = FindButton(lockSetPanel.transform, "mBtnNone");
if (_lockBtnNone != null)
{
_lockBtnNone.onClick.RemoveAllListeners();
_lockBtnNone.onClick.AddListener(SelectNoneLockItems);
}
// 两行条目容器(ScrollView/Viewport/Content 下的 1 / 2)
var content = FindChildByName(lockSetPanel.transform, "Content");
_lockContent = content as RectTransform;
_lockRow1 = content != null ? content.Find("1") : null;
_lockRow2 = content != null ? content.Find("2") : null;
_lockAllFlag = FindChildByName(lockSetPanel.transform, "mGOAllFlag")?.gameObject;
_lockNoneFlag = FindChildByName(lockSetPanel.transform, "mGONoneFlag")?.gameObject;
// 固定文案(照原版 InitView)
SetTextContent(FindChildByName(lockSetPanel.transform, "mTextDes"), LockSetDesText);
SetTextContent(_lockBtnAll != null ? FindChildByName(_lockBtnAll.transform, "Text") : null, "全选");
SetTextContent(_lockBtnNone != null ? FindChildByName(_lockBtnNone.transform, "Text") : null, "全不选");
}
/// <summary>按名字找节点上的按钮(节点上没有就往子节点找)</summary>
private static Button FindButton(Transform root, string name)
{
var tf = FindChildByName(root, name);
if (tf == null) return null;
var btn = tf.GetComponent<Button>();
return btn != null ? btn : tf.GetComponentInChildren<Button>(true);
}
/// <summary>
/// ⭐ 覆写父类:XJMH 长按锁定键打开原版 PW_SetLock 面板。
/// (星际迷航的可锁定敌机是固定的一批 boss/特殊鱼 —— 原版 canAutoLock 配置,
/// 而不是通用版按房间出鱼列表;通用版 SuoDing 不适用这里。)
/// 未拖面板时退回父类通用实现。
/// </summary>
protected override void OpenLockSettingPanel()
{
if (lockSetPanel == null)
{
base.OpenLockSettingPanel();
return;
}
if (lockSetPanel.activeSelf) return;
BuildLockSetList();
OpenScalePanel(lockSetPanel);
}
/// <summary>
/// 生成锁定条目(照原版 PW_SetLock:InitUI):
/// 列表与"帮助-图鉴"同一份数据源(房间出鱼 → 图鉴分类,去掉普通鱼),
/// 第 i 条按奇偶交替放进 1/2 两行,条目 = 克隆 PW_SetLockItem 模板后填图、分值、标记;
/// 列表宽度按实际内容贴合(FitLockContentWidth),不留可滑动的空白。
/// </summary>
private void BuildLockSetList()
{
if (_lockRow1 == null || _lockRow2 == null || lockSetItemTemplate == null)
{
Debug.LogWarning("[XJMH] 锁定面板缺少行容器(1/2)或条目模板,条目无法生成");
return;
}
ClearLockRow(_lockRow1);
ClearLockRow(_lockRow2);
_lockSetItems.Clear();
var cfgs = CollectLockFishCfgs();
for (int i = 0; i < cfgs.Count; i++)
{
var cfg = cfgs[i];
var row = (i % 2 == 0) ? _lockRow1 : _lockRow2; // 原版 1-based 奇数进"1",这里 0-based 偶数进"1",等价
var go = Instantiate(lockSetItemTemplate, row);
go.SetActive(true);
FillLockSetItem(go, cfg);
_lockSetItems.Add(new LockSetItemRef
{
key = cfg.fishNumber,
choose = FindChildByName(go.transform, "_choose")?.gameObject,
});
}
// 列表宽度按实际内容适配(原版公式固定加 60 空白 → 能滑到空处;这里改成刚好贴合)
FitLockContentWidth(cfgs.Count);
RefreshLockSetChecks();
Debug.Log($"[XJMH] 锁定面板条目: {cfgs.Count} 条(图鉴同源)");
}
/// <summary>
/// 收集锁定条目(与"帮助-图鉴"同一份数据源):
/// 房间出鱼(spawnList + 鱼阵解析 + 按鱼号去重)→ 图鉴分类过滤;
/// 去掉普通鱼(保留 黄金/特殊/Boss/特殊Boss,与通用锁定面板 SuoDing 口径一致);
/// 排序两层:①锁定优先级(特殊Boss→Boss→特殊→黄金);②同类按"最大爆奖倍数"从大到小,
/// 倍数取 item 上显示的数字(优先 fishMultipleDesc,回退 fishScore),与通用锁定面板 SuoDing 完全一致)。
/// </summary>
private List<XjmhLockCfg> CollectLockFishCfgs()
{
var result = new List<XjmhLockCfg>();
var mgr = CreateFishManager.Instance;
var spawnList = mgr != null ? mgr.spawnList : null;
if (spawnList == null) return result;
// ① 与图鉴完全相同的收集(含鱼阵解析、去重、鱼类型过滤)
var added = new HashSet<string>();
var entries = new List<HelpTuJianEntry>();
for (int i = 0; i < spawnList.Count; i++)
{
var item = spawnList[i];
if (item == null) continue;
if (item.type == 2)
{
if (mgr.FishArrayDatas != null && mgr.FishArrayDatas.TryGetValue(item.fishNumber, out var yz))
CollectHelpTuJianFish(yz.fishNumber, added, null, entries);
}
else
{
CollectHelpTuJianFish(item.fishNumber, added, item, entries);
}
}
// ② 两层排序 → 过滤普通鱼(rank 0) → 生成展示文本(与图鉴条目同口径)
// ⭐ 第一层:锁定优先级 特殊Boss > Boss鱼 > 特殊鱼 > 黄金鱼
// ⭐ 第二层:同类内按"最大爆奖倍数"从大到小
// 倍数取 item 上显示的那个数字(优先 fishMultipleDesc,回退 fishScore),
// 与通用锁定面板 SuoDing 共用 SuoDingPanel.GetShowMultiple,保证两块面板顺序一致
var ordered = new List<(HelpTuJianEntry entry, double multiple)>();
foreach (var e in entries)
{
if (e.rank == 0) continue;
string fn0 = e.detail != null ? e.detail.fishNumber : null;
if (string.IsNullOrEmpty(fn0)) continue;
if (!GameManager.Instance.dic_fishTypeConfigs.TryGetValue(fn0, out var cfg0)) continue;
ordered.Add((e, SuoDingPanel.GetShowMultiple(cfg0.fishMultipleDesc, cfg0.fishScore)));
}
ordered.Sort((a, b) =>
{
int r = b.entry.rank.CompareTo(a.entry.rank); // 特殊Boss(4) 排最前
if (r != 0) return r;
return b.multiple.CompareTo(a.multiple); // ⭐ 最大爆奖倍数从大到小
});
foreach (var (e, _) in ordered)
{
string fn = e.detail != null ? e.detail.fishNumber : null;
if (string.IsNullOrEmpty(fn)) continue;
if (!GameManager.Instance.dic_fishTypeConfigs.TryGetValue(fn, out var fishCfg)) continue;
string scoreStr;
if (!string.IsNullOrEmpty(fishCfg.fishMultipleDesc)) scoreStr = fishCfg.fishMultipleDesc;
else if (e.isGuDingBeiShu) scoreStr = $"{fishCfg.fishScore}倍";
else scoreStr = $"最高{fishCfg.fishScore}倍";
result.Add(new XjmhLockCfg { name = fishCfg.fishName, fishNumber = fn, score = scoreStr });
}
return result;
}
/// <summary>
/// 把列表 content 宽度贴合实际内容(修复"能滑到空白"):
/// content 锚点横向拉伸(宽 = 视口宽 + sizeDelta.x),所以取 sizeDelta.x = max(0, 两行实际宽度 - 视口宽):
/// 条目装得下 → 0(完全不可滑);装不下 → 刚好能滑到最后一条,右侧不留空白。
/// </summary>
private void FitLockContentWidth(int itemCount)
{
if (_lockContent == null) return;
var viewport = _lockContent.parent as RectTransform; // ScrollView/Viewport
float viewportW = viewport != null ? viewport.rect.width : 0f;
// ⭐ 行内条目数直接按本次生成条数算(0-based 偶数进"1"行)——不要用 row.childCount:
// ClearLockRow 的 Destroy 是延迟销毁,当帧 childCount 会把旧条目也算进去 → 每次打开越算越宽
int row1Count = (itemCount + 1) / 2;
int row2Count = itemCount / 2;
float needW = Mathf.Max(GetLockRowRightEdge(_lockRow1, row1Count), GetLockRowRightEdge(_lockRow2, row2Count));
float extra = Mathf.Max(0f, needW - viewportW);
_lockContent.sizeDelta = new Vector2(extra, _lockContent.sizeDelta.y);
}
/// <summary>一行条目实际用到的最右边缘(相对 content 左边;按 GridLayoutGroup 参数算,不依赖层级/布局刷新时机)</summary>
private static float GetLockRowRightEdge(Transform row, int itemCount)
{
var rt = row as RectTransform;
if (rt == null || itemCount <= 0) return 0f;
var grid = row.GetComponent<GridLayoutGroup>();
float width = grid != null
? grid.padding.left + grid.padding.right + itemCount * grid.cellSize.x + Mathf.Max(0, itemCount - 1) * grid.spacing.x
: rt.rect.width;
return rt.anchoredPosition.x + width; // 行 pivot 在左边,锚点即左边缘
}
/// <summary>
/// 清空一行旧条目。
/// ⭐ 只按"引用相等"跳过模板(模板一般是拖进来的 prefab 资源,不在行节点里);
/// 不能按名字跳过——克隆条目可能和模板同名,会被误跳过导致每次打开越攒越多。
/// </summary>
private void ClearLockRow(Transform row)
{
for (int i = row.childCount - 1; i >= 0; i--)
{
var child = row.GetChild(i).gameObject;
if (lockSetItemTemplate != null && child == lockSetItemTemplate) continue;
Destroy(child);
}
}
/// <summary>填一条锁定条目(照原版:_imgIcon/_textScore/_flag/_choose/_bonus;点击切换勾选)</summary>
private void FillLockSetItem(GameObject item, XjmhLockCfg cfg)
{
if (item == null || cfg == null) return;
// 图标:读"图鉴"同款鱼图(进房时已按 TuJian 标签加载好;没有就保持隐藏)
var iconTf = FindChildByName(item.transform, "_imgIcon");
if (iconTf != null)
{
var img = iconTf.GetComponent<Image>();
if (img == null) img = iconTf.GetComponentInChildren<Image>(true);
var sprite = int.TryParse(cfg.fishNumber, out int num) ? GetTuJianFishSprite(num) : null;
if (img != null && sprite != null)
{
img.sprite = sprite;
img.enabled = true;
}
}
// 分值文本(与图鉴条目同口径:服务端 fishMultipleDesc,缺省按"最高X倍"拼接)
SetTextContent(FindChildByName(item.transform, "_textScore"), cfg.score);
// 特殊标记(原版 setActive(v.special);列表里的鱼都非普通鱼,统一显示)
var flagTf = FindChildByName(item.transform, "_flag");
if (flagTf != null) flagTf.gameObject.SetActive(true);
// 奖金飞机标记:原版锁定的敌人没有 isBonus,恒隐藏
var bonusTf = FindChildByName(item.transform, "_bonus");
if (bonusTf != null) bonusTf.gameObject.SetActive(false);
// 勾选标记(保留上次勾选结果)
var chooseTf = FindChildByName(item.transform, "_choose");
if (chooseTf != null) chooseTf.gameObject.SetActive(_xjLockSelected.Contains(cfg.fishNumber));
// 点击切换勾选(照原版 AddClick(helpItem))
var btn = item.GetComponent<Button>();
if (btn == null) btn = item.GetComponentInChildren<Button>(true);
if (btn != null)
{
string key = cfg.fishNumber;
btn.onClick.RemoveAllListeners();
btn.onClick.AddListener(() =>
{
if (_xjLockSelected.Contains(key)) _xjLockSelected.Remove(key);
else _xjLockSelected.Add(key);
RefreshLockSetChecks();
});
}
}
/// <summary>刷新所有条目的勾选标记 + 全选/全不选指示(照原版 CheckLock)</summary>
private void RefreshLockSetChecks()
{
for (int i = 0; i < _lockSetItems.Count; i++)
{
var it = _lockSetItems[i];
if (it.choose != null) it.choose.SetActive(_xjLockSelected.Contains(it.key));
}
RefreshLockSetFlags();
}
/// <summary>全选/全不选指示:一个没勾 → mGONoneFlag;全勾 → mGOAllFlag(照原版 CheckLock)</summary>
private void RefreshLockSetFlags()
{
int sel = CountLockSelected();
if (_lockNoneFlag != null) _lockNoneFlag.SetActive(sel == 0);
if (_lockAllFlag != null) _lockAllFlag.SetActive(_lockSetItems.Count > 0 && sel == _lockSetItems.Count);
}
/// <summary>当前勾选条数(只数面板里存在的条目,与全选指示联动)</summary>
private int CountLockSelected()
{
int sel = 0;
for (int i = 0; i < _lockSetItems.Count; i++)
if (_xjLockSelected.Contains(_lockSetItems[i].key)) sel++;
return sel;
}
/// <summary>全选:勾上列表里的所有敌人(照原版 mBtnAll)</summary>
private void SelectAllLockItems()
{
for (int i = 0; i < _lockSetItems.Count; i++)
_xjLockSelected.Add(_lockSetItems[i].key);
RefreshLockSetChecks();
}
/// <summary>全不选:清空勾选(照原版 mBtnNone)</summary>
private void SelectNoneLockItems()
{
_xjLockSelected.Clear();
RefreshLockSetChecks();
}
/// <summary>
/// 返回键(照原版 mBtnBack):收起面板并应用勾选——
/// 有勾选 → 开锁定 + 激活自动锁定(之后自动锁定只锁勾选的鱼);没勾 → 正常锁定不限制。
/// </summary>
private void OnLockSetBackClicked()
{
int sel = CountLockSelected();
CloseScalePanel(lockSetPanel);
// 与通用 SuoDing 面板同一套应用流程(开锁定开关 + 激活自动锁定);
// 勾选结果是否限制锁定,由 XJMH 自己的白名单标记 + 覆写的 IsFishAllowedBySuoDing 决定
_xjLockWhiteOn = sel > 0;
ApplySuoDingSelection();
}
/// <summary>
/// ⭐ 覆写父类白名单:XJMH 用原版 PW_SetLock 面板的勾选列表(而不是通用 SuoDingPanel)。
/// 语义与父类一致:只有"锁定面板勾选过至少一条"才进入白名单模式,之后自动锁定只允许锁勾选的鱼;
/// 关掉锁定开关即退出该模式;未进入该模式 / 一个都没勾 → 不限制(正常锁定)。
/// </summary>
protected override bool IsFishAllowedBySuoDing(MonsterContral m)
{
if (!_xjLockWhiteOn) return true;
if (m == null) return false;
if (_xjLockSelected.Count == 0) return true;
return _xjLockSelected.Contains(m.monsterData?.fishTypeConfig?.fishNumber);
}
/// <summary>
/// ⭐ 覆写父类:XJMH 用的是原版 PW_SetLock 面板,没有通用 SuoDingPanel。
/// 一条都没勾选 → 关闭面板时不开启锁定功能(父类 ApplySuoDingSelection 会直接返回)。
/// </summary>
protected override bool HasSuoDingSelection() => _xjLockWhiteOn;
/// <summary>
/// ⭐ 覆写父类:XJMH 的锁定规则与通用玩法不同(不自动换目标):
/// - 直接点锁定(手动点鱼)→ 不激活自动换目标:锁定的鱼死亡/消失后就空着,等玩家重新点;
/// - 图鉴锁定面板(PW_SetLock)勾选后返回 → 走 ApplySuoDingSelection 的显式激活路径,
/// 自动锁定照常工作(鱼死后在勾选的白名单里自动换目标)。
/// </summary>
protected override bool AutoLockOnManualLock => false;
// ==================== 缩放弹窗工具 ====================
/// <summary>缩放弹出:面板激活 + 从 0 放大到 1(OutBack,轻微回弹)</summary>
private void OpenScalePanel(GameObject panel)
{
if (panel == null) return;
panel.SetActive(true);
var t = panel.transform;
t.DOKill();
t.localScale = Vector3.zero;
t.DOScale(Vector3.one, popupOpenTime).SetEase(Ease.OutBack);
}
/// <summary>缩放收起:从 1 缩到 0 后隐藏(已在隐藏态则跳过)</summary>
private void CloseScalePanel(GameObject panel)
{
if (panel == null || !panel.activeSelf) return;
var t = panel.transform;
t.DOKill();
t.DOScale(Vector3.zero, popupCloseTime).SetEase(Ease.InBack)
.OnComplete(() => { if (panel != null) panel.SetActive(false); });
}
// ==================== 工具 ====================
/// <summary>递归按名字查找子物体(兼容未激活节点)</summary>
private static Transform FindChildByName(Transform parent, string name)
{
if (parent == null || string.IsNullOrEmpty(name)) return null;
if (parent.name == name) return parent;
for (int i = 0; i < parent.childCount; i++)
{
var found = FindChildByName(parent.GetChild(i), name);
if (found != null) return found;
}
return null;
}
}