Files
youle_cocos/cocoscreator_projects/YouleNexus/assets/scripts/RoomSceneStart.ts
T
joywayerandClaude Code ec550ba257 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>
2026-09-02 21:22:04 +08:00

100 lines
3.0 KiB
TypeScript

import { _decorator, Component, Node } from 'cc';
import type { PlayerState } from '../framework/platform/stores/types.ts';
import {
applyPlayerCount,
applyPlayerData,
applySeatLayoutPositions,
clearSlots,
computeActiveFlags,
computePositions,
type PlayerSlotLike,
type SeatLayout,
validateCount,
validateLayout,
validateSeat,
} from './room-scene-controller.ts';
export {
applyPlayerCount,
applyPlayerData,
applySeatLayoutPositions,
clearSlots,
computeActiveFlags,
computePositions,
validateCount,
validateLayout,
validateSeat,
type PlayerSlotLike,
};
export type { SeatLayout };
const { ccclass, property } = _decorator;
/**
* RoomSceneStart: 房间场景控制脚本。
*
* 工作流:
* 1. 子游戏初始化 → setPlayerCount(n) + setSeatLayout(layout) 调整可见槽位与位置
* 2. 子游戏收到玩家状态变更 → setPlayerData(seat, data) 注入到对应槽位的 PlayerInfoView
* 3. 子游戏重置房间 → clearSeats() 把当前可见槽位刷成占位数据
*
* 8 个 playerSlots 在场景里预先挂好(Plan Task 2 负责),setPlayerCount 只切 active,
* 不运行时 instantiate 节点(避免预制重复实例化开销)。
*
* 纯逻辑(校验 + 槽位操作 + 视图交互)放在 room-scene-controller.ts(同目录),
* 本文件只 re-export 并做 cc 包装 — 这样单元测试可以直接 import 控制器函数
* 而无需 mock cc 模块(CLAUDE.md 第二准则:RoomSceneStart 的下游消费者不应被 cc 依赖拖累)。
*/
@ccclass('RoomSceneStart')
export class RoomSceneStart extends Component {
@property([Node])
playerSlots: Node[] = [];
private currentCount: number = 4;
private currentLayout: SeatLayout = 'horizontal';
start(): void {
if (!this.playerSlots || this.playerSlots.length !== 8) {
throw new Error(
`[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 {
validateCount(n);
this.currentCount = n;
applyPlayerCount(this.playerSlots as unknown as PlayerSlotLike[], n);
}
public setSeatLayout(layout: SeatLayout): void {
validateLayout(layout);
this.currentLayout = layout;
applySeatLayoutPositions(
this.playerSlots as unknown as PlayerSlotLike[],
layout,
this.currentCount,
);
}
public setPlayerData(seat: number, data: PlayerState): void {
applyPlayerData(
this.playerSlots as unknown as PlayerSlotLike[],
seat,
this.currentCount,
data as unknown as { nickname?: unknown; bean?: unknown; avatar?: unknown },
);
}
public clearSeats(): void {
clearSlots(this.playerSlots as unknown as PlayerSlotLike[], this.currentCount);
}
}