You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1224 lines
44 KiB
1224 lines
44 KiB
//*********************
|
|
// create by 流云
|
|
// time: 2020.12.18
|
|
// desc: UI管理器
|
|
//*********************
|
|
|
|
import { Scene, Node, UITransform, Camera, director, Widget, Layout, Label, Overflow, HorizontalTextAlignment, Layers, Director, Asset, AssetManager, Prefab, assetManager, instantiate, Tween, tween, v3, RichText, game, view, EventTouch, isValid, ResolutionPolicy, screen, Mask } from "cc";
|
|
|
|
import { OpenParam, UIBase } from "./UIBase";
|
|
import { FlyCurrency } from "../../components/tween/FlyCurrency";
|
|
import { PfDiagnostics } from "db://assets/script/diagnostics/PfDiagnostics";
|
|
|
|
/** 与 ConfigRes.UIID.SkillSelect 一致;不可 import ConfigRes,否则会与 ConfigRes→UIManager 循环引用 */
|
|
const SKILL_SELECT_PANEL_ID = 651;
|
|
/** 与 ConfigRes.UIID 一致 */
|
|
const BATTLE_SUCCESS_PANEL_ID = 605;
|
|
const BATTLE_FAIL_PANEL_ID = 606;
|
|
const BATTLE_REVIVE_PANEL_ID = 607;
|
|
/** SkillSelect / 结算分包真重载冷却(避免连续失败时反复 removeBundle) */
|
|
const SKILL_SELECT_BUNDLE_RELOAD_COOLDOWN_MS = 30_000;
|
|
const SETTLEMENT_BUNDLE_RELOAD_COOLDOWN_MS = 30_000;
|
|
|
|
/** loadPrefab 结果:失败时带 stage(bundle/asset)与引擎 err 摘要 */
|
|
type LoadPrefabResult = {
|
|
prefab: Prefab | null;
|
|
stage?: 'bundle' | 'asset';
|
|
err?: string;
|
|
};
|
|
|
|
/**UI信息 */
|
|
export class UIInfo {
|
|
/**UIID */
|
|
id: number = 0;
|
|
/**UI节点 */
|
|
uiNode: Node = null;
|
|
/**UI组件 */
|
|
uiBase: UIBase = null;
|
|
/**预制体资源 */
|
|
prefabRes: Prefab = null;
|
|
/**UI配置 */
|
|
uiconf: IUIConfig = null;
|
|
}
|
|
|
|
/**
|
|
* UI层级(新增层级在此添加枚举即可,层级按枚举顺序显示)
|
|
*/
|
|
export enum UILayer {
|
|
/**2D游戏UI */
|
|
Game2D = `Game2D`,
|
|
/**主UI层 */
|
|
MainUI = `MainUI`,
|
|
/**菜单层 */
|
|
Menu = `Menu`,
|
|
/**全屏面板层 */
|
|
Page = `Page`,
|
|
/**弹窗页面 */
|
|
Pop = `Pop`,
|
|
/**提示页面(模态对话框) */
|
|
Dalog = `Dalog`,
|
|
/**动画层 */
|
|
Anim = `Anim`,
|
|
/**引导页面 */
|
|
Guide = `Guide`,
|
|
/**加载页面 */
|
|
Loading = `Loading`,
|
|
/**调试层 */
|
|
DeBug = `DeBug`,
|
|
/**广告页面 */
|
|
Ad = `Ad`,
|
|
}
|
|
|
|
/**
|
|
* UI配置
|
|
*/
|
|
export interface IUIConfig {
|
|
/**UI描述 */
|
|
desc: string;
|
|
/**UI层级 */
|
|
layer: UILayer;
|
|
/**资源包名 */
|
|
bundle: string;
|
|
/**资源路径 */
|
|
path: string;
|
|
/**资源名称 */
|
|
name: string;
|
|
}
|
|
|
|
|
|
/**
|
|
* UI管理器
|
|
*/
|
|
export class UIManager {
|
|
|
|
private static _ins: UIManager = null;
|
|
static get Ins() {
|
|
if (!this._ins) this._ins = new UIManager();
|
|
return this._ins;
|
|
}
|
|
|
|
/**宽度 */
|
|
get width() {
|
|
return this._root ? this._root.getComponent(UITransform).width : view.getVisibleSize().width;
|
|
}
|
|
/**高度 */
|
|
get height() {
|
|
return this._root ? this._root.getComponent(UITransform).height : view.getVisibleSize().height;
|
|
}
|
|
|
|
/**UI相机 */
|
|
UICamera: Camera = null;
|
|
/**游戏相机 */
|
|
GameCamera: Camera = null;
|
|
|
|
/**是否动态适配屏幕(默认开启) */
|
|
isDynamicAdaptScreen: boolean = true;
|
|
|
|
private _curent_scene: Scene = null;
|
|
private _canvas: Node = null;
|
|
private _root: Node = null;
|
|
private _log: Node = null;
|
|
private _timer_flag = null;
|
|
|
|
|
|
private _layer_map: Map<UILayer, Node> = new Map<UILayer, Node>();
|
|
private _ui_map: Map<number, UIInfo> = new Map<number, UIInfo>();
|
|
/** 同 id 的 openPanel 异步加载未完成时,后续点击复用该 Promise,避免 uiBase 仍为 null 时写 prema */
|
|
private _panelOpenInflight: Map<number, Promise<void>> = new Map();
|
|
private _wait_popIds: any[] = [];
|
|
private _curent_pop = null;
|
|
/** 上次结算分包真重载时间戳(ms) */
|
|
private _settlementBundleReloadMs = 0;
|
|
/** 上次 SkillSelect 分包真重载时间戳(ms) */
|
|
private _skillSelectBundleReloadMs = 0;
|
|
|
|
private uiConfigs: { [key: number]: IUIConfig } = [];
|
|
|
|
/**初始化(切换场景的时候调用) */
|
|
init(config: { [key: number]: IUIConfig }, UICamera?: Camera, GameCamera?: Camera, isDynamicAdaptScreen: boolean = true) {
|
|
this.uiConfigs = config;
|
|
this.UICamera = UICamera;
|
|
this.GameCamera = GameCamera;
|
|
this.isDynamicAdaptScreen = isDynamicAdaptScreen;
|
|
//this.GameCamera.orthoHeight = view.getVisibleSize().height / 2;
|
|
this._curent_scene = director.getScene();
|
|
this._canvas = this._curent_scene.getChildByName(`Canvas`);
|
|
|
|
if (this.isDynamicAdaptScreen) {
|
|
let winsize = screen.windowSize;
|
|
let ratio = winsize.width / winsize.height;
|
|
let drs = view.getDesignResolutionSize();
|
|
let drsRatio = drs.width / drs.height;
|
|
if (ratio > drsRatio) {
|
|
view.setResolutionPolicy(ResolutionPolicy.FIXED_HEIGHT);
|
|
} else {
|
|
view.setResolutionPolicy(ResolutionPolicy.FIXED_WIDTH);
|
|
}
|
|
}
|
|
|
|
//初始化UI根节点
|
|
this._root = this._canvas.getChildByName(`root`);
|
|
if (!this._root) this._root = this.addUILayer(this._canvas, `root`);
|
|
|
|
//初始化层级
|
|
for (let k in UILayer) {
|
|
let v = UILayer[k];
|
|
this._layer_map.set(v, this.addUILayer(this._root, v));
|
|
}
|
|
|
|
|
|
//初始化log层
|
|
this._log = this._root.getChildByName(`log`);
|
|
if (!this._log) {
|
|
this._log = new Node(`log`);
|
|
this._root.addChild(this._log);
|
|
let w = this._log.addComponent(Widget);
|
|
w.isAlignLeft = w.isAlignTop = w.isAlignRight = w.isAlignBottom = true;
|
|
w.left = w.top = w.right = w.bottom = 0;
|
|
w.updateAlignment();
|
|
|
|
let ly = this._log.addComponent(Layout);
|
|
ly.type = Layout.Type.VERTICAL;
|
|
let dc = new Node("dc");
|
|
dc.layer = Layers.Enum.UI_2D;
|
|
let lb = dc.addComponent(Label);
|
|
lb.string = "";
|
|
lb.fontSize = 50;
|
|
this._log.addChild(dc);
|
|
lb.isBold = true;
|
|
let l = new Node("log");
|
|
l.layer = Layers.Enum.UI_2D;
|
|
l.addComponent(UITransform).width = 700;
|
|
lb = l.addComponent(Label);
|
|
lb.horizontalAlign = HorizontalTextAlignment.LEFT;
|
|
lb.overflow = Overflow.RESIZE_HEIGHT;
|
|
lb.string = "";
|
|
lb.fontSize = 30;
|
|
this._log.addChild(l);
|
|
}
|
|
|
|
//轮询队列弹窗
|
|
this._root.uiTransform.schedule(() => {
|
|
this.showPopToLayer();
|
|
}, 0.5);
|
|
// this._log.on(Node.EventType.TOUCH_START, (e: EventTouch) => {
|
|
// GEvent.Ins.emit(GEvent.GlobalTouchStart, e);
|
|
// })
|
|
// this._log.on(Node.EventType.TOUCH_MOVE, (e: EventTouch) => {
|
|
// GEvent.Ins.emit(GEvent.GlobalTouchMove, e);
|
|
// e.preventSwallow = true;
|
|
// e.propagationStopped = true;
|
|
// })
|
|
// this._log.on(Node.EventType.TOUCH_END, (e: EventTouch) => {
|
|
// GEvent.Ins.emit(GEvent.GlobalTouchEnd, e);
|
|
// e.preventSwallow = true;
|
|
// e.propagationStopped = true;
|
|
// })
|
|
// this._log.on(Node.EventType.TOUCH_CANCEL, (e: EventTouch) => {
|
|
// GEvent.Ins.emit(GEvent.GlobalTouchCancel, e);
|
|
// e.preventSwallow = true;
|
|
// e.propagationStopped = true;
|
|
// })
|
|
}
|
|
|
|
|
|
/**
|
|
* 设置底部距离
|
|
* @param value 游戏整个UI底边沿距离屏幕底边沿的设计分辨率像素距离
|
|
*/
|
|
setBottomWidget(value) {
|
|
let w = this._root.getComponent(Widget);
|
|
w.bottom = value;
|
|
}
|
|
|
|
/**
|
|
* 设置顶部距离
|
|
* @param value 游戏整个UI顶部边沿距离屏幕顶部边沿的设计分辨率像素距离
|
|
*/
|
|
setTopWidget(value) {
|
|
let w = this._root.getComponent(Widget);
|
|
w.top = value;
|
|
}
|
|
|
|
|
|
/**
|
|
* 显示一个节点到指定层级
|
|
* @param ui ui节点
|
|
* @param layer 层级
|
|
*/
|
|
showNode(ui, layer: UILayer, index = -2) {
|
|
this._layer_map.get(layer).addChild(ui);
|
|
//index设置为0,确保在最底层
|
|
//index设置为-1,确保在最上层
|
|
//index设置为-2,不设置z-index
|
|
if (index != -2)
|
|
ui.setSiblingIndex(index);
|
|
}
|
|
|
|
/**
|
|
* 打开单例页面
|
|
* @param id 页面ID
|
|
* @param param 参数
|
|
*/
|
|
async openPanel(id: number, param?: OpenParam) {
|
|
const pending = this._panelOpenInflight.get(id);
|
|
if (pending) {
|
|
await pending;
|
|
}
|
|
|
|
let uiInfo: UIInfo = null;
|
|
const traceSkillSelect = id === SKILL_SELECT_PANEL_ID && this.isSkillSelectTreasureOpen(param);
|
|
if (this._ui_map.has(id)) {
|
|
uiInfo = this._ui_map.get(id);
|
|
if (uiInfo.uiBase && uiInfo.uiNode?.isValid) {
|
|
if (this.shouldAbortSkillSelectOpen(id)) {
|
|
this.abortSkillSelectOpen(id, param, traceSkillSelect);
|
|
return;
|
|
}
|
|
if (traceSkillSelect) {
|
|
this._logSkillSelectTrace('load_start', param, { reuse: true });
|
|
}
|
|
uiInfo.uiNode.active = true;
|
|
uiInfo.uiNode.setSiblingIndex(-1);
|
|
uiInfo.uiBase.onAdded(param);
|
|
//console.log("openPanel已打开", id, param);
|
|
if (traceSkillSelect) {
|
|
this.finishSkillSelectTreasureOpen(param);
|
|
}
|
|
return;
|
|
}
|
|
// 节点已销毁或上次 prefab 加载失败会留下空占位,清除后允许重试
|
|
this._ui_map.delete(id);
|
|
}
|
|
|
|
uiInfo = new UIInfo();
|
|
uiInfo.id = id;
|
|
this._ui_map.set(id, uiInfo);
|
|
|
|
let resolveInflight: () => void;
|
|
const inflightPromise = new Promise<void>((resolve) => {
|
|
resolveInflight = resolve;
|
|
});
|
|
this._panelOpenInflight.set(id, inflightPromise);
|
|
|
|
try {
|
|
if (param && param.showLoading && param.showLoading > 0) {
|
|
gg.game.showLoading(param.showLoading);
|
|
}
|
|
|
|
let conf = this.uiConfigs[id];
|
|
let path = conf.path + conf.name;
|
|
let ui = null;
|
|
let loadResult = await this.loadPrefab(conf.bundle, path);
|
|
let p = loadResult.prefab;
|
|
let loadErrDetail = this._formatPrefabLoadDetail(loadResult);
|
|
if (!p && traceSkillSelect) {
|
|
this._logSkillSelectTrace('load_retry', param);
|
|
loadResult = await this.loadPrefab(conf.bundle, path);
|
|
p = loadResult.prefab;
|
|
loadErrDetail = this._formatPrefabLoadDetail(loadResult, true);
|
|
}
|
|
if (!p && this.isSkillSelectPanel(id)) {
|
|
const reloadAgoMs = Date.now() - this._skillSelectBundleReloadMs;
|
|
const reloadOnCooldown = this._skillSelectBundleReloadMs > 0
|
|
&& reloadAgoMs < SKILL_SELECT_BUNDLE_RELOAD_COOLDOWN_MS;
|
|
if (reloadOnCooldown) {
|
|
const remainSec = Math.ceil((SKILL_SELECT_BUNDLE_RELOAD_COOLDOWN_MS - reloadAgoMs) / 1000);
|
|
this._logSkillSelectTrace('bundle_reload_skip', param, { remainSec });
|
|
loadErrDetail = `${loadErrDetail}|reload_skip=cooldown|reload_ago=${Math.floor(reloadAgoMs / 1000)}s|reload_remain=${remainSec}s`;
|
|
} else {
|
|
this._logSkillSelectTrace('bundle_reload', param);
|
|
this._cleanupPanelForBundleReload(id);
|
|
uiInfo = this._ui_map.get(id);
|
|
this._skillSelectBundleReloadMs = Date.now();
|
|
loadResult = await this._reloadBundleAndLoadPrefab(conf.bundle, path);
|
|
p = loadResult.prefab;
|
|
loadErrDetail = `${loadErrDetail}|${this._formatPrefabLoadDetail(loadResult, false, true)}`;
|
|
this._reportSkillSelectBundleReload(!!p, param, conf.bundle, path, loadResult);
|
|
}
|
|
}
|
|
uiInfo.uiconf = conf;
|
|
if (traceSkillSelect) {
|
|
this._logSkillSelectTrace('load_start', param, { reuse: false });
|
|
}
|
|
if (p) {
|
|
if (this.shouldAbortSkillSelectOpen(id)) {
|
|
this.abortSkillSelectOpen(id, param, traceSkillSelect);
|
|
return;
|
|
}
|
|
ui = instantiate(p);
|
|
uiInfo.prefabRes = p;
|
|
uiInfo.uiNode = ui;
|
|
this._layer_map.get(conf.layer).addChild(ui);
|
|
uiInfo.uiBase = ui.getComponent(UIBase);
|
|
if (uiInfo.uiBase) {
|
|
uiInfo.uiBase.uiInfo = uiInfo;
|
|
uiInfo.uiBase.onAdded(param);
|
|
if (param && param.showLoading && param.showLoading > 0) {
|
|
gg.game.hideLoading();
|
|
}
|
|
if (traceSkillSelect) {
|
|
this.finishSkillSelectTreasureOpen(param);
|
|
}
|
|
} else if (this.isSkillSelectPanel(id)) {
|
|
console.error('[UIManager] SkillSelect missing UIBase');
|
|
if (ui?.isValid) {
|
|
ui.destroy();
|
|
}
|
|
this._ui_map.delete(id);
|
|
this._reportSkillSelectOpenFail('missing_uibase', param, `id=${id}`);
|
|
}
|
|
} else {
|
|
const failDetail = `bundle=${conf?.bundle}|path=${path}|${loadErrDetail}`;
|
|
console.error(
|
|
`[UIManager] openPanel prefab load failed id=${id} ${failDetail}`,
|
|
);
|
|
gg.sdk?.pfLogKey?.('UI:OPEN_FAIL', `id=${id}|${failDetail}`);
|
|
this._ui_map.delete(id);
|
|
if (this.isSkillSelectPanel(id)) {
|
|
this._reportSkillSelectOpenFail('prefab_load_failed', param, failDetail);
|
|
} else if (traceSkillSelect) {
|
|
this.notifySkillSelectOpenResult(param, false, 'prefab_load_failed', failDetail);
|
|
}
|
|
}
|
|
} finally {
|
|
this._panelOpenInflight.delete(id);
|
|
resolveInflight();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 关闭单例页面
|
|
* @param id 页面ID
|
|
* @param tweenType 1: 直接关闭 2: 弹窗缩回,同时背景渐变成透明 3: 不缩放,直接渐变到透明
|
|
*/
|
|
async closePanel(id: number, tweenType = 1) {
|
|
if (!this._ui_map.has(id)) return;
|
|
let uiInfo = this._ui_map.get(id);
|
|
if (uiInfo.uiNode && uiInfo.uiNode.isValid) {
|
|
await uiInfo.uiNode.getComponent(UIBase)?.destroyPanel(tweenType);
|
|
uiInfo.prefabRes?.decRef();
|
|
uiInfo.uiNode = null;
|
|
uiInfo.uiBase = null;
|
|
this._ui_map.delete(id);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 关闭所有页面
|
|
* @param ids 排除的页面ID数组(传入的id页面不会关闭)
|
|
*/
|
|
closeAllPanel(ids: number[] = []) {
|
|
let keys = Array.from(this._ui_map.keys());
|
|
for (let id of keys) {
|
|
if (ids.indexOf(id) == -1) {
|
|
this.closePanel(id);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** 等待所有面板关闭完成后再做资源释放(避免节点仍引用贴图导致内存不降) */
|
|
async closeAllPanelAsync(ids: number[] = []): Promise<void> {
|
|
const keys = Array.from(this._ui_map.keys());
|
|
const tasks: Promise<unknown>[] = [];
|
|
for (const id of keys) {
|
|
if (ids.indexOf(id) === -1) {
|
|
tasks.push(this.closePanel(id));
|
|
}
|
|
}
|
|
await Promise.all(tasks);
|
|
}
|
|
|
|
/**
|
|
* 清理 Anim 层上的飞金币/飞道具节点(挂有 FlyCurrency)。
|
|
* 进战斗等会卸载 UI 资源时,若飞行动画仍在跑,Sprite 可能已失效,需在切场景前销毁。
|
|
*/
|
|
clearAnimFlyNodes() {
|
|
const anim = this._layer_map?.get(UILayer.Anim);
|
|
if (!anim?.isValid) {
|
|
return;
|
|
}
|
|
const list = anim.children.slice();
|
|
for (const n of list) {
|
|
if (n.getComponent(FlyCurrency)) {
|
|
Tween.stopAllByTarget(n);
|
|
n.destroy();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 是否存在页面
|
|
* @param id 页面ID
|
|
* @returns
|
|
*/
|
|
hasPanel(id: number) {
|
|
if (!this._ui_map.has(id)) return false;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* 判断是否有其他页面
|
|
* @param id 排除的页面id
|
|
* @returns
|
|
*/
|
|
hasOtherPanel(id: number, sameLayer = true) {
|
|
let conf = this.uiConfigs[id];
|
|
let keys = Array.from(this._ui_map.keys());
|
|
for (let key of keys) {
|
|
let info = this._ui_map.get(key);
|
|
if (sameLayer && info.uiconf.layer != conf.layer) {
|
|
continue;
|
|
}
|
|
if (info.id != id) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 页面是否显示中
|
|
* @param id 页面ID
|
|
* @returns
|
|
*/
|
|
panelIsShow(id: number) {
|
|
if (!this._ui_map.has(id)) return false;
|
|
let uiInfo = this._ui_map.get(id);
|
|
if (!uiInfo.uiNode || !uiInfo.uiBase || !uiInfo.uiNode.isValid || !uiInfo.uiNode.active) return false;
|
|
return true;
|
|
}
|
|
|
|
/** 页面是否正在异步加载中 */
|
|
panelIsOpening(id: number) {
|
|
return this._panelOpenInflight.has(id);
|
|
}
|
|
|
|
/** 从弹窗队列中移除指定 id(避免连点堆积重复弹窗) */
|
|
removeWaitPop(id: number) {
|
|
if (!this._wait_popIds.length) {
|
|
return;
|
|
}
|
|
this._wait_popIds = this._wait_popIds.filter((x) => x.id !== id);
|
|
if (this._curent_pop?.id === id && !this.panelIsShow(id) && !this._panelOpenInflight.has(id)) {
|
|
this._curent_pop = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 打开弹窗(队列式弹窗,连续调用不会一起弹出来,关闭上一个后才会自动弹下一个)
|
|
* @param id 弹窗id
|
|
* @param param 参数
|
|
*/
|
|
popPanel(id: number, param?: OpenParam) {
|
|
this._wait_popIds.push({ id: id, param: param });
|
|
}
|
|
|
|
/**
|
|
* 底部波次宝箱专用:SkillSelect 入队,并在 load/onInit 结果后触发回调。
|
|
*/
|
|
popSkillSelectFromTreasure(param?: OpenParam) {
|
|
this._logSkillSelectTrace('click_enqueue', param);
|
|
this.popPanel(SKILL_SELECT_PANEL_ID, param);
|
|
this.showPopToLayer();
|
|
}
|
|
|
|
private isSkillSelectTreasureOpen(param?: OpenParam): boolean {
|
|
return !!(param?.onSkillSelectOpenSuccess || param?.onSkillSelectOpenFail);
|
|
}
|
|
|
|
/** UISkillSelect.onInit 抛错时调用,直接上报 init_error,避免被误判为 panel_not_visible_after_init */
|
|
reportSkillSelectInitFailed(param: OpenParam | undefined, err: unknown, context?: Record<string, unknown>) {
|
|
const msg = err instanceof Error ? `${err.name}:${err.message}` : String(err);
|
|
this._reportSkillSelectOpenFail('init_error', param, msg.slice(0, 120), context);
|
|
}
|
|
|
|
private _skillSelectBattleOnlyContext(): Record<string, unknown> {
|
|
const battle = gg.game?.CurentBattle;
|
|
return {
|
|
panelType: 'unknown',
|
|
pickSkillIds: [] as number[],
|
|
pickWeaponIds: [] as number[],
|
|
curWeaponIds: battle?.CurUseWeaponArr?.map(w => w?.weaponid).filter(id => id != null) ?? [],
|
|
curSkillIds: battle?.CurUseSkillArr?.map(s => s?.id).filter(id => id != null) ?? [],
|
|
};
|
|
}
|
|
|
|
private _resolveSkillSelectFailContext(explicit?: Record<string, unknown>): Record<string, unknown> {
|
|
if (explicit) {
|
|
return explicit;
|
|
}
|
|
try {
|
|
const uiInfo = this._ui_map.get(SKILL_SELECT_PANEL_ID);
|
|
const panel = uiInfo?.uiBase as { getOpenFailContext?: () => Record<string, unknown> } | null;
|
|
if (panel?.getOpenFailContext) {
|
|
return panel.getOpenFailContext();
|
|
}
|
|
} catch (e) {
|
|
console.error('[UIManager] getOpenFailContext error', e);
|
|
}
|
|
return this._skillSelectBattleOnlyContext();
|
|
}
|
|
|
|
private _compactIdList(ids: unknown, maxLen = 48): string {
|
|
if (!Array.isArray(ids) || ids.length === 0) {
|
|
return '';
|
|
}
|
|
const s = ids.join(',');
|
|
return s.length <= maxLen ? s : s.slice(0, maxLen);
|
|
}
|
|
|
|
private _formatSkillSelectIdContext(ctx: Record<string, unknown>): { pf: string; dy: string } {
|
|
const panelType = String(ctx.panelType ?? 'unknown');
|
|
const curW = this._compactIdList(ctx.curWeaponIds, 40);
|
|
const curS = this._compactIdList(ctx.curSkillIds, 48);
|
|
const pickS = this._compactIdList(ctx.pickSkillIds, 32);
|
|
const pickW = this._compactIdList(ctx.pickWeaponIds, 32);
|
|
const pfParts = [`ptype=${panelType}`];
|
|
if (curW) pfParts.push(`curW=${curW}`);
|
|
if (curS) pfParts.push(`curS=${curS}`);
|
|
if (pickS) pfParts.push(`pickS=${pickS}`);
|
|
if (pickW) pfParts.push(`pickW=${pickW}`);
|
|
const pf = pfParts.join('|');
|
|
const dyParts = [`ptype${panelType}`];
|
|
if (curW) dyParts.push(`curW${curW.replace(/,/g, '_')}`);
|
|
if (curS) dyParts.push(`curS${curS.replace(/,/g, '_')}`);
|
|
if (pickS) dyParts.push(`pickS${pickS.replace(/,/g, '_')}`);
|
|
if (pickW) dyParts.push(`pickW${pickW.replace(/,/g, '_')}`);
|
|
return { pf, dy: dyParts.join('-') };
|
|
}
|
|
|
|
private _skillSelectFailDetail(reason: string, detail?: string, idContext?: Record<string, unknown>, extra: Record<string, unknown> = {}) {
|
|
const battle = gg.game?.CurentBattle;
|
|
const conf = this.uiConfigs[SKILL_SELECT_PANEL_ID];
|
|
const ctx = idContext ?? this._skillSelectBattleOnlyContext();
|
|
return {
|
|
reason,
|
|
detail: detail ?? '',
|
|
wave: battle?.CurentWaveNum ?? 0,
|
|
chapter: battle?.ChapterId ?? gg.data?.doc?.chapter ?? 0,
|
|
openTimes: battle?.IsOpenSkillSelectTimes ?? 0,
|
|
saveBoxNum: battle?.tetriSaveBoxNum ?? 0,
|
|
queueLen: this._wait_popIds.length,
|
|
panelShowing: this.panelIsShow(SKILL_SELECT_PANEL_ID),
|
|
bundleLoaded: conf?.bundle && assetManager.getBundle(conf.bundle) ? 1 : 0,
|
|
bundle: conf?.bundle ?? '',
|
|
prefabPath: conf ? `${conf.path}${conf.name}` : '',
|
|
panelType: ctx.panelType ?? 'unknown',
|
|
pickSkillIds: ctx.pickSkillIds ?? [],
|
|
pickWeaponIds: ctx.pickWeaponIds ?? [],
|
|
curWeaponIds: ctx.curWeaponIds ?? [],
|
|
curSkillIds: ctx.curSkillIds ?? [],
|
|
...this._memWarnFailFields(),
|
|
...extra,
|
|
};
|
|
}
|
|
|
|
private _sanitizeSkillSelectReportSegment(text: string, maxLen: number): string {
|
|
return text.slice(0, maxLen).replace(/[^a-zA-Z0-9\u4e00-\u9fa5_|:-]/g, '_');
|
|
}
|
|
|
|
private _memWarnFailPf(): string {
|
|
return gg.sdk?.formatMemWarnForPf?.() ?? 'memLv=na';
|
|
}
|
|
|
|
private _memWarnFailDy(): string {
|
|
return gg.sdk?.formatMemWarnForDy?.() ?? 'memna';
|
|
}
|
|
|
|
private _memWarnFailFields(): Record<string, unknown> {
|
|
const s = gg.sdk?.getMemWarnState?.() ?? { level: null, agoSec: null };
|
|
return { memLv: s.level, memAgoSec: s.agoSec };
|
|
}
|
|
|
|
/** 宝箱 SkillSelect 流程本地日志(不上传 Pf / ZYZY,避免与 SkillSelectFail 混淆) */
|
|
private _logSkillSelectTrace(step: string, param?: OpenParam, extra: Record<string, unknown> = {}) {
|
|
if (param && !this.isSkillSelectTreasureOpen(param)) {
|
|
return;
|
|
}
|
|
const battle = gg.game?.CurentBattle;
|
|
console.log('[SkillSelectTrace]', JSON.stringify({
|
|
step,
|
|
wave: battle?.CurentWaveNum,
|
|
chapter: battle?.ChapterId ?? gg.data?.doc?.chapter,
|
|
openTimes: battle?.IsOpenSkillSelectTimes,
|
|
saveBoxNum: battle?.tetriSaveBoxNum,
|
|
queueLen: this._wait_popIds.length,
|
|
panelShowing: this.panelIsShow(SKILL_SELECT_PANEL_ID),
|
|
...extra,
|
|
}));
|
|
}
|
|
|
|
private notifySkillSelectOpenResult(param: OpenParam | undefined, success: boolean, reason?: string, detail?: string) {
|
|
if (!param || !this.isSkillSelectTreasureOpen(param)) {
|
|
return;
|
|
}
|
|
if (param._skillSelectResultReported) {
|
|
return;
|
|
}
|
|
if (success) {
|
|
param._skillSelectResultReported = true;
|
|
this._logSkillSelectTrace('open_success', param);
|
|
try {
|
|
param.onSkillSelectOpenSuccess?.();
|
|
} catch (e) {
|
|
console.error('[SkillSelectTrace] onSkillSelectOpenSuccess error', e);
|
|
}
|
|
return;
|
|
}
|
|
this._reportSkillSelectOpenFail(reason ?? 'unknown', param, detail);
|
|
}
|
|
|
|
private finishSkillSelectTreasureOpen(param?: OpenParam, attempt = 0) {
|
|
if (!param || !this.isSkillSelectTreasureOpen(param)) {
|
|
return;
|
|
}
|
|
const tryFinish = () => {
|
|
if (param._skillSelectResultReported) {
|
|
return;
|
|
}
|
|
if (this.panelIsShow(SKILL_SELECT_PANEL_ID)) {
|
|
this.notifySkillSelectOpenResult(param, true);
|
|
return;
|
|
}
|
|
if (attempt < 2) {
|
|
setTimeout(() => this.finishSkillSelectTreasureOpen(param, attempt + 1), attempt === 0 ? 0 : 80);
|
|
return;
|
|
}
|
|
this._reportSkillSelectOpenFail(
|
|
'panel_not_visible_after_init',
|
|
param,
|
|
`attempts=3|inflight=${this._panelOpenInflight.has(SKILL_SELECT_PANEL_ID) ? 1 : 0}`,
|
|
);
|
|
};
|
|
tryFinish();
|
|
}
|
|
|
|
private isSkillSelectPanel(id: number): boolean {
|
|
return id === SKILL_SELECT_PANEL_ID;
|
|
}
|
|
|
|
private isSettlementPanel(id: number): boolean {
|
|
return id === BATTLE_SUCCESS_PANEL_ID
|
|
|| id === BATTLE_FAIL_PANEL_ID
|
|
|| id === BATTLE_REVIVE_PANEL_ID;
|
|
}
|
|
|
|
/** 战斗已结束或正在回大厅时,不再打开 SkillSelect(避免异步 load 完成后访问 null 的 CurentBattle) */
|
|
private shouldAbortSkillSelectOpen(id: number): boolean {
|
|
return this.isSkillSelectPanel(id) && !gg.game?.isBattleContextActive?.();
|
|
}
|
|
|
|
private abortSkillSelectOpen(id: number, param?: OpenParam, traceSkillSelect = false, reason = 'battle_left'): void {
|
|
this._ui_map.delete(id);
|
|
if (traceSkillSelect) {
|
|
this.notifySkillSelectOpenResult(param, false, reason, reason);
|
|
}
|
|
}
|
|
|
|
/** 技能箱打开失败:PfDiagnostics + 宝箱回调 + ZYZY 打点(仅失败路径;防重复) */
|
|
private _reportSkillSelectOpenFail(
|
|
reason: string,
|
|
param?: OpenParam,
|
|
detail?: string,
|
|
explicitContext?: Record<string, unknown>,
|
|
): void {
|
|
if (param && this.isSkillSelectTreasureOpen(param) && param._skillSelectResultReported) {
|
|
return;
|
|
}
|
|
try {
|
|
const idContext = this._resolveSkillSelectFailContext(explicitContext);
|
|
const payload = this._skillSelectFailDetail(reason, detail, idContext);
|
|
const idFmt = this._formatSkillSelectIdContext(idContext);
|
|
const pfParts = [
|
|
`reason=${reason}`,
|
|
`w=${payload.wave}`,
|
|
`ch=${payload.chapter}`,
|
|
`t=${payload.openTimes}`,
|
|
`box=${payload.saveBoxNum}`,
|
|
`bundle=${payload.bundleLoaded}`,
|
|
idFmt.pf,
|
|
this._memWarnFailPf(),
|
|
];
|
|
if (detail) {
|
|
pfParts.push(`msg=${detail.slice(0, 80)}`);
|
|
}
|
|
PfDiagnostics.write(
|
|
'SKILL',
|
|
'OPEN_FAIL',
|
|
pfParts.join('|'),
|
|
reason === 'init_error' ? 120000 : 600000,
|
|
'error',
|
|
);
|
|
|
|
console.error('[SkillSelectFail]', JSON.stringify(payload));
|
|
|
|
if (param && this.isSkillSelectTreasureOpen(param)) {
|
|
param._skillSelectResultReported = true;
|
|
// 本地 Trace,不上传平台
|
|
this._logSkillSelectTrace('open_fail', param, { reason, detail });
|
|
const battle = gg.game?.CurentBattle;
|
|
if (battle) {
|
|
let dy = `SkillSelectFail-${reason}-w${payload.wave}-ch${payload.chapter}-t${payload.openTimes}-box${payload.saveBoxNum}-${idFmt.dy}-${this._memWarnFailDy()}`;
|
|
if (detail) {
|
|
dy += `-d-${this._sanitizeSkillSelectReportSegment(detail, 40)}`;
|
|
}
|
|
gg.sdk.reportDY("inLevel", dy);
|
|
}
|
|
try {
|
|
param.onSkillSelectOpenFail?.(reason, detail, idContext);
|
|
} catch (e) {
|
|
console.error('[SkillSelectTrace] onSkillSelectOpenFail error', e);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error('[UIManager] _reportSkillSelectOpenFail error', e);
|
|
}
|
|
}
|
|
|
|
/** SkillSelect 分包真重载:无论成功/失败均上报(与 OPEN_FAIL 独立) */
|
|
private _reportSkillSelectBundleReload(
|
|
success: boolean,
|
|
param: OpenParam | undefined,
|
|
bundleName: string,
|
|
assetPath: string,
|
|
loadResult: LoadPrefabResult,
|
|
): void {
|
|
try {
|
|
const battle = gg.game?.CurentBattle;
|
|
const wave = battle?.CurentWaveNum ?? 0;
|
|
const chapter = battle?.ChapterId ?? gg.data?.doc?.chapter ?? 0;
|
|
const openTimes = battle?.IsOpenSkillSelectTimes ?? 0;
|
|
const saveBoxNum = battle?.tetriSaveBoxNum ?? 0;
|
|
const result = success ? 'ok' : 'fail';
|
|
const pfParts = [
|
|
`result=${result}`,
|
|
`w=${wave}`,
|
|
`ch=${chapter}`,
|
|
`t=${openTimes}`,
|
|
`box=${saveBoxNum}`,
|
|
`bundle=${bundleName}`,
|
|
`path=${assetPath}`,
|
|
`stage=${loadResult.stage ?? 'unknown'}`,
|
|
this._memWarnFailPf(),
|
|
];
|
|
if (loadResult.err) {
|
|
pfParts.push(`err=${String(loadResult.err).slice(0, 60)}`);
|
|
}
|
|
PfDiagnostics.write(
|
|
'SKILL',
|
|
'OPEN_RELOAD',
|
|
pfParts.join('|'),
|
|
0,
|
|
success ? 'info' : 'warn',
|
|
);
|
|
|
|
let dy = `SkillSelectReload-${result}-w${wave}-ch${chapter}-t${openTimes}-box${saveBoxNum}-${this._memWarnFailDy()}-stage${loadResult.stage ?? 'unknown'}`;
|
|
if (loadResult.err) {
|
|
dy += `-err${this._sanitizeSkillSelectReportSegment(String(loadResult.err), 32)}`;
|
|
}
|
|
gg.sdk.reportDY('inLevel', dy);
|
|
|
|
console.log('[SkillSelectReload]', JSON.stringify({
|
|
result,
|
|
bundle: bundleName,
|
|
path: assetPath,
|
|
stage: loadResult.stage,
|
|
err: loadResult.err ?? '',
|
|
wave,
|
|
chapter,
|
|
openTimes,
|
|
saveBoxNum,
|
|
treasure: !!(param && this.isSkillSelectTreasureOpen(param)),
|
|
}));
|
|
} catch (e) {
|
|
console.error('[UIManager] _reportSkillSelectBundleReload error', e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 战斗结算/复活/胜利:不走队列,避免被 SkillSelect、转盘等挡住。
|
|
* @returns 面板是否已成功显示
|
|
*/
|
|
async openBattleSettlementPanel(id: number, param?: OpenParam): Promise<boolean> {
|
|
this._curent_pop = null;
|
|
this._wait_popIds = this._wait_popIds.filter((x) => x.id !== id);
|
|
await this.openPanel(id, param);
|
|
if (this.panelIsShow(id)) {
|
|
return true;
|
|
}
|
|
|
|
const conf = this.uiConfigs[id];
|
|
if (conf?.bundle && this.isSettlementPanel(id)) {
|
|
const reloadAgoMs = Date.now() - this._settlementBundleReloadMs;
|
|
const reloadOnCooldown = this._settlementBundleReloadMs > 0
|
|
&& reloadAgoMs < SETTLEMENT_BUNDLE_RELOAD_COOLDOWN_MS;
|
|
if (!reloadOnCooldown) {
|
|
console.warn(`[UIManager] openBattleSettlementPanel bundle reload id=${id}`);
|
|
if (this._ui_map.has(id)) {
|
|
this._ui_map.delete(id);
|
|
}
|
|
this._settlementBundleReloadMs = Date.now();
|
|
const path = conf.path + conf.name;
|
|
const loadResult = await this._reloadBundleAndLoadPrefab(conf.bundle, path);
|
|
await this.openPanel(id, param);
|
|
if (this.panelIsShow(id)) {
|
|
return true;
|
|
}
|
|
console.error(
|
|
`[UIManager] openBattleSettlementPanel reload failed id=${id} stage=${loadResult.stage ?? 'unknown'}`,
|
|
);
|
|
gg.sdk?.pfLogKey?.('UI:SETTLE_RELOAD_FAIL', `id=${id}|stage=${loadResult.stage ?? 'unknown'}`);
|
|
}
|
|
}
|
|
|
|
console.error(`[UIManager] openBattleSettlementPanel retry id=${id}`);
|
|
if (this._ui_map.has(id)) {
|
|
this._ui_map.delete(id);
|
|
}
|
|
await this.openPanel(id, param);
|
|
if (this.panelIsShow(id)) {
|
|
return true;
|
|
}
|
|
console.error(`[UIManager] openBattleSettlementPanel failed id=${id}`);
|
|
gg.sdk?.pfLogKey?.('UI:SETTLE_OPEN_FAIL', `id=${id}`);
|
|
const failConf = conf ?? this.uiConfigs[id];
|
|
const panel = failConf?.name ?? String(id);
|
|
const bundleLoaded = failConf?.bundle && assetManager.getBundle(failConf.bundle) ? 1 : 0;
|
|
const battle = gg.game?.CurentBattle;
|
|
const memPf = this._memWarnFailPf();
|
|
PfDiagnostics.write(
|
|
'SETTLE',
|
|
'OPEN_FAIL',
|
|
`panel=${panel}|id=${id}|bundle=${bundleLoaded}|w=${battle?.CurentWaveNum ?? 0}|ch=${battle?.ChapterId ?? 0}|${memPf}`,
|
|
600000,
|
|
'error',
|
|
);
|
|
console.error('[SettleOpenFail]', JSON.stringify({
|
|
panel,
|
|
id,
|
|
bundleLoaded,
|
|
wave: battle?.CurentWaveNum,
|
|
chapter: battle?.ChapterId,
|
|
...this._memWarnFailFields(),
|
|
}));
|
|
if (battle) {
|
|
gg.sdk.reportDY(
|
|
'inLevel',
|
|
`SettleOpenFail-${panel}-id${id}-w${battle.CurentWaveNum}-ch${battle.ChapterId}-bundle${bundleLoaded}-${this._memWarnFailDy()}`,
|
|
);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 清理队列式弹窗队列(切场景/回大厅时避免残留弹窗重复弹出)
|
|
*/
|
|
clearWaitPopQueue() {
|
|
this._wait_popIds.length = 0;
|
|
this._curent_pop = null;
|
|
}
|
|
|
|
|
|
getHaveWaitPopLayer() {
|
|
return this._wait_popIds.length > 0;
|
|
}
|
|
|
|
/**
|
|
* 显示加载提示
|
|
*/
|
|
async showWaitIcon() {
|
|
if (!this._waitIcon) {
|
|
let conf = null;
|
|
for (let k in this.uiConfigs) {
|
|
let v = this.uiConfigs[k];
|
|
if (v.name == "waitIcon") {
|
|
conf = v;
|
|
break;
|
|
}
|
|
}
|
|
if (conf == null)
|
|
return;
|
|
let p = (await this.loadPrefab(conf.bundle, conf.path + conf.name)).prefab;
|
|
if (!p) {
|
|
return;
|
|
}
|
|
this._waitIcon = instantiate(p);
|
|
}
|
|
this.showNode(this._waitIcon, UILayer.Loading);
|
|
}
|
|
|
|
/**
|
|
* 关闭加载提示
|
|
*/
|
|
closeWaitIcon() {
|
|
if (this._waitIcon) {
|
|
this._waitIcon.removeFromParent();
|
|
}
|
|
}
|
|
private _waitIcon: Node = null;
|
|
//显示消息提示,不带自动翻译的
|
|
|
|
/**
|
|
* 显示消息提示,带自动翻译的
|
|
* @param msg 消息
|
|
* @param time 显示时间(秒)
|
|
*/
|
|
async showToast(msg: string, isAutoTranslate: boolean = true, time: number = 1,addPosy: number = 0) {
|
|
//翻译消息
|
|
if (isAutoTranslate) {
|
|
msg = gg.lang.get(msg);
|
|
}
|
|
|
|
if (!msg || msg == "") {
|
|
console.log("showToast msg is empty");
|
|
return;
|
|
}
|
|
|
|
let n: Node = null;
|
|
if (this._msgPool.length > 0) {
|
|
n = this._msgPool.pop();
|
|
} else {
|
|
let conf = null;
|
|
for (let k in this.uiConfigs) {
|
|
let v = this.uiConfigs[k];
|
|
if (v.name == "msg") {
|
|
conf = v;
|
|
break;
|
|
}
|
|
}
|
|
if (conf == null)
|
|
return;
|
|
let p = (await this.loadPrefab(conf.bundle, conf.path + conf.name)).prefab;
|
|
if (!p) {
|
|
return;
|
|
}
|
|
n = instantiate(p);
|
|
}
|
|
n.getComponentInChildren(RichText)!.string = `<b>${msg}</b>`;
|
|
n.getComponentInChildren(Label)!.string = `${msg}`;
|
|
this.showNode(n, UILayer.Ad);
|
|
n.setPosition(0, 0, 0);
|
|
if(addPosy>0){
|
|
n.y += addPosy;
|
|
}
|
|
|
|
tween(n)
|
|
.set({ opacity: 255 })
|
|
.delay(time)
|
|
.by(0.5, { position: v3(0, 100, 0) })
|
|
.call(() => {
|
|
tween(n).to(0.5, { opacity: 0 }).start();
|
|
})
|
|
.by(0.25, { position: v3(0, 50, 0) })
|
|
.call(() => {
|
|
this._msgPool.push(n);
|
|
Tween.stopAllByTarget(n);
|
|
n.removeFromParent();
|
|
let ind = this._msgShowPool.indexOf(n);
|
|
if (ind >= 0)
|
|
this._msgShowPool.splice(ind, 1);
|
|
}).start();
|
|
this._msgShowPool.forEach(x => x.y += 50);
|
|
this._msgShowPool.push(n);
|
|
}
|
|
private _msgPool: Node[] = [];
|
|
private _msgShowPool: Node[] = [];
|
|
|
|
/**
|
|
* 显示动画(显示到UI管理的指定层级)
|
|
* @param n 动画节点
|
|
*/
|
|
showEff(n: Node) {
|
|
this.showNode(n, UILayer.Anim);
|
|
}
|
|
|
|
showLog(str = "", clear = false) {
|
|
if (this._log) {
|
|
this._log.active = true;
|
|
let log = this._log.getChildByName("log").getComponent(Label);
|
|
log.string = clear ? str : log.string + "\n" + str;
|
|
if (this._timer_flag)
|
|
clearInterval(this._timer_flag);
|
|
this._timer_flag = setInterval(this.updateDC.bind(this), 200);
|
|
}
|
|
}
|
|
|
|
closeLog() {
|
|
if (this._log) {
|
|
this._log.active = false;
|
|
}
|
|
}
|
|
|
|
updateDC() {
|
|
if (this._log && this._log.active) {
|
|
let dc = this._log.getChildByName("dc").getComponent(Label);
|
|
dc.string = "当前dc:" + director.root.device.numDrawCalls;
|
|
}
|
|
}
|
|
|
|
private async showPopToLayer() {
|
|
if (this._curent_pop != null) {
|
|
const popId = this._curent_pop.id;
|
|
// 弹窗仍在显示或正在 openPanel 异步加载时阻塞队列,避免加载中被误判为已结束
|
|
if (this.panelIsShow(popId) || this._panelOpenInflight.has(popId)) {
|
|
return;
|
|
}
|
|
this._curent_pop = null;
|
|
}
|
|
|
|
if (this._wait_popIds.length > 0) {
|
|
this._curent_pop = this._wait_popIds.shift();
|
|
let id = this._curent_pop.id;
|
|
let param = this._curent_pop.param;
|
|
if (id === SKILL_SELECT_PANEL_ID && this.isSkillSelectTreasureOpen(param)) {
|
|
this._logSkillSelectTrace('dequeue_start_load', param);
|
|
}
|
|
this.openPanel(id, param);
|
|
}
|
|
}
|
|
|
|
private addUILayer(parent: Node, name: string, layer: UILayer = null) {
|
|
let layerNode = new Node(name);
|
|
parent.addChild(layerNode);
|
|
//layerNode.addComponent(Mask);
|
|
let w = layerNode.addComponent(Widget);
|
|
w.isAlignLeft = w.isAlignTop = w.isAlignRight = w.isAlignBottom = true;
|
|
w.left = w.top = w.right = w.bottom = 0;
|
|
w.updateAlignment();
|
|
if (layer) {
|
|
this._layer_map.set(layer, layerNode);
|
|
}
|
|
return layerNode;
|
|
}
|
|
|
|
private _extractLoadErr(err: unknown): string {
|
|
if (!err) {
|
|
return 'unknown';
|
|
}
|
|
if (typeof err === 'string') {
|
|
return err.slice(0, 120);
|
|
}
|
|
if (err instanceof Error) {
|
|
return err.message.slice(0, 120);
|
|
}
|
|
const anyErr = err as { message?: string };
|
|
if (anyErr.message) {
|
|
return String(anyErr.message).slice(0, 120);
|
|
}
|
|
try {
|
|
return String(err).slice(0, 120);
|
|
} catch {
|
|
return 'unknown';
|
|
}
|
|
}
|
|
|
|
private _formatPrefabLoadDetail(result: LoadPrefabResult, retry = false, bundleReload = false): string {
|
|
const parts: string[] = [];
|
|
if (retry) {
|
|
parts.push('retry=1');
|
|
}
|
|
if (bundleReload) {
|
|
parts.push('reload=1');
|
|
}
|
|
parts.push(`stage=${result.stage ?? 'unknown'}`);
|
|
parts.push(`err=${this._sanitizeSkillSelectReportSegment(result.err ?? 'unknown', 80)}`);
|
|
return parts.join('|');
|
|
}
|
|
|
|
/** SkillSelect 加载失败:卸分包后重新 loadBundle + load(仅失败路径调用) */
|
|
private async _reloadBundleAndLoadPrefab(bundleName: string, assetPath: string): Promise<LoadPrefabResult> {
|
|
try {
|
|
const bundle = assetManager.getBundle(bundleName);
|
|
if (bundle) {
|
|
bundle.releaseAll();
|
|
assetManager.removeBundle(bundle);
|
|
}
|
|
} catch (e) {
|
|
console.warn('[UIManager] _reloadBundleAndLoadPrefab removeBundle failed', bundleName, e);
|
|
}
|
|
return this.loadPrefab(bundleName, assetPath);
|
|
}
|
|
|
|
private _cleanupPanelForBundleReload(id: number): void {
|
|
if (!this._ui_map.has(id)) {
|
|
return;
|
|
}
|
|
const uiInfo = this._ui_map.get(id);
|
|
try {
|
|
if (uiInfo.uiNode?.isValid) {
|
|
Tween.stopAllByTarget(uiInfo.uiNode);
|
|
uiInfo.prefabRes?.decRef();
|
|
uiInfo.uiNode.destroy();
|
|
}
|
|
} catch (e) {
|
|
console.warn('[UIManager] _cleanupPanelForBundleReload destroy failed', id, e);
|
|
}
|
|
uiInfo.uiNode = null;
|
|
uiInfo.uiBase = null;
|
|
uiInfo.prefabRes = null;
|
|
this._ui_map.delete(id);
|
|
const fresh = new UIInfo();
|
|
fresh.id = id;
|
|
this._ui_map.set(id, fresh);
|
|
}
|
|
|
|
private async loadPrefab(bundleName: string, assetPath: string): Promise<LoadPrefabResult> {
|
|
let bundle = assetManager.getBundle(bundleName);
|
|
let bundleErr: string | undefined;
|
|
if (!bundle) {
|
|
const bundleResult = await this.loadBundle(bundleName);
|
|
bundle = bundleResult.bundle;
|
|
bundleErr = bundleResult.err;
|
|
}
|
|
if (!bundle) {
|
|
return {
|
|
prefab: null,
|
|
stage: 'bundle',
|
|
err: bundleErr ?? 'bundle_missing',
|
|
};
|
|
}
|
|
const assetResult = await this.loadRes(bundle, assetPath);
|
|
if (!assetResult.asset) {
|
|
return {
|
|
prefab: null,
|
|
stage: 'asset',
|
|
err: assetResult.err ?? 'asset_missing',
|
|
};
|
|
}
|
|
return { prefab: assetResult.asset as Prefab };
|
|
}
|
|
|
|
private async loadBundle(bundleName: string): Promise<{ bundle: AssetManager.Bundle | null; err?: string }> {
|
|
return new Promise((resolve) => {
|
|
assetManager.loadBundle(bundleName, (err, bundle) => {
|
|
if (err) {
|
|
console.log(`加载Bundle:${bundleName}错误`, err);
|
|
resolve({ bundle: null, err: this._extractLoadErr(err) });
|
|
} else {
|
|
resolve({ bundle });
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
private async loadRes(bundle: AssetManager.Bundle, assetPath: string): Promise<{ asset: Asset | null; err?: string }> {
|
|
return new Promise((resolve) => {
|
|
const cached = bundle.get(assetPath, Prefab);
|
|
if (cached) {
|
|
resolve({ asset: cached });
|
|
return;
|
|
}
|
|
bundle.load(assetPath, Prefab, (err, asset) => {
|
|
if (err) {
|
|
console.log(`加载Bundle:${bundle.name},path:${assetPath}资源错误`, err);
|
|
resolve({ asset: null, err: this._extractLoadErr(err) });
|
|
} else if (!asset) {
|
|
resolve({ asset: null, err: 'asset_null' });
|
|
} else {
|
|
resolve({ asset });
|
|
}
|
|
});
|
|
});
|
|
}
|
|
} |