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:
@@ -1,8 +1,31 @@
|
||||
import { _decorator, Component, Node } from 'cc';
|
||||
import type { PlayerState } from '../framework/platform/stores/types.ts';
|
||||
import { computeActiveFlags, computePositions, type SeatLayout } from './room-scene-layout.ts';
|
||||
import {
|
||||
applyPlayerCount,
|
||||
applyPlayerData,
|
||||
applySeatLayoutPositions,
|
||||
clearSlots,
|
||||
computeActiveFlags,
|
||||
computePositions,
|
||||
type PlayerSlotLike,
|
||||
type SeatLayout,
|
||||
validateCount,
|
||||
validateLayout,
|
||||
validateSeat,
|
||||
} from './room-scene-controller.ts';
|
||||
|
||||
export { computeActiveFlags, computePositions };
|
||||
export {
|
||||
applyPlayerCount,
|
||||
applyPlayerData,
|
||||
applySeatLayoutPositions,
|
||||
clearSlots,
|
||||
computeActiveFlags,
|
||||
computePositions,
|
||||
validateCount,
|
||||
validateLayout,
|
||||
validateSeat,
|
||||
type PlayerSlotLike,
|
||||
};
|
||||
export type { SeatLayout };
|
||||
|
||||
const { ccclass, property } = _decorator;
|
||||
@@ -18,9 +41,9 @@ const { ccclass, property } = _decorator;
|
||||
* 8 个 playerSlots 在场景里预先挂好(Plan Task 2 负责),setPlayerCount 只切 active,
|
||||
* 不运行时 instantiate 节点(避免预制重复实例化开销)。
|
||||
*
|
||||
* 纯函数算法(computeActiveFlags / computePositions)放在 room-scene-layout.ts,
|
||||
* 本文件只 re-export — 这样单元测试可以直接 import 布局函数而无需 mock cc 模块
|
||||
* (CLAUDE.md 第二准则:RoomSceneStart 的下游消费者不应被 cc 依赖拖累)。
|
||||
* 纯逻辑(校验 + 槽位操作 + 视图交互)放在 room-scene-controller.ts(同目录),
|
||||
* 本文件只 re-export 并做 cc 包装 — 这样单元测试可以直接 import 控制器函数
|
||||
* 而无需 mock cc 模块(CLAUDE.md 第二准则:RoomSceneStart 的下游消费者不应被 cc 依赖拖累)。
|
||||
*/
|
||||
@ccclass('RoomSceneStart')
|
||||
export class RoomSceneStart extends Component {
|
||||
@@ -36,71 +59,42 @@ export class RoomSceneStart extends Component {
|
||||
`[RoomSceneStart] playerSlots must have 8 entries, got ${this.playerSlots?.length}`,
|
||||
);
|
||||
}
|
||||
// 校验 playerSlots 内是否有 null/undefined;start 时显式 throw,不兜底。
|
||||
for (let i = 0; i < this.playerSlots.length; i++) {
|
||||
if (!this.playerSlots[i]) {
|
||||
throw new Error(`[RoomSceneStart] playerSlots[${i}] is null/undefined`);
|
||||
}
|
||||
}
|
||||
this.setPlayerCount(this.currentCount);
|
||||
this.setSeatLayout(this.currentLayout);
|
||||
}
|
||||
|
||||
public setPlayerCount(n: number): void {
|
||||
if (n < 1 || n > this.playerSlots.length) {
|
||||
throw new Error(
|
||||
`[RoomSceneStart] setPlayerCount: n must be 1-${this.playerSlots.length}, got ${n}`,
|
||||
);
|
||||
}
|
||||
validateCount(n);
|
||||
this.currentCount = n;
|
||||
const flags = computeActiveFlags(n, this.playerSlots.length);
|
||||
for (let i = 0; i < this.playerSlots.length; i++) {
|
||||
const slot = this.playerSlots[i];
|
||||
if (slot) slot.active = flags[i];
|
||||
}
|
||||
applyPlayerCount(this.playerSlots as unknown as PlayerSlotLike[], n);
|
||||
}
|
||||
|
||||
public setSeatLayout(layout: SeatLayout): void {
|
||||
validateLayout(layout);
|
||||
this.currentLayout = layout;
|
||||
const positions = computePositions(layout, this.currentCount);
|
||||
for (let i = 0; i < this.currentCount && i < positions.length; i++) {
|
||||
const slot = this.playerSlots[i];
|
||||
if (slot) slot.setPosition(positions[i].x, positions[i].y, 0);
|
||||
}
|
||||
applySeatLayoutPositions(
|
||||
this.playerSlots as unknown as PlayerSlotLike[],
|
||||
layout,
|
||||
this.currentCount,
|
||||
);
|
||||
}
|
||||
|
||||
public setPlayerData(seat: number, data: PlayerState): void {
|
||||
if (seat < 1 || seat > this.currentCount) {
|
||||
throw new Error(
|
||||
`[RoomSceneStart] setPlayerData: seat ${seat} out of range 1-${this.currentCount}`,
|
||||
);
|
||||
}
|
||||
const slot = this.playerSlots[seat - 1];
|
||||
if (!slot) return;
|
||||
const view = slot.getComponent('PlayerInfoView') as any;
|
||||
if (view && typeof view.bindTo === 'function') {
|
||||
view.bindTo({ player: { state: { value: data } } });
|
||||
}
|
||||
applyPlayerData(
|
||||
this.playerSlots as unknown as PlayerSlotLike[],
|
||||
seat,
|
||||
this.currentCount,
|
||||
data as unknown as { nickname?: unknown; bean?: unknown; avatar?: unknown },
|
||||
);
|
||||
}
|
||||
|
||||
public clearSeats(): void {
|
||||
const placeholder: PlayerState = {
|
||||
playerid: 0,
|
||||
nickname: '占位',
|
||||
avatar: '',
|
||||
sex: 0,
|
||||
bean: 0,
|
||||
roomcard: 0,
|
||||
score: 0,
|
||||
charm: 0,
|
||||
taskstate: 0,
|
||||
advanced: 0,
|
||||
bankpower: 0,
|
||||
bank: 0,
|
||||
bankpwd: 0,
|
||||
ip: '',
|
||||
sign: null,
|
||||
tel: null,
|
||||
invitecode: null,
|
||||
initCard: 0,
|
||||
initBean: 0,
|
||||
};
|
||||
for (let i = 0; i < this.currentCount; i++) {
|
||||
this.setPlayerData(i + 1, placeholder);
|
||||
}
|
||||
clearSlots(this.playerSlots as unknown as PlayerSlotLike[], this.currentCount);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user