二七王:回退自造的图集叠绘,改用平台既有 SpriteManager.setNumberImage
方案作废并纠正认知:多帧图片精灵【不是】只能显示一位数——SpriteManager 第 1154 行 早有 setNumberImage(spriteId, text, charWidth):把整串文本设给一个多帧图片精灵, 引擎按字符逐帧渲染,精灵宽度按「字符数 × charWidth」自动调整。bc75e40 造的 drawImageRegion + NumberRenderer 是重复轮子,本次整体回退(revert bc75e40)。 - 回退:删除 gameabc-framework/ui/NumberRenderer.js 与其单测,SpriteManager 与 index.html 恢复到改动前(与 bc75e40^ 逐字节一致)。 - ImageResources.js:数字类资源改为 16 帧、固定帧序 0123456789.+-x/p,逐条写明 各帧含义、第16帧后缀(选主张数=「张」;结算/叫分的后缀标注待美术与规则确认,未瞎填)、 单字符宽高与整图尺寸(16 × 单字宽),并说明帧序不可重排、不可省略占位。 - LayoutConstants.js:NUM_STYLE 改为 setNumberImage 所需的 res/charWidth/charHeight/suffix。 - Layout_Action.js:两个数字节点 h 取 charHeight(SSOT),w 留运行时注入 (宽度由 setNumberImage 按字符数自动改,随位数变)。 - Sprites_Action.js / Sprites_Result.js:注释改为 setNumberImage 用法; 结算玩家栏 ACC_TPL_SCORE_NUM 是复制精灵也能用(该接口不依赖绘制回调)。 - test_constants.js:NUM_STYLE 守卫改为校验 charWidth/charHeight/suffix 与 「h 同源、w 运行时注入」;suffix 限定为 setNumberImage 支持的 分/倍/张。 - 文档:清单 §1.3/§1.5/§3.6/§5.4/§6.8 与前端 02 的那一节全部按 setNumberImage 重写, 三种机制改为「文字精灵 / setFrame 切帧 / setNumberImage」,并记下这次误判作为反例。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -717,83 +717,7 @@ var SpriteManager = (function() {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 在精灵上绘制图片的【指定源矩形】(图集裁切绘制)
|
||||
*
|
||||
* 与 drawImage 的区别:
|
||||
* - drawImage = 整图绘制,源矩形固定为图片资源的完整尺寸,一次只能画“一张完整的图”
|
||||
* - drawImageRegion = 可指定源矩形 (srcX, srcY, srcW, srcH),从一张图里裁出任意一格再画
|
||||
*
|
||||
* 典型用途:
|
||||
* - 从等宽排列的图集里裁出第 N 格画上去(如把多位数字逐位画在【同一个】精灵上,
|
||||
* 见 gameabc-framework/ui/NumberRenderer.js)
|
||||
* - 九宫格拉伸、进度条按比例裁切、图集小图标叠绘
|
||||
*
|
||||
* ⚠️ 前提:必须在【精灵的绘制回调中】调用(SpriteEventController.registerDraw 注册的
|
||||
* handler 里,或 registerGlobalDraw 的全局钩子里)。引擎的叠绘只在该精灵绘制的那一
|
||||
* 帧生效,在回调之外调用不会留下任何画面。
|
||||
*
|
||||
* @param {Number} spriteId - 目标精灵ID(在哪个精灵上绘制)
|
||||
* @param {Number} imageId - 要绘制的图片资源ID
|
||||
* @param {Number} destX - 目标X坐标(在精灵内的相对坐标)
|
||||
* @param {Number} destY - 目标Y坐标(在精灵内的相对坐标)
|
||||
* @param {Number} destWidth - 目标绘制宽度
|
||||
* @param {Number} destHeight - 目标绘制高度
|
||||
* @param {Number} srcX - 源图片截取区域的X坐标
|
||||
* @param {Number} srcY - 源图片截取区域的Y坐标
|
||||
* @param {Number} srcWidth - 源图片截取区域的宽度
|
||||
* @param {Number} srcHeight - 源图片截取区域的高度
|
||||
* @returns {Boolean} 是否成功
|
||||
*
|
||||
* @example
|
||||
* // 数字图集单字宽 30、高 40,'7' 是第 8 格(索引 7):
|
||||
* // 把它画到精灵 1617 内的 (0, 0) 处,原样大小
|
||||
* SpriteManager.drawImageRegion(1617, 635, 0, 0, 30, 40, 7 * 30, 0, 30, 40);
|
||||
*/
|
||||
drawImageRegion: function(spriteId, imageId, destX, destY, destWidth, destHeight,
|
||||
srcX, srcY, srcWidth, srcHeight) {
|
||||
if (!_validateSpriteId(spriteId)) return false;
|
||||
|
||||
// 参数验证
|
||||
if (typeof imageId !== 'number' || imageId <= 0) {
|
||||
console.error('❌ SpriteManager.drawImageRegion: 图片资源ID必须是正数:', imageId);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof destX !== 'number' || typeof destY !== 'number') {
|
||||
console.error('❌ SpriteManager.drawImageRegion: 目标坐标必须是数字');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof destWidth !== 'number' || destWidth <= 0 ||
|
||||
typeof destHeight !== 'number' || destHeight <= 0) {
|
||||
console.error('❌ SpriteManager.drawImageRegion: 目标宽度和高度必须是正数');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof srcX !== 'number' || typeof srcY !== 'number') {
|
||||
console.error('❌ SpriteManager.drawImageRegion: 源矩形坐标必须是数字');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof srcWidth !== 'number' || srcWidth <= 0 ||
|
||||
typeof srcHeight !== 'number' || srcHeight <= 0) {
|
||||
console.error('❌ SpriteManager.drawImageRegion: 源矩形宽度和高度必须是正数');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 调用 GameABCUtils.Draw.drawImage(带源矩形的完整版绘图接口)
|
||||
GameABCUtils.Draw.drawImage(spriteId, imageId, destX, destY, destWidth, destHeight,
|
||||
srcX, srcY, srcWidth, srcHeight);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('❌ SpriteManager.drawImageRegion 失败:', error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// ==================== 交互控制 ====================
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,336 +0,0 @@
|
||||
// ============================================================================
|
||||
// NumberRenderer (gameabc-framework / ui, 严格 ES5)
|
||||
// 图集叠绘式数字渲染器——把一个【多位数值】画在【一个】精灵上。游戏中立。
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* NumberRenderer —— 一个精灵画多位美术字数字
|
||||
*
|
||||
* ── 解决什么问题 ────────────────────────────────────────────────────────
|
||||
* 带描边/渐变的美术字数字(倒计时、结算得分、叫分档位分数、张数…)文字精灵做不出,
|
||||
* 只能用图。但「多帧图片精灵 + SpriteManager.setFrame」一个精灵一次只显示【一帧】,
|
||||
* 也就是【一位】数字——两位数就要两个精灵(十位/个位),精灵数迅速膨胀。
|
||||
*
|
||||
* 本模块改用【图集叠绘】:在精灵的绘制回调里,按字宽从数字图集里逐位裁源矩形,
|
||||
* 逐位画到【同一个】精灵内的不同 x 上,于是一个精灵就能显示任意位数。
|
||||
*
|
||||
* ── 三种显示机制的选用(详见 docs/client/development-guide/02 §2)──────────
|
||||
* 文字精灵 SpriteManager.setText 任意长度,但只能用系统字体
|
||||
* 多帧图片精灵 SpriteManager.setFrame 一次一帧,适合「状态切换」(按钮态/花色/牌面)
|
||||
* 图集叠绘 NumberRenderer 一个精灵画多位美术字 ← 本模块
|
||||
*
|
||||
* ── 用法 ────────────────────────────────────────────────────────────────
|
||||
* // 1) 绑定:记住样式 + 注册绘制回调(样式由【调用方】即子游戏提供,本模块不认识业务)
|
||||
* NumberRenderer.bind(spriteId, {
|
||||
* imageId: 635, // 数字图集的图片资源 ID
|
||||
* charWidth: 30, // 图集里单个字符的宽
|
||||
* charHeight: 40, // 图集里单个字符的高
|
||||
* spacing: 0, // 字间距(可为负 = 字符重叠)
|
||||
* align: 'center', // 'left' | 'center' | 'right'
|
||||
* width: 74 // 绘制区宽度(= 精灵内可用宽度,由调用方按布局给)
|
||||
* // charMap: {...} // 可选:整体覆盖默认的「字符 → 图集索引」映射
|
||||
* });
|
||||
*
|
||||
* // 2) 写值(只写数据、不画;引擎每帧回调绘制handler,自然就画出来了)
|
||||
* NumberRenderer.setValue(spriteId, 70);
|
||||
*
|
||||
* // 3) 清空 / 解绑
|
||||
* NumberRenderer.clear(spriteId); // 不显示任何字符(精灵本身仍在)
|
||||
* NumberRenderer.unbind(spriteId); // 注销绘制回调 + 清除状态(组件 onDestroy 里调)
|
||||
*
|
||||
* ── 图集规格(默认映射)────────────────────────────────────────────────
|
||||
* 图集是【等宽连续排列的一行】,第 idx 格的源矩形 = (idx * charWidth, 0, charWidth, charHeight):
|
||||
* 索引 0–9 → 字符 '0'–'9'
|
||||
* 索引 10 → '+' (有些数字图带正负号)
|
||||
* 索引 11 → '-'
|
||||
* 图集若不是这个排列(多了小数点、`分` 等),用 style.charMap 整体覆盖。
|
||||
* 映射不到的字符【显式抛错】,不静默跳过(engineering 总则 7:显式失败优于隐式兜底)。
|
||||
*
|
||||
* ── 依赖与边界 ──────────────────────────────────────────────────────────
|
||||
* - 只调 SpriteManager.drawImageRegion 与 SpriteEventController,不碰 GameABCUtils / 引擎原语。
|
||||
* - 游戏中立:不含任何具体玩法的资源 ID、常量或概念,样式全部由调用方传入。
|
||||
* - 只支持【数字 ID 的预置精灵】:SpriteEventController.registerDraw 要求 spriteId 是数字,
|
||||
* SpriteCopyUtils 复制出的字符串 ID 精灵不接收独立绘制事件,不能用本模块。
|
||||
*/
|
||||
var NumberRenderer = {
|
||||
|
||||
/**
|
||||
* 默认「字符 → 图集索引」映射:等宽连续排列的一行,0–9 后接 + 与 -
|
||||
* @type {Object}
|
||||
*/
|
||||
DEFAULT_CHAR_MAP: {
|
||||
'0': 0, '1': 1, '2': 2, '3': 3, '4': 4,
|
||||
'5': 5, '6': 6, '7': 7, '8': 8, '9': 9,
|
||||
'+': 10,
|
||||
'-': 11
|
||||
},
|
||||
|
||||
/**
|
||||
* 合法的对齐方式
|
||||
* @type {Array}
|
||||
*/
|
||||
ALIGNS: ['left', 'center', 'right'],
|
||||
|
||||
/**
|
||||
* 已绑定精灵的状态表 { spriteId: { style: {...}, value: String|null, items: Array } }
|
||||
* items 是 layout() 的结果,在 setValue 时算好、绘制回调里直接重放
|
||||
* @type {Object}
|
||||
*/
|
||||
_bound: {},
|
||||
|
||||
// ========================================================================
|
||||
// 纯函数:布局计算(不碰引擎、可脱离运行时单测)
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* 校验样式对象;不合法直接抛错(显式失败)
|
||||
* @param {Object} style - 样式
|
||||
* @returns {Object} 归一化后的样式副本
|
||||
*/
|
||||
normalizeStyle: function (style) {
|
||||
if (!style || typeof style !== 'object') {
|
||||
throw new Error('NumberRenderer: style 必须是对象');
|
||||
}
|
||||
if (typeof style.imageId !== 'number' || style.imageId <= 0) {
|
||||
throw new Error('NumberRenderer: style.imageId 必须是正数,收到 ' + style.imageId);
|
||||
}
|
||||
if (typeof style.charWidth !== 'number' || style.charWidth <= 0) {
|
||||
throw new Error('NumberRenderer: style.charWidth 必须是正数,收到 ' + style.charWidth);
|
||||
}
|
||||
if (typeof style.charHeight !== 'number' || style.charHeight <= 0) {
|
||||
throw new Error('NumberRenderer: style.charHeight 必须是正数,收到 ' + style.charHeight);
|
||||
}
|
||||
if (typeof style.width !== 'number' || style.width <= 0) {
|
||||
throw new Error('NumberRenderer: style.width(绘制区宽度)必须是正数,收到 ' + style.width);
|
||||
}
|
||||
if (typeof style.spacing !== 'undefined' && typeof style.spacing !== 'number') {
|
||||
throw new Error('NumberRenderer: style.spacing 必须是数字(可为负),收到 ' + style.spacing);
|
||||
}
|
||||
if (this.ALIGNS.indexOf(style.align) < 0) {
|
||||
throw new Error('NumberRenderer: style.align 必须是 left/center/right 之一,收到 ' + style.align);
|
||||
}
|
||||
if (typeof style.charMap !== 'undefined' &&
|
||||
(!style.charMap || typeof style.charMap !== 'object')) {
|
||||
throw new Error('NumberRenderer: style.charMap 必须是对象');
|
||||
}
|
||||
|
||||
return {
|
||||
imageId: style.imageId,
|
||||
charWidth: style.charWidth,
|
||||
charHeight: style.charHeight,
|
||||
spacing: (typeof style.spacing === 'number') ? style.spacing : 0,
|
||||
align: style.align,
|
||||
width: style.width,
|
||||
charMap: style.charMap || this.DEFAULT_CHAR_MAP
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* 计算一个数值在精灵内的逐字符绘制矩形(纯函数,无副作用)
|
||||
*
|
||||
* 第 idx 个字符(idx = charMap[字符])的源矩形固定为
|
||||
* (idx * charWidth, 0, charWidth, charHeight)
|
||||
* 起始 x 由 style.width 与总宽 n*charWidth + (n-1)*spacing 按 align 算出。
|
||||
* 【不读精灵实际尺寸】——宽度一律由 style 给,保证本函数可脱离引擎单测。
|
||||
*
|
||||
* @param {String|Number|null} value - 要显示的值(null/undefined/'' = 不显示任何字符)
|
||||
* @param {Object} style - 样式(字段见文件头;本函数内部会先 normalizeStyle)
|
||||
* @returns {Array} [{ char, index, srcX, srcY, srcW, srcH, destX, destY, destW, destH }, ...]
|
||||
* @throws {Error} 样式非法、或字符映射不到图集索引
|
||||
*
|
||||
* @example
|
||||
* NumberRenderer.layout(70, { imageId: 635, charWidth: 30, charHeight: 40,
|
||||
* spacing: 0, align: 'left', width: 74 });
|
||||
* // → [ {char:'7', index:7, srcX:210, ..., destX: 0, ...},
|
||||
* // {char:'0', index:0, srcX:0, ..., destX:30, ...} ]
|
||||
*/
|
||||
layout: function (value, style) {
|
||||
var s = this.normalizeStyle(style);
|
||||
|
||||
if (value === null || typeof value === 'undefined') { return []; }
|
||||
var text = String(value);
|
||||
if (text.length === 0) { return []; }
|
||||
|
||||
var n = text.length;
|
||||
var totalWidth = n * s.charWidth + (n - 1) * s.spacing;
|
||||
|
||||
var startX;
|
||||
if (s.align === 'left') {
|
||||
startX = 0;
|
||||
} else if (s.align === 'right') {
|
||||
startX = s.width - totalWidth;
|
||||
} else {
|
||||
startX = (s.width - totalWidth) / 2;
|
||||
}
|
||||
|
||||
var items = [];
|
||||
for (var i = 0; i < n; i++) {
|
||||
var ch = text.charAt(i);
|
||||
if (!Object.prototype.hasOwnProperty.call(s.charMap, ch)) {
|
||||
throw new Error('NumberRenderer: 字符 "' + ch + '" 不在 charMap 里(值 "' + text +
|
||||
'")——图集没有这一格,请补映射或改用文字精灵');
|
||||
}
|
||||
var idx = s.charMap[ch];
|
||||
if (typeof idx !== 'number' || idx < 0) {
|
||||
throw new Error('NumberRenderer: 字符 "' + ch + '" 映射到非法索引 ' + idx);
|
||||
}
|
||||
items.push({
|
||||
char: ch,
|
||||
index: idx,
|
||||
srcX: idx * s.charWidth,
|
||||
srcY: 0,
|
||||
srcW: s.charWidth,
|
||||
srcH: s.charHeight,
|
||||
destX: startX + i * (s.charWidth + s.spacing),
|
||||
destY: 0,
|
||||
destW: s.charWidth,
|
||||
destH: s.charHeight
|
||||
});
|
||||
}
|
||||
return items;
|
||||
},
|
||||
|
||||
// ========================================================================
|
||||
// 绑定 / 写值 / 清空 / 解绑
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* 绑定精灵:记下样式并注册绘制回调
|
||||
*
|
||||
* 重复 bind 同一个精灵 = 换样式(回调只注册一次,值被重置为空)。
|
||||
*
|
||||
* @param {Number} spriteId - 精灵ID(必须是编辑器预置的数字 ID)
|
||||
* @param {Object} style - 样式(字段见文件头)
|
||||
* @returns {Boolean} 是否成功
|
||||
* @throws {Error} 样式非法(显式失败,不静默返回 false)
|
||||
*/
|
||||
bind: function (spriteId, style) {
|
||||
if (typeof spriteId !== 'number') {
|
||||
throw new Error('NumberRenderer.bind: spriteId 必须是数字(复制精灵不支持叠绘)');
|
||||
}
|
||||
if (typeof SpriteEventController === 'undefined') {
|
||||
console.error('❌ NumberRenderer.bind: 缺少依赖 SpriteEventController');
|
||||
return false;
|
||||
}
|
||||
|
||||
var normalized = this.normalizeStyle(style);
|
||||
this._bound[spriteId] = { style: normalized, value: null, items: [] };
|
||||
|
||||
var self = this;
|
||||
return SpriteEventController.registerDraw(spriteId, function (e) {
|
||||
self._draw(e.spriteId);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 写值(只写数据,不画;实际绘制由引擎每帧回调完成)
|
||||
*
|
||||
* 逐字符矩形在此刻就算好并缓存:一是让「字符映射不到」这类错误在【调用处】当场抛出,
|
||||
* 而不是每帧在绘制回调里刷屏;二是避免每帧重复计算与分配。
|
||||
*
|
||||
* @param {Number} spriteId - 精灵ID(须先 bind)
|
||||
* @param {String|Number} value - 要显示的值
|
||||
* @returns {Boolean} 是否成功
|
||||
* @throws {Error} 未绑定、或值含图集里没有的字符
|
||||
*/
|
||||
setValue: function (spriteId, value) {
|
||||
var state = this._bound[spriteId];
|
||||
if (!state) {
|
||||
throw new Error('NumberRenderer.setValue: 精灵 ' + spriteId + ' 尚未 bind');
|
||||
}
|
||||
state.items = this.layout(value, state.style);
|
||||
state.value = (value === null || typeof value === 'undefined') ? null : String(value);
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* 清空:不显示任何字符(精灵本身与绑定都还在)
|
||||
* @param {Number} spriteId - 精灵ID(须先 bind)
|
||||
* @returns {Boolean} 是否成功
|
||||
* @throws {Error} 未绑定
|
||||
*/
|
||||
clear: function (spriteId) {
|
||||
var state = this._bound[spriteId];
|
||||
if (!state) {
|
||||
throw new Error('NumberRenderer.clear: 精灵 ' + spriteId + ' 尚未 bind');
|
||||
}
|
||||
state.value = null;
|
||||
state.items = [];
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* 解绑:注销绘制回调并清除状态(精灵销毁/组件 onDestroy 时必须调,防回调残留)
|
||||
* @param {Number} spriteId - 精灵ID
|
||||
* @returns {Boolean} 是否成功
|
||||
*/
|
||||
unbind: function (spriteId) {
|
||||
if (typeof spriteId !== 'number') {
|
||||
console.error('❌ NumberRenderer.unbind: spriteId 必须是数字:', spriteId);
|
||||
return false;
|
||||
}
|
||||
delete this._bound[spriteId];
|
||||
|
||||
if (typeof SpriteEventController === 'undefined') {
|
||||
console.error('❌ NumberRenderer.unbind: 缺少依赖 SpriteEventController');
|
||||
return false;
|
||||
}
|
||||
return SpriteEventController.unregister(spriteId, 'draw');
|
||||
},
|
||||
|
||||
/**
|
||||
* 取当前值(调试/自检用)
|
||||
* @param {Number} spriteId - 精灵ID
|
||||
* @returns {String|null} 当前值;未绑定返回 null
|
||||
*/
|
||||
getValue: function (spriteId) {
|
||||
var state = this._bound[spriteId];
|
||||
return state ? state.value : null;
|
||||
},
|
||||
|
||||
/**
|
||||
* 是否已绑定
|
||||
* @param {Number} spriteId - 精灵ID
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
isBound: function (spriteId) {
|
||||
return Object.prototype.hasOwnProperty.call(this._bound, spriteId);
|
||||
},
|
||||
|
||||
// ========================================================================
|
||||
// 内部:绘制回调(由引擎每帧触发)
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* 绘制回调实现:把缓存的逐字符矩形画到精灵上
|
||||
* @param {Number} spriteId - 精灵ID
|
||||
* @private
|
||||
*/
|
||||
_draw: function (spriteId) {
|
||||
var state = this._bound[spriteId];
|
||||
if (!state || state.items.length === 0) { return; }
|
||||
|
||||
if (typeof SpriteManager === 'undefined' || !SpriteManager.drawImageRegion) {
|
||||
console.error('❌ NumberRenderer: 缺少依赖 SpriteManager.drawImageRegion');
|
||||
return;
|
||||
}
|
||||
|
||||
var imageId = state.style.imageId;
|
||||
for (var i = 0; i < state.items.length; i++) {
|
||||
var it = state.items[i];
|
||||
SpriteManager.drawImageRegion(spriteId, imageId,
|
||||
it.destX, it.destY, it.destW, it.destH,
|
||||
it.srcX, it.srcY, it.srcW, it.srcH);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 导出
|
||||
// ============================================================================
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = NumberRenderer;
|
||||
} else if (typeof window !== 'undefined') {
|
||||
window.NumberRenderer = NumberRenderer;
|
||||
}
|
||||
Reference in New Issue
Block a user