/** * Room.scene 的纯函数布局/可见性算法。 * * 设计意图:RoomSceneStart.ts 依赖 Cocos(cc 模块),在 node + tsx 下不可加载; * 把不依赖 cc 的纯逻辑抽到本文件,让单元测试可直接 import 而无需 mock cc。 * RoomSceneStart 在内部 import 并 re-export 这两个函数,作为公共 API 的一部分。 */ /** 房间布局方案。horizontal 已落地;其余 3 种留 layout 子任务(plan 范围外)。 */ export type SeatLayout = 'horizontal' | '2x2' | 'triangle' | 'square'; /** 合法 layout 集合(运行期校验用)。CLAUDE.md 第二准则:不静默 fallback,直接 throw。 */ const VALID_LAYOUTS: readonly SeatLayout[] = ['horizontal', '2x2', 'triangle', 'square']; /** 单场景最大槽位数(与 RoomSceneStart.playerSlots 长度一致)。 */ export const MAX_SEATS = 8; /** 槽位之间的固定像素间距(layout 子任务会参数化)。 */ const SLOT_SPACING = 300; /** * 给定显示槽位数 + 槽位总数,返回 active 标志数组。 * 越界 / 非整数 / NaN 全部 throw(CLAUDE.md 第二准则:不兜底)。 */ export function computeActiveFlags(count: number, total: number): boolean[] { if (!Number.isInteger(count) || Number.isNaN(count)) { throw new Error(`count must be an integer, got ${count}`); } if (count < 1 || count > total) { throw new Error(`count must be 1-${total}, got ${count}`); } const flags: boolean[] = new Array(total); for (let i = 0; i < total; i++) flags[i] = (i < count); return flags; } /** * 给定 layout + 玩家数,返回每个槽位的局部坐标。 * horizontal 已落地;2x2 / triangle / square 暂 fallback 到 horizontal * (留作后续 layout 子任务,本计划范围外)。 * * 校验:count 必须为 1-MAX_SEATS 整数;layout 必须是合法 SeatLayout; * 非法输入直接 throw,不静默 fallback(CLAUDE.md 第二准则)。 */ export function computePositions( layout: SeatLayout, count: number, ): Array<{ x: number; y: number }> { if (!VALID_LAYOUTS.includes(layout)) { throw new Error(`layout must be one of ${VALID_LAYOUTS.join('|')}, got ${String(layout)}`); } if (!Number.isInteger(count) || Number.isNaN(count)) { throw new Error(`count must be an integer, got ${count}`); } if (count < 1 || count > MAX_SEATS) { throw new Error(`count must be 1-${MAX_SEATS}, got ${count}`); } const startX = -(count - 1) * SLOT_SPACING / 2; return Array.from({ length: count }, (_, i) => ({ x: startX + i * SLOT_SPACING, y: 0 })); }