From d1b60275a42c8784190bba600c8177b88ac3ea38 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Wed, 26 Aug 2026 22:26:21 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BA=8C=E4=B8=83=E7=8E=8B=EF=BC=9A=E5=BB=BA?= =?UTF-8?q?=E6=88=BF=E9=80=89=E9=A1=B9=E9=85=8D=E7=BD=AE=E8=A1=A5=E7=B2=BE?= =?UTF-8?q?=E7=81=B5=E9=94=AE=E3=80=81bit=20=E7=BB=9F=E4=B8=80=E6=8C=82?= =?UTF-8?q?=E5=88=B0=20item=E3=80=81=E8=A1=A5=20||=20{}=20=E4=BF=9D?= =?UTF-8?q?=E6=8A=A4=E5=A3=B0=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sprites_CreateRoom.js 的注释说「配置里每个选项声明自己用哪个精灵键」,但 EQW_RoomOptions 的 items 只有 label/value,渲染器只剩「按下标拼字符串」一条路——而那正是同文件明文禁止的; 何况也拼不对(组的 key 是 'rules',精灵却叫 ROOM_BOX_RULE_1)。 - 每个类别加 titleSprite,每个选项加 box/text 精灵键,房卡附注加 noteSprite。 - bit 统一挂到每个 item 上(单选组各项写同一个 bit),拼 roomtype 的循环对 radio/checkbox 是同一个形状(item → 写 value 到 bit),不再按组/项两种层级分支; checkbox 项补上 value:'1'。 - 补上其他常量文件都有的 `|| {}` 保护性声明。 - 守卫新增:精灵键必须存在且互不重复、必须恰好覆盖建房 View 预置的全部选项精灵、 bit 不得再挂在组上、单选组内 bit 一致、多选组 bit 互异、位号刚好铺满 0–4 (对齐服务端 class.config.js 的 IDX_ASET..IDX_NOCHECK)。 Co-Authored-By: Claude Opus 5 (1M context) --- .../codes/config/Layout_CreateRoom.js | 38 ++++++++++----- client/tests/test_constants.js | 47 +++++++++++++++++++ 2 files changed, 72 insertions(+), 13 deletions(-) diff --git a/client/js/01_SubGame/codes/config/Layout_CreateRoom.js b/client/js/01_SubGame/codes/config/Layout_CreateRoom.js index 146bbd7..b607417 100644 --- a/client/js/01_SubGame/codes/config/Layout_CreateRoom.js +++ b/client/js/01_SubGame/codes/config/Layout_CreateRoom.js @@ -80,23 +80,35 @@ EQW_Layout.ROOM_OPTION_OVERFLOW_GRID = { //整个界面由本配置驱动渲染,加一个选项 = 配置里加一条,不改渲染逻辑(工程总则 §5 OCP / §6 配置优先)。 //type: 'radio' 单选 / 'checkbox' 多选 / 'radioOptional' 单选可不选 //选择框帧号 = (useCheckbox ? 3 : 1) + (selected ? 1 : 0),useCheckbox = (checkbox 或 radioOptional) -//【roomtype 拼串只在一处实现】每个选项自带 bit 与 value,拼串就是遍历本配置, -//不得在别处按下标硬拼(与服务端 class.config.js parse() 对称的 SSOT 要求) -var EQW_RoomOptions = { +// +//【精灵键由配置给出】每个类别声明自己的标题精灵 titleSprite,每个选项声明自己的选择框精灵 box +//与文字精灵 text,附注声明 noteSprite——渲染器一律按键名取精灵,【不得按下标拼字符串】 +//(组的 key 是 'rules'、精灵却叫 ROOM_BOX_RULE_1,本来就拼不出来)。 +// +//【roomtype 拼串只在一处实现】拼串就是遍历本配置,不得在别处按下标硬拼 +//(与服务端 class.config.js parse() 对称的 SSOT 要求)。bit/value 统一挂在【每个 item 上】: +// - radio:组内各 item 写同一个 bit,选中哪个就把该 item 的 value 写进该位; +// - checkbox:勾选写 item.value、不勾写 '0'。 +//这样拼串循环对两种 type 是同一个形状(item → 写 value 到 bit),不必按组/项两种层级分支 +var EQW_RoomOptions = EQW_RoomOptions || { categories: [ - { titleFrame: 1, /* 牌局 */ groups: [ - { key: 'aset', type: 'radio', bit: 0, - items: [ { label: '6局', value: '0' }, { label: '12局', value: '1' } ] }, - { key: 'card', type: 'radio', bit: 1, note: 'cardCost', - items: [ { label: '房主扣卡', value: '0' }, { label: 'AA每人扣卡', value: '1' } ] } + { titleFrame: 1, titleSprite: 'ROOM_CAT_TITLE_1', /* 牌局 */ groups: [ + { key: 'aset', type: 'radio', + items: [ { label: '6局', value: '0', bit: 0, box: 'ROOM_BOX_ASET_1', text: 'ROOM_TEXT_ASET_1' }, + { label: '12局', value: '1', bit: 0, box: 'ROOM_BOX_ASET_2', text: 'ROOM_TEXT_ASET_2' } ] }, + { key: 'card', type: 'radio', note: 'cardCost', noteSprite: 'ROOM_TEXT_CARD_COST', + items: [ { label: '房主扣卡', value: '0', bit: 1, box: 'ROOM_BOX_CARD_1', text: 'ROOM_TEXT_CARD_1' }, + { label: 'AA每人扣卡', value: '1', bit: 1, box: 'ROOM_BOX_CARD_2', text: 'ROOM_TEXT_CARD_2' } ] } ] }, - { titleFrame: 2, /* 模式 */ groups: [ - { key: 'peek', type: 'radio', bit: 4, - items: [ { label: '可查牌', value: '0' }, { label: '不查牌', value: '1' } ] } + { titleFrame: 2, titleSprite: 'ROOM_CAT_TITLE_2', /* 模式 */ groups: [ + { key: 'peek', type: 'radio', + items: [ { label: '可查牌', value: '0', bit: 4, box: 'ROOM_BOX_PEEK_1', text: 'ROOM_TEXT_PEEK_1' }, + { label: '不查牌', value: '1', bit: 4, box: 'ROOM_BOX_PEEK_2', text: 'ROOM_TEXT_PEEK_2' } ] } ] }, - { titleFrame: 3, /* 规则 */ groups: [ + { titleFrame: 3, titleSprite: 'ROOM_CAT_TITLE_3', /* 规则 */ groups: [ { key: 'rules', type: 'checkbox', - items: [ { label: '傍王', bit: 2 }, { label: '爬坡', bit: 3 } ] } + items: [ { label: '傍王', value: '1', bit: 2, box: 'ROOM_BOX_RULE_1', text: 'ROOM_TEXT_RULE_1' }, + { label: '爬坡', value: '1', bit: 3, box: 'ROOM_BOX_RULE_2', text: 'ROOM_TEXT_RULE_2' } ] } ] } ], //房卡消耗联动(协议 §0.5):不硬编码在 UI 代码里 diff --git a/client/tests/test_constants.js b/client/tests/test_constants.js index 1513768..c206f95 100644 --- a/client/tests/test_constants.js +++ b/client/tests/test_constants.js @@ -267,6 +267,53 @@ function checkKind(node, path) { Object.keys(EQW_Layout).forEach(k => checkKind(EQW_Layout[k], k)); t.eq('布局 kind 全部合法', badKinds, []); +// ---- 建房选项配置:精灵键由配置给出,渲染器不按下标拼字符串 ---- +const optProblems = []; +const usedOptSprites = {}; +function useSprite(where, key) { + if (!key) { optProblems.push(where + ' 未声明精灵键'); return; } + if (!index.hasOwnProperty(key)) { optProblems.push(where + ' -> 精灵键不存在: ' + key); return; } + if (usedOptSprites[key]) { optProblems.push(where + ' -> 精灵键被重复引用: ' + key); } + usedOptSprites[key] = true; +} +const bitsSeen = {}; +EQW_RoomOptions.categories.forEach((cat, ci) => { + useSprite('类别' + ci + ' titleSprite', cat.titleSprite); + cat.groups.forEach(g => { + if (g.hasOwnProperty('bit')) { optProblems.push('组 ' + g.key + ' 仍把 bit 挂在组上(应统一到 item)'); } + if (g.note) { useSprite('组 ' + g.key + ' noteSprite', g.noteSprite); } + if (['radio', 'checkbox', 'radioOptional'].indexOf(g.type) < 0) { + optProblems.push('组 ' + g.key + ' -> 未知 type: ' + g.type); + } + g.items.forEach((it, ii) => { + const where = '组 ' + g.key + ' 第' + ii + '项'; + useSprite(where + ' box', it.box); + useSprite(where + ' text', it.text); + if (typeof it.bit !== 'number') { optProblems.push(where + ' 缺 bit'); } + if (it.value !== '0' && it.value !== '1') { optProblems.push(where + ' value 非 0/1: ' + it.value); } + bitsSeen[it.bit] = true; + // 单选组内各项必须共用同一个 bit(同一位互斥取值) + if (g.type !== 'checkbox' && it.bit !== g.items[0].bit) { + optProblems.push(where + ' 与同组首项 bit 不一致'); + } + }); + // 多选组各项必须占用互不相同的位 + if (g.type === 'checkbox') { + const bs = g.items.map(it => it.bit); + if (new Set(bs).size !== bs.length) { optProblems.push('组 ' + g.key + ' 多选各项 bit 重复'); } + } + }); +}); +// 配置声明的精灵必须恰好覆盖建房 View 里预置的全部选项精灵(多一个少一个都说明两边不同步) +Object.keys(EQW_Sprites.CreateRoomView.CreateRoom).forEach(k => { + if (k === 'id') { return; } + if (!usedOptSprites[k]) { optProblems.push('精灵 ' + k + ' 没有任何配置项引用它'); } +}); +if (optProblems.length) { console.log('建房选项配置问题:\n ' + optProblems.join('\n ')); } +t.eq('建房选项精灵键与 bit 声明完整', optProblems, []); +// roomtype 是 5 位串(服务端 class.config.js IDX_ASET..IDX_NOCHECK = 0..4),位号必须刚好铺满 +t.eq('roomtype 位号铺满 0-4', Object.keys(bitsSeen).map(Number).sort((a, b) => a - b), [0, 1, 2, 3, 4]); + // ---- 牌尺寸档引用的资源键存在 ---- ['L', 'M', 'S'].forEach(size => { t.eq('CARD_SIZE.' + size + ' 的资源存在', EQW_Images.hasOwnProperty(EQW_Layout.CARD_SIZE[size].res), true);