'use strict'; const fs = require('fs'); const path = require('path'); const { tryEditorRequests } = require('./cocos-project'); const { openAsset } = require('../assets'); const TEMPLATE_PATH = path.join(__dirname, '..', '..', 'resources', 'template.scene'); function buildSceneContent(sceneName, templatePath) { const tplPath = templatePath || TEMPLATE_PATH; let content; try { content = fs.readFileSync(tplPath, 'utf8'); } catch (err) { throw new Error(`Failed to read scene template at ${tplPath}: ${err.message}`); } const json = JSON.parse(content); if (json[0] && json[0]._name !== undefined) { json[0]._name = sceneName; } if (json[1] && json[1]._name !== undefined) { json[1]._name = sceneName; } return JSON.stringify(json); } function createSceneManagementTools({ createSchema, sceneBridge }) { return [ { name: 'create_scene', profile: 'full', description: '[core] Create a new scene asset in the Cocos project and optionally open it.', inputSchema: createSchema( { sceneName: { type: 'string', description: 'Name of the new scene (without extension).' }, savePath: { type: 'string', description: 'Asset-db URL, e.g. db://assets/scenes/NewScene.scene.' }, open: { type: 'boolean', description: 'Whether to open the scene after creation. Default true.' }, overwrite: { type: 'boolean', description: 'Overwrite if the asset already exists. Default false.' }, }, ['sceneName', 'savePath'] ), handler: async (args) => { const sceneName = String(args.sceneName || '').trim(); const savePath = String(args.savePath || '').trim(); if (!sceneName) { throw new Error('sceneName is required.'); } if (!savePath) { throw new Error('savePath is required.'); } if (!savePath.endsWith('.scene')) { throw new Error('savePath must end with .scene'); } const content = buildSceneContent(sceneName); const createArgs = [savePath, content]; if (args.overwrite === true) { createArgs.push({ overwrite: true }); } const result = await tryEditorRequests([ { channel: 'asset-db', method: 'create-asset', args: createArgs }, ]); const url = (result.result && (result.result.url || result.result.source)) || savePath; const uuid = result.result && result.result.uuid; if (args.open !== false) { try { await openAsset(url); } catch (_) { // Opening is best-effort; creation succeeded. } } return { created: true, sceneName, url, uuid, opened: args.open !== false, }; }, }, { name: 'query_scene_state', profile: 'core', description: '[specialist] Query scene state: dirty (unsaved changes), ready, or soft-reload.', inputSchema: createSchema( { action: { type: 'string', enum: ['is_dirty', 'is_ready', 'soft_reload'], description: 'State query or action.', }, }, ['action'] ), handler: async (args) => { const action = String(args.action || '').trim(); const IPC_MAP = { is_dirty: [ { channel: 'scene', method: 'query-dirty' }, ], is_ready: [ { channel: 'scene', method: 'query-is-ready' }, ], soft_reload: [ { channel: 'scene', method: 'soft-reload' }, ], }; const candidates = IPC_MAP[action]; if (!candidates) { throw new Error(`Unknown action: ${action}. Must be one of: is_dirty, is_ready, soft_reload.`); } const result = await tryEditorRequests(candidates); return { action, result: result.result, }; }, }, { name: 'copy_paste_node', profile: 'full', description: '[core] Copy, cut, or paste scene nodes via the Cocos editor clipboard.', inputSchema: createSchema( { action: { type: 'string', enum: ['copy', 'cut', 'paste', 'duplicate'], description: 'Clipboard or duplication action.', }, uuids: { type: 'array', items: { type: 'string' }, description: 'Node UUIDs to copy, cut, or duplicate.', }, target: { type: 'string', description: 'Target parent node UUID for paste.' }, keepWorldTransform: { type: 'boolean', description: 'Keep world transform on paste. Default false.', }, }, ['action'] ), handler: async (args) => { const action = String(args.action || '').trim(); if (action === 'copy' || action === 'cut') { const uuids = Array.isArray(args.uuids) ? args.uuids.filter(Boolean) : []; if (uuids.length === 0) { throw new Error(`uuids is required for action: ${action}.`); } const method = action === 'copy' ? 'copy-node' : 'cut-node'; const result = await tryEditorRequests([ { channel: 'scene', method, args: [uuids] }, ]); return { action, uuids, result: result.result }; } if (action === 'duplicate') { const uuids = Array.isArray(args.uuids) ? args.uuids.filter(Boolean) : []; if (uuids.length === 0) { throw new Error('uuids is required for action: duplicate.'); } const result = await tryEditorRequests([ { channel: 'scene', method: 'duplicate-node', args: [uuids] }, ]); return { action, uuids, result: result.result }; } if (action === 'paste') { const target = String(args.target || '').trim(); if (!target) { throw new Error('target is required for action: paste.'); } const keepWorldTransform = args.keepWorldTransform === true; const pasteOpts = { target, keepWorldTransform }; const uuids = Array.isArray(args.uuids) ? args.uuids.filter(Boolean) : []; if (uuids.length > 0) { pasteOpts.uuids = uuids; } const result = await tryEditorRequests([ { channel: 'scene', method: 'paste-node', args: [pasteOpts], }, ]); return { action, target, keepWorldTransform, result: result.result }; } throw new Error(`Unknown action: ${action}. Must be one of: copy, cut, paste, duplicate.`); }, }, { name: 'rename_node', profile: 'full', description: '[core] Rename a scene node.', inputSchema: createSchema( { path: { type: 'string', description: 'Node hierarchy path.' }, uuid: { type: 'string', description: 'Node UUID.' }, name: { type: 'string', description: 'Fallback exact node name.' }, newName: { type: 'string', description: 'New name for the node.' }, }, ['newName'] ), handler: async (args) => sceneBridge.call('renameNode', args), }, { name: 'reparent_node', profile: 'full', description: '[core] Move a node to a new parent in the scene hierarchy.', inputSchema: createSchema( { path: { type: 'string', description: 'Node hierarchy path to move.' }, uuid: { type: 'string', description: 'Node UUID to move.' }, name: { type: 'string', description: 'Fallback exact node name.' }, targetPath: { type: 'string', description: 'New parent node path.' }, targetUuid: { type: 'string', description: 'New parent node UUID.' }, targetName: { type: 'string', description: 'Fallback exact new parent name.' }, siblingIndex: { type: 'number', description: 'Optional insert position among siblings. Default: append.', }, keepWorldTransform: { type: 'boolean', description: 'Keep world transform after reparenting. Default false.', }, }, [] ), handler: async (args) => sceneBridge.call('reparentNode', args), }, ]; } module.exports = { buildSceneContent, createSceneManagementTools, };