二七王:数字改回「一个数字一个精灵」,数字资源改为等宽连续图

有了图集叠绘能力,多位数不再需要每位一个精灵:

- Sprites_Action.js:选主花色张数从每花色两个(_TENS/_ONES)改回每花色一个
  (MAIN_SUIT_COUNT_1..4 = 1674–1677),空出的 1678–1681 按本文件既有写法记跳号、不复用;
  叫分档位 CALL_BTN_NUM_1..14 数量不变,注释改为图集叠绘(现在能正确显示两位数)。
- ImageResources.js:NUM_COUNTDOWN / NUM_RESULT_WIN / NUM_RESULT_LOSE / NUM_CALL_SCORE /
  NUM_MAIN_SUIT_COUNT 不再是编辑器配帧的多帧资源,改为等宽连续排列的一张图,
  逐条写明字符排列顺序、单字符宽高、整图尺寸(给美术的出图规格)。
- Layout_Action.js:张数的十位/个位两节点合并为一个;两个数字节点的 w/h 直接引用
  NUM_STYLE(SSOT),不再由调用方运行时注入。
- Sprites_Result.js:记下 ACC_TPL_SCORE_NUM 是复制精灵、无法用 NumberRenderer 的风险。
- 单测:精灵总数 382→378、布局节点 62→61(按实际更新,未放宽下界);
  新增 NUM_STYLE 资源键/字段合法性与「布局节点 w/h 与样式同源」守卫。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-26 23:53:16 +08:00
co-authored by Claude Opus 5
parent 3789e0814e
commit 2fe0f661b4
6 changed files with 83 additions and 44 deletions
@@ -280,23 +280,31 @@ var EQW_Images = EQW_Images || {
LINE_DIVIDER: 613, LINE_DIVIDER: 613,
//===================== §3.6 数字与符号 ===================== //===================== §3.6 数字与符号 =====================
//
//【2026-08-26 出图方式变更】以下 NUM_* 数字类资源【不是】编辑器里配好帧数的多帧资源,
//而是【等宽连续排列的一张图】:所有字符横向排成一行、每格等宽,图里不留间隙。
//代码不用 setFrame(一个精灵一次只显示一帧=只能显示一位数),改为按字宽裁源矩形叠绘:
//第 idx 格的源矩形 = (idx * 单字宽, 0, 单字宽, 单字高),由框架 NumberRenderer 逐位画在
//【同一个】精灵上,故一个多位数只需一个精灵。样式(单字宽高/字间距/对齐/绘制区宽)见
//EQW_Layout.NUM_STYLE.*(LayoutConstants.js),本处只记出图规格。
//美术出图请严格按「字符排列顺序 + 单字符宽高 + 整图尺寸」执行,字符必须等宽对齐格子。
//用途:倒计时数字(带描边/渐变美术字) //用途:倒计时数字(带描边/渐变美术字)
//帧数:10 //排列:等宽连续一行,字符顺序 0 1 2 3 4 5 6 7 8 9(索引 0–9)
//帧说明:帧1–10 = 0–9 //单字符尺寸:44×60
//尺寸:44×60 //整图尺寸:440×60(10 格 × 44)
NUM_COUNTDOWN: 631, NUM_COUNTDOWN: 631,
//用途:结算得分数字(赢,橙,带描边/渐变美术字) //用途:结算得分数字(赢,橙,带描边/渐变美术字)
//帧数:12 //排列:等宽连续一行,字符顺序 0 1 2 3 4 5 6 7 8 9 + -(索引 0–9 为数字,10 = 「+」,11 = 「-」)
//帧说明:帧1–10 = 0–9,帧11 = +,帧12 = - //单字符尺寸:46×62
//尺寸:46×62 //整图尺寸:552×62(12 格 × 46)
NUM_RESULT_WIN: 632, NUM_RESULT_WIN: 632,
//用途:结算得分数字(输,蓝,带描边/渐变美术字) //用途:结算得分数字(输,蓝,带描边/渐变美术字)
//帧数:12 //排列:与 NUM_RESULT_WIN 完全一致——等宽连续一行,0–9 后接 「+」「-」(共 12 格),仅配色不同
//帧说明:帧1–10 = 0–9,帧11 = +,帧12 = - //单字符尺寸:46×62
//尺寸:46×62 //整图尺寸:552×62(12 格 × 46)
NUM_RESULT_LOSE: 633, NUM_RESULT_LOSE: 633,
//用途:判定结果动画(大光/小光/过庄/升N级/投降)——帧动画资源,播放参数进 AnimationConfigs【待设计 D-8】 //用途:判定结果动画(大光/小光/过庄/升N级/投降)——帧动画资源,播放参数进 AnimationConfigs【待设计 D-8】
@@ -306,15 +314,16 @@ var EQW_Images = EQW_Images || {
ANI_JUDGE: 634, ANI_JUDGE: 634,
//用途:叫分档位的分数数字(深棕描边美术字,带描边/渐变,文字精灵做不出此效果) //用途:叫分档位的分数数字(深棕描边美术字,带描边/渐变,文字精灵做不出此效果)
//帧数:10 //排列:等宽连续一行,字符顺序 0 1 2 3 4 5 6 7 8 9(索引 0–9)
//帧说明:帧1–10 = 0–9 //单字符尺寸:30×40
//尺寸:30×40 //整图尺寸:300×40(10 格 × 30)
//说明:14 个档位中 13 个是两位数(70…10),靠叠绘一个精灵画两位,见 Sprites_Action.js 的 CALL_BTN_NUM_1..14
NUM_CALL_SCORE: 635, NUM_CALL_SCORE: 635,
//用途:选主面板花色张数数字(美术单独出图,字号配色与以上几套不同;十位/个位各占一个精灵,见 Sprites_Action.js) //用途:选主面板花色张数数字(美术单独出图,字号配色与以上几套不同;一个花色一个精灵,见 Sprites_Action.js)
//帧数:10 //排列:等宽连续一行,字符顺序 0 1 2 3 4 5 6 7 8 9(索引 0–9)
//帧说明:帧1–10 = 0–9 //单字符尺寸:24×32(估值,与选主按钮 128×55 协调估算,出图前须与最终设计稿核对)
//尺寸:24×32(估值,与选主按钮 128×55 协调估算,出图前须与最终设计稿核对) //整图尺寸:240×32(10 格 × 24)
NUM_MAIN_SUIT_COUNT: 636, NUM_MAIN_SUIT_COUNT: 636,
//===================== §3.7 浮层与气泡 ===================== //===================== §3.7 浮层与气泡 =====================
@@ -18,12 +18,14 @@ EQW_Layout.CALL_BTN_GRID = {
anchor: 'center', fillOrder: 'row' anchor: 'center', fillOrder: 'row'
}; };
//档位分数文字:贴对应档位按钮居中。CALL_BTN_1..14 是 14 个固定精灵,但本节点是共用模板, //档位分数数字:贴对应档位按钮居中。CALL_BTN_1..14 是 14 个固定精灵,但本节点是共用模板,
//target 由调用方按第几档注入(不预置单一 target)。宽高随数字位数变化,清单未给固定值, //target 由调用方按第几档注入(不预置单一 target)。
//资源 NUM_CALL_SCORE 单字位 30×40 仅供参考,不作为本节点的 w/h //2026-08-26:改为图集叠绘(NumberRenderer),一个精灵画完整分数,故宽高【不再随位数变化】——
//直接取 NUM_STYLE.CALL_SCORE 的绘制区宽与单字高(SSOT:数值只在 LayoutConstants 定义一处)
EQW_Layout.CALL_BTN_SCORE_TEXT = { EQW_Layout.CALL_BTN_SCORE_TEXT = {
kind: 'attach', targetKind: 'sprite', hAlign: 'center', vAlign: 'middle', offsetY: 4, kind: 'attach', targetKind: 'sprite', hAlign: 'center', vAlign: 'middle', offsetY: 4,
runtime: ['target', 'w', 'h'] w: EQW_Layout.NUM_STYLE.CALL_SCORE.width, h: EQW_Layout.NUM_STYLE.CALL_SCORE.charHeight,
runtime: ['target']
}; };
//档位子数角标:贴对应档位按钮右上角,纯文字无底图,清单未给宽高(同上,调用方注入 target) //档位子数角标:贴对应档位按钮右上角,纯文字无底图,清单未给宽高(同上,调用方注入 target)
@@ -49,15 +51,11 @@ EQW_Layout.MAIN_SUIT_LINE = {
itemWidth: 128, itemHeight: 55, spacing: 9, anchor: 'left' itemWidth: 128, itemHeight: 55, spacing: 9, anchor: 'left'
}; };
//张数数字(十位):贴对应花色按钮下方偏左,资源 NUM_MAIN_SUIT_COUNT 24×32(十位为 0 时该精灵隐藏,调用方注入 target) //张数数字:贴对应花色按钮下方居中,一个花色【一个】精灵(图集叠绘,两位数由 NumberRenderer 逐位画)。
EQW_Layout.MAIN_SUIT_COUNT_TENS = { //2026-08-26:原十位/个位两节点合并为本节点;宽高取 NUM_STYLE.MAIN_SUIT_COUNT(SSOT,调用方注入 target)
kind: 'attach', targetKind: 'sprite', hAlign: 'center', vAlign: 'bottom', offsetX: -12, offsetY: -6, w: 24, h: 32, EQW_Layout.MAIN_SUIT_COUNT = {
runtime: ['target'] kind: 'attach', targetKind: 'sprite', hAlign: 'center', vAlign: 'bottom', offsetY: -6,
}; w: EQW_Layout.NUM_STYLE.MAIN_SUIT_COUNT.width, h: EQW_Layout.NUM_STYLE.MAIN_SUIT_COUNT.charHeight,
//张数数字(个位):贴对应花色按钮下方偏右,紧邻十位,资源 NUM_MAIN_SUIT_COUNT 24×32(调用方注入 target)
EQW_Layout.MAIN_SUIT_COUNT_ONES = {
kind: 'attach', targetKind: 'sprite', hAlign: 'center', vAlign: 'bottom', offsetX: 12, offsetY: -6, w: 24, h: 32,
runtime: ['target'] runtime: ['target']
}; };
@@ -36,7 +36,9 @@ EQW_Sprites.CallPanelView = {
CALL_BTN_13: 1615, CALL_BTN_13: 1615,
CALL_BTN_14: 1616, CALL_BTN_14: 1616,
//—— 14 档位分数数字(图片型数字精灵),资源 EQW_Images.NUM_CALL_SCORE,帧1–10=数字0–9,按档位分数(70..5)拼帧 —— //—— 14 档位分数数字(图集叠绘数字精灵),资源 EQW_Images.NUM_CALL_SCORE,样式 EQW_Layout.NUM_STYLE.CALL_SCORE。
// 一个精灵画完整档位分数:NumberRenderer 按字宽从等宽连续数字图里逐位裁源矩形叠绘,
// 故 14 档(其中 13 档是两位数)各只需【一个】精灵(2026-08-26 改,原多帧 setFrame 一次只显示一位、两位数根本画不出)——
CALL_BTN_NUM_1: 1617, CALL_BTN_NUM_1: 1617,
CALL_BTN_NUM_2: 1618, CALL_BTN_NUM_2: 1618,
CALL_BTN_NUM_3: 1619, CALL_BTN_NUM_3: 1619,
@@ -78,9 +80,11 @@ EQW_Sprites.CallPanelView = {
//以下键号在源清单中被显式判定「不需要」,本文件不占用,留空号以便追溯(美术改一张图 5 帧出图, //以下键号在源清单中被显式判定「不需要」,本文件不占用,留空号以便追溯(美术改一张图 5 帧出图,
//花色图标与右上角角标底已并入 BTN_SUIT_CHOOSE 各帧,2026-08-26): //花色图标与右上角角标底已并入 BTN_SUIT_CHOOSE 各帧,2026-08-26):
// 1656–1659 MAIN_SUIT_ICON_1..4 —— 花色图标并入 BTN_SUIT_CHOOSE 帧1–4 // 1656–1659 MAIN_SUIT_ICON_1..4 —— 花色图标并入 BTN_SUIT_CHOOSE 帧1–4
// 1660–1663 MAIN_SUIT_COUNT_1..4 —— 原文字精灵,改为下方 8 个数字图片精灵(张数最多两位) // 1660–1663 MAIN_SUIT_COUNT_1..4 —— 原文字精灵,改为下方数字图片精灵(键名沿用,ID 改到 1674–1677)
// 1664–1667 MAIN_SUIT_PAIR_BG_1..4 —— 角标底并入 BTN_SUIT_CHOOSE 帧1–4 // 1664–1667 MAIN_SUIT_PAIR_BG_1..4 —— 角标底并入 BTN_SUIT_CHOOSE 帧1–4
// 1673 MAIN_BTN_SURRENDER_TEXT —— 「投降」二字并入 BTN_SUIT_CHOOSE 帧5 // 1673 MAIN_BTN_SURRENDER_TEXT —— 「投降」二字并入 BTN_SUIT_CHOOSE 帧5
// 1678–1681 原 MAIN_SUIT_COUNT_3/4_TENS/ONES —— 张数改为「一个花色一个精灵」(图集叠绘,见下),
// 上一轮的十位/个位拆分作废,空出的 4 个 ID 不复用(2026-08-26)
EQW_Sprites.ChooseMainView = { EQW_Sprites.ChooseMainView = {
Layer: EQW_Layers.ACTION, //104 Layer: EQW_Layers.ACTION, //104
@@ -103,16 +107,14 @@ EQW_Sprites.ChooseMainView = {
MAIN_SUIT_PAIR_TEXT_3: 1670, MAIN_SUIT_PAIR_TEXT_3: 1670,
MAIN_SUIT_PAIR_TEXT_4: 1671, MAIN_SUIT_PAIR_TEXT_4: 1671,
//—— 8 个花色张数数字图片精灵:该花色在庄家手中的张数(两副牌单花色最多 26 张,两位数), //—— 4 个花色张数数字精灵(图集叠绘,一花色一个):该花色在庄家手中的张数
// 资源 EQW_Images.NUM_MAIN_SUIT_COUNT,帧 = 数字+1(帧1–10=0–9),十位为 0 时该精灵隐藏、个位始终显示 —— // (两副牌单花色最多 26 张,两位数),资源 EQW_Images.NUM_MAIN_SUIT_COUNT,
MAIN_SUIT_COUNT_1_TENS: 1674, // 样式 EQW_Layout.NUM_STYLE.MAIN_SUIT_COUNT;NumberRenderer 逐位裁源矩形画在同一精灵上,
MAIN_SUIT_COUNT_1_ONES: 1675, // 不再需要十位/个位各一个精灵(2026-08-26)——
MAIN_SUIT_COUNT_2_TENS: 1676, MAIN_SUIT_COUNT_1: 1674,
MAIN_SUIT_COUNT_2_ONES: 1677, MAIN_SUIT_COUNT_2: 1675,
MAIN_SUIT_COUNT_3_TENS: 1678, MAIN_SUIT_COUNT_3: 1676,
MAIN_SUIT_COUNT_3_ONES: 1679, MAIN_SUIT_COUNT_4: 1677,
MAIN_SUIT_COUNT_4_TENS: 1680,
MAIN_SUIT_COUNT_4_ONES: 1681,
MAIN_BTN_SURRENDER: 1672 //图片:「投降」按钮,资源 EQW_Images.BTN_SUIT_CHOOSE 帧5(已含底与「投降」字) MAIN_BTN_SURRENDER: 1672 //图片:「投降」按钮,资源 EQW_Images.BTN_SUIT_CHOOSE 帧5(已含底与「投降」字)
} }
@@ -97,6 +97,10 @@ EQW_Sprites.AccountView = {
ACC_TPL_AVATAR: 1912, //模板:头像,运行时贴玩家头像 ACC_TPL_AVATAR: 1912, //模板:头像,运行时贴玩家头像
ACC_TPL_TEXT: 1913, //模板:通用文字(昵称/ID/各项分数,一行一个) ACC_TPL_TEXT: 1913, //模板:通用文字(昵称/ID/各项分数,一行一个)
ACC_TPL_SCORE_NUM: 1914, //模板:右侧总分大字(数字精灵),资源 EQW_Images.NUM_RESULT_WIN / NUM_RESULT_LOSE ACC_TPL_SCORE_NUM: 1914, //模板:右侧总分大字(数字精灵),资源 EQW_Images.NUM_RESULT_WIN / NUM_RESULT_LOSE
//【2026-08-26 待定】NUM_RESULT_* 已改为等宽连续图 + 图集叠绘(见 ImageResources.js §3.6),
//而本精灵是【复制模板】、运行时 ID 为字符串,SpriteEventController.registerDraw 只收数字 ID、
//复制精灵也不单独收绘制事件 → 无法用 NumberRenderer。玩家栏总分的呈现方式待 F 阶段定
//(改文字精灵 / 改预置精灵 / 由父容器统一叠绘),此处先记风险不改结构
ACC_TPL_DIVIDER: 1915, //模板:行间分隔线,资源 EQW_Images.LINE_DIVIDER ACC_TPL_DIVIDER: 1915, //模板:行间分隔线,资源 EQW_Images.LINE_DIVIDER
//—— 底部 4 个功能按钮 —— //—— 底部 4 个功能按钮 ——
+28 -2
View File
@@ -43,7 +43,7 @@ const index = EQW_SpriteIndex.build(EQW_Sprites);
const spriteVals = Object.keys(index).map(k => index[k]); const spriteVals = Object.keys(index).map(k => index[k]);
// 钉住精确数量:松下界(如 >200)删掉整个 View 都还能通过。 // 钉住精确数量:松下界(如 >200)删掉整个 View 都还能通过。
// 增删精灵时连同本数字一起改——有意的顺手更新,不是有意的就红给你看 // 增删精灵时连同本数字一起改——有意的顺手更新,不是有意的就红给你看
t.eq('精灵总数', spriteVals.length, 382); t.eq('精灵总数', spriteVals.length, 378);
const spriteOk = spriteVals.every(v => v >= 1001 && v <= 2999); const spriteOk = spriteVals.every(v => v >= 1001 && v <= 2999);
if (!spriteOk) { console.log('越界精灵: ' + JSON.stringify(spriteVals.filter(v => v < 1001 || v > 2999))); } if (!spriteOk) { console.log('越界精灵: ' + JSON.stringify(spriteVals.filter(v => v < 1001 || v > 2999))); }
t.eq('精灵落在 1001-2999', spriteOk, true); t.eq('精灵落在 1001-2999', spriteOk, true);
@@ -238,7 +238,7 @@ t.eq('全部布局节点可求解、矩形数与坐标都对', solveFailures, []
// 钉住节点总数:松下界(如 >40)删掉整个 View 都还能通过,任何意外的增删都要在这里可见。 // 钉住节点总数:松下界(如 >40)删掉整个 View 都还能通过,任何意外的增删都要在这里可见。
// 增删布局节点时,连同本数字一起改——改动是有意的就顺手更新,不是有意的就红给你看 // 增删布局节点时,连同本数字一起改——改动是有意的就顺手更新,不是有意的就红给你看
t.eq('布局节点总数', allNodes.length, 62); t.eq('布局节点总数', allNodes.length, 61);
// ---- 布局文件是纯数据:不含函数 ---- // ---- 布局文件是纯数据:不含函数 ----
const funcs = []; const funcs = [];
@@ -319,4 +319,30 @@ t.eq('roomtype 位号铺满 0-4', Object.keys(bitsSeen).map(Number).sort((a, b)
t.eq('CARD_SIZE.' + size + ' 的资源存在', EQW_Images.hasOwnProperty(EQW_Layout.CARD_SIZE[size].res), true); t.eq('CARD_SIZE.' + size + ' 的资源存在', EQW_Images.hasOwnProperty(EQW_Layout.CARD_SIZE[size].res), true);
}); });
// ---- 数字样式(图集叠绘):资源键存在、字段齐全合法 ----
// 与 CARD_SIZE 同一约定:纯数据配置只存资源【键名】,调用方用 EQW_Images[res] 取真实 ID
const numStyleProblems = [];
Object.keys(EQW_Layout.NUM_STYLE).forEach(key => {
const s = EQW_Layout.NUM_STYLE[key];
if (!EQW_Images.hasOwnProperty(s.res)) { numStyleProblems.push(key + ' 的资源键不存在: ' + s.res); }
['charWidth', 'charHeight', 'width'].forEach(f => {
if (typeof s[f] !== 'number' || s[f] <= 0) { numStyleProblems.push(key + '.' + f + ' 非正数: ' + s[f]); }
});
if (typeof s.spacing !== 'number') { numStyleProblems.push(key + '.spacing 非数字: ' + s.spacing); }
if (['left', 'center', 'right'].indexOf(s.align) < 0) { numStyleProblems.push(key + '.align 非法: ' + s.align); }
// 绘制区至少放得下一个字符,否则这套样式一开始就画不出东西
if (s.width < s.charWidth) { numStyleProblems.push(key + ' 绘制区比单字还窄'); }
});
if (numStyleProblems.length) { console.log('数字样式问题:\n ' + numStyleProblems.join('\n ')); }
t.eq('NUM_STYLE 资源键存在且字段合法', numStyleProblems, []);
// 引用 NUM_STYLE 的布局节点必须与样式同源(SSOT:数值只在 LayoutConstants 定义一处,
// 有人在 Layout_Action.js 里手抄一个不同的数就在这里变红)
[['CALL_BTN_SCORE_TEXT', 'CALL_SCORE'], ['MAIN_SUIT_COUNT', 'MAIN_SUIT_COUNT']].forEach(pair => {
const node = EQW_Layout[pair[0]];
const st = EQW_Layout.NUM_STYLE[pair[1]];
t.eq(pair[0] + '.w 与 NUM_STYLE.' + pair[1] + '.width 同源', node.w, st.width);
t.eq(pair[0] + '.h 与 NUM_STYLE.' + pair[1] + '.charHeight 同源', node.h, st.charHeight);
});
process.exit(t.done('constants') ? 0 : 1); process.exit(t.done('constants') ? 0 : 1);
+1 -1
View File
@@ -83,6 +83,6 @@ let realIndex = {};
try { realIndex = EQW_SpriteIndex.build(EQW_Sprites); } catch (e) { realOk = false; console.log('建索引失败: ' + e.message); } try { realIndex = EQW_SpriteIndex.build(EQW_Sprites); } catch (e) { realOk = false; console.log('建索引失败: ' + e.message); }
t.eq('真实精灵常量无重复键', realOk, true); t.eq('真实精灵常量无重复键', realOk, true);
// 钉住精确数量(与 test_constants.js 同一个数):松下界删掉整个 View 都还能通过 // 钉住精确数量(与 test_constants.js 同一个数):松下界删掉整个 View 都还能通过
t.eq('真实精灵数量', Object.keys(realIndex).length, 382); t.eq('真实精灵数量', Object.keys(realIndex).length, 378);
process.exit(t.done('spriteindex') ? 0 : 1); process.exit(t.done('spriteindex') ? 0 : 1);