diff --git a/client/index.html b/client/index.html index c5c1ac2..c7c9747 100644 --- a/client/index.html +++ b/client/index.html @@ -191,8 +191,6 @@ - - diff --git a/client/js/01_SubGame/codes/config/ImageResources.js b/client/js/01_SubGame/codes/config/ImageResources.js index 735baa0..7e67701 100644 --- a/client/js/01_SubGame/codes/config/ImageResources.js +++ b/client/js/01_SubGame/codes/config/ImageResources.js @@ -281,30 +281,41 @@ var EQW_Images = EQW_Images || { //===================== §3.6 数字与符号 ===================== // - //【2026-08-26 出图方式变更】以下 NUM_* 数字类资源【不是】编辑器里配好帧数的多帧资源, - //而是【等宽连续排列的一张图】:所有字符横向排成一行、每格等宽,图里不留间隙。 - //代码不用 setFrame(一个精灵一次只显示一帧=只能显示一位数),改为按字宽裁源矩形叠绘: - //第 idx 格的源矩形 = (idx * 单字宽, 0, 单字宽, 单字高),由框架 NumberRenderer 逐位画在 - //【同一个】精灵上,故一个多位数只需一个精灵。样式(单字宽高/字间距/对齐/绘制区宽)见 - //EQW_Layout.NUM_STYLE.*(LayoutConstants.js),本处只记出图规格。 - //美术出图请严格按「字符排列顺序 + 单字符宽高 + 整图尺寸」执行,字符必须等宽对齐格子。 + //【数字类资源的统一规范:16 帧固定帧序】(2026-08-26 订正) + //以下 NUM_* 一律是【多帧图片资源,16 帧】,帧序固定为:0 1 2 3 4 5 6 7 8 9 . + - x / p + // 帧1–10 = 数字 0–9 帧11 = 「.」 帧12 = 「+」 帧13 = 「-」 + // 帧14 = 「x」 帧15 = 「/」 帧16 = 可变后缀(该套资源自己的后缀字,如「分」「倍」「张」) + // + //用法前提:显示多位数字【不拆精灵、不用 setFrame 逐位切】,而是用平台既有接口 + // SpriteManager.setNumberImage(spriteId, text, charWidth) + //把整串文本设给【一个】多帧图片精灵,引擎按字符逐帧渲染,精灵宽度由该接口按 + //【字符数 × charWidth】自动调整(charWidth 见 EQW_Layout.NUM_STYLE.*,本处只记出图规格)。 + // + //出图要求:帧号由字符直接换算('0'–'9' → 帧1–10,后缀 → 帧16),所以帧序【不可重排、不可省略占位】—— + //即使某套资源用不到 . + - x / 这几帧,也要按顺序留出帧位,否则帧号对不上、画面串位。 + //整图尺寸一律 = 16 × 单字符宽(横向 16 帧)。 //用途:倒计时数字(带描边/渐变美术字) - //排列:等宽连续一行,字符顺序 0 1 2 3 4 5 6 7 8 9(索引 0–9) + //帧数:16(固定帧序 0123456789.+-x/p) + //帧说明:帧1–10 = 0–9;帧11–15 = . + - x /(本套用不到,仅占位); + // 帧16 = 后缀【本套无后缀,出空白占位】 //单字符尺寸:44×60 - //整图尺寸:440×60(10 格 × 44) + //整图尺寸:704×60(16 帧 × 44) NUM_COUNTDOWN: 631, //用途:结算得分数字(赢,橙,带描边/渐变美术字) - //排列:等宽连续一行,字符顺序 0 1 2 3 4 5 6 7 8 9 + -(索引 0–9 为数字,10 = 「+」,11 = 「-」) + //帧数:16(固定帧序 0123456789.+-x/p) + //帧说明:帧1–10 = 0–9;帧12 = 「+」、帧13 = 「-」(得分带正负号,必出);帧11/14/15 = . x /(占位); + // 帧16 = 后缀【待美术与规则确认:结算大字目前只显示数字,若日后要显示「+120分」则此帧出「分」】 //单字符尺寸:46×62 - //整图尺寸:552×62(12 格 × 46) + //整图尺寸:736×62(16 帧 × 46) NUM_RESULT_WIN: 632, //用途:结算得分数字(输,蓝,带描边/渐变美术字) - //排列:与 NUM_RESULT_WIN 完全一致——等宽连续一行,0–9 后接 「+」「-」(共 12 格),仅配色不同 + //帧数:16(固定帧序 0123456789.+-x/p) + //帧说明:与 NUM_RESULT_WIN 完全一致(含帧16 待确认项),仅配色不同 //单字符尺寸:46×62 - //整图尺寸:552×62(12 格 × 46) + //整图尺寸:736×62(16 帧 × 46) NUM_RESULT_LOSE: 633, //用途:判定结果动画(大光/小光/过庄/升N级/投降)——帧动画资源,播放参数进 AnimationConfigs【待设计 D-8】 @@ -314,16 +325,21 @@ var EQW_Images = EQW_Images || { ANI_JUDGE: 634, //用途:叫分档位的分数数字(深棕描边美术字,带描边/渐变,文字精灵做不出此效果) - //排列:等宽连续一行,字符顺序 0 1 2 3 4 5 6 7 8 9(索引 0–9) + //帧数:16(固定帧序 0123456789.+-x/p) + //帧说明:帧1–10 = 0–9;帧11–15 = . + - x /(本套用不到,仅占位); + // 帧16 = 后缀【待确认:档位面板只显示分数数字(70…5),若日后要显示「70分」则此帧出「分」】 //单字符尺寸:30×40 - //整图尺寸:300×40(10 格 × 30) - //说明:14 个档位中 13 个是两位数(70…10),靠叠绘一个精灵画两位,见 Sprites_Action.js 的 CALL_BTN_NUM_1..14 + //整图尺寸:480×40(16 帧 × 30) + //说明:14 个档位中 13 个是两位数(70…10),整串交给 setNumberImage,一档【一个】精灵, + // 见 Sprites_Action.js 的 CALL_BTN_NUM_1..14;面板上的「N子」角标是文字精灵,不走本资源 NUM_CALL_SCORE: 635, //用途:选主面板花色张数数字(美术单独出图,字号配色与以上几套不同;一个花色一个精灵,见 Sprites_Action.js) - //排列:等宽连续一行,字符顺序 0 1 2 3 4 5 6 7 8 9(索引 0–9) + //帧数:16(固定帧序 0123456789.+-x/p) + //帧说明:帧1–10 = 0–9;帧11–15 = . + - x /(本套用不到,仅占位); + // 帧16 = 后缀「张」(显示形如「26张」,后缀与数字等宽,同为单字符宽) //单字符尺寸:24×32(估值,与选主按钮 128×55 协调估算,出图前须与最终设计稿核对) - //整图尺寸:240×32(10 格 × 24) + //整图尺寸:384×32(16 帧 × 24) NUM_MAIN_SUIT_COUNT: 636, //===================== §3.7 浮层与气泡 ===================== diff --git a/client/js/01_SubGame/codes/config/LayoutConstants.js b/client/js/01_SubGame/codes/config/LayoutConstants.js index af3710f..5a67200 100644 --- a/client/js/01_SubGame/codes/config/LayoutConstants.js +++ b/client/js/01_SubGame/codes/config/LayoutConstants.js @@ -32,30 +32,31 @@ EQW_Layout.TEXT_STYLE = { ROOM_CARD_COST_NOTE: { fontSize: 16, color: '#999999' } //建房房卡消耗附注(§6.9b) }; -//数字样式预设(清单 §3.6 / §6.8,2026-08-26 新增)——图集叠绘数字用,一个精灵画多位美术字。 -//每套对应一张【等宽连续排列】的数字图:字符横向一行排开,第 idx 格的源矩形 = (idx*charWidth, 0, charWidth, charHeight)。 -//渲染由框架的 NumberRenderer 完成(NumberRenderer.bind(spriteId, style) → setValue); -//本表是【零裸值红线】的落点:charWidth/charHeight/spacing/align/width 只在此定义,业务代码只引用不内联。 +//多帧图数字样式(清单 §3.6 / §6.8,2026-08-26 新增)——配合平台既有的 +//SpriteManager.setNumberImage(spriteId, text, charWidth):把整串数字设给【一个】多帧图片精灵, +//引擎按字符逐帧渲染,精灵宽度由该接口按【字符数 × charWidth】自动调整。 +//故多位数不需要拆成十位/个位多个精灵,也不用 setFrame 逐位切。 +//本表是【零裸值红线】的落点:charWidth/charHeight 只在此定义一处,业务代码只引用不内联。 // -//字段:res —— 图片资源【键名】(与 CARD_SIZE.res 同一约定:纯数据文件不直接引用 EQW_Images, -// 调用方用 EQW_Images[style.res] 取真实 ID) -// charWidth/charHeight —— 图集单字符宽高(= 源矩形尺寸,也是原样绘制的目标尺寸) -// spacing —— 字间距(可为负=字符重叠) -// align —— 在绘制区内的水平对齐:'left'|'center'|'right' -// width —— 绘制区宽度(= 承载精灵的宽度;各 Layout_*.js 的对应节点直接引用本值,保持 SSOT) +//字段:res —— 图片资源【键名】(与 CARD_SIZE.res 同一约定:纯数据文件不直接引用 EQW_Images, +// 调用方用 EQW_Images[style.res] 取真实 ID) +// charWidth —— 单字符宽(传给 setNumberImage;精灵宽 = 字符数 × charWidth,运行时自动变) +// charHeight —— 单字符高(setNumberImage 不改高度,此值供布局节点定 h) +// suffix —— 该套资源第 16 帧承载的后缀字(无后缀为 '';见 ImageResources.js §3.6 的帧序说明) EQW_Layout.NUM_STYLE = { - //倒计时(§2.4):最多两位(秒) - COUNTDOWN: { res: 'NUM_COUNTDOWN', charWidth: 44, charHeight: 60, spacing: 0, align: 'center', width: 88 }, + //倒计时(§2.4):纯数字无后缀,秒数最多两位 + COUNTDOWN: { res: 'NUM_COUNTDOWN', charWidth: 44, charHeight: 60, suffix: '' }, - //结算得分 · 赢(§1.8/§6.9):带符号,最多「符号 + 四位」 - RESULT_WIN: { res: 'NUM_RESULT_WIN', charWidth: 46, charHeight: 62, spacing: 0, align: 'center', width: 230 }, + //结算得分 · 赢(§1.8/§6.9):用到 '+'(帧12)与 '-'(帧13);后缀是否为「分」待定,见资源注释 + RESULT_WIN: { res: 'NUM_RESULT_WIN', charWidth: 46, charHeight: 62, suffix: '' }, - //结算得分 · 输(§1.8/§6.9):帧序与赢的一致,只是配色不同 - RESULT_LOSE: { res: 'NUM_RESULT_LOSE', charWidth: 46, charHeight: 62, spacing: 0, align: 'center', width: 230 }, + //结算得分 · 输(§1.8/§6.9):帧序与赢的一致,仅配色不同 + RESULT_LOSE: { res: 'NUM_RESULT_LOSE', charWidth: 46, charHeight: 62, suffix: '' }, - //叫分档位分数(§1.3/§6.8):14 档中 13 档是两位数;绘制区宽 = 档位按钮宽 74,居中压在按钮上 - CALL_SCORE: { res: 'NUM_CALL_SCORE', charWidth: 30, charHeight: 40, spacing: 0, align: 'center', width: 74 }, + //叫分档位分数(§1.3/§6.8):14 档中 13 档是两位数,整串交给 setNumberImage,无后缀 + //(面板上的「N子」角标是文字精灵,不走这套) + CALL_SCORE: { res: 'NUM_CALL_SCORE', charWidth: 30, charHeight: 40, suffix: '' }, - //选主面板花色张数(§1.5/§6.8):两副牌单花色最多 26 张,最多两位 - MAIN_SUIT_COUNT: { res: 'NUM_MAIN_SUIT_COUNT', charWidth: 24, charHeight: 32, spacing: 0, align: 'center', width: 48 } + //选主面板花色张数(§1.5/§6.8):显示「N张」,「张」= 第 16 帧;单花色最多 26 张 + MAIN_SUIT_COUNT: { res: 'NUM_MAIN_SUIT_COUNT', charWidth: 24, charHeight: 32, suffix: '张' } }; diff --git a/client/js/01_SubGame/codes/config/Layout_Action.js b/client/js/01_SubGame/codes/config/Layout_Action.js index 2eb2472..2af8b4e 100644 --- a/client/js/01_SubGame/codes/config/Layout_Action.js +++ b/client/js/01_SubGame/codes/config/Layout_Action.js @@ -20,12 +20,12 @@ EQW_Layout.CALL_BTN_GRID = { //档位分数数字:贴对应档位按钮居中。CALL_BTN_1..14 是 14 个固定精灵,但本节点是共用模板, //target 由调用方按第几档注入(不预置单一 target)。 -//2026-08-26:改为图集叠绘(NumberRenderer),一个精灵画完整分数,故宽高【不再随位数变化】—— -//直接取 NUM_STYLE.CALL_SCORE 的绘制区宽与单字高(SSOT:数值只在 LayoutConstants 定义一处) +//宽度随位数变化(SpriteManager.setNumberImage 会按【字符数 × charWidth】自动改精灵宽), +//故 w 留到运行时注入;高度恒为单字高,取 NUM_STYLE.CALL_SCORE.charHeight(SSOT,2026-08-26) EQW_Layout.CALL_BTN_SCORE_TEXT = { kind: 'attach', targetKind: 'sprite', hAlign: 'center', vAlign: 'middle', offsetY: 4, - w: EQW_Layout.NUM_STYLE.CALL_SCORE.width, h: EQW_Layout.NUM_STYLE.CALL_SCORE.charHeight, - runtime: ['target'] + h: EQW_Layout.NUM_STYLE.CALL_SCORE.charHeight, + runtime: ['target', 'w'] }; //档位子数角标:贴对应档位按钮右上角,纯文字无底图,清单未给宽高(同上,调用方注入 target) @@ -51,12 +51,13 @@ EQW_Layout.MAIN_SUIT_LINE = { itemWidth: 128, itemHeight: 55, spacing: 9, anchor: 'left' }; -//张数数字:贴对应花色按钮下方居中,一个花色【一个】精灵(图集叠绘,两位数由 NumberRenderer 逐位画)。 -//2026-08-26:原十位/个位两节点合并为本节点;宽高取 NUM_STYLE.MAIN_SUIT_COUNT(SSOT,调用方注入 target) +//张数数字:贴对应花色按钮下方居中,一个花色【一个】精灵,整串「N张」由 setNumberImage 显示。 +//2026-08-26:原十位/个位两节点合并为本节点;宽度随位数变化(2 或 3 个字符)留到运行时注入, +//高度恒为单字高,取 NUM_STYLE.MAIN_SUIT_COUNT.charHeight(SSOT) EQW_Layout.MAIN_SUIT_COUNT = { 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, - runtime: ['target'] + h: EQW_Layout.NUM_STYLE.MAIN_SUIT_COUNT.charHeight, + runtime: ['target', 'w'] }; //对数角标文字:贴对应花色按钮右上角(按钮帧内已含角标底,本节点只定位叠加的文字)。 diff --git a/client/js/01_SubGame/codes/config/Sprites_Action.js b/client/js/01_SubGame/codes/config/Sprites_Action.js index 0ae998a..dde6a4f 100644 --- a/client/js/01_SubGame/codes/config/Sprites_Action.js +++ b/client/js/01_SubGame/codes/config/Sprites_Action.js @@ -36,9 +36,10 @@ EQW_Sprites.CallPanelView = { CALL_BTN_13: 1615, CALL_BTN_14: 1616, - //—— 14 档位分数数字(图集叠绘数字精灵),资源 EQW_Images.NUM_CALL_SCORE,样式 EQW_Layout.NUM_STYLE.CALL_SCORE。 - // 一个精灵画完整档位分数:NumberRenderer 按字宽从等宽连续数字图里逐位裁源矩形叠绘, - // 故 14 档(其中 13 档是两位数)各只需【一个】精灵(2026-08-26 改,原多帧 setFrame 一次只显示一位、两位数根本画不出)—— + //—— 14 档位分数数字(多帧图数字精灵),资源 EQW_Images.NUM_CALL_SCORE(16 帧固定帧序), + // 字符宽见 EQW_Layout.NUM_STYLE.CALL_SCORE.charWidth。 + // 显示用 SpriteManager.setNumberImage(spriteId, '70', charWidth):整串交给引擎按字符逐帧渲染、 + // 精灵宽度自动按字符数调整,故 14 档(其中 13 档是两位数)各只需【一个】精灵,不拆十位/个位 —— CALL_BTN_NUM_1: 1617, CALL_BTN_NUM_2: 1618, CALL_BTN_NUM_3: 1619, @@ -83,8 +84,9 @@ EQW_Sprites.CallPanelView = { // 1660–1663 MAIN_SUIT_COUNT_1..4 —— 原文字精灵,改为下方数字图片精灵(键名沿用,ID 改到 1674–1677) // 1664–1667 MAIN_SUIT_PAIR_BG_1..4 —— 角标底并入 BTN_SUIT_CHOOSE 帧1–4 // 1673 MAIN_BTN_SURRENDER_TEXT —— 「投降」二字并入 BTN_SUIT_CHOOSE 帧5 -// 1678–1681 原 MAIN_SUIT_COUNT_3/4_TENS/ONES —— 张数改为「一个花色一个精灵」(图集叠绘,见下), -// 上一轮的十位/个位拆分作废,空出的 4 个 ID 不复用(2026-08-26) +// 1678–1681 原 MAIN_SUIT_COUNT_3/4_TENS/ONES —— 张数改回「一个花色一个精灵」(多帧图数字,见下: +// SpriteManager.setNumberImage 一个精灵就能显示整串),上一轮的十位/个位拆分作废, +// 空出的 4 个 ID 不复用(2026-08-26) EQW_Sprites.ChooseMainView = { Layer: EQW_Layers.ACTION, //104 @@ -107,10 +109,11 @@ EQW_Sprites.ChooseMainView = { MAIN_SUIT_PAIR_TEXT_3: 1670, MAIN_SUIT_PAIR_TEXT_4: 1671, - //—— 4 个花色张数数字精灵(图集叠绘,一花色一个):该花色在庄家手中的张数 - // (两副牌单花色最多 26 张,两位数),资源 EQW_Images.NUM_MAIN_SUIT_COUNT, - // 样式 EQW_Layout.NUM_STYLE.MAIN_SUIT_COUNT;NumberRenderer 逐位裁源矩形画在同一精灵上, - // 不再需要十位/个位各一个精灵(2026-08-26)—— + //—— 4 个花色张数数字精灵(多帧图数字精灵,一花色一个):该花色在庄家手中的张数 + // (两副牌单花色最多 26 张,两位数),资源 EQW_Images.NUM_MAIN_SUIT_COUNT(16 帧固定帧序, + // 第16帧=后缀「张」),字符宽见 EQW_Layout.NUM_STYLE.MAIN_SUIT_COUNT.charWidth。 + // 显示用 SpriteManager.setNumberImage(spriteId, '26张', charWidth),整串一个精灵搞定, + // 不拆十位/个位(2026-08-26)—— MAIN_SUIT_COUNT_1: 1674, MAIN_SUIT_COUNT_2: 1675, MAIN_SUIT_COUNT_3: 1676, diff --git a/client/js/01_SubGame/codes/config/Sprites_Result.js b/client/js/01_SubGame/codes/config/Sprites_Result.js index c54a9e5..6ea9028 100644 --- a/client/js/01_SubGame/codes/config/Sprites_Result.js +++ b/client/js/01_SubGame/codes/config/Sprites_Result.js @@ -96,11 +96,10 @@ EQW_Sprites.AccountView = { ACC_TPL_RANK: 1911, //模板:名次徽章,资源 EQW_Images.BADGE_RANK,帧=名次1–3 ACC_TPL_AVATAR: 1912, //模板:头像,运行时贴玩家头像 ACC_TPL_TEXT: 1913, //模板:通用文字(昵称/ID/各项分数,一行一个) - 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_SCORE_NUM: 1914, //模板:右侧总分大字(多帧图数字精灵),资源 EQW_Images.NUM_RESULT_WIN / NUM_RESULT_LOSE, + //显示用 SpriteManager.setNumberImage(复制精灵ID, '+120', charWidth)—— + //setNumberImage 走的是 TEXT/WIDTH 属性,_validateSpriteId 也接受复制精灵的字符串 ID, + //故复制出来的玩家栏总分同样一个精灵显示整串(2026-08-26 核对) ACC_TPL_DIVIDER: 1915, //模板:行间分隔线,资源 EQW_Images.LINE_DIVIDER //—— 底部 4 个功能按钮 —— diff --git a/client/js/gameabc-framework/core/SpriteManager.js b/client/js/gameabc-framework/core/SpriteManager.js index c5854fc..4ed4188 100644 --- a/client/js/gameabc-framework/core/SpriteManager.js +++ b/client/js/gameabc-framework/core/SpriteManager.js @@ -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; - } - }, - + // ==================== 交互控制 ==================== /** diff --git a/client/js/gameabc-framework/ui/NumberRenderer.js b/client/js/gameabc-framework/ui/NumberRenderer.js deleted file mode 100644 index b98bf3c..0000000 --- a/client/js/gameabc-framework/ui/NumberRenderer.js +++ /dev/null @@ -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; -} diff --git a/client/tests/test_constants.js b/client/tests/test_constants.js index 5b865e8..31f8966 100644 --- a/client/tests/test_constants.js +++ b/client/tests/test_constants.js @@ -319,30 +319,34 @@ 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); }); -// ---- 数字样式(图集叠绘):资源键存在、字段齐全合法 ---- +// ---- 多帧图数字样式(配合 SpriteManager.setNumberImage):资源键存在、字段齐全合法 ---- // 与 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 => { + // charWidth 是 setNumberImage 的必需入参(该接口对非正数直接返回 false) + ['charWidth', 'charHeight'].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 + ' 绘制区比单字还窄'); } + // 后缀落在资源第 16 帧,只能是 0 或 1 个字符;且必须是 setNumberImage 认识的那三个 + // (编码表 /[分倍张]/ → 'g' → 帧16;写别的字会被原样丢给引擎,按未知字符渲染) + if (typeof s.suffix !== 'string') { numStyleProblems.push(key + '.suffix 必须是字符串: ' + s.suffix); } + else if (s.suffix !== '' && ['分', '倍', '张'].indexOf(s.suffix) < 0) { + numStyleProblems.push(key + '.suffix 不是 setNumberImage 支持的后缀(分/倍/张): ' + s.suffix); + } }); if (numStyleProblems.length) { console.log('数字样式问题:\n ' + numStyleProblems.join('\n ')); } t.eq('NUM_STYLE 资源键存在且字段合法', numStyleProblems, []); // 引用 NUM_STYLE 的布局节点必须与样式同源(SSOT:数值只在 LayoutConstants 定义一处, -// 有人在 Layout_Action.js 里手抄一个不同的数就在这里变红) +// 有人在 Layout_Action.js 里手抄一个不同的数就在这里变红)。 +// 注意 w 不在此列:setNumberImage 会按【字符数 × charWidth】自动改精灵宽,故 w 由运行时注入 [['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); + t.eq(pair[0] + '.w 留到运行时注入', node.runtime.indexOf('w') >= 0 && typeof node.w === 'undefined', true); }); process.exit(t.done('constants') ? 0 : 1); diff --git a/client/tests/test_numberrenderer.js b/client/tests/test_numberrenderer.js deleted file mode 100644 index 5d2bd63..0000000 --- a/client/tests/test_numberrenderer.js +++ /dev/null @@ -1,159 +0,0 @@ -// NumberRenderer.layout:图集叠绘的逐字符矩形计算(纯函数,唯一可脱离引擎单测的部分) -const { load, throws } = require('./_load'); -const t = require('./_assert')(); - -load('client/js/gameabc-framework/ui/NumberRenderer.js'); - -// 基准样式:单字 30×40、无间距、绘制区宽 100 -function style(over) { - const s = { imageId: 635, charWidth: 30, charHeight: 40, spacing: 0, align: 'left', width: 100 }; - Object.keys(over || {}).forEach(k => { s[k] = over[k]; }); - return s; -} -// 只取关心的字段,断言更可读 -const brief = items => items.map(i => ({ c: i.char, srcX: i.srcX, destX: i.destX })); - -// ---- 正面:多位数逐位裁源矩形,第 idx 格 = (idx*charWidth, 0, charWidth, charHeight) ---- -const two = NumberRenderer.layout(70, style()); -t.eq('两位数:字符数', two.length, 2); -t.eq('两位数:逐位源/目标 x', brief(two), [ - { c: '7', srcX: 210, destX: 0 }, - { c: '0', srcX: 0, destX: 30 } -]); -t.eq('两位数:源矩形完整字段', two[0], { - char: '7', index: 7, srcX: 210, srcY: 0, srcW: 30, srcH: 40, - destX: 0, destY: 0, destW: 30, destH: 40 -}); - -// ---- 正面:单位数 ---- -t.eq('单位数', brief(NumberRenderer.layout(5, style())), [{ c: '5', srcX: 150, destX: 0 }]); - -// ---- 正面:字符串入参与数字入参等价 ---- -t.eq('字符串入参等价', brief(NumberRenderer.layout('70', style())), brief(two)); - -// ---- 正面:四位数(结算得分级别) ---- -t.eq('四位数', brief(NumberRenderer.layout(1234, style({ width: 200 }))), [ - { c: '1', srcX: 30, destX: 0 }, - { c: '2', srcX: 60, destX: 30 }, - { c: '3', srcX: 90, destX: 60 }, - { c: '4', srcX: 120, destX: 90 } -]); - -// ---- 正面:负号 → 默认映射索引 11;正号 → 10 ---- -t.eq('负号', brief(NumberRenderer.layout('-5', style())), [ - { c: '-', srcX: 330, destX: 0 }, - { c: '5', srcX: 150, destX: 30 } -]); -t.eq('正号', brief(NumberRenderer.layout('+18', style())), [ - { c: '+', srcX: 300, destX: 0 }, - { c: '1', srcX: 30, destX: 30 }, - { c: '8', srcX: 240, destX: 60 } -]); - -// ---- 正面:三种对齐(总宽 = 2*30 + 1*0 = 60,绘制区宽 100)---- -t.eq('align left 起始 x', NumberRenderer.layout(70, style({ align: 'left' }))[0].destX, 0); -t.eq('align center 起始 x', NumberRenderer.layout(70, style({ align: 'center' }))[0].destX, 20); -t.eq('align right 起始 x', NumberRenderer.layout(70, style({ align: 'right' }))[0].destX, 40); - -// ---- 边界:绘制区恰好等于总宽时,三种对齐结果一致 ---- -t.eq('绘制区等于总宽时 center 起始 x', NumberRenderer.layout(70, style({ align: 'center', width: 60 }))[0].destX, 0); -t.eq('绘制区等于总宽时 right 起始 x', NumberRenderer.layout(70, style({ align: 'right', width: 60 }))[0].destX, 0); - -// ---- 边界:内容超出绘制区时不裁剪,center/right 起始 x 为负(由调用方自行调宽)---- -t.eq('超宽 center 起始 x 为负', NumberRenderer.layout(1234, style({ align: 'center' }))[0].destX, -10); - -// ---- 边界:字间距为负(字符重叠)---- -t.eq('负字间距(重叠 6px)', brief(NumberRenderer.layout(123, style({ spacing: -6 }))), [ - { c: '1', srcX: 30, destX: 0 }, - { c: '2', srcX: 60, destX: 24 }, - { c: '3', srcX: 90, destX: 48 } -]); -// 负间距下总宽 = 3*30 + 2*(-6) = 78,居中起始 x = (100-78)/2 = 11 -t.eq('负字间距 + center', NumberRenderer.layout(123, style({ spacing: -6, align: 'center' }))[0].destX, 11); - -// ---- 边界:正字间距 ---- -t.eq('正字间距 4px', brief(NumberRenderer.layout(12, style({ spacing: 4 }))), [ - { c: '1', srcX: 30, destX: 0 }, - { c: '2', srcX: 60, destX: 34 } -]); - -// ---- 边界:空值不产生任何字符 ---- -t.eq('null 值', NumberRenderer.layout(null, style()), []); -t.eq('undefined 值', NumberRenderer.layout(undefined, style()), []); -t.eq('空串', NumberRenderer.layout('', style()), []); -t.eq('数值 0 照常显示(不被当空值)', brief(NumberRenderer.layout(0, style())), [{ c: '0', srcX: 0, destX: 0 }]); - -// ---- 正面:charMap 可整体覆盖默认映射(图集排列不同的数字图)---- -const zhMap = { '0': 0, '1': 1, '2': 2, '张': 3 }; -t.eq('自定义 charMap', brief(NumberRenderer.layout('12张', style({ charMap: zhMap }))), [ - { c: '1', srcX: 30, destX: 0 }, - { c: '2', srcX: 60, destX: 30 }, - { c: '张', srcX: 90, destX: 60 } -]); -// 覆盖是整体替换:默认的 '-' 在自定义表里没有 → 抛错,不回退到默认表 -t.eq('charMap 是整体覆盖而非合并', throws(() => NumberRenderer.layout('-1', style({ charMap: zhMap }))), true); - -// ---- 反面:映射不到的字符必须显式抛错,不静默跳过 ---- -t.eq('字母抛错', throws(() => NumberRenderer.layout('7A', style())), true); -t.eq('小数点抛错(默认图集没有这一格)', throws(() => NumberRenderer.layout('1.5', style())), true); -t.eq('中文抛错', throws(() => NumberRenderer.layout('5分', style())), true); -t.eq('空格抛错', throws(() => NumberRenderer.layout('7 0', style())), true); -// 抛错而不是返回残缺结果:错误信息要点名是哪个字符 -let msg = ''; -try { NumberRenderer.layout('7A', style()); } catch (e) { msg = e.message; } -t.eq('错误信息含出问题的字符', msg.indexOf('"A"') >= 0, true); - -// ---- 反面:样式非法一律抛错(显式失败优于隐式兜底)---- -t.eq('style 缺失抛错', throws(() => NumberRenderer.layout(70, null)), true); -t.eq('imageId 非正数抛错', throws(() => NumberRenderer.layout(70, style({ imageId: 0 }))), true); -t.eq('charWidth 非正数抛错', throws(() => NumberRenderer.layout(70, style({ charWidth: 0 }))), true); -t.eq('charHeight 缺失抛错', throws(() => NumberRenderer.layout(70, style({ charHeight: undefined }))), true); -t.eq('width 缺失抛错', throws(() => NumberRenderer.layout(70, style({ width: undefined }))), true); -t.eq('align 非法抛错', throws(() => NumberRenderer.layout(70, style({ align: 'middle' }))), true); -t.eq('align 缺失抛错', throws(() => NumberRenderer.layout(70, style({ align: undefined }))), true); -t.eq('spacing 非数字抛错', throws(() => NumberRenderer.layout(70, style({ spacing: '0' }))), true); -t.eq('charMap 非对象抛错', throws(() => NumberRenderer.layout(70, style({ charMap: 'x' }))), true); -// spacing 省略时按 0 处理(唯一的可选项) -t.eq('spacing 省略 = 0', NumberRenderer.layout(70, style({ spacing: undefined }))[1].destX, 30); - -// ---- 默认映射本身:0–9 连续,+/- 接在后面 ---- -t.eq('默认映射 0-9 连续', [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(n => NumberRenderer.DEFAULT_CHAR_MAP[String(n)]), - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); -t.eq('默认映射 + 为 10', NumberRenderer.DEFAULT_CHAR_MAP['+'], 10); -t.eq('默认映射 - 为 11', NumberRenderer.DEFAULT_CHAR_MAP['-'], 11); - -// ---- 子游戏的 5 套数字样式都能通过校验,且各自的典型值画得下 ---- -load('client/js/01_SubGame/codes/config/ImageResources.js'); -load('client/js/01_SubGame/codes/config/LayoutConstants.js'); - -const CASES = { - COUNTDOWN: '15', // 倒计时秒数,最多两位 - RESULT_WIN: '+1234', // 带符号四位 - RESULT_LOSE: '-1234', - CALL_SCORE: '70', // 叫分档位最大值 - MAIN_SUIT_COUNT: '26' // 两副牌单花色最多 26 张 -}; -const styleProblems = []; -Object.keys(CASES).forEach(key => { - const st = EQW_Layout.NUM_STYLE[key]; - if (!st) { styleProblems.push(key + ' 缺样式'); return; } - if (!EQW_Images.hasOwnProperty(st.res)) { styleProblems.push(key + ' 的资源键不存在: ' + st.res); return; } - // 用真实资源 ID 组出完整 style(业务代码也是这么组的:纯数据配置只存资源键名) - const real = { imageId: EQW_Images[st.res], charWidth: st.charWidth, charHeight: st.charHeight, - spacing: st.spacing, align: st.align, width: st.width }; - let items; - try { items = NumberRenderer.layout(CASES[key], real); } - catch (e) { styleProblems.push(key + ' -> ' + e.message); return; } - if (items.length !== CASES[key].length) { styleProblems.push(key + ' 字符数不对'); return; } - // 典型最长值必须画得进绘制区(否则 width 配小了,画面会溢出到精灵外) - const right = items[items.length - 1].destX + items[items.length - 1].destW; - if (items[0].destX < 0 || right > st.width) { - styleProblems.push(key + ' 典型最长值 "' + CASES[key] + '" 超出绘制区 width=' + st.width + - '(实际 ' + items[0].destX + '..' + right + ')'); - } -}); -if (styleProblems.length) { console.log('数字样式问题:\n ' + styleProblems.join('\n ')); } -t.eq('5 套数字样式合法且典型最长值画得下', styleProblems, []); -t.eq('数字样式恰好 5 套', Object.keys(EQW_Layout.NUM_STYLE).sort(), Object.keys(CASES).sort()); - -process.exit(t.done('numberrenderer') ? 0 : 1); diff --git a/docs/client/development-guide/02-渲染与UI组件体系.md b/docs/client/development-guide/02-渲染与UI组件体系.md index 7aa371d..8b118d1 100644 --- a/docs/client/development-guide/02-渲染与UI组件体系.md +++ b/docs/client/development-guide/02-渲染与UI组件体系.md @@ -26,9 +26,10 @@ UI 组件 → SpriteManager(业务级 API:ID 范围校验 + 单位换算) | `setPosition(id, x, y)` | 设置坐标 | | | `setScale(id, scale)` | 缩放 | 业务用倍数(`1.2`),框架自动转引擎百分比 | | `setOpacity(id, o)` | 透明度 | 业务用 `0.0–1.0`,框架自动转 `0–255` | -| `setText(id, text)` / `setTextWithWidth(...)` | 文字 | 文字精灵 | -| `drawImage(id, imgId, dx,dy,dw,dh)` | 在精灵上叠绘整张图 | **只能在绘制回调里调**(见 §5) | -| `drawImageRegion(id, imgId, dx,dy,dw,dh, sx,sy,sw,sh)` | 在精灵上叠绘图片的**指定源矩形** | 从图集里裁一格再画,同上只在绘制回调里调 | +| `setText(id, text)` | 文字 | 文字精灵,系统字体 | +| `setTextWithWidth(id, text, charWidth)` | 文字 + 自动定宽 | **文字精灵**:设文本并把宽度设为「字符数 × `charWidth`」,中文按 2 个字符计 | +| `setNumberImage(id, text, charWidth)` | **多帧图美术字数字** | **多帧图片精灵**:一个精灵显示**整串**(见下) | +| `drawImage(id, imgId, dx,dy,dw,dh)` | 在精灵上叠绘整张图 | **只能在绘制回调里调**(见 §5),用于标记叠加 | | `showGroup/hideGroup(gid)` | 群组批量 | 整块 UI 显隐 | | `showLayer/hideLayer(lid)` | 图层批量 | 整个界面显隐 | | `exists(id)` | 存在性检查 | | @@ -57,48 +58,48 @@ SpriteManager.show(sid); SpriteManager.setFrame(sid, card.code - 1); ID 必须与编辑器中实际存在的精灵**完全一致**。超范围 → 校验失败返回 `false`;编辑器里不存在的 ID → 操作静默无效。**绝不随意编造 ID。** -### 三种显示机制:文字精灵 / 多帧图片精灵 / 图集叠绘 +### 三种显示机制:文字精灵 / 多帧图片精灵切帧 / 多帧图数字 -同样是"在精灵上显示内容",引擎给了三条路,**能力边界完全不同,选错了会在后期变成返工**: +同样是"在精灵上显示内容",`SpriteManager` 给了三条路,**能力边界完全不同,选错了会在后期变成返工**: | 机制 | API | 一次能显示 | 字形 | 适用 | |------|-----|-----------|------|------| -| **文字精灵** | `setText(id, text)` | **任意长度**的字符串 | **系统字体**(只能调字号/颜色,做不出描边、渐变、投影) | 纯数据型文本:昵称、局数、`N对`/`主N` 角标、`2子`、提示文案 | -| **多帧图片精灵** | `setFrame(id, frame)` | **一帧**(整张精灵就是那一帧) | 美术出图 | **状态切换**:按钮可用/置灰、单选框选中/未选、花色图标、牌面(一张牌 = 一帧) | -| **图集叠绘** | `NumberRenderer`(底层 `drawImageRegion`) | **一行任意个字符**,都画在**同一个**精灵内 | 美术出图 | **多位美术字**:倒计时、结算得分、叫分档位分数、张数、比分 | +| **文字精灵** | `setText(id, text)`、`setTextWithWidth(id, text, charWidth)` | **任意长度**的文本 | **系统字体**(只能调字号/颜色,做不出描边、渐变、投影) | 纯数据型文本:昵称、局数、`N对`/`主N` 角标、`2子`、提示文案 | +| **多帧图片精灵切帧** | `setFrame(id, frame)` | **一帧**(整张精灵就是那一帧) | 美术出图 | **"选其一"的状态**:按钮可用/置灰、单选框选中/未选、花色图标、牌面(一张牌 = 一帧) | +| **多帧图数字** | `setNumberImage(id, text, charWidth)` | **整串**(多位数字 + 符号 + 后缀),都在**同一个**精灵里 | 美术出图 | **多位美术字数字**:倒计时、结算得分、叫分档位分数、张数、比分 | **选用判据(按顺序问自己三个问题)**: -1. **要不要美术字形**(描边/渐变/投影)?不要 → **文字精灵**,到此为止,最省。 -2. 要美术字形,那**一次只显示一个符号**吗(一张牌、一个图标、一个按钮态)?是 → **多帧图片精灵** + `setFrame`。 -3. 要美术字形、且**可能不止一个字符**(两位数、带正负号、可变长度)?→ **图集叠绘**。 +1. **要不要美术字形**(描边/渐变/投影)?不要 → **文字精灵**,到此为止,最省。需要按字符数定宽就用 `setTextWithWidth`(中文按 2 个字符计)。 +2. 要美术字形,显示的是**"若干形态里选一个"**吗(一张牌、一个图标、一个按钮态)?是 → **`setFrame`**。 +3. 要美术字形,显示的是**一串数字/符号**(可能两位、带正负号、带 `分`/`张` 后缀)?→ **`setNumberImage`**。 -> ⚠️ **最容易踩的坑:用多帧图片精灵去显示多位数。** -> `setFrame` 是"整个精灵显示图集的第 N 帧",**一个精灵一次只能显示一位数字**。于是两位数被迫拆成"十位精灵 + 个位精灵",三位数拆三个……精灵数量随位数线性膨胀,布局、显隐、清理全都要按位处理(十位为 0 还要单独隐藏),而**位数一变就得重排**。 -> 真实事故:某处 14 个档位按钮各配了**一个**分数精灵,而 14 档里 13 档是两位数——按 `setFrame` 方案**根本渲染不出来**,直到改成叠绘才修好。 -> **判据一句话:值可能超过一个字符,就不要用 `setFrame`。** +> ⚠️ **最容易踩的坑:显示多位数字时拆成多个精灵,或用 `setFrame` 逐位切。** +> `setFrame` 是"整个精灵显示第 N 帧",**一个精灵一次只显示一位**。有人据此把两位数拆成"十位精灵 + 个位精灵",三位数拆三个——精灵数随位数线性膨胀,布局、显隐、清理全要按位处理(十位为 0 还得单独隐藏),位数一变就得重排。 +> **平台早就给了 `setNumberImage`,一个精灵显示整串,别再造这个轮子。** +> 真实事故:某次评审误判"一个精灵只能显示一位",先把选主张数拆成十位/个位 8 个精灵,随后又差点为此新造一套"图集叠绘"渲染器——直到发现 `SpriteManager` 第 1154 行本来就有这个接口。 +> **判据一句话:值可能超过一个字符,用 `setNumberImage`,既不拆精灵也不用 `setFrame`。** -**图集叠绘怎么工作**:`GameABCUtils.Draw.drawImage` 支持**源矩形**,`SpriteManager.drawImageRegion` 把这能力暴露到业务层——可以从一张图里裁出任意区域,画到精灵内的任意位置。数字图出成**等宽连续排列的一行**,第 `idx` 格的源矩形就是 `(idx × 单字宽, 0, 单字宽, 单字高)`;把每一位分别裁出来、画到精灵内递增的 x 上,一个精灵就显示了一个多位数。 +**`setNumberImage` 怎么工作**(读实现,不要凭印象):它把文本里的符号编码后设给精灵的 TEXT 属性,由**引擎按字符逐帧渲染**(帧号由字符直接换算),并把精灵宽度设为 **`字符数 × charWidth`**。所以: ```js -// 框架的 NumberRenderer 封装了这套逻辑,业务侧只有三步(样式全部来自常量,禁裸值) -NumberRenderer.bind(SPRITE_ID, { - imageId: ImageResources.NUM_SCORE, // 等宽连续排列的数字图 - charWidth: 30, charHeight: 40, // 图集单字宽高 - spacing: 0, // 字间距(可为负 = 重叠) - align: 'center', width: 74 // 在绘制区内怎么对齐、绘制区多宽 -}); -NumberRenderer.setValue(SPRITE_ID, 70); // 只写值;引擎每帧回调时才真正画 -NumberRenderer.unbind(SPRITE_ID); // 组件 onDestroy 里注销回调,防残留 +// 资源须是 16 帧、帧序固定为 0123456789.+-x/p 的多帧图片 +SpriteManager.setNumberImage(spriteId, '70', charWidth); // 叫分档位分数(两位数,一个精灵) +SpriteManager.setNumberImage(spriteId, '+120', charWidth); // 结算得分(帧12 = '+'、帧13 = '-') +SpriteManager.setNumberImage(spriteId, '26张', charWidth); // 带后缀('张' 落在第 16 帧) +// charWidth 来自常量文件,禁裸值;精灵宽度由该接口自动设,无需再 setSize ``` -**三条前提,缺一不可**: +**资源规格(写进图片资源常量的注释里,供美术照做)**: -- **叠绘只能在精灵的绘制回调里进行**(`SpriteEventController.registerDraw` 注册的 handler 内),在回调之外调 `drawImageRegion` 不会留下任何画面——因为引擎只在绘制那一帧接受叠绘。 -- **绘制回调依赖平台事件链路接通**:`Game_Modify.gamemydraw` →(转发壳)→ 子游戏 hook → `SpriteEventController.handleDraw`。这条链断了,叠绘、按钮点击一起静默失效(见 §5)。 -- **只对编辑器预置的数字 ID 精灵有效**:`SpriteCopyUtils` 复制出的字符串 ID 精灵不接收独立绘制事件,不能用 `NumberRenderer`;那种场合改用文字精灵,或由父容器统一叠绘。 +| 帧 | 1–10 | 11 | 12 | 13 | 14 | 15 | 16 | +|----|------|----|----|----|----|----|----| +| 内容 | `0`–`9` | `.` | `+` | `-` | `x` | `/` | **可变后缀**(每套资源自己的后缀字,只支持 `分` / `倍` / `张`) | -**图集出图规格**(写进图片资源常量的注释里,供美术照做):字符**排列顺序**(如 `0…9` 后接 `+`、`-`)、**单字符宽高**、**整图尺寸**;字符必须严格等宽对齐格子、格间不留缝,否则裁出来的字会串位。 +- **帧序不可重排、不可省略占位**:帧号由字符直接换算,用不到的帧位也要按顺序留出来,否则帧号对不上、画面串位。 +- **16 帧等宽**(后缀字与数字同宽),整图宽 = `16 × 单字符宽`;单字符宽即代码里传的 `charWidth`。 +- 第 16 帧的后缀只认 `分` / `倍` / `张` 这三个字(接口内部把它们统一编码到该帧),别的字写进去不会被识别。 +- **复制精灵也能用**:`setNumberImage` 走的是属性设置、不依赖绘制回调,`SpriteCopyUtils` 复制出的字符串 ID 精灵同样适用。 --- @@ -218,7 +219,7 @@ SpriteEventController.registerMouseMove(sprites.HAND_AREA, function (event) { se 支持的事件类型:`mouseDown` / `mouseDownNoMove`(长按) / `mouseUp` / `mouseMove`(拖拽) / `drawBegin` / `draw`。 -> ⚠️ **转发这一段必须真的接上,且参数顺序要逐个对齐 `handleXxx` 的签名**。转发缺失或参数错位不会报错,只会让**所有**精灵交互与叠绘**静默失效**——按钮点了没反应、叠绘一片空白,却查不到任何异常日志。接线时对着 `SpriteEventController` 里各 `handleXxx` 的形参表逐个核对,转发层**只转发、不写业务**。 +> ⚠️ **转发这一段必须真的接上,且参数顺序要逐个对齐 `handleXxx` 的签名**。转发缺失或参数错位不会报错,只会让**所有**精灵交互与绘制回调**静默失效**——按钮点了没反应、叠绘的标记一片空白,却查不到任何异常日志。接线时对着 `SpriteEventController` 里各 `handleXxx` 的形参表逐个核对,转发层**只转发、不写业务**。 对**每个**绘制精灵统一处理(不针对某个固定 ID)用 `registerGlobalDraw(fn)`——框架对每个 draw 事件都回调它、但**不认识**其业务含义,保持中立(“精牌标记”这类玩法叠绘即以此挂载,框架零感知,见 05「框架中立」)。 @@ -265,7 +266,7 @@ list.onClick = function (type, rowIndex, rowData) { /* ... */ }; |------|---------| | UI 只调 `SpriteManager` | 直接调 `GameABCUtils`/引擎原生 API | | 一精灵多帧、`setFrame` 切换**状态** | 为每种牌面建一个精灵 | -| 多位美术字用 `NumberRenderer` 叠绘,**一个精灵画完整个数** | 用 `setFrame` 显示多位数(一帧只有一位)/ 拆十位个位各建一个精灵 | +| 多位美术字数字用 `setNumberImage`,**一个精灵显示整串** | 用 `setFrame` 逐位切 / 拆十位个位各建一个精灵 / 另造一套数字渲染器 | | 精灵 ID 1001–3000、群组 ≥201、从编辑器查证 | 随意编造 ID / 超范围 ID | | 检查 `SpriteManager` 返回值 | 忽略 `false` 返回 | | 精灵/资源/坐标全进常量文件 | 在业务代码内联裸数字 | diff --git a/docs_dev/二七王-UI资源与精灵清单.md b/docs_dev/二七王-UI资源与精灵清单.md index da1c00e..14c694c 100644 --- a/docs_dev/二七王-UI资源与精灵清单.md +++ b/docs_dev/二七王-UI资源与精灵清单.md @@ -535,15 +535,15 @@ EQW_RoomOptions = { | 元素 | 精灵类型 | 说明 | | --- | --- | --- | -| **叫分的分数**(70/65/…/5) | **图集叠绘数字精灵**(资源 `NUM_CALL_SCORE`,样式 `NUM_STYLE.CALL_SCORE`) | 带描边、需与图一致的美术字;**一个精灵画完整两位数** | +| **叫分的分数**(70/65/…/5) | **多帧图数字精灵**(资源 `NUM_CALL_SCORE`,`SpriteManager.setNumberImage`) | 带描边、需与图一致的美术字;**一个精灵显示整串**(两位数不拆精灵) | | **角标的子数**(`2子`/`7子`…) | **文字精灵**(`SpriteManager.setText`) | 随爬坡开关动态变,值域 2–15,用文字精灵最省 | 故每档 **3 个精灵**:按钮底(含角标底)+ 分数数字精灵 + 角标文字精灵。 -> **2026-08-26 修订(档位分数的实现方式)**:14 档里 **13 档是两位数**(70…10),而每档只有**一个**分数精灵。 -> 多帧图片精灵靠 `setFrame` 切帧、**一次只显示一帧=只能显示一位数**,按原方案这 13 档**根本渲染不出来**(要么显示不全,要么被迫每档再加一个十位精灵、精灵数翻倍)。 -> 因此改为**图集叠绘**:`NUM_CALL_SCORE` 出成等宽连续的一张图(§3.6),由框架 `NumberRenderer` 在精灵的绘制回调里按字宽逐位裁源矩形、画在**同一个**精灵内的不同 x 上。 -> 于是 `CALL_BTN_NUM_1..14` **保持 14 个不变**,且现在能正确显示两位数。用法:`NumberRenderer.bind(spriteId, style)` → `setValue(spriteId, 70)`,样式取 `EQW_Layout.NUM_STYLE.CALL_SCORE`(§6.8)。 +> **2026-08-26 补充(档位分数的实现方式,务必照做)**:14 档里 **13 档是两位数**(70…10),而每档只有**一个**分数精灵——这是**对的**,不要拆成十位/个位两个精灵。 +> 显示走平台既有接口 `SpriteManager.setNumberImage(spriteId, '70', charWidth)`:整串文本交给一个多帧图片精灵,引擎按字符逐帧渲染,精灵宽度按**字符数 × `charWidth`** 自动调整。 +> **不要用 `setFrame` 逐位切**——`setFrame` 是"整个精灵显示第 N 帧",一次只能显示一位,用它做多位数必然要拆精灵。 +> 资源 `NUM_CALL_SCORE` 须按 §3.6 的 **16 帧固定帧序**出图;`charWidth` 取 `EQW_Layout.NUM_STYLE.CALL_SCORE.charWidth`(§6.8,业务代码不写裸值)。 - 可选性规则(design §4.2,**由服务端权威 `currcall` 决定,前端只据其置灰、不自行推导规则**): - 暂定庄家首叫:5–70 全部可选,**无 `不叫`**; - 其后:只有**严格低于 `currcall`** 的档位可选,`不叫` 可选。 @@ -586,11 +586,11 @@ EQW_RoomOptions = { > **2026-08-26 修订(其一)**:美术侧实际出图方式与原规格不符——花色按钮改为**一张图 5 帧**(帧1–4=四花色按钮,每帧已含按钮底+花色图标+右上角蓝色角标底;帧5=投降按钮,已含按钮底+「投降」字),花色图标、角标底、投降文字不再单独出精灵。按此改写以下条目,代码同步见 `ImageResources.js`/`Sprites_Action.js`/`Layout_Action.js`。 > -> **2026-08-26 修订(其二)**:张数数字改为**图集叠绘**(§3.6)——一个精灵就能画完两位数,故**每花色一个数字精灵**(`MAIN_SUIT_COUNT_1..4` = 1674–1677),当日早前定的「十位/个位各一个精灵」(8 个)作废,空出的 1678–1681 不复用。 +> **2026-08-26 修订(其二)**:张数数字用平台既有的 `SpriteManager.setNumberImage`(§3.6)——一个精灵就能显示整串 `26张`,故**每花色一个数字精灵**(`MAIN_SUIT_COUNT_1..4` = 1674–1677);当日早前定的「十位/个位各一个精灵」(8 个)**作废**,空出的 1678–1681 不复用。 - 标题斜角条:`亮主` 金色字 + 倒计时数字。 - **4 个花色按钮**:每个按钮由**一个精灵**呈现,帧 = 花色序号 1–4(服务端 flower 编号),该帧已含「按钮底 + 花色图标 + 右上角蓝色角标底」;角标底之上叠加一个**对数文字精灵**(`N对`)。 - - 按钮下方叠加**张数数字精灵**:该花色在庄家手中的**总张数**——两副牌单花色最多 26 张(两位数),用**图集叠绘**在**一个**精灵上画完(`NumberRenderer`,无需拆十位/个位),资源见 §3.6 的 `NUM_MAIN_SUIT_COUNT`、样式见 §6.8 的 `NUM_STYLE.MAIN_SUIT_COUNT` + - 按钮下方叠加**张数数字精灵**:该花色在庄家手中的**总张数**——两副牌单花色最多 26 张(两位数),用 `SpriteManager.setNumberImage(spriteId, '26张', charWidth)` 在**一个**精灵上显示整串(无需拆十位/个位),资源见 §3.6 的 `NUM_MAIN_SUIT_COUNT`(第16帧=`张`)、`charWidth` 见 §6.8 的 `NUM_STYLE.MAIN_SUIT_COUNT` - 右上角标文字 = 该花色的**对子数**(design §4/§11 明确要求) - **张数与对子数都由前端据 `MyCards`/`cards` 自行统计**,协议 §「ChooseMain」明确「服务端不额外下发」。 - **投降按钮**:仅 `touxiang === 1`(即叫分 70)时显示,与 4 个花色按钮并列;按钮为**一个精灵**(`BTN_SUIT_CHOOSE` 帧5,已含底与「投降」字),不再单独出文字精灵。 @@ -1175,24 +1175,30 @@ y≈648–720 深色条。左侧头像/昵称/分数由平台渲染(§0.2) ### 3.6 数字与符号 -> **2026-08-26 修订(重要,出图方式变更)**:数字类资源 631/632/633/635/636 **不再是编辑器里配好帧数的多帧资源**,改为**等宽连续排列的一张图**——所有字符横向排成一行、每格等宽、格间不留缝。 -> 缘由:多帧图片精灵靠 `setFrame` 切帧,**一个精灵一次只显示一帧=只能显示一位数**;两位数就得两个精灵(十位/个位),精灵数迅速膨胀,而 `CALL_BTN_NUM_1..14` 每档只有一个精灵、14 档里 13 档是两位数,**按多帧方案根本渲染不出来**。 -> 新方案是**图集叠绘**:在精灵的绘制回调里按字宽裁源矩形,第 `idx` 格的源矩形 = `(idx × 单字宽, 0, 单字宽, 单字高)`,逐位画在**同一个**精灵上,于是**一个多位数只用一个精灵**。实现见框架 `gameabc-framework/ui/NumberRenderer.js`(`bind` / `setValue` / `clear` / `unbind`)与 `SpriteManager.drawImageRegion`;三种显示机制的选用判据见前端文档 `02-渲染与UI组件体系.md`。 -> **样式(单字宽高 / 字间距 / 对齐 / 绘制区宽)在 `EQW_Layout.NUM_STYLE.*`(`LayoutConstants.js`)集中配置**,本表只记出图规格。 +> **2026-08-26 修订(重要,出图规范订正)**:数字类资源 631/632/633/635/636 一律出成 **16 帧、固定帧序 `0123456789.+-x/p`** 的多帧图片资源。 +> 缘由:平台的 `SpriteManager.setNumberImage(spriteId, text, charWidth)` 就是**为多位美术字数字准备的现成接口**——把整串文本设给**一个**多帧图片精灵,引擎按字符逐帧渲染,并按**字符数 × `charWidth`** 自动调整精灵宽度。所以**一个精灵就能显示一整串数字**:既不用拆十位/个位,也不用 `setFrame` 逐位切(`setFrame` 是"整个精灵显示第 N 帧",只适合状态/花色/牌面这类"选其一")。 +> 帧号由字符**直接换算**(`'0'–'9'` → 帧1–10,后缀字 → 帧16),因此帧序**不可重排、不可省略占位**;用不到的帧位也要按顺序留出来,否则帧号对不上、画面串位。 +> 三种显示机制的选用判据见前端文档 `02-渲染与UI组件体系.md`;`charWidth`/`charHeight` 等数值在 `EQW_Layout.NUM_STYLE.*`(`LayoutConstants.js`)集中配置,本表只记出图规格。 -| ID | 键名 | 用途 | 字符排列(等宽连续一行) | 单字符尺寸 | 整图尺寸 | -| --- | --- | --- | --- | --- | --- | -| 631 | `NUM_COUNTDOWN` | 倒计时数字 | `0 1 2 3 4 5 6 7 8 9`(索引 0–9) | 44 × 60 | **440 × 60**(10 格) | -| 635 | `NUM_CALL_SCORE` | **叫分档位的分数数字**(深棕描边美术字) | `0 1 2 3 4 5 6 7 8 9`(索引 0–9) | 30 × 40 | **300 × 40**(10 格) | -| 632 | `NUM_RESULT_WIN` | 结算得分数字(赢,橙) | `0 1 2 3 4 5 6 7 8 9 + -`(索引 0–9 数字、10 = `+`、11 = `-`) | 46 × 62 | **552 × 62**(12 格) | -| 633 | `NUM_RESULT_LOSE` | 结算得分数字(输,蓝) | 与 632 完全一致,仅配色不同 | 46 × 62 | **552 × 62**(12 格) | -| 634 | `ANI_JUDGE` | **判定结果动画**(大光 / 小光 / 过庄 / 升N级 / 投降)——帧动画资源,播放参数进 `AnimationConfigs`【待设计 D-8】。**注意:这是帧动画,不属于本节的等宽数字图** | 待定 | 待定 | 待定 | -| 636 | `NUM_MAIN_SUIT_COUNT` | **选主面板花色张数数字**(美术单独出图,字号配色与以上几套不同;**一个花色一个精灵**) | `0 1 2 3 4 5 6 7 8 9`(索引 0–9) | 24 × 32(估值) | **240 × 32**(10 格) | +**固定帧序(所有 NUM_\* 通用)**: -> **出图要求**:字符必须**严格等宽对齐格子**(第 N 个字符的左边缘恰好在 `N × 单字宽`),否则裁出来的字会串位;格与格之间**不留间隙**,字间距由代码的 `spacing` 控制(可为负=重叠)。 -> **带描边/渐变的美术字用图集叠绘数字精灵**(倒计时、结算得分、叫分分数、选主张数),文字精灵做不出这种效果——631–636 都要出图。 -> **纯数据型小字用文字精灵**(`SpriteManager.setText`):叫分角标的子数(值域 2–15,随爬坡开关变)、`主N`/`对N` 角标、局数、亮牌统计等。 -> **2026-08-26 新增 636**:选主张数原计划走文字精灵,但美术侧单独出了一套数字图(字号配色不同于既有几套),且张数最多两位(两副牌单花色最多 26 张);同日改为图集叠绘后,**十位/个位不再拆两个精灵**,每花色一个精灵即可。 +| 帧 | 1–10 | 11 | 12 | 13 | 14 | 15 | 16 | +| --- | --- | --- | --- | --- | --- | --- | --- | +| 内容 | `0`–`9` | `.` | `+` | `-` | `x` | `/` | **可变后缀**(该套资源自己的后缀字:`分`/`倍`/`张`) | + +| ID | 键名 | 用途 | 帧数 | 第 16 帧(后缀) | 单字符尺寸 | 整图尺寸 | +| --- | --- | --- | --- | --- | --- | --- | +| 631 | `NUM_COUNTDOWN` | 倒计时数字 | **16** | **无后缀**(出空白占位) | 44 × 60 | **704 × 60**(16 × 44) | +| 635 | `NUM_CALL_SCORE` | **叫分档位的分数数字**(深棕描边美术字) | **16** | 【**待美术与规则确认**】档位只显示分数数字;若日后要显示 `70分` 则此帧出 `分` | 30 × 40 | **480 × 40**(16 × 30) | +| 632 | `NUM_RESULT_WIN` | 结算得分数字(赢,橙)——用到帧12 `+`、帧13 `-` | **16** | 【**待美术与规则确认**】结算大字目前只显示数字,也可能是 `分` | 46 × 62 | **736 × 62**(16 × 46) | +| 633 | `NUM_RESULT_LOSE` | 结算得分数字(输,蓝) | **16** | 同 632(含待确认项),仅配色不同 | 46 × 62 | **736 × 62**(16 × 46) | +| 634 | `ANI_JUDGE` | **判定结果动画**(大光 / 小光 / 过庄 / 升N级 / 投降)——帧动画资源,播放参数进 `AnimationConfigs`【待设计 D-8】。**注意:这是帧动画,不适用本节的 16 帧数字规范** | 待定 | —— | 待定 | 待定 | +| 636 | `NUM_MAIN_SUIT_COUNT` | **选主面板花色张数数字**(美术单独出图,字号配色与以上几套不同;**一个花色一个精灵**) | **16** | **`张`**(显示形如 `26张`) | 24 × 32(估值) | **384 × 32**(16 × 24) | + +> **出图要求**:16 帧**等宽**(后缀字与数字同宽,因为宽度按"字符数 × 单字宽"算);帧序严格照上表,用不到的帧位(如某些套的 `.` `x` `/`)**仍要占位**。 +> **带描边/渐变的美术字用多帧图数字精灵 + `setNumberImage`**(倒计时、结算得分、叫分分数、选主张数),文字精灵做不出这种效果——631–636 都要出图。 +> **纯数据型小字用文字精灵**(`SpriteManager.setText` / `setTextWithWidth`):叫分角标的子数(值域 2–15,随爬坡开关变)、`主N`/`对N` 角标、局数、亮牌统计等。 +> **2026-08-26 新增 636**:选主张数原计划走文字精灵,但美术侧单独出了一套数字图(字号配色不同于既有几套);同日一度定为「十位/个位各一个精灵」,**现已撤销**——`setNumberImage` 一个精灵就能显示 `26张`。 ### 3.7 浮层与气泡 @@ -1321,7 +1327,7 @@ design 与协议均**未定义任何音效**。以下是按牌类游戏常规推 | | 1601 | `CALL_HINT_BG` | 图片 | 提示条底 | `BAR_HINT_TOP` | | | 1602 | `CALL_HINT_TEXT` | 文字 | `70分坐庄才可投降` | —— | | | 1603–1616 | `CALL_BTN_1..14` | 图片 ×14 | 档位按钮底(**含角标底**,70…5) | `BTN_CALL_SCORE` 帧1–3 按档位位置固定 / 帧4 灰 | -| | 1617–1630 | `CALL_BTN_NUM_1..14` | **图集叠绘数字精灵** ×14 | 档位分数(70…5),**一个精灵画完整两位数**(2026-08-26,见 §3.6) | `NUM_CALL_SCORE` + `NUM_STYLE.CALL_SCORE` | +| | 1617–1630 | `CALL_BTN_NUM_1..14` | **多帧图数字精灵** ×14 | 档位分数(70…5),**一个精灵显示整串**(`setNumberImage`,两位数不拆精灵;2026-08-26,见 §1.3/§3.6) | `NUM_CALL_SCORE`(16 帧)+ `NUM_STYLE.CALL_SCORE.charWidth` | | | 1631–1644 | `CALL_BTN_BADGE_1..14` | **文字** ×14 | 该档 `multiple` 子数(`2子`…`15子`),随爬坡开关变 | —— | | | 1645 | `CALL_BTN_NONE` | 图片 | `不叫` | `BTN_NO_CALL` | | | 1646 | `CALL_BTN_NONE_TEXT` | 文字 | —— | —— | @@ -1329,10 +1335,10 @@ design 与协议均**未定义任何音效**。以下是按牌类游戏常规推 | | 1651 | `MAIN_TITLE_TEXT` | 文字 | `亮主` | —— | | | 1652–1655 | `MAIN_SUIT_BTN_1..4` | 图片 ×4 | 花色按钮(**已含按钮底+花色图标+角标底**),帧=花色序1–4 | `BTN_SUIT_CHOOSE` 帧1–4 | | | 1668–1671 | `MAIN_SUIT_PAIR_TEXT_1..4` | 文字 ×4 | `N对`,叠在对应按钮角标底之上 | —— | -| | 1674–1677 | `MAIN_SUIT_COUNT_1..4` | **图集叠绘数字精灵** ×4 | 该花色在庄家手中的**张数**(最多两位,**一个花色一个精灵**;2026-08-26 由十位/个位 8 个精灵改回 4 个,见 §3.6) | `NUM_MAIN_SUIT_COUNT` + `NUM_STYLE.MAIN_SUIT_COUNT` | +| | 1674–1677 | `MAIN_SUIT_COUNT_1..4` | **多帧图数字精灵** ×4 | 该花色在庄家手中的**张数**,显示 `26张`(**一个花色一个精灵**,`setNumberImage`;2026-08-26 由十位/个位 8 个精灵改回 4 个,见 §3.6) | `NUM_MAIN_SUIT_COUNT`(16 帧,第16帧=`张`)+ `NUM_STYLE.MAIN_SUIT_COUNT.charWidth` | | | 1672 | `MAIN_BTN_SURRENDER` | 图片 | `投降`(**已含按钮底+文字**) | `BTN_SUIT_CHOOSE` 帧5 | | | ~~1656–1659 / 1660–1663 / 1664–1667 / 1673~~ | ~~`MAIN_SUIT_ICON_1..4` / 原 `MAIN_SUIT_COUNT_1..4`(文字精灵)/ `MAIN_SUIT_PAIR_BG_1..4` / `MAIN_BTN_SURRENDER_TEXT`~~ | —— | **不需要**:分别并入按钮帧、或改为上方 1674–1677 的数字精灵,空出的 ID 不复用 | —— | -| | ~~1678–1681~~ | ~~`MAIN_SUIT_COUNT_3/4_TENS/ONES`~~ | —— | **不需要**(2026-08-26):改用图集叠绘后一个精灵就能画两位数,十位/个位拆分作废,空出的 4 个 ID 不复用 | —— | +| | ~~1678–1681~~ | ~~`MAIN_SUIT_COUNT_3/4_TENS/ONES`~~ | —— | **不需要**(2026-08-26):`setNumberImage` 一个精灵就能显示整串,十位/个位拆分作废,空出的 4 个 ID 不复用 | —— | | **222** 埋牌操作条 | 1700 | `BURY_BTN_TIP` | 图片 | `提示` | `BTN_TIP` | | | 1701 | `BURY_BTN_TIP_TEXT` | 文字 | —— | —— | | | 1702 | `BURY_BTN_SUBMIT` | 图片 | `埋牌` | `BTN_BURY` 帧1可提交/帧2灰 | @@ -1716,20 +1722,21 @@ CARD_SIZE: { ### 6.8 阶段操作区配置 > **2026-08-26 修订(其一)**:选主面板花色图标、对数角标底已并入按钮帧(见 §1.5/§3.3),对应布局节点删除。 -> **2026-08-26 修订(其二)**:数字改为图集叠绘(§3.6)后,一个多位数只占一个精灵——花色张数的「十位 / 个位」两个节点**合并为一个** `MAIN_SUIT_COUNT`;两个数字节点的 `w/h` 不再随位数变化,直接引用 `EQW_Layout.NUM_STYLE.*` 的 `width` / `charHeight`(SSOT:数值只在 `LayoutConstants.js` 定义一处,布局文件不再手抄)。 +> **2026-08-26 修订(其二)**:数字统一走 `SpriteManager.setNumberImage`(§3.6)后,一个多位数只占一个精灵——花色张数的「十位 / 个位」两个节点**合并为一个** `MAIN_SUIT_COUNT`。 +> 两个数字节点的 `h` 恒为单字高,直接引用 `EQW_Layout.NUM_STYLE.*.charHeight`(SSOT:数值只在 `LayoutConstants.js` 定义一处,布局文件不再手抄);`w` 则**留到运行时注入**——`setNumberImage` 会按「字符数 × `charWidth`」自动改精灵宽,位数变宽度就变。 | 部件 | kind | 参数(实测估值) | | --- | --- | --- | | 叫分 · 面板底 | `point` | `x:355 y:140 w:575 h:170` | | 叫分 · 提示条 | `point` | `x:470 y:100 w:345 h:33` | | **叫分 · 14 档按钮** | `grid` | `anchorX:640 anchorY:160 cols:7 rows:2 itemWidth:74 itemHeight:56 spacingX:7 spacingY:24 anchor:center fillOrder:row` | -| 叫分 · 档位分数数字 | `attach` | `target:对应档位按钮, hAlign:center, vAlign:middle, offsetY:4, w:NUM_STYLE.CALL_SCORE.width(74), h:NUM_STYLE.CALL_SCORE.charHeight(40)` | +| 叫分 · 档位分数数字 | `attach` | `target:对应档位按钮, hAlign:center, vAlign:middle, offsetY:4, h:NUM_STYLE.CALL_SCORE.charHeight(40)`;`w` 运行时注入(= 字符数 × 30,由 `setNumberImage` 自动设) | | 叫分 · 档位子数角标 | `attach` | `target:对应档位按钮, corner:topRight, offsetX:-30, offsetY:2` | | 叫分 · `不叫` 按钮 | `point` | `x:553 y:345 w:184 h:57` | | 叫分 · 自己叫分大字 | `point` | `x:570 y:320 w:140 h:70`,`textStyle{fontSize:52, align:center}` | | 选主 · 标题斜角条 | `point` | `x:330 y:315 w:425 h:50` | | **选主 · 4 花色 + 投降** | `line` | `direction:horizontal anchorX:330 anchorY:373 itemWidth:128 itemHeight:55 spacing:9 anchor:left`(共 5 项;投降不显示时只排 4 项,`anchor:left` 保证前 4 个位置不变) | -| 选主 · 张数 | `attach` | `target:对应花色按钮, hAlign:center, vAlign:bottom, offsetY:-6, w:NUM_STYLE.MAIN_SUIT_COUNT.width(48), h:NUM_STYLE.MAIN_SUIT_COUNT.charHeight(32)`(一个花色一个精灵,两位数由叠绘逐位画) | +| 选主 · 张数 | `attach` | `target:对应花色按钮, hAlign:center, vAlign:bottom, offsetY:-6, h:NUM_STYLE.MAIN_SUIT_COUNT.charHeight(32)`;`w` 运行时注入(`9张`=2 字符、`26张`=3 字符,宽度随之变)。一个花色一个精灵 | | 选主 · 对数角标文字 | `attach` | `target:对应花色按钮, corner:topRight, offsetX:-36, offsetY:2`(按钮帧内已含角标底,本节点只定位叠加文字) | | 埋牌 · 操作条 | `line` + `items` | `direction:horizontal anchorX:640 anchorY:165 itemHeight:63 spacing:45 anchor:center`,`items:[180,60,180]`(提示 / 倒计时 / 埋牌) | | 出牌 · 操作条 | `line` + `items` | `direction:horizontal anchorX:640 anchorY:345 itemHeight:55 spacing:35 anchor:center`,`items:[75,157]`(倒计时 / 出牌) | @@ -1737,17 +1744,20 @@ CARD_SIZE: { | 状态提示条 · 等待类 | `point` | `x:500 y:373 w:320 h:47`(§2.8) | | 状态提示条 · 即时反馈类 | `point` | `x:462 y:172 w:363 h:50`;另配 `toastDuration:1500` | -**数字样式配置 `EQW_Layout.NUM_STYLE`**(2026-08-26 新增,在 `LayoutConstants.js`,与 `CARD_SIZE`/`TEXT_STYLE` 并列)——图集叠绘数字的**唯一数值出处**,业务代码不许内联这些值: +**多帧图数字样式配置 `EQW_Layout.NUM_STYLE`**(2026-08-26 新增,在 `LayoutConstants.js`,与 `CARD_SIZE`/`TEXT_STYLE` 并列)——`setNumberImage` 所需数值的**唯一出处**,业务代码不许内联: -| 键 | `res`(资源键名) | `charWidth × charHeight` | `spacing` | `align` | `width`(绘制区宽) | 说明 | -| --- | --- | --- | --- | --- | --- | --- | -| `COUNTDOWN` | `NUM_COUNTDOWN` | 44 × 60 | 0 | center | 88 | 秒数最多两位 | -| `RESULT_WIN` | `NUM_RESULT_WIN` | 46 × 62 | 0 | center | 230 | 符号 + 最多四位 | -| `RESULT_LOSE` | `NUM_RESULT_LOSE` | 46 × 62 | 0 | center | 230 | 同上,仅配色不同 | -| `CALL_SCORE` | `NUM_CALL_SCORE` | 30 × 40 | 0 | center | 74 | = 档位按钮宽,居中压在按钮上 | -| `MAIN_SUIT_COUNT` | `NUM_MAIN_SUIT_COUNT` | 24 × 32 | 0 | center | 48 | 单花色最多 26 张 | +| 键 | `res`(资源键名) | `charWidth` | `charHeight` | `suffix`(第16帧后缀) | 典型显示 | +| --- | --- | --- | --- | --- | --- | +| `COUNTDOWN` | `NUM_COUNTDOWN` | 44 | 60 | `''`(无) | `15` | +| `RESULT_WIN` | `NUM_RESULT_WIN` | 46 | 62 | `''`(第16帧待确认) | `+120` | +| `RESULT_LOSE` | `NUM_RESULT_LOSE` | 46 | 62 | `''`(同上) | `-120` | +| `CALL_SCORE` | `NUM_CALL_SCORE` | 30 | 40 | `''`(第16帧待确认) | `70` | +| `MAIN_SUIT_COUNT` | `NUM_MAIN_SUIT_COUNT` | 24 | 32 | `张` | `26张` | -> `res` 存的是**资源键名**而非 ID(与 `CARD_SIZE.res` 同一约定):布局文件是纯数据、不引用其他模块,调用方用 `EQW_Images[style.res]` 取真实 ID 再传给 `NumberRenderer.bind`。 +> - `charWidth` 是 `setNumberImage` 的入参,**精灵宽度 = 字符数 × `charWidth`**(该接口自动设),所以布局节点的 `w` 是运行时值、不写死在配置里。 +> - `charHeight` 供布局节点定 `h`(`setNumberImage` 不改高度)。 +> - `suffix` 只能是 `''` / `分` / `倍` / `张`——接口内部把这三个字统一编码到第 16 帧,写别的字不会被识别。 +> - `res` 存的是**资源键名**而非 ID(与 `CARD_SIZE.res` 同一约定):布局文件是纯数据、不引用其他模块,调用方用 `EQW_Images[style.res]` 取真实 ID。 ### 6.9 结算区配置