删除hook,框架改动
This commit is contained in:
@@ -832,7 +832,7 @@ var GameABCUtils = (function() {
|
||||
};
|
||||
|
||||
// console.log('📐 注册裁剪区域: 容器=' + containerId +
|
||||
// ', 区域=(' + x + ',' + y + ',' + w + ',' + h + ')');
|
||||
// ', 区域=(' + x + ',' + y + ',' + w + ',' + h + ')');
|
||||
return true;
|
||||
},
|
||||
|
||||
|
||||
@@ -283,7 +283,7 @@ var SpriteManager = (function() {
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log('🎨 SpriteManager: 开始初始化 (重构版 v2.0)');
|
||||
// console.log('🎨 SpriteManager: 开始初始化 (重构版 v2.0)');
|
||||
|
||||
// 检查依赖
|
||||
if (typeof GameABCUtils === 'undefined') {
|
||||
@@ -292,8 +292,8 @@ var SpriteManager = (function() {
|
||||
}
|
||||
|
||||
_isInitialized = true;
|
||||
console.log('✅ SpriteManager: 初始化完成');
|
||||
console.log('ℹ️ SpriteManager: 设计原则 - 精灵由编辑器创建,代码只操作属性');
|
||||
// console.log('✅ SpriteManager: 初始化完成');
|
||||
// console.log('ℹ️ SpriteManager: 设计原则 - 精灵由编辑器创建,代码只操作属性');
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
@@ -95,21 +95,21 @@ var RpcHelper = {
|
||||
}
|
||||
|
||||
// 5. 调试信息输出
|
||||
if (debug || Game_Config.Debugger.isDebugger) {
|
||||
console.log('RpcHelper发送RPC:', {
|
||||
app: app,
|
||||
route: route,
|
||||
rpc: rpc,
|
||||
platform_data: {
|
||||
agentid: data.agentid,
|
||||
gameid: data.gameid,
|
||||
playerid: data.playerid,
|
||||
roomcode: data.roomcode
|
||||
},
|
||||
game_data: gameData,
|
||||
custom_data: customData
|
||||
});
|
||||
}
|
||||
// if (debug || Game_Config.Debugger.isDebugger) {
|
||||
// console.log('RpcHelper发送RPC:', {
|
||||
// app: app,
|
||||
// route: route,
|
||||
// rpc: rpc,
|
||||
// platform_data: {
|
||||
// agentid: data.agentid,
|
||||
// gameid: data.gameid,
|
||||
// playerid: data.playerid,
|
||||
// roomcode: data.roomcode
|
||||
// },
|
||||
// game_data: gameData,
|
||||
// custom_data: customData
|
||||
// });
|
||||
// }
|
||||
|
||||
// 6. 通过框架发送RPC请求
|
||||
Utl.sendData(app, route, rpc, data);
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
// ============================================================
|
||||
// SpineBackendCanvas —— Canvas 2D 渲染后端
|
||||
// 由 SpineMgr 在 WebGL 不可用时使用。逻辑迁自 SpineMgr.updateAndDraw,行为不变。
|
||||
//
|
||||
// 已知性能特征:开启 triangleRendering 后,spine-canvas 对每个三角形都执行一次
|
||||
// clip() 加一次整张图集的 drawImage。mesh 密集的骨架(如 qixingshisanlan 2784
|
||||
// 三角形)在 iOS WKWebView 上会因 clip 叠加非轴对齐变换失去 GPU 快路径而卡顿。
|
||||
// 故本后端仅作回退,主路径见 SpineBackendWebGL。
|
||||
// ============================================================
|
||||
(function(){
|
||||
|
||||
var SpineBackendCanvas = {
|
||||
|
||||
name: "canvas",
|
||||
|
||||
_renderer: null,
|
||||
_ctx: null,
|
||||
|
||||
// 是否可用:spine 运行时须提供 Canvas 2D 的 SkeletonRenderer
|
||||
isAvailable: function() {
|
||||
return !!(window.spine && window.spine.SkeletonRenderer && !window.spine.SceneRenderer);
|
||||
},
|
||||
|
||||
createAssetManager: function(basePath) {
|
||||
return new spine.AssetManager(basePath);
|
||||
},
|
||||
|
||||
// Canvas 后端:骨骼位置归零,实际位置由 drawEntry 的 ctx.translate 控制。
|
||||
// 由 SpineMgr 在 updateWorldTransform 之前调用 —— 之后再改 skeleton.x/y 不会重算
|
||||
// 世界变换,本帧不生效。
|
||||
placeEntry: function(entry) {
|
||||
entry.skeleton.x = 0;
|
||||
entry.skeleton.y = 0;
|
||||
},
|
||||
|
||||
// 每帧开始:取 gameabc 的绘制上下文
|
||||
beginFrame: function() {
|
||||
var ctx = gameabc_face.dc;
|
||||
if (!ctx) return false;
|
||||
this._ctx = ctx;
|
||||
if (!this._renderer) {
|
||||
this._renderer = new spine.SkeletonRenderer(ctx);
|
||||
this._renderer.triangleRendering = true; // ★ 启用三角形渲染以支持 Mesh 网格附件
|
||||
}
|
||||
// 确保 renderer 用的是当前 ctx (gameabc可能重建)
|
||||
this._renderer.ctx = ctx;
|
||||
return true;
|
||||
},
|
||||
|
||||
drawEntry: function(entry) {
|
||||
var ctx = this._ctx;
|
||||
ctx.save();
|
||||
ctx.translate(entry.x, entry.y);
|
||||
ctx.scale(1, -1); // ★ Spine Y-up → Canvas Y-down(scale 已烘焙进 SkeletonJson.scale)
|
||||
this._renderer.draw(entry.skeleton);
|
||||
ctx.restore();
|
||||
},
|
||||
|
||||
endFrame: function() {
|
||||
this._ctx = null;
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof module !== "undefined" && module.exports) {
|
||||
module.exports = SpineBackendCanvas;
|
||||
}
|
||||
window.SpineBackendCanvas = SpineBackendCanvas;
|
||||
|
||||
})();
|
||||
@@ -0,0 +1,315 @@
|
||||
// ============================================================
|
||||
// SpineBackendWebGL —— WebGL 渲染后端(主路径)
|
||||
//
|
||||
// 在主画布之上叠加一张透明 canvas 绘制 Spine。相比 Canvas 2D 后端,mesh 密集的
|
||||
// 骨架从"每三角形一次 clip + 整图 drawImage"变为一次 draw call。
|
||||
//
|
||||
// 坐标标定:不逆向混淆的 gameabc.min.js,而是每帧从 ctx.getTransform() 读取
|
||||
// 权威变换矩阵,交由 SpineOverlayTransform 换算成 MVP。
|
||||
//
|
||||
// 真机实测确认的完整链路(gameabc_face.dc 属于一张离屏画布,并非 DOM 中可见的
|
||||
// #canvas,两者是完全不同的两个对象):
|
||||
// Spine 坐标 --ctx.getTransform()--> 离屏画布像素(gameabc_face.dc.canvas,
|
||||
// 实测 backing 1280x720,不在 DOM,getBoundingClientRect() 恒为 0)
|
||||
// --gameabc 内部 blit--> 显示画布 #canvas(DOM 中可见,实测 backing 1912x880)
|
||||
// --CSS--> 屏幕(实测 rect 956x440)
|
||||
//
|
||||
// 因此本文件区分两个几何来源,各司其职、不可合并成一个:
|
||||
// - NDC 分母(buildMvp 的 canvasW/canvasH):取 gameabc_face.dc.canvas 的
|
||||
// width/height —— Spine 坐标是喂给 dc 的,分母必须与其同源。
|
||||
// - 叠加层的定位/CSS 尺寸/drawing buffer:取显示画布 #canvas 的
|
||||
// getBoundingClientRect() 与 width/height(backing) —— 叠加层要盖在屏幕上
|
||||
// 可见的画面上,且 drawing buffer 尺寸须与主画布有效分辨率一致。
|
||||
// 历史教训:曾把两者当成同一个对象 —— 用 #canvas 的尺寸做 NDC 分母则缩放错
|
||||
// (X/Y 倍数不同、宽高比歪);后又改成全部从 dc.canvas 取,结果叠加层去对齐
|
||||
// 离屏画布拿到的 rect(0,0,0,0),_syncSize 每帧提前 return,特效完全不显示。
|
||||
// ============================================================
|
||||
(function(){
|
||||
|
||||
// gameabc 的全局 logmessage 默认走 mode 0,是空操作(见 gameabc.min.js 的实现),
|
||||
// 诊断日志必须直接走 console 才能在浏览器与真机 WebView 里看到。
|
||||
function spineLog(msg) {
|
||||
if (typeof console !== "undefined" && console.log) {
|
||||
console.log(msg);
|
||||
}
|
||||
}
|
||||
function spineWarn(msg) {
|
||||
if (typeof console !== "undefined" && console.warn) {
|
||||
console.warn(msg);
|
||||
}
|
||||
}
|
||||
|
||||
var SpineBackendWebGL = {
|
||||
|
||||
name: "webgl",
|
||||
|
||||
_canvas: null,
|
||||
_gl: null,
|
||||
_context: null, // spine.ManagedWebGLRenderingContext
|
||||
_shader: null,
|
||||
_batcher: null,
|
||||
_renderer: null,
|
||||
_mvp: null, // 复用的 16 元素数组,避免每帧分配
|
||||
_loggedCanvasIdentity: false, // 权威画布诊断日志只打一次,避免每帧刷屏
|
||||
_contextLost: false,
|
||||
_hadVisible: false, // 上一帧是否有可见 entry(用于空闲帧跳过)
|
||||
_assetManager: null,
|
||||
_lossHandlersBound: false, // context lost 监听只绑一次
|
||||
|
||||
// 是否可用:spine 运行时须是 webgl 版(提供 SceneRenderer 等 GL 专有类)
|
||||
isAvailable: function() {
|
||||
return !!(window.spine && window.spine.SceneRenderer && window.SpineOverlayTransform);
|
||||
},
|
||||
|
||||
// (内部) 惰性初始化 GL 资源。失败返回 false,SpineMgr 侧不再绘制。
|
||||
_ensureGL: function() {
|
||||
if (this._gl) return true;
|
||||
|
||||
this._canvas = document.getElementById("spine_gl");
|
||||
if (!this._canvas) {
|
||||
spineWarn("[SpineBackendWebGL] 缺少 spine_gl 元素");
|
||||
return false;
|
||||
}
|
||||
|
||||
var gl = null;
|
||||
try {
|
||||
// alpha:true 让叠加层透出下方主画布;premultipliedAlpha 交由 spine 按图集 pma 标记处理
|
||||
var opts = { alpha: true, premultipliedAlpha: true, antialias: true, depth: false, stencil: false };
|
||||
gl = this._canvas.getContext("webgl", opts) || this._canvas.getContext("experimental-webgl", opts);
|
||||
} catch (err) {
|
||||
spineWarn("[SpineBackendWebGL] getContext 异常: " + err.message);
|
||||
return false;
|
||||
}
|
||||
if (!gl) {
|
||||
spineWarn("[SpineBackendWebGL] 无法取得 WebGL context");
|
||||
return false;
|
||||
}
|
||||
|
||||
this._gl = gl;
|
||||
this._bindContextLossHandlers();
|
||||
|
||||
// context 恢复时复用同一个 ManagedWebGLRenderingContext:
|
||||
// canvas.getContext() 恢复后返回的是同一个 WebGLRenderingContext 对象,
|
||||
// 重新 new 会让新纹理注册到新 managed context,而 _assetManager 仍挂在旧的上,
|
||||
// 造成新旧分叉。shader / batcher / renderer 持有失效的 GPU 资源,则必须重建。
|
||||
if (!this._context) {
|
||||
this._context = new spine.ManagedWebGLRenderingContext(gl);
|
||||
}
|
||||
|
||||
// 复用 _context 的代价:Shader / Mesh 构造时都会 context.addRestorable(this),
|
||||
// 只有 dispose() 才 removeRestorable。每次 context 恢复都会重走本段,
|
||||
// 旧对象若不释放就永久堆在 restorables 里(旧 shader 程序 + batcher 顶点/索引缓冲),
|
||||
// 恢复次数越多占用越大。context 已丢失时 gl.deleteShader/deleteBuffer 按规范是 no-op,
|
||||
// 安全;仍加 try/catch 防御,释放失败不能挡住重建。
|
||||
// (SkeletonRenderer 不持有 GL 资源、无 dispose,不需处理。)
|
||||
if (this._shader) {
|
||||
try {
|
||||
this._shader.dispose();
|
||||
} catch (errShader) {
|
||||
spineWarn("[SpineBackendWebGL] 释放旧 shader 失败(已忽略): " + errShader.message);
|
||||
}
|
||||
this._shader = null;
|
||||
}
|
||||
if (this._batcher) {
|
||||
try {
|
||||
this._batcher.dispose();
|
||||
} catch (errBatcher) {
|
||||
spineWarn("[SpineBackendWebGL] 释放旧 batcher 失败(已忽略): " + errBatcher.message);
|
||||
}
|
||||
this._batcher = null;
|
||||
}
|
||||
|
||||
this._shader = spine.Shader.newTwoColoredTextured(this._context);
|
||||
this._batcher = new spine.PolygonBatcher(this._context);
|
||||
// 中和 blend 模式:spine-canvas 的 drawTriangles 只跟踪 slot.data.blendMode 却从不应用
|
||||
// (该文件全文无 globalCompositeOperation),即迁移前所有槽都是按 source-over 画的。
|
||||
// spine-webgl 的 SkeletonRenderer 会真的调 batcher.setBlendMode(),在"清成透明黑"的
|
||||
// 叠加层上,screen 槽会输出非预乘颜色导致过亮/白边,multiply 槽会因 srcRgb=DST_COLOR
|
||||
// 而 RGB 恒 0 变成纯黑剪影(本项目 start 有 9 个 screen 槽,而 game_start 每局必播)。
|
||||
// additive 在叠加层上恰好等价于 source-over,无差异。
|
||||
// 本次迁移只求性能、不引入视觉变化,故在此中和,保持 begin() 设定的 Normal 混合
|
||||
// (SkeletonRenderer.premultipliedAlpha 默认 false,与 PolygonBatcher 构造时的
|
||||
// SRC_ALPHA / ONE_MINUS_SRC_ALPHA 默认值一致,即"未中和时 Normal 槽会设成的那一组")。
|
||||
// 是否放开真实 blend,留作将来一次独立的、经美术确认的改动。
|
||||
// ⚠️ 必须紧跟每一次 new PolygonBatcher —— context 恢复会重建 batcher,写在别处即失效。
|
||||
this._batcher.setBlendMode = function() {};
|
||||
this._renderer = new spine.SkeletonRenderer(this._context);
|
||||
this._mvp = new Array(16);
|
||||
|
||||
gl.disable(gl.DEPTH_TEST);
|
||||
gl.enable(gl.BLEND);
|
||||
return true;
|
||||
},
|
||||
|
||||
// (内部) WebGL context lost / restored。
|
||||
// iOS 在内存压力或切后台返回时会真实触发;不处理的表现是"特效突然全部消失且不再恢复"。
|
||||
// ⚠️ 只能绑一次:context restored 后 _gl 被置 null,_ensureGL 会重新走一遍,
|
||||
// 若不加此闩,每恢复一次就多挂一对监听器,重复触发且泄漏。
|
||||
_bindContextLossHandlers: function() {
|
||||
if (this._lossHandlersBound) return;
|
||||
this._lossHandlersBound = true;
|
||||
var self = this;
|
||||
this._canvas.addEventListener("webglcontextlost", function(ev){
|
||||
ev.preventDefault(); // 必须 preventDefault,否则不会触发 restored
|
||||
self._contextLost = true;
|
||||
spineWarn("[SpineBackendWebGL] context lost");
|
||||
}, false);
|
||||
this._canvas.addEventListener("webglcontextrestored", function(){
|
||||
spineLog("[SpineBackendWebGL] context restored,重建 GL 资源与纹理");
|
||||
self._contextLost = false;
|
||||
self._gl = null; // 迫使 _ensureGL 重建 shader/batcher/renderer
|
||||
self._hadVisible = false; // 复位空闲帧状态机,避免恢复后多发一次 clear
|
||||
if (self._onContextRestored) self._onContextRestored();
|
||||
}, false);
|
||||
},
|
||||
|
||||
// 由 SpineMgr 注入的纹理重建回调(Task 7 接入)
|
||||
_onContextRestored: null,
|
||||
|
||||
createAssetManager: function(basePath) {
|
||||
if (!this._ensureGL()) {
|
||||
// GL 不可用时不静默降级成"看似成功",直接抛出让 SpineMgr.init 失败,
|
||||
// 由 index.html 的探测脚本保证不会走到这里。
|
||||
throw new Error("[SpineBackendWebGL] GL 初始化失败,无法创建 AssetManager");
|
||||
}
|
||||
this._assetManager = new spine.AssetManager(this._context, basePath);
|
||||
return this._assetManager;
|
||||
},
|
||||
|
||||
// WebGL 后端:位移并入骨骼,使所有 entry 共用同一 MVP,可批量绘制。
|
||||
// y 取负是因为 MVP 中含 Spine Y-up → 画布 Y-down 的翻转(见 SpineOverlayTransform 推导)。
|
||||
placeEntry: function(entry) {
|
||||
entry.skeleton.x = entry.x;
|
||||
entry.skeleton.y = -entry.y;
|
||||
},
|
||||
|
||||
beginFrame: function(visibleCount) {
|
||||
if (this._contextLost) return false;
|
||||
if (!this._ensureGL()) return false;
|
||||
|
||||
// 绘制缓冲画布(NDC 分母来源):Spine 坐标是喂给 dc 的,分母必须与其同源,
|
||||
// 只能每帧从 dc 派生(createAssetManager 阶段 dc 可能还不存在,故不能在
|
||||
// _ensureGL 里固定取一次)。数据源唯一,取不到即报错跳帧,禁止 || 兜底。
|
||||
var ctx = gameabc_face.dc;
|
||||
if (!ctx) return false;
|
||||
var drawCanvas = ctx.canvas;
|
||||
if (!drawCanvas) {
|
||||
spineWarn("[SpineBackendWebGL] gameabc_face.dc.canvas 不存在(非标准 CanvasRenderingContext2D 实现),本帧跳过");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 显示画布(叠加层定位/CSS 尺寸/drawing buffer 来源):index.html 中
|
||||
// <div id="ifastgame"><canvas id="canvas"> 是 gameabc 平台约定的固定
|
||||
// 显示画布契约,并非按 id 猜——但仍须校验存在性/在 DOM/rect 有效,
|
||||
// 任一项不满足都不能静默继续绘制。
|
||||
var displayCanvas = (typeof document !== "undefined" && document.getElementById) ? document.getElementById("canvas") : null;
|
||||
if (!displayCanvas) {
|
||||
spineWarn("[SpineBackendWebGL] 找不到显示画布 document.getElementById(\"canvas\"),本帧跳过");
|
||||
return false;
|
||||
}
|
||||
if (!displayCanvas.parentNode) {
|
||||
spineWarn("[SpineBackendWebGL] 显示画布 #canvas 不在 DOM 中,本帧跳过");
|
||||
return false;
|
||||
}
|
||||
var displayRect = displayCanvas.getBoundingClientRect();
|
||||
if (!displayRect || displayRect.width <= 0 || displayRect.height <= 0) {
|
||||
spineWarn("[SpineBackendWebGL] 显示画布 #canvas 的 rect 尺寸无效,本帧跳过");
|
||||
return false;
|
||||
}
|
||||
|
||||
this._logCanvasIdentityOnce(drawCanvas, displayCanvas, displayRect);
|
||||
|
||||
// 空闲帧跳过:麻将大部分时间无特效在播,不能因叠加层存在就每帧空 clear 全屏。
|
||||
// 仅在从"有"变"无"的那一帧 clear 一次收尾。
|
||||
if (visibleCount === 0) {
|
||||
if (!this._hadVisible) return false;
|
||||
this._hadVisible = false;
|
||||
this._syncSize(displayCanvas);
|
||||
this._gl.clearColor(0, 0, 0, 0);
|
||||
this._gl.clear(this._gl.COLOR_BUFFER_BIT);
|
||||
return false;
|
||||
}
|
||||
this._hadVisible = true;
|
||||
|
||||
if (!this._syncSize(displayCanvas)) return false;
|
||||
|
||||
var m = this._readTransform(ctx);
|
||||
SpineOverlayTransform.buildMvp(m, drawCanvas.width, drawCanvas.height, this._mvp);
|
||||
|
||||
var gl = this._gl;
|
||||
gl.clearColor(0, 0, 0, 0);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
|
||||
this._shader.bind();
|
||||
this._shader.setUniformi(spine.Shader.SAMPLER, 0);
|
||||
this._shader.setUniform4x4f(spine.Shader.MVP_MATRIX, this._mvp);
|
||||
this._batcher.begin(this._shader);
|
||||
return true;
|
||||
},
|
||||
|
||||
// (内部) 读取 gameabc 绘制上下文的权威变换矩阵。
|
||||
// getTransform() 自 iOS Safari 11 起支持,远低于本项目已验证的 WebView 基线;
|
||||
// 取不到时退化为单位矩阵(此时叠加层按绘制缓冲画布 dc.canvas 的 backing 尺寸直接映射)。
|
||||
_readTransform: function(ctx) {
|
||||
if (typeof ctx.getTransform === "function") {
|
||||
var t = ctx.getTransform();
|
||||
return { a: t.a, b: t.b, c: t.c, d: t.d, e: t.e, f: t.f };
|
||||
}
|
||||
return { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 };
|
||||
},
|
||||
|
||||
// (内部) 一次性诊断日志:同时记录两个几何来源的身份与数值,便于后续核对
|
||||
// ——绘制缓冲画布(dc.canvas,NDC 分母)与显示画布(#canvas,叠加层定位/尺寸)
|
||||
// 是否为同一个对象、各自的 backing 与显示画布的 rect。
|
||||
_logCanvasIdentityOnce: function(drawCanvas, displayCanvas, displayRect) {
|
||||
if (this._loggedCanvasIdentity) return;
|
||||
this._loggedCanvasIdentity = true;
|
||||
var drawIdDesc = drawCanvas.id ? ("#" + drawCanvas.id) : "(无 id)";
|
||||
spineLog("[SpineBackendWebGL] 绘制缓冲画布(gameabc_face.dc.canvas) = " + drawIdDesc +
|
||||
",backing=" + drawCanvas.width + "x" + drawCanvas.height +
|
||||
";显示画布(#canvas) backing=" + displayCanvas.width + "x" + displayCanvas.height +
|
||||
",rect=(" + displayRect.left + "," + displayRect.top + "," + displayRect.width + "," + displayRect.height + ")" +
|
||||
",两者是否同一对象=" + (drawCanvas === displayCanvas));
|
||||
},
|
||||
|
||||
// (内部) 每帧把叠加层的位置与尺寸镜像到显示画布(#canvas)的实际显示区域。
|
||||
// drawing buffer 尺寸直接取显示画布的 backing(width/height),不再用
|
||||
// rect × devicePixelRatio 计算——与主画布有效分辨率一致即可,用 DPR 会开出
|
||||
// 比主画布 backing 还大的 buffer,白白多占显存。
|
||||
_syncSize: function(displayCanvas) {
|
||||
var rect = displayCanvas.getBoundingClientRect();
|
||||
if (rect.width <= 0 || rect.height <= 0) return false;
|
||||
|
||||
var host = this._canvas.parentNode;
|
||||
host.style.left = rect.left + "px";
|
||||
host.style.top = rect.top + "px";
|
||||
this._canvas.style.width = rect.width + "px";
|
||||
this._canvas.style.height = rect.height + "px";
|
||||
|
||||
var bw = displayCanvas.width;
|
||||
var bh = displayCanvas.height;
|
||||
if (bw <= 0 || bh <= 0) return false;
|
||||
if (this._canvas.width !== bw || this._canvas.height !== bh) {
|
||||
this._canvas.width = bw;
|
||||
this._canvas.height = bh;
|
||||
}
|
||||
this._gl.viewport(0, 0, bw, bh);
|
||||
return true;
|
||||
},
|
||||
|
||||
drawEntry: function(entry) {
|
||||
this._renderer.draw(this._batcher, entry.skeleton);
|
||||
},
|
||||
|
||||
endFrame: function() {
|
||||
this._batcher.end();
|
||||
this._shader.unbind();
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof module !== "undefined" && module.exports) {
|
||||
module.exports = SpineBackendWebGL;
|
||||
}
|
||||
window.SpineBackendWebGL = SpineBackendWebGL;
|
||||
|
||||
})();
|
||||
@@ -5,12 +5,25 @@
|
||||
// ============================================================
|
||||
(function(){
|
||||
|
||||
// gameabc 的全局 logmessage 默认走 mode 0,是空操作(见 gameabc.min.js 的实现),
|
||||
// 诊断日志必须直接走 console 才能在浏览器与真机 WebView 里看到。
|
||||
function spineLog(msg) {
|
||||
if (typeof console !== "undefined" && console.log) {
|
||||
console.log(msg);
|
||||
}
|
||||
}
|
||||
function spineWarn(msg) {
|
||||
if (typeof console !== "undefined" && console.warn) {
|
||||
console.warn(msg);
|
||||
}
|
||||
}
|
||||
|
||||
var SpineMgr = {
|
||||
|
||||
// --- 内部状态 ---
|
||||
_entries: {}, // {id: EntryObject}
|
||||
_assetManager: null,
|
||||
_renderer: null, // spine.SkeletonRenderer (Canvas 2D)
|
||||
_backend: null, // 渲染后端(SpineBackendWebGL / SpineBackendCanvas)
|
||||
_lastTime: 0, // 上一帧时间戳(ms)
|
||||
_inited: false,
|
||||
_loadedPaths: {}, // 已请求加载的资源路径,避免重复请求
|
||||
@@ -27,15 +40,50 @@ var SpineMgr = {
|
||||
// ----------------------------------------------------------
|
||||
init: function(basePath) {
|
||||
if (!window.spine) {
|
||||
logmessage("[SpineMgr] spine-canvas.js 未加载,请检查引用");
|
||||
spineWarn("[SpineMgr] spine 运行时未加载,请检查 index.html 的探测脚本");
|
||||
return;
|
||||
}
|
||||
this._backend = this._selectBackend();
|
||||
if (!this._backend) {
|
||||
spineWarn("[SpineMgr] 无可用渲染后端");
|
||||
return;
|
||||
}
|
||||
spineLog("[SpineMgr] 渲染后端: " + this._backend.name);
|
||||
this._basePath = basePath || this._basePath;
|
||||
this._assetManager = new spine.AssetManager(this._basePath);
|
||||
// 挂接 context lost 恢复回调(仅 WebGL 后端有此机制)
|
||||
var self = this;
|
||||
if (this._backend.name === "webgl") {
|
||||
this._backend._onContextRestored = function() {
|
||||
self._rebuildAfterContextRestored();
|
||||
};
|
||||
}
|
||||
// 后端初始化失败必须显式记录并让 _inited 保持 false,但不能让异常向上抛:
|
||||
// init 由文件末尾的预加载块经 _ensureInit() 调用,异常会中断整个 IIFE,
|
||||
// 导致后面的 gameenddraw 拦截根本不安装 —— Spine 从此永不绘制且无迹可循。
|
||||
// _inited 保持 false 时,各 API 走既有的 if (!this._inited) return 静默路径。
|
||||
try {
|
||||
this._assetManager = this._backend.createAssetManager(this._basePath);
|
||||
} catch (err) {
|
||||
spineWarn("[SpineMgr] 后端初始化失败: " + err.message);
|
||||
this._backend = null;
|
||||
return;
|
||||
}
|
||||
this._lastTime = Date.now();
|
||||
this._inited = true;
|
||||
},
|
||||
|
||||
// (内部) 选定渲染后端。index.html 的探测脚本已决定加载哪套 spine 运行时,
|
||||
// 此处按运行时实际提供的能力对应上后端实现。
|
||||
_selectBackend: function() {
|
||||
if (window.SpineBackendWebGL && SpineBackendWebGL.isAvailable()) {
|
||||
return SpineBackendWebGL;
|
||||
}
|
||||
if (window.SpineBackendCanvas && SpineBackendCanvas.isAvailable()) {
|
||||
return SpineBackendCanvas;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
// (内部) 确保已初始化
|
||||
_ensureInit: function() {
|
||||
if (!this._inited) {
|
||||
@@ -43,6 +91,42 @@ var SpineMgr = {
|
||||
}
|
||||
},
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// (内部) _preloadAll()
|
||||
// 按 gameabc_face.spineAssets 清单,把全部 Spine 资源注入当前 AssetManager 并发起加载。
|
||||
// ⚠️ 必须整批注入,不能只补"当前已存在的 entry":
|
||||
// 规避 file:// 下 XHR CORS 用的 rawDataUris 是 Downloader 实例私有的
|
||||
// (AssetManagerBase 构造时默认 new Downloader()),换一个新 AssetManager
|
||||
// 就等于换了一份空 rawDataUris。若只补已存在的 entry,context 恢复之后
|
||||
// 首次触发的新特效(如天胡)会走真实 XHR 被 CORS 拦掉,_tryBuild 里 require()
|
||||
// 抛异常 → ready 恒 false → 每帧重试且永不显示。
|
||||
// 返回本次注册的资源组数。
|
||||
// ----------------------------------------------------------
|
||||
_preloadAll: function() {
|
||||
var list = gameabc_face.spineAssets;
|
||||
if (!list || list.length === 0) return 0;
|
||||
var textData = gameabc_face.spineTextData || {};
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
var name = list[i];
|
||||
var jsonKey = name + ".json";
|
||||
var atlasKey = name + ".atlas";
|
||||
// 将嵌入文本数据注册为 rawDataURI,Spine Downloader 会优先从内存读取
|
||||
if (textData[jsonKey]) {
|
||||
this._assetManager.setRawDataURI(jsonKey, "data:," + textData[jsonKey]);
|
||||
}
|
||||
if (textData[atlasKey]) {
|
||||
this._assetManager.setRawDataURI(atlasKey, "data:," + textData[atlasKey]);
|
||||
}
|
||||
this._assetManager.loadText(jsonKey);
|
||||
this._loadedPaths[jsonKey] = true;
|
||||
this._assetManager.loadTextureAtlas(atlasKey);
|
||||
this._loadedPaths[atlasKey] = true;
|
||||
}
|
||||
spineLog("[SpineMgr] 预加载 " + list.length + " 组 Spine 资源" +
|
||||
(Object.keys(textData).length > 0 ? "(使用嵌入数据)" : "(使用网络请求)"));
|
||||
return list.length;
|
||||
},
|
||||
|
||||
// (内部) 自动加载:根据 id 约定文件名 id.json / id.atlas
|
||||
// 主流 SkeletonJson.scale 方案:从 _scaleMap 读取目标缩放,传入 load() 在解析时烘焙
|
||||
_autoLoad: function(id) {
|
||||
@@ -101,16 +185,45 @@ var SpineMgr = {
|
||||
visible: true,
|
||||
ready: false,
|
||||
_hideOnComplete: false, // 播放完成后是否自动隐藏
|
||||
_hideAfterCompletes: 0 // 剩余多少次 complete 后触发隐藏
|
||||
_hideAfterCompletes: 0, // 剩余多少次 complete 后触发隐藏
|
||||
_buildFailLogged: false // 构建失败日志只记一次(_tryBuild 每帧调用)
|
||||
};
|
||||
},
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// ensureEntry(id, jsonFile, atlasFile, option)
|
||||
// 确保 id 对应的 entry 存在并达到 option 指定的状态。
|
||||
// entry 生命周期归 SpineMgr 管,调用方无需掌握"不在就 load、在就 setScale"的内部规则,
|
||||
// 也无需从 id 推导文件名(entryId 与资源文件名不一定同名,如 ting_0 -> ting.json)。
|
||||
// ----------------------------------------------------------
|
||||
ensureEntry: function(id, jsonFile, atlasFile, option) {
|
||||
var e = this._entries[id];
|
||||
if (!e) {
|
||||
this.load(id, jsonFile, atlasFile, option);
|
||||
return;
|
||||
}
|
||||
if (option && option.scale !== undefined) {
|
||||
this.setScale(id, option.scale);
|
||||
}
|
||||
},
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// isReady() 后端是否已就绪、能真正播放 Spine
|
||||
// 探测阶段的 canvas 兜底只覆盖"WebGL 不可用";若探测通过但 GL 初始化失败
|
||||
// (叠加层 DOM 缺失 / 并发 context 耗尽 / shader 编译失败),此时 canvas 运行时
|
||||
// 并未加载、回不去,调用方应据此走各自既有的帧动画降级路径,
|
||||
// 而不是让 Spine 静默失效却以为播成功了。
|
||||
// ----------------------------------------------------------
|
||||
isReady: function() {
|
||||
return !!this._inited;
|
||||
},
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// (内部) 资源加载完成后实例化骨骼
|
||||
// 同一 jsonFile+scale 的 SkeletonData 只解析一次,多个 entry 共享
|
||||
// ----------------------------------------------------------
|
||||
_buildEntry: function(entry) {
|
||||
logmessage("[SpineMgr] _buildEntry: " + entry._id + " skelJson.scale=" + entry.scale);
|
||||
spineLog("[SpineMgr] _buildEntry: " + entry._id + " skelJson.scale=" + entry.scale);
|
||||
var cacheKey = entry.jsonFile + "_s" + entry.scale;
|
||||
var skelData = this._skelDataCache[cacheKey];
|
||||
if (!skelData) {
|
||||
@@ -122,9 +235,9 @@ var SpineMgr = {
|
||||
this._assetManager.require(entry.jsonFile)
|
||||
);
|
||||
this._skelDataCache[cacheKey] = skelData;
|
||||
logmessage("[SpineMgr] SkeletonData 解析并缓存: " + cacheKey);
|
||||
spineLog("[SpineMgr] SkeletonData 解析并缓存: " + cacheKey);
|
||||
} else {
|
||||
logmessage("[SpineMgr] SkeletonData 命中缓存: " + cacheKey);
|
||||
spineLog("[SpineMgr] SkeletonData 命中缓存: " + cacheKey);
|
||||
}
|
||||
entry.skeleton = new spine.Skeleton(skelData);
|
||||
|
||||
@@ -179,10 +292,15 @@ var SpineMgr = {
|
||||
e._id = id;
|
||||
try {
|
||||
this._buildEntry(e);
|
||||
logmessage("[SpineMgr] " + id + " 构建完成");
|
||||
spineLog("[SpineMgr] " + id + " 构建完成");
|
||||
this._flushCmds(id);
|
||||
} catch(err) {
|
||||
logmessage("[SpineMgr] " + id + " 构建失败: " + err.message);
|
||||
// 重试本身必须保留(资源可能只是还没到齐),但日志只记一次:
|
||||
// 本方法每帧调用,持续失败时会以约 60 条/秒刷屏并淹没其他日志。
|
||||
if (!e._buildFailLogged) {
|
||||
e._buildFailLogged = true;
|
||||
spineWarn("[SpineMgr] " + id + " 构建失败(仅记一次): " + err.message);
|
||||
}
|
||||
e.ready = false;
|
||||
}
|
||||
}
|
||||
@@ -190,6 +308,79 @@ var SpineMgr = {
|
||||
return true;
|
||||
},
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// (内部) WebGL context 恢复后重建全部 GPU 资源
|
||||
// context lost 会让 AssetManager 中缓存的 GLTexture 与 shader 一并作废,
|
||||
// 必须重新加载资源并重建所有 entry,否则表现为"特效再也不显示"。
|
||||
// ----------------------------------------------------------
|
||||
_rebuildAfterContextRestored: function() {
|
||||
spineLog("[SpineMgr] 重建 Spine 资源(context restored)");
|
||||
|
||||
// 记录当前 entry 的声明式状态,重建后按原样恢复
|
||||
var saved = [];
|
||||
for (var id in this._entries) {
|
||||
var e = this._entries[id];
|
||||
saved.push({
|
||||
id: id,
|
||||
jsonFile: e.jsonFile,
|
||||
atlasFile: e.atlasFile,
|
||||
x: e.x,
|
||||
y: e.y,
|
||||
scale: e.scale,
|
||||
skin: e.skin,
|
||||
mixDur: e.mixDur
|
||||
});
|
||||
}
|
||||
|
||||
// 丢弃全部缓存:SkeletonData 持有已作废的 GLTexture 引用,不能复用
|
||||
this._entries = {};
|
||||
this._pendingCmds = {};
|
||||
this._skelDataCache = {};
|
||||
this._loadedPaths = {};
|
||||
|
||||
// 先释放旧 AssetManager 再换新的:GLTexture 构造时会 context.addRestorable(this),
|
||||
// 只有 dispose() 才 removeRestorable。本后端刻意复用同一个 ManagedWebGLRenderingContext
|
||||
// (见 SpineBackendWebGL._ensureGL 注释),若不释放,每恢复一次就把 23 页图集的
|
||||
// GLTexture 永久留在 restorables 里(解码位图约 137MB)—— 而 context lost 的诱因
|
||||
// 正是内存压力,泄漏会把 lost/restore 变成越恢复越糟的死循环。
|
||||
// context 已丢失时 gl.deleteTexture 等按规范是 no-op,安全;仍加 try/catch 防御,
|
||||
// 释放失败不能挡住后面的重建。
|
||||
if (this._assetManager) {
|
||||
try {
|
||||
this._assetManager.dispose();
|
||||
} catch (errDispose) {
|
||||
spineWarn("[SpineMgr] 释放旧 AssetManager 失败(已忽略): " + errDispose.message);
|
||||
}
|
||||
this._assetManager = null;
|
||||
}
|
||||
|
||||
// 本方法在 webglcontextrestored 事件回调中执行,异常会逸出到事件循环,
|
||||
// 故与 init 同样显式捕获:重建失败就停在不可用状态并留下日志。
|
||||
try {
|
||||
this._assetManager = this._backend.createAssetManager(this._basePath);
|
||||
} catch (err) {
|
||||
spineWarn("[SpineMgr] context 恢复后重建 AssetManager 失败: " + err.message);
|
||||
this._inited = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 整批重注入 rawDataURI 并重新发起加载:新 AssetManager = 新 Downloader = 空 rawDataUris,
|
||||
// 只补 saved 里的资源会让恢复后首次触发的新特效在 file:// 下被 CORS 拦死(见 _preloadAll 注释)。
|
||||
// _preloadAll 会把全部资源写进 _loadedPaths,故下面按 saved 重建 entry 时
|
||||
// load() 的 if (!this._loadedPaths[...]) 判断会正确跳过重复请求。
|
||||
this._preloadAll();
|
||||
|
||||
for (var i = 0; i < saved.length; i++) {
|
||||
var s = saved[i];
|
||||
this.load(s.id, s.jsonFile, s.atlasFile, {
|
||||
x: s.x, y: s.y, scale: s.scale, skin: s.skin, mixDuration: s.mixDur
|
||||
});
|
||||
// 重建后默认不可见:正在播的动画已随 context 丢失,
|
||||
// 强行续播会出现半截动画,交由下一次业务触发重新播放。
|
||||
this._entries[s.id].visible = false;
|
||||
}
|
||||
},
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// updateAndDraw(ctx) 每帧自动调用
|
||||
// ctx: gameabc_face.dc (Canvas 2D Context)
|
||||
@@ -203,29 +394,33 @@ var SpineMgr = {
|
||||
this._lastTime = now;
|
||||
if (dt <= 0 || dt > 0.5) dt = 1/30;
|
||||
|
||||
if (!this._renderer) {
|
||||
this._renderer = new spine.SkeletonRenderer(ctx);
|
||||
this._renderer.triangleRendering = true; // ★ 启用三角形渲染以支持 Mesh 网格附件
|
||||
}
|
||||
// 确保 renderer 用的是当前 ctx (gameabc可能重建)
|
||||
this._renderer.ctx = ctx;
|
||||
|
||||
// 先推进动画状态:即使本帧无法绘制(后端未就绪 / context lost),
|
||||
// 动画时间轴与 complete 事件也必须照常推进,否则回调链会卡死。
|
||||
var visibleList = [];
|
||||
for (var id in this._entries) {
|
||||
var e = this._entries[id];
|
||||
if (!e.ready || !e.visible) continue;
|
||||
|
||||
e.state.update(dt);
|
||||
e.state.apply(e.skeleton);
|
||||
// 骨骼位置归零,由 ctx.translate 控制实际位置
|
||||
e.skeleton.x = 0;
|
||||
e.skeleton.y = 0;
|
||||
// 骨骼位置由后端决定:Canvas 后端归零(用 ctx.translate 定位),
|
||||
// WebGL 后端写入 x/-y(位移并入骨骼,使所有 entry 共用同一 MVP)。
|
||||
// 必须在 updateWorldTransform 之前设置,否则不生效于本帧。
|
||||
this._backend.placeEntry(e);
|
||||
e.skeleton.updateWorldTransform(spine.Physics.update);
|
||||
visibleList.push(e);
|
||||
}
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(e.x, e.y);
|
||||
ctx.scale(1, -1); // ★ Spine Y-up → Canvas Y-down(scale 已烘焙进 SkeletonJson.scale)
|
||||
this._renderer.draw(e.skeleton);
|
||||
ctx.restore();
|
||||
if (!this._backend.beginFrame(visibleList.length)) return;
|
||||
// endFrame 必须配对执行:WebGL 后端的 PolygonBatcher 只在 endFrame 里 end(),
|
||||
// 若 drawEntry 抛出而漏了 end(),batcher.isDrawing 会永久停在 true,
|
||||
// 下一帧 begin() 即抛 "PolygonBatch is already drawing" 并冒到 gameenddraw —— Spine 永久黑屏。
|
||||
try {
|
||||
for (var i = 0; i < visibleList.length; i++) {
|
||||
this._backend.drawEntry(visibleList[i]);
|
||||
}
|
||||
} finally {
|
||||
this._backend.endFrame();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -281,10 +476,13 @@ var SpineMgr = {
|
||||
return;
|
||||
}
|
||||
if (e.scale === sx) return; // 已是目标 scale,无需重建
|
||||
// 目标 scale 与已烘焙 scale 不同 → 清除旧 entry,下次 _autoLoad 将以新 scale 重建
|
||||
// 目标 scale 与已烘焙 scale 不同 → 清除旧 entry 并以新 scale 重建。
|
||||
// 文件名必须取自 entry(load 时记录的权威值):entryId 与资源文件名不一定同名
|
||||
// (如 entryId=ting_0 对应 ting.json),按 id 推导会去加载不存在的文件。
|
||||
var savedX = e.x, savedY = e.y;
|
||||
var savedJson = e.jsonFile, savedAtlas = e.atlasFile;
|
||||
this.remove(id);
|
||||
this.load(id, id + ".json", id + ".atlas", { scale: sx, x: savedX, y: savedY });
|
||||
this.load(id, savedJson, savedAtlas, { scale: sx, x: savedX, y: savedY });
|
||||
},
|
||||
|
||||
setFlip: function(id, flipX, flipY) {
|
||||
@@ -423,26 +621,16 @@ gameabc_face.spineMgr = SpineMgr;
|
||||
// 彻底避免 file:// 协议下 XHR CORS 拦截问题
|
||||
if (gameabc_face.spineAssets && gameabc_face.spineAssets.length > 0) {
|
||||
SpineMgr._ensureInit();
|
||||
var list = gameabc_face.spineAssets;
|
||||
var textData = gameabc_face.spineTextData || {};
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
var name = list[i];
|
||||
var jsonKey = name + ".json";
|
||||
var atlasKey = name + ".atlas";
|
||||
// 将嵌入文本数据注册为 rawDataURI,Spine Downloader 会优先从内存读取
|
||||
if (textData[jsonKey]) {
|
||||
SpineMgr._assetManager.setRawDataURI(jsonKey, "data:," + textData[jsonKey]);
|
||||
}
|
||||
if (textData[atlasKey]) {
|
||||
SpineMgr._assetManager.setRawDataURI(atlasKey, "data:," + textData[atlasKey]);
|
||||
}
|
||||
SpineMgr._assetManager.loadText(jsonKey);
|
||||
SpineMgr._loadedPaths[jsonKey] = true;
|
||||
SpineMgr._assetManager.loadTextureAtlas(atlasKey);
|
||||
SpineMgr._loadedPaths[atlasKey] = true;
|
||||
if (!SpineMgr._inited) {
|
||||
// 后端未就绪:跳过预加载,但必须继续往下走完 gameenddraw 拦截的安装。
|
||||
// 此处不能 return —— 预加载块在 IIFE 函数体内,return 会退出整个 IIFE,
|
||||
// 反而导致拦截装不上,正是本守卫要避免的后果。
|
||||
// 回归锁:codes/test/node/spineMgr.gameEndDrawGuard.test.js
|
||||
spineWarn("[SpineMgr] 后端未就绪,跳过 Spine 资源预加载");
|
||||
} else {
|
||||
// 与 context 恢复后的重注入共用同一份实现,避免两处清单分叉
|
||||
SpineMgr._preloadAll();
|
||||
}
|
||||
logmessage("[SpineMgr] 预加载 " + list.length + " 组 Spine 资源" +
|
||||
(Object.keys(textData).length > 0 ? "(使用嵌入数据)" : "(使用网络请求)"));
|
||||
}
|
||||
|
||||
// ★ 用 defineProperty 拦截 gameenddraw 赋值
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
// ============================================================
|
||||
// SpineOverlayTransform —— WebGL 叠加层坐标标定(纯函数)
|
||||
//
|
||||
// 职责:把 gameabc 主画布的权威变换矩阵换算成 WebGL 的 MVP 矩阵。
|
||||
// 无副作用、无 DOM 依赖,可独立单测。
|
||||
//
|
||||
// 为什么不逆向 gameabc.min.js:那是混淆代码(变量名全打乱),读不出它内部如何
|
||||
// 映射坐标。而 Spine 绘制发生在 gameenddraw(gameabc 绘制流程末尾),此刻 ctx
|
||||
// 上已带着 gameabc 设好的全局变换 —— 直接向 ctx 要矩阵即可,权威、且 gameabc
|
||||
// 后续改缩放/响应旋转时自动跟随。
|
||||
//
|
||||
// ⚠️ 真机实测确认:Spine 实际画在 gameabc_face.dc.canvas 这张离屏画布上
|
||||
// (backing 约 1280x720,不在 DOM),gameabc 再把它 blit 到 DOM 中可见的显示
|
||||
// 画布 #canvas(backing 约 1912x880),最后经 CSS 缩放到屏幕。本函数的
|
||||
// canvasW/canvasH 必须传绘制缓冲画布(dc.canvas)的 backing 尺寸,因为
|
||||
// Spine 坐标正是喂给这张画布的,分母须与其同源;叠加层自身的定位/CSS 尺寸/
|
||||
// drawing buffer 则是另一个来源(显示画布 #canvas),由调用方
|
||||
// SpineBackendWebGL 负责,不在本函数职责内。
|
||||
//
|
||||
// 推导(Spine 世界坐标 Y-up,画布 Y-down):
|
||||
// 位移由调用方经 skeleton.x = e.x; skeleton.y = -e.y 承担,本矩阵只处理
|
||||
// scale(1,-1) 与投影,等价于 Canvas 后端的 ctx.translate(e.x,e.y)+ctx.scale(1,-1)。
|
||||
//
|
||||
// 1) Spine 世界 → 逻辑坐标: Lx = vx, Ly = -vy
|
||||
// 2) 逻辑 → 画布像素: Px = a*Lx + c*Ly + e, Py = b*Lx + d*Ly + f
|
||||
// 3) 画布像素 → NDC: x = 2*Px/W - 1, y = 1 - 2*Py/H
|
||||
//
|
||||
// 合并:
|
||||
// x_ndc = (2a/W)*vx + (-2c/W)*vy + (2e/W - 1)
|
||||
// y_ndc = (-2b/H)*vx + ( 2d/H)*vy + (1 - 2f/H)
|
||||
//
|
||||
// 注意:叠加层 drawing buffer 尺寸在推导中被约掉(gl.viewport 覆盖整个 buffer,
|
||||
// NDC 自动铺满),故本矩阵只依赖 ctx 变换与绘制缓冲画布 backing 尺寸。因此所有
|
||||
// entry 共用同一个 MVP,可一次 batcher.begin/end 批量绘制。
|
||||
// ============================================================
|
||||
(function(){
|
||||
|
||||
var SpineOverlayTransform = {
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// buildMvp(m, canvasW, canvasH, out)
|
||||
// m : {a,b,c,d,e,f} —— ctx.getTransform() 的六元素
|
||||
// canvasW : 绘制缓冲画布(gameabc_face.dc.canvas)的 backing 宽(实测约 1280)
|
||||
// canvasH : 绘制缓冲画布(gameabc_face.dc.canvas)的 backing 高(实测约 720)
|
||||
// out : 可选,长度 16 的复用数组(避免每帧分配)
|
||||
// 返回列主序 4x4 矩阵
|
||||
// ----------------------------------------------------------
|
||||
buildMvp: function(m, canvasW, canvasH, out) {
|
||||
var r = out || new Array(16);
|
||||
|
||||
var sx = 2 / canvasW;
|
||||
var sy = 2 / canvasH;
|
||||
|
||||
// 第 0 列:vx 的系数
|
||||
r[0] = sx * m.a;
|
||||
r[1] = -sy * m.b;
|
||||
r[2] = 0;
|
||||
r[3] = 0;
|
||||
|
||||
// 第 1 列:vy 的系数
|
||||
r[4] = -sx * m.c;
|
||||
r[5] = sy * m.d;
|
||||
r[6] = 0;
|
||||
r[7] = 0;
|
||||
|
||||
// 第 2 列:z 保持
|
||||
r[8] = 0;
|
||||
r[9] = 0;
|
||||
r[10] = 1;
|
||||
r[11] = 0;
|
||||
|
||||
// 第 3 列:平移
|
||||
r[12] = sx * m.e - 1;
|
||||
r[13] = 1 - sy * m.f;
|
||||
r[14] = 0;
|
||||
r[15] = 1;
|
||||
|
||||
return r;
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof module !== "undefined" && module.exports) {
|
||||
module.exports = SpineOverlayTransform;
|
||||
}
|
||||
// 友乐运行时有 module 无 require 且共享全局作用域,须无条件暴露全局名
|
||||
window.SpineOverlayTransform = SpineOverlayTransform;
|
||||
|
||||
})();
|
||||
@@ -810,7 +810,7 @@ var AnimationManager = AnimationManager || {};
|
||||
}
|
||||
|
||||
_initialized = true;
|
||||
console.log('[AnimationManager] Initialized (v2.0.0 optimized)');
|
||||
// console.log('[AnimationManager] Initialized (v2.0.0 optimized)');
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -164,7 +164,7 @@ var AudioManager = AudioManager || {};
|
||||
return;
|
||||
}
|
||||
_initialized = true;
|
||||
console.log('[AudioManager] Initialized (v4.0.0)');
|
||||
// console.log('[AudioManager] Initialized (v4.0.0)');
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -80,7 +80,7 @@ var EventBus = {
|
||||
};
|
||||
|
||||
this._listeners[eventName].push(listener);
|
||||
console.log('✅ EventBus: 订阅事件', eventName);
|
||||
// console.log('✅ EventBus: 订阅事件', eventName);
|
||||
|
||||
// 返回取消订阅函数
|
||||
var self = this;
|
||||
@@ -121,7 +121,7 @@ var EventBus = {
|
||||
// 如果没有指定callback,取消该事件的所有监听器
|
||||
if (!callback) {
|
||||
delete this._listeners[eventName];
|
||||
console.log('✅ EventBus: 取消所有', eventName, '监听器');
|
||||
// console.log('✅ EventBus: 取消所有', eventName, '监听器');
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ var EventBus = {
|
||||
for (var i = listeners.length - 1; i >= 0; i--) {
|
||||
if (listeners[i].callback === callback) {
|
||||
listeners.splice(i, 1);
|
||||
console.log('✅ EventBus: 取消', eventName, '监听器');
|
||||
// console.log('✅ EventBus: 取消', eventName, '监听器');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -152,7 +152,7 @@ var EventBus = {
|
||||
var listeners = this._listeners[eventName].slice(); // 复制数组,避免迭代中修改
|
||||
var count = 0;
|
||||
|
||||
console.log('📡 EventBus: 触发事件', eventName, data);
|
||||
// console.log('📡 EventBus: 触发事件', eventName, data);
|
||||
|
||||
for (var i = 0; i < listeners.length; i++) {
|
||||
try {
|
||||
@@ -204,7 +204,7 @@ var EventBus = {
|
||||
*/
|
||||
clear: function() {
|
||||
this._listeners = {};
|
||||
console.log('✅ EventBus: 已清空所有监听器');
|
||||
// console.log('✅ EventBus: 已清空所有监听器');
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -214,7 +214,7 @@ var EventBus = {
|
||||
clearEvent: function(eventName) {
|
||||
if (this._listeners[eventName]) {
|
||||
delete this._listeners[eventName];
|
||||
console.log('✅ EventBus: 已清空', eventName, '的所有监听器');
|
||||
// console.log('✅ EventBus: 已清空', eventName, '的所有监听器');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -79,7 +79,7 @@ UIBootstrap.init = function () {
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log('[UIBootstrap] 开始初始化 UI 系统...');
|
||||
// console.log('[UIBootstrap] 开始初始化 UI 系统...');
|
||||
|
||||
try {
|
||||
if (!this._checkDependencies()) { return false; }
|
||||
@@ -102,7 +102,7 @@ UIBootstrap.init = function () {
|
||||
this._registerViews();
|
||||
|
||||
this.initialized = true;
|
||||
console.log('[UIBootstrap] UI 系统初始化完成,共', Object.keys(this.views).length, '个组件');
|
||||
// console.log('[UIBootstrap] UI 系统初始化完成,共', Object.keys(this.views).length, '个组件');
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('[UIBootstrap] 初始化失败:', err);
|
||||
|
||||
@@ -1,36 +1,38 @@
|
||||
// ============================================================================
|
||||
// 00_SubGame_Config.template.js —— 平台配置项模板(结构由平台定义,子游戏填【值】)
|
||||
// 这是配置文件(C 类,非转发壳)。复制后按本游戏界面/玩法调整各项值。
|
||||
// 严格 ES5。
|
||||
// 游戏配置 (Game Configuration)
|
||||
// ============================================================================
|
||||
|
||||
var Game_Config = Game_Config || {};
|
||||
var Game_Config = Game_Config||{};//相关配置
|
||||
|
||||
|
||||
|
||||
Game_Config.Debugger={//调试配置
|
||||
Game_Config.Debugger={//调试配置
|
||||
isDebugger : true,// debugger模式下会将所有收发的包输出到控制台(正式发布改为false)
|
||||
AutoLogin : true,//debugger模式下是否需要记住登录状态自动登录(正式发布改为true)
|
||||
isSubmitError : false,//是否需要服务器收集错误信息调试时可根据需要(正式发布改为true)
|
||||
visitorLogin : true,//隐藏式游客登录
|
||||
isSubmitError : false,//是否需要服务器收集错误信息调试时可根据需要(正式发布改为true)
|
||||
visitorLogin : true,//隐藏式游客登录
|
||||
visiblePay:true,//审核通过后是否显示支付按钮
|
||||
serverType:1,//0->正式服务器 1->本地服务器 http://ylyxservice1.0791ts.cn/config/update_json.txt
|
||||
gameserver:""//子游戏填自己的更新地址
|
||||
};
|
||||
gameserver:"https://tsgames.daoqi88.cn/config_test/beta_update_jsonv2.txt"+"?"+ifast_random(100000)
|
||||
// gameserver:"https://tsgames.daoqi88.cn/config_test/test_update_jsonv2.txt"+"?"+ifast_random(100000)
|
||||
//gameserver:"https://gameotherwork.ld2sw.cn/update_json/update_json_haiwai.txt"+"?"+ifast_random(100000)
|
||||
// gameserver:"https://tsgames.daoqi88.cn/config/update_jsonv2.txt"+"?"+ifast_random(100000)
|
||||
|
||||
};
|
||||
|
||||
Game_Config.Max = {
|
||||
SumOfRoomtype:3,//创建时房间类型roomtype数组长度
|
||||
SumOfRoomtype:3,//创建时房间类型roomtype数组长度
|
||||
ShowChat:2000,//聊天停留时间
|
||||
PlayerCnt:4,//房间最大人数(按房间最大人数设置)
|
||||
PlayerCnt:4,//房间最大人数
|
||||
group:500,//游戏最大群组号
|
||||
showtime:10,//加载等待最少时间
|
||||
reconnecttime:30000,//唤醒游戏间隔(超过间隔断线重连)
|
||||
showtime:10,//加载等待最少时间
|
||||
reconnecttime:30000,//唤醒游戏间隔(超过间隔断线重连)
|
||||
Mainnickname:10,//主界面玩家昵称显示长度(字符长度)
|
||||
Infonickname:16//个人信息显示长度(字符长度)
|
||||
Infonickname:16//个人信息显示长度(字符长度)
|
||||
};
|
||||
Game_Config.Combat = {
|
||||
height:80,//战绩页单行高度高度
|
||||
up_y:190,//战绩页裁剪y坐标
|
||||
height:80,//战绩页单行高度高度
|
||||
up_y:190,//战绩页裁剪y坐标
|
||||
bannerheight:60,//战绩横幅高度
|
||||
bannery:80,//战绩横幅Y左边(相对48精灵)
|
||||
btnbg:50
|
||||
@@ -42,46 +44,46 @@ Game_Config.Info = {
|
||||
otherPositionDefault:true,//其他主界面玩家信息(昵称、积分)是否采用默认对齐方式对齐居中点为头像中点
|
||||
myPositionDefault:true,//其他主界面玩家信息(昵称、积分)是否采用默认对齐方式对齐居中点为头像中点
|
||||
myPosition:130,//自己信息对齐点(根据游戏界面自行修改)
|
||||
position:[],//其他主界面玩家信息(昵称、积分)居中对齐的x坐标 注意是其他玩家不包括自己从下家开始
|
||||
position:[],//其他主界面玩家信息(昵称、积分)居中对齐的x坐标 注意是其他玩家不包括自己从下家开始
|
||||
//TextContent:["1","2","3","4","5","6","7"],//常用语内容
|
||||
TextContent:["你好","谢谢","再来","快点","不要","加油",":)"],//常用语内容
|
||||
TextContentMp3:["","","","","","",""],//常用语对应音效
|
||||
TextContent:["你好","2","3","4","5","6","7"],//常用语内容
|
||||
TextContentMp3:["","","","","","",""],//常用语对应音效
|
||||
};
|
||||
Game_Config.Share={//分享参数
|
||||
appdownload:"",//下载链接(无需配置,从服务器获取)
|
||||
title:"游戏名",//(分享标题)
|
||||
description:"",//分享描述
|
||||
Game_Config.Share={//分享参数
|
||||
appdownload:"",//下载链接(无需配置,从服务器获取)
|
||||
title:"Test",//(分享标题)
|
||||
description:"hello world",//分享描述
|
||||
gameTitle:"",//游戏中的分享标题模板工程会自动将游戏名字分享出去、不必写在这个变量里
|
||||
gameDescription:""//游戏中分享描述
|
||||
gameDescription:""//游戏中分享描述
|
||||
};
|
||||
Game_Config.Chat={//游戏内聊天配置信息
|
||||
LimitLength:40,//聊天最大长度(字节长度)
|
||||
textwidth:12.5,//聊天显示文字的宽度(未改动聊天显示文字大小无需改动无需修改)
|
||||
ChatDis:[30,12],//聊天气泡与内容的间隔0位置左右间隔(最好不要低于30)1位置上下间隔
|
||||
isLeft:[1,0,0,1,1],//聊天气泡是否为以左边为基准线(坐标/基准按各游戏界面调整)
|
||||
ChatLoc:[[35,517],[1100,315],[1100,105],[175,105],[175,315]]//聊天气泡的基准点位置(注意是基准点位置!)(坐标/基准按各游戏界面调整)
|
||||
isLeft:[1,0,0,1,1],//聊天气泡是否为以左边为基准线
|
||||
ChatLoc:[[35,517],[1100,315],[1100,105],[175,105],[175,315]]//聊天气泡的基准点位置(注意是基准点位置!)
|
||||
|
||||
};
|
||||
Game_Config.Voice={//游戏内聊天配置信息
|
||||
VoiceTime:600,//播放语音动画的时间一般情况无需无需修改
|
||||
VoiceDis:[30,12],//语音气泡与内容的间隔0位置左右间隔(最好不要低于30)1位置上下间隔
|
||||
isLeft:[1,0,0,1,1],//气泡是否为以左边为基准线(坐标/基准按各游戏界面调整)
|
||||
VoiceLoc:[[35,517],[1100,315],[1100,105],[175,105],[175,315]]//气泡的基准点位置(注意是基准点位置!)(坐标/基准按各游戏界面调整)
|
||||
isLeft:[1,0,0,1,1],//气泡是否为以左边为基准线
|
||||
VoiceLoc:[[35,517],[1100,315],[1100,105],[175,105],[175,315]]//气泡的基准点位置(注意是基准点位置!)
|
||||
};
|
||||
Game_Config.Setting={
|
||||
Ads1:"本游戏仅供娱乐休闲使用,\n道具及虚拟货币不可兑换\n现金或实物,不代表任何\n真实财富价值。",
|
||||
//Ads2:"\n 或客服号:\n",
|
||||
board:"",//从后台获取设置无效
|
||||
Ads1:"本游戏仅为娱乐休\n闲使用,道具所有\n 游戏通用\n\n游戏问题请联系微\n 信公众号:\n",
|
||||
//Ads2:"\n 或客服号:\n",
|
||||
board:"",//从后台获取设置无效
|
||||
//info:[" 客服信息","QQ:","",""],//客服QQ提醒
|
||||
info:[" 客服信息","QQ:","手机:","微信:"],//客服QQ提醒
|
||||
board_blength:24,//通知页面一行最大字节数(无需设置)
|
||||
charge:"充值提示",//充值提示
|
||||
info:[" 客服信息","QQ:","手机:","微信:"],//客服QQ提醒
|
||||
board_blength:24,//通知页面一行最大字节数(无需设置)
|
||||
charge:"关注友乐微信公众号充值",//充值提示
|
||||
};
|
||||
Game_Config.Protocol={
|
||||
x:270,//协议图片初始x坐标
|
||||
x:270,//协议图片初始x坐标
|
||||
y:70,//协议图片初始y坐标
|
||||
h:560,//协议图片显示高度
|
||||
w:729//协议图片显示宽度
|
||||
h:560,//协议图片显示高度
|
||||
w:729//协议图片显示宽度
|
||||
};
|
||||
Game_Config.Help={//帮助
|
||||
x:300,//帮助图片初始x坐标
|
||||
@@ -89,27 +91,27 @@ Game_Config.Help={//帮助
|
||||
w:729,//帮助图片显示高度
|
||||
h:450//帮助图片显示宽度
|
||||
};
|
||||
Game_Config.Notice={//滚动公告
|
||||
x1:0,//无需设置
|
||||
x2:0,//无需设置
|
||||
y:0,//无需设置
|
||||
Game_Config.Notice={//滚动公告
|
||||
x1:0,//无需设置
|
||||
x2:0,//无需设置
|
||||
y:0,//无需设置
|
||||
h:0,//无需设置
|
||||
//以上参数用来截取滚动公告的显示,截取位置是与滚动公告底大小位置一致
|
||||
speed:0.1,//滚动公告的滚动速度
|
||||
width:15//滚动公告的内容字体的字节长度(汉字是两个字节)
|
||||
//以上参数用来截取滚动公告的显示,截取位置是与滚动公告底大小位置一致
|
||||
speed:0.1,//滚动公告的滚动速度
|
||||
width:15//滚动公告的内容字体的字节长度(汉字是两个字节)
|
||||
};
|
||||
Game_Config.Feedback = {//反馈配置
|
||||
maxLen:250 //反馈内容最大长度
|
||||
Game_Config.Feedback = {//反馈配置
|
||||
maxLen:250 //反馈内容最大长度
|
||||
};
|
||||
Game_Config.shakeList ={//摇一摇事件的回调ID、需要添加时在此处添加回调事件写在Utl_Input.js中的Utl.shakeEvent()中
|
||||
Game_Config.shakeList ={//摇一摇事件的回调ID、需要添加时在此处添加回调事件写在Utl_Input.js中的Utl.shakeEvent()中
|
||||
nil:0,
|
||||
startwar:1
|
||||
};
|
||||
Game_Config.soundList ={//声音资源名
|
||||
MenuSceneMusic:"",//大厅界面背景音
|
||||
MainSceneMusic:""//游戏主界面背景音
|
||||
Game_Config.soundList ={//声音资源名
|
||||
MenuSceneMusic:"",//大厅界面背景音
|
||||
MainSceneMusic:""//游戏主界面背景音
|
||||
};
|
||||
Game_Config.ClickButton = {//需要设置点击音效的按钮(只有有弹窗的按钮设置此音效有效、按钮已经设置好、子游戏不允许修改)
|
||||
Game_Config.ClickButton = {//需要设置点击音效的按钮(只有有弹窗的按钮设置此音效有效、按钮已经设置好、子游戏不允许修改)
|
||||
src_1:"",//点击时播放的声音资源文件( 不需要播放则不填,下同)
|
||||
src_2:""//弹窗时播放的音效
|
||||
}
|
||||
@@ -120,11 +122,28 @@ Game_Config.loginButton = {//登录按钮信息
|
||||
x3:488,//审核版本游客登录按钮x
|
||||
};
|
||||
Game_Config.sysConfig = {
|
||||
mainMenuButton:false,//是否进入主菜单界面隐藏四个按钮
|
||||
mainScenePlayerInfo:false,//是否隐藏主界面玩家信息
|
||||
mainSceneButton:false,//是否隐藏主界面按钮
|
||||
shareRoom:false,//接收到星星场是否屏蔽平台框架显示
|
||||
changeSeat:false,//是否屏蔽平台自动刷新换座后界面
|
||||
hideNotice:false,//隐藏公告栏
|
||||
vipInfinite:false,//是否为百人场
|
||||
mainMenuButton:false,//是否进入主菜单界面隐藏四个按钮
|
||||
mainScenePlayerInfo:false,//是否隐藏主界面玩家信息
|
||||
mainSceneButton:false,//是否隐藏主界面按钮
|
||||
shareRoom:false,//接收到星星场是否屏蔽平台框架显示
|
||||
changeSeat:false,//是否屏蔽平台自动刷新换座后界面
|
||||
hideNotice:false,//隐藏公告栏
|
||||
vipInfinite:false,//是否为百人场
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -46,6 +46,52 @@ Game_Modify.CreateRoomData = {
|
||||
Type_2: 0
|
||||
};
|
||||
|
||||
// ---------- 玩家信息/头像默认布局(供 02 转发壳 updatePlayerInfoUI 的默认实现使用)----------
|
||||
// 02_SubGame_Input.js 的 updatePlayerInfoUI 在【无 SubGameHooks 实现】时提供默认头像等 UI 渲染;
|
||||
// 转发壳不得引用 codes,故把默认实现所需布局放在此处(Game_Modify 下),与子游戏 codes 内
|
||||
// GameView.updatePlayerInfoUI 使用的布局逻辑保持一致。子游戏若自定义头像 UI,则用 hook 覆盖、此配置不生效。
|
||||
// POSITIONS[configIndex]:ind0=南(自己)/ind1=东/ind2=北/ind3=西;渲染精灵 346+ind 底框、376+ind 头像框、
|
||||
// 406+ind 昵称、436+ind 分数、116+ind 头像图、群组 43+ind。坐标基于设计分辨率 1280x720。
|
||||
Game_Modify.PLAYER_INFO_LAYOUT = {
|
||||
POSITIONS: {
|
||||
0: { FRAME_X: 6, FRAME_Y: 592, FRAME_WIDTH: 90, FRAME_HEIGHT: 115,
|
||||
AVATAR: { OFFSET_X: 5, OFFSET_Y: 5, WIDTH: 80, HEIGHT: 80,
|
||||
NAME_OFFSET_X: -400, NAME_OFFSET_Y: 888, SCORE_OFFSET_X: 0, SCORE_OFFSET_Y: 88 },
|
||||
NAME_CENTER_ALIGN: true },
|
||||
1: { FRAME_X: 1186, FRAME_Y: 85, FRAME_WIDTH: 90, FRAME_HEIGHT: 115,
|
||||
AVATAR: { OFFSET_X: 5, OFFSET_Y: 5, WIDTH: 80, HEIGHT: 80,
|
||||
NAME_OFFSET_X: 905, NAME_OFFSET_Y: -888, SCORE_OFFSET_X: 0, SCORE_OFFSET_Y: 88 },
|
||||
NAME_CENTER_ALIGN: true },
|
||||
2: { FRAME_X: 1006, FRAME_Y: 5, FRAME_WIDTH: 90, FRAME_HEIGHT: 115,
|
||||
AVATAR: { OFFSET_X: 5, OFFSET_Y: 5, WIDTH: 80, HEIGHT: 80,
|
||||
NAME_OFFSET_X: 905, NAME_OFFSET_Y: -888, SCORE_OFFSET_X: 0, SCORE_OFFSET_Y: 88 },
|
||||
NAME_CENTER_ALIGN: true },
|
||||
3: { FRAME_X: 7, FRAME_Y: 59, FRAME_WIDTH: 90, FRAME_HEIGHT: 115,
|
||||
AVATAR: { OFFSET_X: 5, OFFSET_Y: 5, WIDTH: 80, HEIGHT: 80,
|
||||
NAME_OFFSET_X: -605, NAME_OFFSET_Y: -888, SCORE_OFFSET_X: 0, SCORE_OFFSET_Y: 88 },
|
||||
NAME_CENTER_ALIGN: true }
|
||||
},
|
||||
TEXT_STYLE: { NAME_FONT_SIZE: 14, NAME_MAX_LENGTH: 6, SCORE_FONT_SIZE: 12, CHAR_WIDTH: 9 }
|
||||
};
|
||||
|
||||
// ---------- 桌面聊天/语音气泡默认布局(供 02 转发壳 ShowChat/gameui_play_voice/gameui_stop_voice 使用)----------
|
||||
// 平台默认按固定显示位序取 Game_Config.Chat/Voice 的固定坐标(固定人数模式);此处提供可变人数版:
|
||||
// 按 updatePlayerInfoUI 的 configIndex(物理位) 定位,使气泡随人数(2/3/4)贴合各玩家。
|
||||
// 仅定义每个物理位(0南/1东/2北/3西)的气泡框左上角锚点 X/Y 与朝向 IS_LEFT;气泡内边距/字宽/显示时长等
|
||||
// 尺寸复用平台 Game_Config.Chat/Voice/Max(与人数无关)。精灵:聊天 背景552+ind/文字582+ind,
|
||||
// 语音 背景612+ind/内容642+ind。IS_LEFT=true 气泡向右展开(用 IMG_LEFT)、false 向左展开(用 IMG_RIGHT)。
|
||||
// 坐标基于 1280x720,锚点参考 PLAYER_INFO_LAYOUT 各位底框,可按实际界面微调。
|
||||
Game_Modify.BUBBLE_LAYOUT = {
|
||||
IMG_LEFT: 83, // 左向气泡背景图资源ID(气泡向右展开)
|
||||
IMG_RIGHT: 86, // 右向气泡背景图资源ID(气泡向左展开)
|
||||
POSITIONS: {
|
||||
0: { X: 100, Y: 540, IS_LEFT: true }, // 南(自己)
|
||||
1: { X: 1176, Y: 120, IS_LEFT: false }, // 东
|
||||
2: { X: 970, Y: 20, IS_LEFT: false }, // 北
|
||||
3: { X: 120, Y: 70, IS_LEFT: true } // 西
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// ---------- 接口转发壳(A 类:无 hook 即 no-op) ----------
|
||||
Game_Modify.utlmousedown = function (gameid, spid, downx, downy, no1, no2, no3, no4, no5, no6) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// ============================================================================
|
||||
// 02_SubGame_Input.template.js —— 平台输入/回调接口「转发壳」模板(框架维护,子游戏不碰)
|
||||
// 02_SubGame_Input.js —— 平台输入/回调接口「转发壳」(框架维护,子游戏不碰)
|
||||
// 平台按全局名调用 gameHallImport.* / Game_Modify.*;本文件统一转发到 SubGameHooks 同名 hook。
|
||||
// 子游戏只需在 codes/ 的 SubGameHooks 里实现需要的接口(见 SubGameHooks.template.js)。
|
||||
// 子游戏只需在 codes/ 的 SubGameHooks 里实现需要的接口(见 codes/SubGameHooks.js)。
|
||||
// 严格 ES5。接口清单与默认值依据 spec §9。
|
||||
// ============================================================================
|
||||
|
||||
@@ -293,8 +293,210 @@ Game_Modify.getMaxPlayerCount = function (roomtype) {
|
||||
return (Game_Config.Max && Game_Config.Max.PlayerCnt) || 4;
|
||||
};
|
||||
|
||||
// B 类:更新玩家信息 UI,无 hook 返回 false(让平台默认处理)
|
||||
// 内部辅助(非平台接口):把相对显示位序 relPos 映射为 { ind:精灵槽, configIndex:物理布局位 }。
|
||||
// 2/3 人时 ind≠configIndex;无效返回 null。updatePlayerInfoUI 与桌面气泡默认实现共用此映射,
|
||||
// 保证头像面板与聊天/语音气泡落在同一玩家位。
|
||||
Game_Modify._resolveDisplaySlot = function (relPos, playerCount) {
|
||||
var ind = -1, configIndex = -1;
|
||||
if (playerCount === 2) {
|
||||
if (relPos === 0) { ind = 0; configIndex = 0; }
|
||||
else if (relPos === 1) { ind = 1; configIndex = 2; }
|
||||
} else if (playerCount === 3) {
|
||||
if (relPos === 0) { ind = 0; configIndex = 0; }
|
||||
else if (relPos === 1) { ind = 1; configIndex = 1; }
|
||||
else if (relPos === 2) { ind = 2; configIndex = 3; }
|
||||
} else {
|
||||
if (relPos === 0) { ind = 0; configIndex = 0; }
|
||||
else if (relPos === 1) { ind = 1; configIndex = 1; }
|
||||
else if (relPos === 2) { ind = 2; configIndex = 2; }
|
||||
else if (relPos === 3) { ind = 3; configIndex = 3; }
|
||||
}
|
||||
if (ind === -1) return null;
|
||||
return { ind: ind, configIndex: configIndex };
|
||||
};
|
||||
|
||||
// B 类:更新指定座位的玩家信息 UI(昵称/分数/头像)。
|
||||
// 有 hook 则委托子游戏;无 hook 时由本模板提供默认头像等 UI 的显示逻辑,其逻辑与子游戏
|
||||
// codes 内 GameView.updatePlayerInfoUI 保持一致,布局取自 Game_Modify.PLAYER_INFO_LAYOUT(见 01)。
|
||||
// 精灵操作一律经框架 SpriteManager(setPosition/setSize/setText/show|hideGroup),不直接调
|
||||
// set_self/set_group 等引擎原语;头像加载走平台 Func.up_imgurl(非精灵原语,SpriteManager 无对应能力)。
|
||||
// 不引用任何 codes 模块。平台 GameUI.updatePlayerInfoUI 会对每个座位逐一调用本接口,故按单座位渲染。
|
||||
Game_Modify.updatePlayerInfoUI = function (targetSeat) {
|
||||
if (window.SubGameHooks && SubGameHooks.updatePlayerInfoUI) return SubGameHooks.updatePlayerInfoUI(targetSeat);
|
||||
return false;
|
||||
|
||||
// ---- 模板默认:与 GameView.updatePlayerInfoUI 一致的单座位渲染 ----
|
||||
if (typeof Desk === 'undefined' || typeof C_Player === 'undefined' || typeof SpriteManager === 'undefined') return false;
|
||||
var layout = Game_Modify.PLAYER_INFO_LAYOUT;
|
||||
if (!layout || !layout.POSITIONS) return false;
|
||||
if (!Desk.PlayerList || targetSeat < 0 || targetSeat >= Desk.PlayerList.length) return false;
|
||||
|
||||
var selfSeat = (typeof C_Player.seat === 'number') ? C_Player.seat : 0;
|
||||
var playerCount = (Game_Config.Max && Game_Config.Max.PlayerCnt) || 4;
|
||||
var relPos = (targetSeat - selfSeat + playerCount) % playerCount;
|
||||
|
||||
// 显示槽 ind 与布局位 configIndex(2/3 人时两者不同)
|
||||
var slot = Game_Modify._resolveDisplaySlot(relPos, playerCount);
|
||||
if (!slot) return false;
|
||||
var ind = slot.ind, configIndex = slot.configIndex;
|
||||
|
||||
var player = Desk.PlayerList[targetSeat];
|
||||
// 空座位:隐藏该位玩家信息群组
|
||||
if (!player || !player.playerid || player.playerid === -1) {
|
||||
SpriteManager.hideGroup(43 + ind);
|
||||
return true;
|
||||
}
|
||||
|
||||
var posConfig = layout.POSITIONS[configIndex];
|
||||
if (!posConfig) return false;
|
||||
|
||||
// 底框(346+ind)
|
||||
if (posConfig.FRAME_X !== undefined) {
|
||||
SpriteManager.setPosition(346 + ind, posConfig.FRAME_X, posConfig.FRAME_Y);
|
||||
SpriteManager.setSize(346 + ind, posConfig.FRAME_WIDTH, posConfig.FRAME_HEIGHT);
|
||||
}
|
||||
|
||||
// 头像框(376+ind):基于底框 + 相对偏移
|
||||
var avatarX = posConfig.FRAME_X + posConfig.AVATAR.OFFSET_X;
|
||||
var avatarY = posConfig.FRAME_Y + posConfig.AVATAR.OFFSET_Y;
|
||||
var avatarWidth = posConfig.AVATAR.WIDTH;
|
||||
var avatarHeight = posConfig.AVATAR.HEIGHT;
|
||||
var nameOffsetX = posConfig.AVATAR.NAME_OFFSET_X || 0;
|
||||
var nameOffsetY = posConfig.AVATAR.NAME_OFFSET_Y || 0;
|
||||
var scoreOffsetX = posConfig.AVATAR.SCORE_OFFSET_X || 0;
|
||||
var scoreOffsetY = posConfig.AVATAR.SCORE_OFFSET_Y || 0;
|
||||
SpriteManager.setPosition(376 + ind, avatarX, avatarY);
|
||||
SpriteManager.setSize(376 + ind, avatarWidth, avatarHeight);
|
||||
|
||||
var textStyle = layout.TEXT_STYLE || {};
|
||||
var maxLen = textStyle.NAME_MAX_LENGTH || 8;
|
||||
var charWidth = textStyle.CHAR_WIDTH || 7;
|
||||
|
||||
// 昵称(406+ind)
|
||||
var nickname = Func.subString(player.nickname || '', maxLen, true);
|
||||
SpriteManager.setText(406 + ind, nickname);
|
||||
var nameX = avatarX + nameOffsetX;
|
||||
if (posConfig.NAME_CENTER_ALIGN) {
|
||||
var textWidth = (typeof nickname.gblen === 'function') ? nickname.gblen() * charWidth : nickname.length * charWidth;
|
||||
nameX = avatarX + avatarWidth / 2 - textWidth / 2;
|
||||
}
|
||||
SpriteManager.setPosition(406 + ind, nameX, avatarY + nameOffsetY);
|
||||
|
||||
// 分数(436+ind)
|
||||
var score = (typeof player.score === 'number') ? player.score : 0;
|
||||
SpriteManager.setText(436 + ind, score);
|
||||
var scoreX = avatarX + scoreOffsetX;
|
||||
if (posConfig.NAME_CENTER_ALIGN) {
|
||||
var scoreWidth = String(score).length * charWidth;
|
||||
scoreX = avatarX + avatarWidth / 2 - scoreWidth / 2;
|
||||
}
|
||||
SpriteManager.setPosition(436 + ind, scoreX, avatarY + scoreOffsetY);
|
||||
|
||||
// 头像图(116+ind):Func.up_imgurl 为平台头像加载接口(非引擎精灵原语)
|
||||
if (player.avatar && typeof Func !== 'undefined' && typeof Func.up_imgurl === 'function') {
|
||||
Func.up_imgurl(116 + ind, player.avatar);
|
||||
}
|
||||
|
||||
// 显示该座位玩家信息群组
|
||||
SpriteManager.showGroup(43 + ind);
|
||||
return true;
|
||||
};
|
||||
// 桌面文字聊天气泡:有 hook 委托子游戏;无 hook 时按【可变人数】默认渲染,使气泡贴合各玩家。
|
||||
// seat 为绝对座位;经 SpriteManager 操作精灵(背景552+ind/文字582+ind),位置取自 Game_Modify.BUBBLE_LAYOUT
|
||||
// 的物理位配置,尺寸(内边距/字宽/显示时长)复用 Game_Config.Chat/Max;不直接调引擎原语、不引用 codes。
|
||||
Game_Modify.ShowChat = function (seat, text) {
|
||||
if (window.SubGameHooks && SubGameHooks.ShowChat) return SubGameHooks.ShowChat(seat, text);
|
||||
GameUI.CloseChat();
|
||||
if (typeof Desk === 'undefined' || typeof C_Player === 'undefined' || typeof SpriteManager === 'undefined') return;
|
||||
var layout = Game_Modify.BUBBLE_LAYOUT;
|
||||
if (!layout || !layout.POSITIONS) return;
|
||||
var playerCount = (Game_Config.Max && Game_Config.Max.PlayerCnt) || 4;
|
||||
var relPos = (seat - C_Player.seat + playerCount) % playerCount;
|
||||
var slot = Game_Modify._resolveDisplaySlot(relPos, playerCount);
|
||||
if (!slot) return;
|
||||
var pos = layout.POSITIONS[slot.configIndex];
|
||||
if (!pos) return;
|
||||
var ind = slot.ind;
|
||||
|
||||
text = String(text);
|
||||
var chat = Game_Config.Chat || {};
|
||||
var dis_x = (chat.ChatDis && chat.ChatDis[0]) || 0;
|
||||
var dis_y = (chat.ChatDis && chat.ChatDis[1]) || 0;
|
||||
var text_w = chat.textwidth || 14;
|
||||
var text_len = (typeof text.gblen === 'function') ? text.gblen() : text.length;
|
||||
var bgW = text_len * text_w + dis_x * 2;
|
||||
|
||||
SpriteManager.setText(582 + ind, text);
|
||||
if (pos.IS_LEFT) {
|
||||
SpriteManager.setPosition(552 + ind, pos.X, pos.Y);
|
||||
SpriteManager.setPosition(582 + ind, pos.X + dis_x, pos.Y + dis_y);
|
||||
SpriteManager.setImage(552 + ind, layout.IMG_LEFT);
|
||||
} else {
|
||||
SpriteManager.setPosition(552 + ind, pos.X - bgW, pos.Y);
|
||||
SpriteManager.setPosition(582 + ind, pos.X - text_len * text_w - dis_x, pos.Y + dis_y);
|
||||
SpriteManager.setImage(552 + ind, layout.IMG_RIGHT);
|
||||
}
|
||||
SpriteManager.setWidth(552 + ind, bgW);
|
||||
SpriteManager.show(552 + ind);
|
||||
SpriteManager.show(582 + ind);
|
||||
|
||||
// 自动隐藏:复用平台配置的显示时长(观感与固定人数版一致),按槽位去重定时器
|
||||
var dur = (Game_Config.Max && Game_Config.Max.ShowChat) || 3000;
|
||||
Game_Modify._chatHideTimers = Game_Modify._chatHideTimers || {};
|
||||
if (Game_Modify._chatHideTimers[ind]) { clearTimeout(Game_Modify._chatHideTimers[ind]); }
|
||||
Game_Modify._chatHideTimers[ind] = setTimeout(function () {
|
||||
SpriteManager.hide(552 + ind);
|
||||
SpriteManager.hide(582 + ind);
|
||||
Game_Modify._chatHideTimers[ind] = null;
|
||||
}, dur);
|
||||
};
|
||||
|
||||
// 桌面语音气泡(播放):有 hook 委托子游戏;无 hook 时按【可变人数】默认渲染。
|
||||
// uiIndex 已是相对显示位序(relPos);经 SpriteManager 操作精灵(背景612+ind/内容642+ind),
|
||||
// 位置取自 Game_Modify.BUBBLE_LAYOUT,气泡宽度依据内容精灵当前宽度;尺寸复用 Game_Config.Voice。
|
||||
Game_Modify.gameui_play_voice = function (uiIndex) {
|
||||
if (window.SubGameHooks && SubGameHooks.gameui_play_voice) return SubGameHooks.gameui_play_voice(uiIndex);
|
||||
if (typeof SpriteManager === 'undefined') return;
|
||||
var layout = Game_Modify.BUBBLE_LAYOUT;
|
||||
if (!layout || !layout.POSITIONS) return;
|
||||
var playerCount = (Game_Config.Max && Game_Config.Max.PlayerCnt) || 4;
|
||||
var relPos = ((Number(uiIndex) % playerCount) + playerCount) % playerCount;
|
||||
var slot = Game_Modify._resolveDisplaySlot(relPos, playerCount);
|
||||
if (!slot) return;
|
||||
var pos = layout.POSITIONS[slot.configIndex];
|
||||
if (!pos) return;
|
||||
var ind = slot.ind;
|
||||
|
||||
var voice = Game_Config.Voice || {};
|
||||
var dis_x = (voice.VoiceDis && voice.VoiceDis[0]) || 0;
|
||||
var dis_y = (voice.VoiceDis && voice.VoiceDis[1]) || 0;
|
||||
var contentSize = SpriteManager.getSize(642 + ind);
|
||||
var text_len = (contentSize && typeof contentSize.width === 'number') ? contentSize.width : 0;
|
||||
var bgW = text_len + dis_x * 2;
|
||||
|
||||
if (pos.IS_LEFT) {
|
||||
SpriteManager.setPosition(612 + ind, pos.X, pos.Y);
|
||||
SpriteManager.setPosition(642 + ind, pos.X + dis_x, pos.Y + dis_y);
|
||||
SpriteManager.setImage(612 + ind, layout.IMG_LEFT);
|
||||
} else {
|
||||
SpriteManager.setPosition(612 + ind, pos.X - bgW, pos.Y);
|
||||
SpriteManager.setPosition(642 + ind, pos.X - text_len - dis_x, pos.Y + dis_y);
|
||||
SpriteManager.setImage(612 + ind, layout.IMG_RIGHT);
|
||||
}
|
||||
SpriteManager.setWidth(612 + ind, bgW);
|
||||
SpriteManager.show(612 + ind);
|
||||
SpriteManager.show(642 + ind);
|
||||
play_ani(1, 642 + ind, 43, 1, 3, 0, Game_Config.Voice.VoiceTime, 0, 0, 0, 0, 0, 0);
|
||||
};
|
||||
|
||||
// 桌面语音气泡(停止):有 hook 委托子游戏;无 hook 时隐藏对应槽位的语音气泡。
|
||||
Game_Modify.gameui_stop_voice = function (uiIndex) {
|
||||
if (window.SubGameHooks && SubGameHooks.gameui_stop_voice) return SubGameHooks.gameui_stop_voice(uiIndex);
|
||||
if (typeof SpriteManager === 'undefined') return;
|
||||
var playerCount = (Game_Config.Max && Game_Config.Max.PlayerCnt) || 4;
|
||||
var relPos = ((Number(uiIndex) % playerCount) + playerCount) % playerCount;
|
||||
var slot = Game_Modify._resolveDisplaySlot(relPos, playerCount);
|
||||
if (!slot) return;
|
||||
var ind = slot.ind;
|
||||
SpriteManager.hide(612 + ind);
|
||||
SpriteManager.hide(642 + ind);
|
||||
};
|
||||
@@ -241,7 +241,7 @@ var BaseComponent = (function() {
|
||||
this.isDestroyed = false;
|
||||
this.isInitialized = true;
|
||||
|
||||
console.log('🎨 BaseComponent: 初始化组件 "' + this.name + '" (图层: ' + this.layer + ')');
|
||||
// // console.log('🎨 BaseComponent: 初始化组件 "' + this.name + '" (图层: ' + this.layer + ')');
|
||||
|
||||
// 调用子类的init方法
|
||||
try {
|
||||
@@ -274,7 +274,7 @@ var BaseComponent = (function() {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('🙈 BaseComponent: 隐藏组件 "' + this.name + '" 的所有精灵 (数量: ' + this.sprites.length + ')');
|
||||
// console.log('🙈 BaseComponent: 隐藏组件 "' + this.name + '" 的所有精灵 (数量: ' + this.sprites.length + ')');
|
||||
|
||||
for (var i = 0; i < this.sprites.length; i++) {
|
||||
var spriteId = this.sprites[i];
|
||||
@@ -293,7 +293,7 @@ var BaseComponent = (function() {
|
||||
*/
|
||||
init: function(config) {
|
||||
// 子类覆盖此方法
|
||||
console.log('📝 BaseComponent.init: 请在子类中覆盖此方法');
|
||||
// console.log('📝 BaseComponent.init: 请在子类中覆盖此方法');
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -312,11 +312,11 @@ var BaseComponent = (function() {
|
||||
}
|
||||
|
||||
if (this.isVisible) {
|
||||
console.log('ℹ️ BaseComponent.show: 组件已可见,跳过:', this.name);
|
||||
// console.log('ℹ️ BaseComponent.show: 组件已可见,跳过:', this.name);
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log('👁️ BaseComponent: 显示组件 "' + this.name + '" (图层: ' + this.layer + ')');
|
||||
// console.log('👁️ BaseComponent: 显示组件 "' + this.name + '" (图层: ' + this.layer + ')');
|
||||
|
||||
// 显示图层
|
||||
SpriteManager.showGroup(this.layer);
|
||||
@@ -344,11 +344,11 @@ var BaseComponent = (function() {
|
||||
}
|
||||
|
||||
if (!this.isVisible) {
|
||||
console.log('ℹ️ BaseComponent.hide: 组件已隐藏,跳过:', this.name);
|
||||
// console.log('ℹ️ BaseComponent.hide: 组件已隐藏,跳过:', this.name);
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log('🙈 BaseComponent: 隐藏组件 "' + this.name + '" (图层: ' + this.layer + ')');
|
||||
// console.log('🙈 BaseComponent: 隐藏组件 "' + this.name + '" (图层: ' + this.layer + ')');
|
||||
|
||||
// 隐藏图层
|
||||
SpriteManager.hideGroup(this.layer);
|
||||
@@ -375,7 +375,7 @@ var BaseComponent = (function() {
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log('💥 BaseComponent: 销毁组件 "' + this.name + '"');
|
||||
// console.log('💥 BaseComponent: 销毁组件 "' + this.name + '"');
|
||||
|
||||
// 移除所有事件监听
|
||||
this._removeAllEventListeners();
|
||||
@@ -447,7 +447,7 @@ var BaseComponent = (function() {
|
||||
// 记录监听器
|
||||
this.eventListeners[eventName] = wrappedHandler;
|
||||
|
||||
console.log('📡 BaseComponent: 组件 "' + this.name + '" 注册事件:', eventName);
|
||||
// console.log('📡 BaseComponent: 组件 "' + this.name + '" 注册事件:', eventName);
|
||||
|
||||
return true;
|
||||
},
|
||||
@@ -467,7 +467,7 @@ var BaseComponent = (function() {
|
||||
EventBus.off(eventName, handler);
|
||||
delete this.eventListeners[eventName];
|
||||
|
||||
console.log('📴 BaseComponent: 组件 "' + this.name + '" 移除事件:', eventName);
|
||||
// console.log('📴 BaseComponent: 组件 "' + this.name + '" 移除事件:', eventName);
|
||||
|
||||
return true;
|
||||
},
|
||||
@@ -483,7 +483,7 @@ var BaseComponent = (function() {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('📤 BaseComponent: 组件 "' + this.name + '" 触发事件:', eventName);
|
||||
// console.log('📤 BaseComponent: 组件 "' + this.name + '" 触发事件:', eventName);
|
||||
EventBus.emit(eventName, data);
|
||||
},
|
||||
|
||||
@@ -511,7 +511,7 @@ var BaseComponent = (function() {
|
||||
}
|
||||
|
||||
this.sprites.push(spriteId);
|
||||
// console.log('➕ BaseComponent: 组件 "' + this.name + '" 添加精灵:', spriteId);
|
||||
// // console.log('➕ BaseComponent: 组件 "' + this.name + '" 添加精灵:', spriteId);
|
||||
|
||||
return true;
|
||||
},
|
||||
@@ -529,7 +529,7 @@ var BaseComponent = (function() {
|
||||
}
|
||||
|
||||
this.sprites.splice(index, 1);
|
||||
console.log('➖ BaseComponent: 组件 "' + this.name + '" 移除精灵:', spriteId);
|
||||
// console.log('➖ BaseComponent: 组件 "' + this.name + '" 移除精灵:', spriteId);
|
||||
|
||||
return true;
|
||||
},
|
||||
@@ -596,7 +596,7 @@ var BaseComponent = (function() {
|
||||
*/
|
||||
printState: function() {
|
||||
var state = this.getState();
|
||||
console.log('📊 组件状态:', JSON.stringify(state, null, 2));
|
||||
// console.log('📊 组件状态:', JSON.stringify(state, null, 2));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -84,7 +84,21 @@
|
||||
* scrollSensitivity: 5, // 移动5px以上才触发滚动
|
||||
* bounceSpeed: 1 // 回弹速度
|
||||
* });
|
||||
*
|
||||
*
|
||||
* 【横向列表(左右滑动)】
|
||||
* var themeList = new DynamicSpriteList({
|
||||
* containerId: sprites.LIST_CONTAINER,
|
||||
* clipArea: { x: 340, y: 240, width: 600, height: 160 },
|
||||
* orientation: 'horizontal', // 默认 'vertical'
|
||||
* itemWidth: 262, // 主轴步长(项宽 + 项间距),与纵向 rowHeight 对称
|
||||
* renderRow: function (ctx) {
|
||||
* // 横向下 ctx.addSprite 的 x 会叠加该项的主轴基准,y 原样
|
||||
* var sid = ctx.addSprite('preview', TPL_ID, 0, 0);
|
||||
* }
|
||||
* });
|
||||
* // 事件转发:横向传 offmovex(纵向传 offmovey)
|
||||
* themeList.handleMouseMove(event.spriteId, event.offset.x);
|
||||
*
|
||||
* 【设置数据(触发渲染)】
|
||||
* // 数据数组中每个元素对应一行
|
||||
* // setData 会清除旧行、重新创建新行
|
||||
@@ -176,6 +190,26 @@
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
/**
|
||||
* 主轴/交叉轴的引擎操作码与 clipArea 字段映射
|
||||
* 前缀 DSL_ 是必要的:友乐运行时所有 <script> 共享同一全局作用域,
|
||||
* 通用名(如 LIST_AXIS)会与其它文件互相覆盖。
|
||||
* 操作码:18=左右移动 19=上下移动 20=设宽 21=设高
|
||||
* (docs/important/client/精灵操作详细指南.md:38-41)
|
||||
*/
|
||||
var DSL_LIST_AXIS = {
|
||||
vertical: {
|
||||
posOp: 19, sizeOp: 21, crossPosOp: 18, crossSizeOp: 20,
|
||||
clipPos: 'y', clipSize: 'height', crossClipPos: 'x', crossClipSize: 'width',
|
||||
stepField: 'rowHeight'
|
||||
},
|
||||
horizontal: {
|
||||
posOp: 18, sizeOp: 20, crossPosOp: 19, crossSizeOp: 21,
|
||||
clipPos: 'x', clipSize: 'width', crossClipPos: 'y', crossClipSize: 'height',
|
||||
stepField: 'itemWidth'
|
||||
}
|
||||
};
|
||||
|
||||
function DynamicSpriteList(config) {
|
||||
// ========================================================================
|
||||
// 参数验证
|
||||
@@ -189,8 +223,11 @@ function DynamicSpriteList(config) {
|
||||
if (!config.clipArea) {
|
||||
throw new Error('DynamicSpriteList: clipArea is required');
|
||||
}
|
||||
if (typeof config.rowHeight !== 'number' || config.rowHeight <= 0) {
|
||||
throw new Error('DynamicSpriteList: rowHeight must be a positive number');
|
||||
var orientation = (config.orientation === 'horizontal') ? 'horizontal' : 'vertical';
|
||||
var axis = DSL_LIST_AXIS[orientation];
|
||||
var step = config[axis.stepField];
|
||||
if (typeof step !== 'number' || step <= 0) {
|
||||
throw new Error('DynamicSpriteList: ' + axis.stepField + ' must be a positive number');
|
||||
}
|
||||
var hasTemplates = config.templates && typeof config.templates === 'object';
|
||||
var hasRenderRow = typeof config.renderRow === 'function';
|
||||
@@ -220,11 +257,31 @@ function DynamicSpriteList(config) {
|
||||
};
|
||||
|
||||
/**
|
||||
* 每行高度
|
||||
* 排列方向:'vertical'(默认) | 'horizontal'
|
||||
* @type {string}
|
||||
*/
|
||||
this.orientation = orientation;
|
||||
|
||||
/**
|
||||
* 轴映射(操作码 + clipArea 取用字段),内部所有方向相关操作一律经此表
|
||||
* @type {Object}
|
||||
* @private
|
||||
*/
|
||||
this._axis = axis;
|
||||
|
||||
/**
|
||||
* 主轴步长:纵向=行高(rowHeight),横向=项宽含间距(itemWidth)
|
||||
* @type {number}
|
||||
* @private
|
||||
*/
|
||||
this._step = step;
|
||||
|
||||
/**
|
||||
* 每行高度(纵向语义保留字段;横向下等于主轴步长)
|
||||
* @type {number}
|
||||
*/
|
||||
this.rowHeight = config.rowHeight;
|
||||
|
||||
this.rowHeight = step;
|
||||
|
||||
/**
|
||||
* 模板精灵配置
|
||||
* @type {Object}
|
||||
@@ -431,31 +488,31 @@ DynamicSpriteList.prototype.destroy = function() {
|
||||
/**
|
||||
* 滚动到指定位置
|
||||
*
|
||||
* @param {number} y - 目标Y坐标
|
||||
* @param {number} offset - 目标主轴偏移量(纵向=Y方向,横向=X方向)
|
||||
* @param {boolean} [animated=true] - 是否使用动画
|
||||
* @returns {DynamicSpriteList} 返回自身,支持链式调用
|
||||
*/
|
||||
DynamicSpriteList.prototype.scrollTo = function(y, animated) {
|
||||
var currentY = get_self(this.containerId, 19, 0, 0, 0);
|
||||
var targetY = this.clipArea.y - y;
|
||||
|
||||
// 边界限制
|
||||
var contentHeight = this._data.length * this.rowHeight;
|
||||
var minY = this.clipArea.y + this.clipArea.height - contentHeight;
|
||||
if (targetY < minY) {
|
||||
targetY = minY;
|
||||
}
|
||||
if (targetY > this.clipArea.y) {
|
||||
targetY = this.clipArea.y;
|
||||
}
|
||||
|
||||
DynamicSpriteList.prototype.scrollTo = function(offset, animated) {
|
||||
var axis = this._axis;
|
||||
var clipStart = this.clipArea[axis.clipPos];
|
||||
var clipSize = this.clipArea[axis.clipSize];
|
||||
var current = get_self(this.containerId, axis.posOp, 0, 0, 0);
|
||||
var target = clipStart - offset;
|
||||
|
||||
// 边界钳制(双向):先钳末端,再钳起点。
|
||||
// 内容不足一屏时 min > clipStart,两条钳制叠加后恒为起点——这正是期望行为。
|
||||
var contentSize = this._data.length * this._step;
|
||||
var min = clipStart + clipSize - contentSize;
|
||||
if (target < min) { target = min; }
|
||||
if (target > clipStart) { target = clipStart; }
|
||||
|
||||
if (animated !== false) {
|
||||
play_ani(1, this.containerId, 19, currentY, targetY, 0,
|
||||
Math.abs(targetY - currentY), 0, 0, 0, this.bounceSpeed, 0, 0, 0);
|
||||
play_ani(1, this.containerId, axis.posOp, current, target, 0,
|
||||
Math.abs(target - current), 0, 0, 0, this.bounceSpeed, 0, 0, 0);
|
||||
} else {
|
||||
set_self(this.containerId, 19, targetY, 0, 0, 0);
|
||||
set_self(this.containerId, axis.posOp, target, 0, 0, 0);
|
||||
}
|
||||
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
@@ -476,8 +533,8 @@ DynamicSpriteList.prototype.scrollToTop = function(animated) {
|
||||
* @returns {DynamicSpriteList} 返回自身,支持链式调用
|
||||
*/
|
||||
DynamicSpriteList.prototype.scrollToBottom = function(animated) {
|
||||
var contentHeight = this._data.length * this.rowHeight;
|
||||
var maxScroll = contentHeight - this.clipArea.height;
|
||||
var contentSize = this._data.length * this._step;
|
||||
var maxScroll = contentSize - this.clipArea[this._axis.clipSize];
|
||||
if (maxScroll < 0) {
|
||||
maxScroll = 0;
|
||||
}
|
||||
@@ -492,7 +549,7 @@ DynamicSpriteList.prototype.scrollToBottom = function(animated) {
|
||||
* @returns {DynamicSpriteList} 返回自身,支持链式调用
|
||||
*/
|
||||
DynamicSpriteList.prototype.scrollToRow = function(rowIndex, animated) {
|
||||
var y = rowIndex * this.rowHeight;
|
||||
var y = rowIndex * this._step;
|
||||
return this.scrollTo(y, animated);
|
||||
};
|
||||
|
||||
@@ -531,36 +588,35 @@ DynamicSpriteList.prototype.handleMouseDown = function(spid, x, y) {
|
||||
* 需要在 utlmousemove 回调中调用
|
||||
*
|
||||
* @param {number} spid - 移动的精灵ID
|
||||
* @param {number} offsetY - Y方向移动偏移量
|
||||
* @param {number} offsetMain - 主轴方向移动偏移量(纵向传 Y 方向偏移,横向传 X 方向偏移)
|
||||
* @returns {boolean} 如果事件被处理返回 true
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* // 在 SpriteEventController 或 Game_Modify 中
|
||||
* // 在 SpriteEventController 或 Game_Modify 中(纵向列表传 offmovey,横向列表传 offmovex)
|
||||
* utlmousemove: function(gameid, spid, downx, downy, movex, movey, timelong, offmovex, offmovey) {
|
||||
* if (myList.handleMouseMove(spid, offmovey)) {
|
||||
* if (myList.handleMouseMove(spid, offmovey)) { // 横向列表改传 offmovex
|
||||
* return; // 事件已处理
|
||||
* }
|
||||
* // 其他处理...
|
||||
* }
|
||||
*/
|
||||
DynamicSpriteList.prototype.handleMouseMove = function(spid, offsetY) {
|
||||
DynamicSpriteList.prototype.handleMouseMove = function(spid, offsetMain) {
|
||||
if (!this.enableScroll) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if (spid === this.containerId) {
|
||||
if (Math.abs(offsetY) > this.scrollSensitivity) {
|
||||
// 增量移动容器
|
||||
set_self(this.containerId, 19, offsetY, 1, 0, 0);
|
||||
if (Math.abs(offsetMain) > this.scrollSensitivity) {
|
||||
var axis = this._axis;
|
||||
// 增量移动容器(第4参 1 = 相对增量)
|
||||
set_self(this.containerId, axis.posOp, offsetMain, 1, 0, 0);
|
||||
this._isSliding = true;
|
||||
|
||||
// 触发滚动回调
|
||||
|
||||
if (typeof this.onScroll === 'function') {
|
||||
var currentY = get_self(this.containerId, 19, 0, 0, 0);
|
||||
var scrollY = this.clipArea.y - currentY;
|
||||
var contentHeight = this._data.length * this.rowHeight;
|
||||
var maxScrollY = contentHeight - this.clipArea.height;
|
||||
this.onScroll(scrollY, maxScrollY > 0 ? maxScrollY : 0);
|
||||
var current = get_self(this.containerId, axis.posOp, 0, 0, 0);
|
||||
var scrolled = this.clipArea[axis.clipPos] - current;
|
||||
var maxScroll = this._data.length * this._step - this.clipArea[axis.clipSize];
|
||||
this.onScroll(scrolled, maxScroll > 0 ? maxScroll : 0);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -661,29 +717,30 @@ DynamicSpriteList.prototype._addSpriteInternal = function (name, templateSpriteI
|
||||
* @private
|
||||
*/
|
||||
DynamicSpriteList.prototype._render = function() {
|
||||
set_self(this.containerId, 18, this.clipArea.x, 0, 0);
|
||||
set_self(this.containerId, 19, this.clipArea.y, 0, 0);
|
||||
var contentHeight = this._data.length * this.rowHeight;
|
||||
set_self(this.containerId, 21, contentHeight, 0, 0);
|
||||
var axis = this._axis;
|
||||
set_self(this.containerId, axis.posOp, this.clipArea[axis.clipPos], 0, 0);
|
||||
set_self(this.containerId, axis.crossPosOp, this.clipArea[axis.crossClipPos], 0, 0);
|
||||
var contentSize = this._data.length * this._step;
|
||||
set_self(this.containerId, axis.sizeOp, contentSize, 0, 0);
|
||||
|
||||
if (this.templates) {
|
||||
var tnames = Object.keys(this.templates);
|
||||
if (tnames.length > 0) {
|
||||
var w = get_self(this.templates[tnames[0]].spriteId, 20, 0, 0, 0);
|
||||
set_self(this.containerId, 20, w, 0, 0);
|
||||
// 交叉轴尺寸取首个模板的对应尺寸(纵向=宽 op20,横向=高 op21)
|
||||
var crossFromTpl = get_self(this.templates[tnames[0]].spriteId, axis.crossSizeOp, 0, 0, 0);
|
||||
set_self(this.containerId, axis.crossSizeOp, crossFromTpl, 0, 0);
|
||||
}
|
||||
} else {
|
||||
// renderRow 模式:容器宽度取裁剪区宽,保证整行可视宽都在容器 hit 区(点击/拖动检测)内。
|
||||
// 否则容器保持编辑器预置窄宽度,超出部分点击/拖动报的 spid≠containerId 而失效(对账 bak set_self(fSpid,20,bg宽))。
|
||||
set_self(this.containerId, 20, this.clipArea.width, 0, 0);
|
||||
// renderRow 模式:交叉轴尺寸取裁剪区对应边,保证整个可视区都在容器 hit 区(点击/拖动检测)内
|
||||
set_self(this.containerId, axis.crossSizeOp, this.clipArea[axis.crossClipSize], 0, 0);
|
||||
}
|
||||
|
||||
for (var rowIndex = 0; rowIndex < this._data.length; rowIndex++) {
|
||||
var rowData = this._data[rowIndex];
|
||||
var rowY = rowIndex * this.rowHeight;
|
||||
var rowBase = rowIndex * this._step;
|
||||
|
||||
if (this.renderRow) {
|
||||
this.renderRow(this._makeRowContext(rowIndex, rowData, rowY));
|
||||
this.renderRow(this._makeRowContext(rowIndex, rowData, rowBase));
|
||||
} else {
|
||||
var names = Object.keys(this.templates);
|
||||
for (var i = 0; i < names.length; i++) {
|
||||
@@ -691,7 +748,9 @@ DynamicSpriteList.prototype._render = function() {
|
||||
var t = this.templates[name];
|
||||
var ox = (t.offset && t.offset.x) || 0;
|
||||
var oy = (t.offset && t.offset.y) || 0;
|
||||
var sid = this._addSpriteInternal(name, t.spriteId, ox, rowY + oy, rowIndex, t.clickable !== false);
|
||||
var px = (this.orientation === 'horizontal') ? (rowBase + ox) : ox;
|
||||
var py = (this.orientation === 'horizontal') ? oy : (rowBase + oy);
|
||||
var sid = this._addSpriteInternal(name, t.spriteId, px, py, rowIndex, t.clickable !== false);
|
||||
if (t.textProperty && rowData[t.textProperty] !== undefined) {
|
||||
set_self(sid, 7, rowData[t.textProperty], 0, 0);
|
||||
}
|
||||
@@ -708,19 +767,27 @@ DynamicSpriteList.prototype._render = function() {
|
||||
* 创建行上下文对象(传给 renderRow 回调)
|
||||
* @param {number} rowIndex - 行索引
|
||||
* @param {Object} rowData - 行数据
|
||||
* @param {number} rowBaseY - 行基准 Y 坐标
|
||||
* @param {number} rowBase - 行基准坐标(纵向=Y,横向=X)
|
||||
* @returns {Object} 行上下文
|
||||
* @private
|
||||
*/
|
||||
DynamicSpriteList.prototype._makeRowContext = function (rowIndex, rowData, rowBaseY) {
|
||||
DynamicSpriteList.prototype._makeRowContext = function (rowIndex, rowData, rowBase) {
|
||||
var self = this;
|
||||
var isH = (this.orientation === 'horizontal');
|
||||
return {
|
||||
rowIndex: rowIndex,
|
||||
rowData: rowData,
|
||||
rowBaseY: rowBaseY,
|
||||
rowBase: rowBase,
|
||||
// 保留 rowBaseY:renderRow 回调的既有字段协议,不得破坏。
|
||||
// 当前生产消费方为 RecordView.js(唯一使用本组件的调用方);
|
||||
// 回归测试 DynamicSpriteList.test.js 对该字段有硬断言。
|
||||
rowBaseY: rowBase,
|
||||
orientation: this.orientation,
|
||||
containerId: this.containerId,
|
||||
addSprite: function (name, templateSpriteId, x, y) {
|
||||
return self._addSpriteInternal(name, templateSpriteId, x, rowBaseY + y, rowIndex, true);
|
||||
var px = isH ? (rowBase + x) : x;
|
||||
var py = isH ? y : (rowBase + y);
|
||||
return self._addSpriteInternal(name, templateSpriteId, px, py, rowIndex, true);
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -730,38 +797,39 @@ DynamicSpriteList.prototype._makeRowContext = function (rowIndex, rowData, rowBa
|
||||
* @private
|
||||
*/
|
||||
DynamicSpriteList.prototype._handleBounce = function() {
|
||||
var currentY = get_self(this.containerId, 19, 0, 0, 0);
|
||||
var contentHeight = get_self(this.containerId, 21, 0, 0, 0);
|
||||
var clipY = this.clipArea.y;
|
||||
var clipH = this.clipArea.height;
|
||||
|
||||
var targetY = currentY;
|
||||
var axis = this._axis;
|
||||
var current = get_self(this.containerId, axis.posOp, 0, 0, 0);
|
||||
var contentSize = get_self(this.containerId, axis.sizeOp, 0, 0, 0);
|
||||
var clipStart = this.clipArea[axis.clipPos];
|
||||
var clipSize = this.clipArea[axis.clipSize];
|
||||
|
||||
var target = current;
|
||||
var needBounce = false;
|
||||
|
||||
if (contentHeight <= clipH) {
|
||||
// 内容不足一屏,回弹到顶部
|
||||
if (currentY !== clipY) {
|
||||
targetY = clipY;
|
||||
|
||||
if (contentSize <= clipSize) {
|
||||
// 内容不足一屏,回弹到起点
|
||||
if (current !== clipStart) {
|
||||
target = clipStart;
|
||||
needBounce = true;
|
||||
}
|
||||
} else {
|
||||
if (currentY > clipY) {
|
||||
// 超出顶部
|
||||
targetY = clipY;
|
||||
if (current > clipStart) {
|
||||
// 越过起点
|
||||
target = clipStart;
|
||||
needBounce = true;
|
||||
} else {
|
||||
var minY = clipY + clipH - contentHeight;
|
||||
if (currentY < minY) {
|
||||
// 超出底部
|
||||
targetY = minY;
|
||||
var min = clipStart + clipSize - contentSize;
|
||||
if (current < min) {
|
||||
// 越过末端
|
||||
target = min;
|
||||
needBounce = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (needBounce) {
|
||||
play_ani(1, this.containerId, 19, currentY, targetY, 0,
|
||||
Math.abs(targetY - currentY), 0, 0, 0, this.bounceSpeed, 0, 0, 0);
|
||||
play_ani(1, this.containerId, axis.posOp, current, target, 0,
|
||||
Math.abs(target - current), 0, 0, 0, this.bounceSpeed, 0, 0, 0);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -772,6 +840,16 @@ DynamicSpriteList.prototype._handleBounce = function() {
|
||||
* @private
|
||||
*/
|
||||
DynamicSpriteList.prototype._handleClick = function(x, y) {
|
||||
// 裁剪区外的点击一律不算数。
|
||||
// 原因:引擎的 set_clip 只裁【绘制】、不裁【命中】,而 _render 会把容器的主轴尺寸
|
||||
// 撑到内容全长(远超可视区)。于是滑出可视区、屏幕上已经看不见的那些项,其矩形
|
||||
// 仍在容器内,ifast_check_add 照样判为命中 —— 用户点到列表旁的空白带,却选中了
|
||||
// 一个根本看不见的项。这里按 clipArea 兜住,横纵通用。
|
||||
var a = this.clipArea;
|
||||
if (x < a.x || x >= a.x + a.width || y < a.y || y >= a.y + a.height) {
|
||||
return;
|
||||
}
|
||||
|
||||
var clickedTag = ifast_check_add(this.containerId, x, y);
|
||||
if (clickedTag === -99999999) { return; }
|
||||
var rec = this._tagIndex[clickedTag];
|
||||
|
||||
@@ -63,6 +63,10 @@ var RecordView = {
|
||||
if (typeof gradeinfo[i].gameinfo1 === 'string') {
|
||||
gradeinfo[i].gameinfo1 = JSON.parse(gradeinfo[i].gameinfo1);
|
||||
}
|
||||
// roomtype 归一:平台战绩记录里 roomtype 可能被多做了一层 JSON 序列化,
|
||||
// 形如 '"11221000100003"'。下游权威 RoomConfigUtils.parse 严格要求 14 位裸字符串
|
||||
// (故意不做多格式兼容),故在此收包边界剥掉多余的 JSON 引号层。
|
||||
gradeinfo[i].roomtype = this._normalizeRoomtype(gradeinfo[i].roomtype);
|
||||
if (i === 0) { this._gradeIdx1 = gradeinfo[i].idx; }
|
||||
if (i === gradeinfo.length - 1) { this._gradeIdx2 = gradeinfo[i].idx; }
|
||||
}
|
||||
@@ -70,6 +74,17 @@ var RecordView = {
|
||||
this.open();
|
||||
},
|
||||
|
||||
// 剥掉 roomtype 上多余的 JSON 序列化层:'"11221000100003"' → '11221000100003'。
|
||||
// 多层引号(极端情况)循环剥;非字符串或无引号原样返回;JSON.parse 失败即停止不吞坏值。
|
||||
_normalizeRoomtype: function (rt) {
|
||||
var v = rt, guard = 0;
|
||||
while (typeof v === 'string' && v.length >= 2 && v.charAt(0) === '"' && guard < 5) {
|
||||
try { v = JSON.parse(v); } catch (e) { break; }
|
||||
guard++;
|
||||
}
|
||||
return v;
|
||||
},
|
||||
|
||||
// 翻页/类型 → 发获取战绩请求(平台通用协议)。类型切换 _requestGrade(type,null);翻页 _requestGrade(_,direction)。
|
||||
_requestGrade: function (arg1, direction) {
|
||||
var data = { agentid: GameData.AgentId, playerid: C_Player.playerid, gameid: GameData.GameId };
|
||||
@@ -77,9 +92,9 @@ var RecordView = {
|
||||
this._type = arg1; // 类型切换
|
||||
data.type = arg1;
|
||||
} else {
|
||||
data.type = this._type; // 翻页:type 用当前,direction + gradeidx
|
||||
data.type = this._type; // 翻页:type 用当前,direction + idx
|
||||
data.direction = direction;
|
||||
data.gradeidx = (direction === 1) ? this._gradeIdx1 : this._gradeIdx2;
|
||||
data.idx = (direction === 1) ? this._gradeIdx1 : this._gradeIdx2;
|
||||
}
|
||||
if (typeof Net !== 'undefined' && Net.Send_get_player_grade1) { Net.Send_get_player_grade1(data); }
|
||||
},
|
||||
@@ -89,7 +104,7 @@ var RecordView = {
|
||||
_requestReplayData: function (gameIndex) {
|
||||
var data = { agentid: GameData.AgentId, playerid: C_Player.playerid, gameid: GameData.GameId };
|
||||
if (this._gradeData && this._gradeData[gameIndex] && typeof this._gradeData[gameIndex].idx !== 'undefined') {
|
||||
data.gradeidx = this._gradeData[gameIndex].idx;
|
||||
data.idx = this._gradeData[gameIndex].idx;
|
||||
}
|
||||
if (typeof Net !== 'undefined' && Net.Send_get_player_grade2) { Net.Send_get_player_grade2(data); }
|
||||
},
|
||||
|
||||
@@ -145,7 +145,7 @@ var UIManager = (function () {
|
||||
SpriteManager.hide(LOADING_UI.SPRITES.LOADING_ICON);
|
||||
SpriteManager.hide(LOADING_UI.SPRITES.LOADING_TEXT);
|
||||
SpriteManager.hideGroup(LOADING_UI.GROUP_ID);
|
||||
console.log('[UIManager] Loading UI 初始化 (Layer ' + LOADING_UI.LAYER + ')');
|
||||
// console.log('[UIManager] Loading UI 初始化 (Layer ' + LOADING_UI.LAYER + ')');
|
||||
}
|
||||
|
||||
/** @private */
|
||||
@@ -153,7 +153,7 @@ var UIManager = (function () {
|
||||
SpriteManager.hide(MESSAGE_UI.SPRITES.BACKGROUND);
|
||||
SpriteManager.hide(MESSAGE_UI.SPRITES.TEXT);
|
||||
SpriteManager.hideGroup(MESSAGE_UI.GROUP_ID);
|
||||
console.log('[UIManager] Message UI 初始化 (Layer ' + MESSAGE_UI.LAYER + ')');
|
||||
// console.log('[UIManager] Message UI 初始化 (Layer ' + MESSAGE_UI.LAYER + ')');
|
||||
}
|
||||
|
||||
/** @private */
|
||||
@@ -164,7 +164,7 @@ var UIManager = (function () {
|
||||
SpriteManager.hide(CONFIRM_UI.SPRITES.BTN_CONFIRM);
|
||||
SpriteManager.hide(CONFIRM_UI.SPRITES.BTN_CANCEL);
|
||||
SpriteManager.hideGroup(CONFIRM_UI.GROUP_ID);
|
||||
console.log('[UIManager] Confirm UI 初始化 (Layer ' + CONFIRM_UI.LAYER + ')');
|
||||
// console.log('[UIManager] Confirm UI 初始化 (Layer ' + CONFIRM_UI.LAYER + ')');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -227,7 +227,7 @@ var UIManager = (function () {
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log('[UIManager] 初始化 (v1.0.0)');
|
||||
// console.log('[UIManager] 初始化 (v1.0.0)');
|
||||
|
||||
try {
|
||||
_components = {};
|
||||
@@ -239,7 +239,7 @@ var UIManager = (function () {
|
||||
|
||||
_isInitialized = true;
|
||||
this.initialized = true;
|
||||
console.log('[UIManager] 初始化完成');
|
||||
// console.log('[UIManager] 初始化完成');
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('[UIManager] 初始化失败:', err);
|
||||
@@ -279,7 +279,7 @@ var UIManager = (function () {
|
||||
console.warn('[UIManager] registerComponent: 覆盖已存在组件:', name);
|
||||
}
|
||||
_components[name] = component;
|
||||
console.log('[UIManager] 注册组件 "' + name + '"');
|
||||
// console.log('[UIManager] 注册组件 "' + name + '"');
|
||||
return true;
|
||||
},
|
||||
|
||||
@@ -305,7 +305,7 @@ var UIManager = (function () {
|
||||
var c = _components[name];
|
||||
if (c.isVisible) { c.hide(); }
|
||||
delete _components[name];
|
||||
console.log('[UIManager] 注销组件 "' + name + '"');
|
||||
// console.log('[UIManager] 注销组件 "' + name + '"');
|
||||
return true;
|
||||
},
|
||||
|
||||
@@ -321,7 +321,7 @@ var UIManager = (function () {
|
||||
}
|
||||
_components[name].destroy();
|
||||
delete _components[name];
|
||||
console.log('[UIManager] 销毁组件 "' + name + '"');
|
||||
// console.log('[UIManager] 销毁组件 "' + name + '"');
|
||||
return true;
|
||||
},
|
||||
|
||||
@@ -371,7 +371,7 @@ var UIManager = (function () {
|
||||
}
|
||||
}
|
||||
_scenes[sceneName] = componentNames;
|
||||
console.log('[UIManager] 注册场景 "' + sceneName + '" (' + componentNames.length + ' 个组件)');
|
||||
// console.log('[UIManager] 注册场景 "' + sceneName + '" (' + componentNames.length + ' 个组件)');
|
||||
return true;
|
||||
},
|
||||
|
||||
@@ -390,7 +390,7 @@ var UIManager = (function () {
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log('[UIManager] 切换场景 "' + (_currentScene || 'null') + '" → "' + sceneName + '"');
|
||||
// console.log('[UIManager] 切换场景 "' + (_currentScene || 'null') + '" → "' + sceneName + '"');
|
||||
_hideCurrentScene();
|
||||
_showScene(sceneName);
|
||||
|
||||
@@ -447,7 +447,7 @@ var UIManager = (function () {
|
||||
* @param {string} message
|
||||
*/
|
||||
showToast: function (message) {
|
||||
console.log('[UIManager] Toast:', message);
|
||||
// console.log('[UIManager] Toast:', message);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -458,7 +458,7 @@ var UIManager = (function () {
|
||||
*/
|
||||
showEffectToast: function (tag, title, description) {
|
||||
var msg = description ? (title + ':' + description) : title;
|
||||
console.log('[UIManager] EffectToast [' + tag + ']:', msg);
|
||||
// console.log('[UIManager] EffectToast [' + tag + ']:', msg);
|
||||
this.showToast(msg);
|
||||
},
|
||||
|
||||
@@ -575,14 +575,14 @@ var UIManager = (function () {
|
||||
* 打印状态(调试用)
|
||||
*/
|
||||
printState: function () {
|
||||
console.log('[UIManager] 状态:', JSON.stringify(this.getState(), null, 2));
|
||||
// console.log('[UIManager] 状态:', JSON.stringify(this.getState(), null, 2));
|
||||
},
|
||||
|
||||
/**
|
||||
* 销毁 UIManager(销毁所有组件并重置状态)
|
||||
*/
|
||||
destroy: function () {
|
||||
console.log('[UIManager] 开始销毁');
|
||||
// console.log('[UIManager] 开始销毁');
|
||||
for (var name in _components) {
|
||||
if (_components.hasOwnProperty(name)) { this.destroyComponent(name); }
|
||||
}
|
||||
@@ -594,7 +594,7 @@ var UIManager = (function () {
|
||||
_currentScene = null;
|
||||
_isInitialized = false;
|
||||
this.initialized = false;
|
||||
console.log('[UIManager] 销毁完成');
|
||||
// console.log('[UIManager] 销毁完成');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user