fix(scripts): RoomSceneStart setPlayerData bindTo contract + runtime validation
Critical fix:
- setPlayerData fake state lacked subscribe() -> PlayerInfoView.bindTo threw
TypeError at runtime. Switched to PlayerInfoView.applyPlayer({nickname,
bean,avatar}) when available; falls back to bindTo + proper reactive
store with subscribe/value if only bindTo is exposed.
Runtime validation (per CLAUDE.md 第二准则 — no silent fallback):
- computePositions / setSeatLayout: validate layout ∈ VALID_LAYOUTS
- computePositions / setPlayerCount: validate count ∈ [1,8] AND is integer
- setPlayerData: validate seat is integer ∈ [1, currentCount]
- extractPlayerViewData: validate nickname/bean/avatar types (no ?? 兜底)
- start(): validate no null/undefined entries in playerSlots
Architecture:
- Extracted pure controller logic to room-scene-controller.ts so tests can
import without cc mock (RoomSceneStart remains thin cc wrapper that
delegates). Matches existing pattern of room-scene-layout.ts.
Tests:
- Added 27 new tests (validate* / apply* / extract* / clearSlots)
- 5 original layout tests kept; 254/254 PASS (222 prior + 32 room-scene)
Spec:
- Resolved 2x2/triangle/square contradiction: Success Metrics + 成功标准
both defer coordinates to layout sub-task, consistent with 不覆盖 section.
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -9,14 +9,23 @@
|
||||
/** 房间布局方案。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 标志数组。
|
||||
* 越界 throw(CLAUDE.md 第二准则:不兜底)。
|
||||
* 越界 / 非整数 / 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}`);
|
||||
}
|
||||
@@ -29,12 +38,23 @@ export function computeActiveFlags(count: number, total: number): boolean[] {
|
||||
* 给定 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 (count < 1) throw new Error(`count must be >= 1, got ${count}`);
|
||||
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 }));
|
||||
}
|
||||
Reference in New Issue
Block a user