2950 lines
134 KiB
C#
2950 lines
134 KiB
C#
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;
|
||
|
||
[Header("炮台解锁限制(索引=按钮顺序 0绿/1蓝/2金/3VIP;非VIP只能用第一个)")]
|
||
[Tooltip("每个炮台所需贵族(VIP)等级,0=不限制。当前规则:只要是 VIP(≥1),后面三个都能用")]
|
||
public int[] ptUnlockVipLevel = { 0, 1, 1, 1 };
|
||
[Tooltip("每个炮台是否额外要求月卡在有效期内(当前规则不启用月卡限制,需要时把第 4 个勾上)")]
|
||
public bool[] ptUnlockNeedMonthCard = { false, false, false, false };
|
||
[Tooltip("贵族等级不足时按钮 LimitTip 的提示文案,{0}=所需贵族等级(原版 LevelLimit)")]
|
||
public string ptLockTipVipFormat = "贵族{0}解锁";
|
||
[Tooltip("需要月卡时按钮 LimitTip 的提示文案(原版 MemberBattery2)")]
|
||
public string ptLockTipMonthCard = "月卡专属";
|
||
[Tooltip("未解锁炮台按钮上的遮挡灰幕颜色(半透明灰)")]
|
||
public Color ptLockMaskColor = new Color(0f, 0f, 0f, 0.55f);
|
||
|
||
// 每个按钮的未解锁灰幕(懒创建后缓存复用;面板销毁时随之销毁)
|
||
private readonly Dictionary<Button, GameObject> _ptLockMasks = new Dictionary<Button, GameObject>();
|
||
|
||
// ⭐ 原版 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>玩法 BGM(加载完成确认点会再置一次,防止被大厅面板 Hide 覆盖)</summary>
|
||
protected override int GameBgmAudioId => 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);
|
||
if (!available) continue;
|
||
|
||
// ⭐ 解锁限制(原版 PW_SelectBatteryView:InitBattery):未解锁 → 按钮上 LimitTip 显示提示文案
|
||
// 非 VIP 玩家后面几个炮台不解锁,只能切第一个(点击时再拦一次,见 OnPTButtonClicked)
|
||
string lockTip = GetPTLockTip(i);
|
||
bool locked = lockTip != null;
|
||
|
||
var limitTip = FindChildByName(_ptButtons[i].transform, "LimitTip");
|
||
if (limitTip != null)
|
||
{
|
||
if (limitTip.gameObject.activeSelf != locked) limitTip.gameObject.SetActive(locked);
|
||
if (locked) SetTextContent(limitTip, lockTip);
|
||
}
|
||
|
||
// ⭐ 未解锁再压一层半透明灰幕遮挡(纯视觉,不挡点击;压在 LimitTip 下面保证提示可见)
|
||
SetPTLockMask(_ptButtons[i], locked);
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
// ⭐ 解锁限制:未解锁(贵族等级不足 / 需要月卡)不允许切换,保持面板打开
|
||
// (原版 SelectBattery:等级不够 → HallShowTip(LevelNotEnough) 后 return)
|
||
string lockTip = GetPTLockTip(index);
|
||
if (lockTip != null)
|
||
{
|
||
var btn = index >= 0 && index < _ptButtons.Length ? _ptButtons[index] : null;
|
||
var tipNode = btn != null ? FindChildByName(btn.transform, "LimitTip") : null;
|
||
if (tipNode != null)
|
||
{
|
||
if (!tipNode.gameObject.activeSelf) tipNode.gameObject.SetActive(true);
|
||
SetTextContent(tipNode, lockTip);
|
||
}
|
||
Debug.Log($"[XJMH] 炮台{index}未解锁({lockTip}),不切换。当前贵族等级={GetLocalVipLevel()}");
|
||
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>本地玩家贵族(VIP)等级(0=非VIP)</summary>
|
||
private static int GetLocalVipLevel()
|
||
{
|
||
var u = GameManager.Instance != null ? GameManager.Instance.user : null;
|
||
return u != null && u.assets != null ? u.assets.vipLevel : 0;
|
||
}
|
||
|
||
/// <summary>本地玩家月卡是否在有效期内(userCardEndTime;兼容秒/毫秒时间戳)</summary>
|
||
private static bool HasMonthCard()
|
||
{
|
||
var u = GameManager.Instance != null ? GameManager.Instance.user : null;
|
||
if (u == null || u.assets == null) return false;
|
||
long end = u.assets.userCardEndTime;
|
||
if (end <= 0) return false;
|
||
if (end < 100000000000L) end *= 1000L; // 秒 → 毫秒
|
||
long now = BeijingTimeManager.Instance != null ? BeijingTimeManager.Instance.CurrentTimestampMs : 0L;
|
||
return end > now;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 该索引炮台的解锁提示文案:未解锁返回提示文字(显示在按钮 LimitTip 上并禁止切换),已解锁返回 null。
|
||
/// 判定顺序:月卡限制 → 贵族(VIP)等级限制(对应面板上的 ptUnlockNeedMonthCard / ptUnlockVipLevel)。
|
||
/// </summary>
|
||
private string GetPTLockTip(int index)
|
||
{
|
||
if (ptUnlockNeedMonthCard != null && index < ptUnlockNeedMonthCard.Length
|
||
&& ptUnlockNeedMonthCard[index] && !HasMonthCard())
|
||
return string.IsNullOrEmpty(ptLockTipMonthCard) ? "月卡专属" : ptLockTipMonthCard;
|
||
|
||
int need = (ptUnlockVipLevel != null && index < ptUnlockVipLevel.Length) ? ptUnlockVipLevel[index] : 0;
|
||
if (GetLocalVipLevel() < need)
|
||
return string.IsNullOrEmpty(ptLockTipVipFormat) ? $"VIP{need}解锁" : string.Format(ptLockTipVipFormat, need);
|
||
|
||
return null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置按钮上的"未解锁灰幕"显隐(懒创建:半透明灰 Image 铺满按钮,纯视觉、不挡点击)。
|
||
/// 灰幕压在 LimitTip 下面,保证"贵族X解锁"提示仍然可见。
|
||
/// </summary>
|
||
private void SetPTLockMask(Button btn, bool show)
|
||
{
|
||
if (btn == null) return;
|
||
|
||
if (!_ptLockMasks.TryGetValue(btn, out var mask) || mask == null)
|
||
{
|
||
var go = new GameObject("[PTLockMask]", typeof(RectTransform), typeof(Image));
|
||
var rt = (RectTransform)go.transform;
|
||
rt.SetParent(btn.transform, false);
|
||
rt.anchorMin = Vector2.zero;
|
||
rt.anchorMax = Vector2.one;
|
||
rt.offsetMin = Vector2.zero;
|
||
rt.offsetMax = Vector2.zero;
|
||
|
||
var img = go.GetComponent<Image>();
|
||
img.color = ptLockMaskColor;
|
||
img.raycastTarget = false; // 纯遮挡:点击仍能落到按钮上走"未解锁"提示
|
||
|
||
// 排在 LimitTip 之前(层级更低),提示文字不会被灰幕盖住
|
||
var tip = FindChildByName(btn.transform, "LimitTip");
|
||
if (tip != null) rt.SetSiblingIndex(Mathf.Max(0, tip.GetSiblingIndex()));
|
||
|
||
mask = go;
|
||
_ptLockMasks[btn] = mask;
|
||
}
|
||
|
||
// 颜色每次同步(Play 模式下面板上改完立即生效)
|
||
var maskImg = mask.GetComponent<Image>();
|
||
if (maskImg != null && maskImg.color != ptLockMaskColor) maskImg.color = ptLockMaskColor;
|
||
|
||
if (mask.activeSelf != show) mask.SetActive(show);
|
||
}
|
||
|
||
// ==================== 金币柱 ====================
|
||
|
||
/// <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 <= 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 < 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;
|
||
}
|
||
} |