diff --git a/cocoscreator_projects/package-lock.json b/cocoscreator_projects/package-lock.json index e93d431..3f999ba 100644 --- a/cocoscreator_projects/package-lock.json +++ b/cocoscreator_projects/package-lock.json @@ -10,6 +10,7 @@ "devDependencies": { "@types/node": "^26.0.1", "@types/ws": "^8.18.1", + "pngjs": "^7.0.0", "tsx": "^4.22.4", "typescript": "^6.0.3", "ws": "^8.21.0" @@ -504,6 +505,15 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/pngjs": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/pngjs/-/pngjs-7.0.0.tgz", + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", + "dev": true, + "engines": { + "node": ">=14.19.0" + } + }, "node_modules/tsx": { "version": "4.22.4", "resolved": "https://registry.npmmirror.com/tsx/-/tsx-4.22.4.tgz", diff --git a/cocoscreator_projects/package.json b/cocoscreator_projects/package.json index d1bdab9..4547113 100644 --- a/cocoscreator_projects/package.json +++ b/cocoscreator_projects/package.json @@ -11,12 +11,15 @@ "bump-cocos": "node scripts/bump-cocos.mjs", "check-skin": "node scripts/check-skin.mjs", "build-game": "node scripts/build-game.mjs", + "convert-ui": "node scripts/convert-ui.mjs", + "verify-frames": "node scripts/verify-frames.mjs", "test:framework": "node scripts/run-framework-tests.mjs", "typecheck:framework": "tsc -p tsconfig.framework.json --noEmit" }, "devDependencies": { "@types/node": "^26.0.1", "@types/ws": "^8.18.1", + "pngjs": "^7.0.0", "tsx": "^4.22.4", "typescript": "^6.0.3", "ws": "^8.21.0" diff --git a/cocoscreator_projects/scripts/test/ui-source.test.mjs b/cocoscreator_projects/scripts/test/ui-source.test.mjs new file mode 100644 index 0000000..6e1609c --- /dev/null +++ b/cocoscreator_projects/scripts/test/ui-source.test.mjs @@ -0,0 +1,78 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { join } from 'node:path'; +import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { ROOT } from '../lib/paths.mjs'; +import { loadSource, SourceError } from '../ui-migration/gameabc-source.mjs'; + +const REAL_SOURCE = join(ROOT, '..', 'projects', 'Game_Surface_3', 'output'); + +function fakeSource(overrides = {}) { + const dir = mkdtempSync(join(tmpdir(), 'gameabc-src-')); + const files = { + 'gameabc_Object.json': { + ObjectList: [ + {}, + { Property: { ObjectID: 1, ObjectType: 2, ObjectName: '背景', Left: 0, Top: 0, Width: 1280, Height: 720, BelongLayerID: 1, IndexOfLayer: 1, ImageFileID: 1, FrameStyle: 0, FrameIndex: 0, L9: 0, T9: 0, R9: 0, B9: 0, GroupID: 1, VoiceFileID: 0, TimerInterval: 0 }, Event: { mousedown: 1, mouseup: 1 } }, + ], + }, + 'gameabc_Layer.json': { LayerList: [{}, { Property: { LayerID: 1, LayerName: 'Logo_Layer' }, ObjectList: [1] }] }, + 'gameabc_GroupList.json': { GroupList: [{ GroupID: 1, ObjectList: [1] }] }, + 'gameabc_Image.json': { ImageFileList: [{}, { id: 1, w_all: 1280, h_all: 720, w: 1, h: 1, frame_all: 1, bmp: '00001.png', w1: 1280, h1: 720 }] }, + ...overrides, + }; + for (const [name, content] of Object.entries(files)) { + writeFileSync(join(dir, name), JSON.stringify(content)); + } + return dir; +} + +test('loadSource 解析对象、层、组、图,并附上 events', () => { + const dir = fakeSource(); + try { + const s = loadSource(dir); + assert.equal(s.objects.size, 1); + assert.equal(s.objects.get(1).ObjectName, '背景'); + assert.deepEqual(s.objects.get(1).events, ['mousedown', 'mouseup']); + assert.deepEqual(s.layers, [{ layerId: 1, name: 'Logo_Layer', objectIds: [1] }]); + assert.deepEqual(s.groups, [{ groupId: 1, objectIds: [1] }]); + assert.equal(s.images.get(1).bmp, '00001.png'); + } finally { rmSync(dir, { recursive: true, force: true }); } +}); + +test('loadSource 跳过源数据里的空占位槽 {}', () => { + const dir = fakeSource(); + try { + const s = loadSource(dir); + assert.equal(s.objects.size, 1, '首个 {} 占位不应变成对象'); + assert.equal(s.layers.length, 1); + } finally { rmSync(dir, { recursive: true, force: true }); } +}); + +test('loadSource 对网格与帧数不符的图显式抛错(不兜底)', () => { + const dir = fakeSource({ + 'gameabc_Image.json': { ImageFileList: [{}, { id: 1, w_all: 100, h_all: 100, w: 3, h: 4, frame_all: 11, bmp: 'bad.png', w1: 33, h1: 25 }] }, + }); + try { + assert.throws(() => loadSource(dir), SourceError); + assert.throws(() => loadSource(dir), /bad\.png/); + } finally { rmSync(dir, { recursive: true, force: true }); } +}); + +test('loadSource 对缺失文件显式抛错', () => { + const dir = mkdtempSync(join(tmpdir(), 'gameabc-empty-')); + try { + assert.throws(() => loadSource(dir), /缺少源数据文件/); + } finally { rmSync(dir, { recursive: true, force: true }); } +}); + +test('loadSource 在真实源数据上得到已知规模(回归护栏)', () => { + const s = loadSource(REAL_SOURCE); + assert.equal(s.objects.size, 991, '对象总数'); + assert.equal(s.layers.length, 55, '界面数'); + assert.equal(s.groups.length, 80, '分组数'); + assert.equal(s.images.size, 439, '图片数'); + const multi = [...s.images.values()].filter((i) => i.frame_all > 1); + assert.equal(multi.length, 139, '多帧图数'); +}); diff --git a/cocoscreator_projects/scripts/ui-migration/gameabc-source.mjs b/cocoscreator_projects/scripts/ui-migration/gameabc-source.mjs new file mode 100644 index 0000000..b64d967 --- /dev/null +++ b/cocoscreator_projects/scripts/ui-migration/gameabc-source.mjs @@ -0,0 +1,55 @@ +import { readFileSync, existsSync } from 'node:fs'; +import { join } from 'node:path'; + +/** 源数据非法时抛出。第二准则:缺失/非法一律显式暴露,不兜底。 */ +export class SourceError extends Error { + constructor(message) { super(message); this.name = 'SourceError'; } +} + +function readJson(dir, name) { + const p = join(dir, name); + if (!existsSync(p)) throw new SourceError(`缺少源数据文件: ${p}`); + try { + return JSON.parse(readFileSync(p, 'utf8')); + } catch (e) { + throw new SourceError(`源数据 JSON 解析失败: ${p} — ${e.message}`); + } +} + +/** + * 读取并校验 gameabc 导出数据。 + * 源数组用 `{}` 占位空槽(下标即 ID),须跳过。 + */ +export function loadSource(outputDir) { + const objects = new Map(); + for (const entry of readJson(outputDir, 'gameabc_Object.json').ObjectList ?? []) { + if (!entry || !entry.Property) continue; + const p = entry.Property; + objects.set(p.ObjectID, { ...p, events: Object.keys(entry.Event ?? {}) }); + } + + const images = new Map(); + for (const i of readJson(outputDir, 'gameabc_Image.json').ImageFileList ?? []) { + if (!i || i.id === undefined) continue; + if (i.w * i.h !== i.frame_all) { + throw new SourceError( + `图 ${i.bmp} 的网格与帧数不符: ${i.w}x${i.h}=${i.w * i.h} != frame_all=${i.frame_all}`, + ); + } + images.set(i.id, i); + } + + const layers = (readJson(outputDir, 'gameabc_Layer.json').LayerList ?? []) + .filter((l) => l && l.Property) + .map((l) => ({ + layerId: l.Property.LayerID, + name: l.Property.LayerName, + objectIds: l.ObjectList ?? [], + })); + + const groups = (readJson(outputDir, 'gameabc_GroupList.json').GroupList ?? []) + .filter((g) => g && g.GroupID !== undefined) + .map((g) => ({ groupId: g.GroupID, objectIds: g.ObjectList ?? [] })); + + return { objects, images, layers, groups }; +}