主要改动: - 切到 funplay-cocos-mcp v0.5.1 (用户级配置, 项目级 .mcp.json 删除) - 仓库文档/CLAUDE.md/.gitignore 等清理过时 cocos-mcp-server 引用 - memory 文件同步: cocos-mcp-setup/path/blocker/spriteframe-uuid/prefab-persist 等加 funplay 实测警告 - memory 新建 funplay-cocos-mcp-pending-verification.md (后已被实测覆盖) - spec/plan/data: - docs/superpowers/specs/2026-09-02-legacy-layer-migration-design.md - docs/superpowers/plans/2026-09-02-legacy-layer-migration.md - docs/superpowers/data/layer-spirit-summary.json - YouleNexus: profiles.ts / defaults.ts / PlayerInfoView.prefab / scene 改动 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
94 lines
3.1 KiB
TypeScript
94 lines
3.1 KiB
TypeScript
import { _decorator, Component, Label, Sprite } from 'cc';
|
|
|
|
const { ccclass, property } = _decorator;
|
|
|
|
/**
|
|
* PlayerInfoView: 响应式视图组件(自包含,便于 Cocos 编辑器挂载)。
|
|
*
|
|
* 设计取舍:不 import framework/store-binder,让本组件保持"纯 cc.Component 子类 +
|
|
* 极简内联 binder"的单文件结构,方便通过 funplay-cocos-mcp 的 mount 流程挂到 prefab
|
|
* (挂脚本路径与持久化限制见 memory `prefab-persist-mount-script` /
|
|
* `cocos-mcp-mount-script-path`)。
|
|
*
|
|
* 实际订阅契约与 framework/ui/store-binder 一致:
|
|
* - bindTo(binder) 接收外部 binder 实例
|
|
* - 调用方持有 binder,内部 subscribe + 自动刷新 Label.string
|
|
*
|
|
* 后续如需让 framework/store-binder 与本组件复用同一份逻辑,可把 subscribe
|
|
* 抽到一个 cc-agnostic 的 pure module;目前以"单文件自包含"落地,优先确保编辑器挂载稳定。
|
|
*/
|
|
|
|
/** 内联极简 binder(语义与 framework/ui/store-binder 对齐) */
|
|
interface ViewBinder {
|
|
bind<T>(store: ViewStore<T>, slot: string, cb: (value: T) => void): void;
|
|
unbindAll(): void;
|
|
}
|
|
|
|
/** 极简 store 契约:暴露 subscribe 与 value */
|
|
interface ViewStore<T> {
|
|
subscribe(cb: (value: T, prev: T) => void): () => void;
|
|
value: T;
|
|
}
|
|
|
|
function createViewBinder(): ViewBinder {
|
|
const unsubs: Array<() => void> = [];
|
|
return {
|
|
bind<T>(store: ViewStore<T>, _slot: string, cb: (value: T) => void) {
|
|
// 首次同步:bind 时立即用当前 value 跑一次 callback
|
|
try { cb(store.value); }
|
|
catch (e) { console.error('[PlayerInfoView] initial callback error:', e); }
|
|
const unsub = store.subscribe((value) => cb(value));
|
|
unsubs.push(unsub);
|
|
},
|
|
unbindAll() {
|
|
while (unsubs.length > 0) {
|
|
const u = unsubs.pop();
|
|
try { u?.(); } catch { /* 退订隔离 */ }
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
@ccclass('PlayerInfoView')
|
|
export class PlayerInfoView extends Component {
|
|
@property(Label)
|
|
nicknameLabel: Label = null!;
|
|
|
|
@property(Label)
|
|
beanLabel: Label = null!;
|
|
|
|
@property(Sprite)
|
|
avatarSprite: Sprite = null!;
|
|
|
|
private binder: ViewBinder | null = null;
|
|
private session: { player: { state: ViewStore<any> } } | null = null;
|
|
|
|
/**
|
|
* 由场景初始化代码调用,把视图绑定到 session.player.state。
|
|
* 调用方负责传入 session(典型用法:PlatformSession)。
|
|
*
|
|
* 兼容性:session.player.state 需有 subscribe/value 两个成员——framework
|
|
* 的 ReadonlyReactive<T> 已具备。
|
|
*/
|
|
bindTo(session: { player: { state: ViewStore<any> } }): void {
|
|
this.unbindIfNeeded();
|
|
this.session = session;
|
|
this.binder = createViewBinder();
|
|
this.binder.bind(session.player.state, 'state', (p: any) => this.applyPlayer(p));
|
|
}
|
|
|
|
onDestroy(): void { this.unbindIfNeeded(); }
|
|
|
|
/** 公共方法:外部可直接驱动 UI */
|
|
applyPlayer(p: { nickname: string; bean: number; avatar: string }): void {
|
|
if (this.nicknameLabel) this.nicknameLabel.string = p.nickname ?? '';
|
|
if (this.beanLabel) this.beanLabel.string = String(p.bean ?? 0);
|
|
// avatar 加载留给业务侧扩展
|
|
}
|
|
|
|
private unbindIfNeeded(): void {
|
|
this.binder?.unbindAll();
|
|
this.binder = null;
|
|
this.session = null;
|
|
}
|
|
} |