主要改动: - 切到 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>
166 lines
5.6 KiB
JavaScript
166 lines
5.6 KiB
JavaScript
import {
|
||
DESIGN_WIDTH, DESIGN_HEIGHT, CANVAS_WIDTH, CANVAS_HEIGHT,
|
||
toCocosPosition, inferWidget, stretchWidget, isFullScreen, isOffscreen,
|
||
} from './coords.mjs';
|
||
import { frameFileName, padWidth } from './frames.mjs';
|
||
import { layerBucket, imageBuckets } from './buckets.mjs';
|
||
|
||
/** 中间描述组装。见 spec §1。 */
|
||
|
||
const OBJECT_TYPE_SPRITE = 2;
|
||
const OBJECT_TYPE_TEXT = 4;
|
||
|
||
/**
|
||
* 帧号越界(旧数据里美术重导时帧数变少,但对象上的 FrameIndex 没跟着更新)。
|
||
* 只拦这一种已知情况——预判而非 try/catch,免得连带吞掉 frameFileName 里真正的实现 bug。
|
||
* 单帧图(frame_all<=1)的 FrameIndex 本就与渲染无关,不算越界。
|
||
*/
|
||
function isFrameIndexOutOfRange(obj, image) {
|
||
const requested = obj.FrameIndex || 1;
|
||
return image.frame_all > 1 && requested > image.frame_all;
|
||
}
|
||
|
||
function spriteBlock(obj, image, bucket, warnings) {
|
||
const dir = `atlas-${bucket}`;
|
||
const sliced = obj.L9 || obj.T9 || obj.R9 || obj.B9;
|
||
const outOfRange = isFrameIndexOutOfRange(obj, image);
|
||
const frameIndex = outOfRange ? 1 : (obj.FrameIndex || 1);
|
||
|
||
if (outOfRange) {
|
||
warnings.push({
|
||
objectId: obj.ObjectID,
|
||
layerId: obj.BelongLayerID,
|
||
imageFileId: obj.ImageFileID,
|
||
frameIndex: obj.FrameIndex,
|
||
frameCount: image.frame_all,
|
||
reason: `对象 ${obj.ObjectID}(${obj.ObjectName})引用 ${image.bmp} 的帧 ${obj.FrameIndex},`
|
||
+ `超出该图帧数 ${image.frame_all}(很可能是美术重导后 FrameIndex 未同步更新),已回退为帧 1`,
|
||
});
|
||
}
|
||
|
||
const block = {
|
||
frame: `${dir}/${frameFileName(image.bmp, frameIndex, image.frame_all)}`,
|
||
type: sliced ? 'sliced' : 'simple',
|
||
};
|
||
if (image.frame_all > 1) {
|
||
block.frameSet = {
|
||
prefix: `${dir}/${image.bmp.replace(/\.png$/i, '')}`,
|
||
count: image.frame_all,
|
||
pad: padWidth(image.frame_all),
|
||
};
|
||
}
|
||
if (sliced) {
|
||
block.slice = { left: obj.L9, right: obj.R9, top: obj.T9, bottom: obj.B9 };
|
||
}
|
||
return { block, outOfRange };
|
||
}
|
||
|
||
function toNode(obj, source, imageBucketMap, warnings) {
|
||
// 拉伸(四边置0)适用于全部满屏对象(spec §2.4: 实测 52 个),不论是否层内首个。
|
||
// backgroundNodeId(层内唯一)是另一件事:仅用于"首个+满屏"的背景判定,与此处解耦。
|
||
const stretch = isFullScreen(obj.Width, obj.Height);
|
||
const node = {
|
||
id: obj.ObjectID,
|
||
name: obj.ObjectName,
|
||
kind: obj.ObjectType === OBJECT_TYPE_TEXT ? 'label' : 'sprite',
|
||
position: toCocosPosition(obj.Left, obj.Top, obj.Width, obj.Height),
|
||
size: { width: obj.Width, height: obj.Height },
|
||
siblingIndex: obj.IndexOfLayer,
|
||
widget: stretch ? stretchWidget() : inferWidget(obj.Left, obj.Width),
|
||
flags: {
|
||
stretch,
|
||
offscreen: isOffscreen(obj.Left, obj.Top, obj.Width, obj.Height),
|
||
animation: obj.FrameStyle === 1,
|
||
frameIndexOutOfRange: false,
|
||
},
|
||
legacy: {
|
||
objectId: obj.ObjectID,
|
||
imageFileId: obj.ImageFileID ?? 0,
|
||
frameIndex: obj.FrameIndex ?? 0,
|
||
frameStyle: obj.FrameStyle ?? 0,
|
||
groupId: obj.GroupID ?? 0,
|
||
voiceFileId: obj.VoiceFileID ?? 0,
|
||
timerInterval: obj.TimerInterval ?? 0,
|
||
left: obj.Left,
|
||
top: obj.Top,
|
||
events: obj.events ?? [],
|
||
},
|
||
};
|
||
|
||
if (node.kind === 'sprite' && obj.ImageFileID) {
|
||
const image = source.images.get(obj.ImageFileID);
|
||
if (!image) {
|
||
throw new Error(
|
||
`对象 ${obj.ObjectID}(${obj.ObjectName})引用了不存在的图 ImageFileID=${obj.ImageFileID}`,
|
||
);
|
||
}
|
||
const { block, outOfRange } = spriteBlock(
|
||
obj, image, imageBucketMap.get(obj.ImageFileID) ?? 'unassigned', warnings,
|
||
);
|
||
node.sprite = block;
|
||
node.flags.frameIndexOutOfRange = outOfRange;
|
||
}
|
||
return node;
|
||
}
|
||
|
||
export function convertLayer(source, layer, imageBucketMap) {
|
||
const objs = layer.objectIds
|
||
.map((id) => source.objects.get(id))
|
||
.filter(Boolean);
|
||
|
||
const first = objs[0];
|
||
const backgroundNodeId =
|
||
first && isFullScreen(first.Width, first.Height) ? first.ObjectID : null;
|
||
|
||
const warnings = [];
|
||
const nodes = objs
|
||
.map((o) => toNode(o, source, imageBucketMap, warnings))
|
||
.sort((a, b) => a.siblingIndex - b.siblingIndex);
|
||
|
||
const nodeIdSet = new Set(nodes.map((n) => n.id));
|
||
const groups = source.groups
|
||
.map((g) => ({ groupId: g.groupId, nodeIds: g.objectIds.filter((id) => nodeIdSet.has(id)) }))
|
||
.filter((g) => g.nodeIds.length > 0);
|
||
|
||
return {
|
||
layerId: layer.layerId,
|
||
name: layer.name,
|
||
bucket: layerBucket(layer.layerId, layer.name),
|
||
backgroundNodeId,
|
||
groups,
|
||
nodes,
|
||
warnings,
|
||
};
|
||
}
|
||
|
||
export function convertAll(source) {
|
||
const imageBucketMap = imageBuckets(source);
|
||
const layers = source.layers.map((l) => convertLayer(source, l, imageBucketMap));
|
||
|
||
const objectIndex = {};
|
||
const warnings = [];
|
||
for (const doc of layers) {
|
||
for (const n of doc.nodes) objectIndex[String(n.id)] = { layerId: doc.layerId, nodeId: n.id };
|
||
warnings.push(...doc.warnings);
|
||
}
|
||
|
||
const manifest = {
|
||
generatedBy: 'scripts/convert-ui.mjs',
|
||
// 内容区基准(旧数据事实),坐标/三区据此判定,不随设计分辨率改变。
|
||
designWidth: DESIGN_WIDTH,
|
||
designHeight: DESIGN_HEIGHT,
|
||
// 设计分辨率画布(20:9),落地 Layer/Group 根节点尺寸据此设定。
|
||
canvasWidth: CANVAS_WIDTH,
|
||
canvasHeight: CANVAS_HEIGHT,
|
||
layers: layers.map((d) => ({
|
||
layerId: d.layerId,
|
||
name: d.name,
|
||
bucket: d.bucket,
|
||
file: `${String(d.layerId).padStart(3, '0')}-${d.name}.json`,
|
||
nodeCount: d.nodes.length,
|
||
})),
|
||
objectIndex,
|
||
};
|
||
return { layers, manifest, warnings };
|
||
}
|