'use strict'; module.paths.push(Editor.App.path + '/node_modules'); const cc = require('cc'); const { Node, director, Vec3, Quat, Color, assetManager, instantiate, Prefab, SceneAsset, js, Component, EventHandler, Canvas, UITransform, Label, Sprite, Button, Widget, Camera, Animation, AnimationClip, } = cc; const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor; function getScene() { const scene = director.getScene(); if (!scene) { throw new Error('No active scene is loaded.'); } return scene; } function getComponentNames(node) { const components = Array.isArray(node.components) ? node.components : []; return components .map((component) => component && component.constructor && component.constructor.name) .filter(Boolean); } function getSerializableKeys(target) { const keys = new Set(); let current = target; let depth = 0; while (current && current !== Object.prototype && depth < 4) { for (const key of Object.keys(current)) { if (!key.startsWith('_')) { keys.add(key); } } current = Object.getPrototypeOf(current); depth += 1; } return Array.from(keys); } function getNodePath(node) { const names = []; let current = node; const scene = getScene(); while (current && current !== scene) { names.unshift(current.name); current = current.parent; } return names.join('/'); } function vectorToObject(value) { if (!value) { return null; } return { x: value.x, y: value.y, z: value.z }; } function quatToObject(value) { if (!value) { return null; } return { x: value.x, y: value.y, z: value.z, w: value.w }; } function colorToObject(value) { if (!value) { return null; } return { r: value.r, g: value.g, b: value.b, a: value.a }; } function summarizeNode(node, depth, maxDepth, includeComponents, includeInactive) { if (!includeInactive && !node.active) { return null; } const summary = { name: node.name, path: getNodePath(node), uuid: node.uuid, active: Boolean(node.active), layer: node.layer, position: vectorToObject(node.position), rotation: quatToObject(node.rotation), scale: vectorToObject(node.scale), }; if (includeComponents) { summary.components = getComponentNames(node); } if (depth < maxDepth) { const children = []; for (const child of node.children) { const childSummary = summarizeNode(child, depth + 1, maxDepth, includeComponents, includeInactive); if (childSummary) { children.push(childSummary); } } summary.children = children; } else { summary.childCount = node.children.length; } return summary; } function walkNodes(visitor, node) { visitor(node); for (const child of node.children) { walkNodes(visitor, child); } } function findNodeByPath(nodePath) { if (!nodePath) { return null; } const segments = String(nodePath) .split('/') .map((segment) => segment.trim()) .filter(Boolean); let current = getScene(); if (segments[0] === current.name) { segments.shift(); } for (const segment of segments) { current = current.children.find((child) => child.name === segment); if (!current) { return null; } } return current; } function findNodeByUuid(uuid) { if (!uuid) { return null; } let match = null; walkNodes((node) => { if (!match && node.uuid === uuid) { match = node; } }, getScene()); return match; } function findNodeByName(name) { if (!name) { return null; } let match = null; walkNodes((node) => { if (!match && node.name === name) { match = node; } }, getScene()); return match; } function findNode(input) { return findNodeByUuid(input.uuid) || findNodeByPath(input.path) || findNodeByName(input.name); } function resolveComponentClass(componentName) { if (!componentName) { return null; } if (typeof componentName !== 'string') { return componentName; } const direct = js && typeof js.getClassByName === 'function' ? js.getClassByName(componentName) : null; if (direct) { return direct; } const candidates = [componentName, `cc.${componentName}`]; for (const candidate of candidates) { const found = js && typeof js.getClassByName === 'function' ? js.getClassByName(candidate) : null; if (found) { return found; } } return null; } function findComponent(node, options = {}) { if (!node) { return null; } if (Number.isInteger(options.index)) { return node.components[options.index] || null; } if (options.componentName) { const exact = node.components.find((component) => component && component.constructor && component.constructor.name === options.componentName); if (exact) { return exact; } const componentClass = resolveComponentClass(options.componentName); if (componentClass) { return node.getComponent(componentClass); } } return null; } function getEventHandlerClass() { return (Component && Component.EventHandler) || EventHandler || null; } function serializeEventHandler(handler) { if (!handler) { return null; } return { target: handler.target && handler.target.name ? getNodePath(handler.target) : '', targetUuid: handler.target && handler.target.uuid ? handler.target.uuid : '', component: handler.component || '', handler: handler.handler || '', customEventData: handler.customEventData || '', }; } function getOrAddComponent(node, componentClass) { return node.getComponent(componentClass) || node.addComponent(componentClass); } function configureNodeBasics(node, options = {}) { if (options.position) { node.setPosition(options.position.x || 0, options.position.y || 0, options.position.z || 0); } if (options.scale) { node.setScale(options.scale.x || 1, options.scale.y || 1, options.scale.z || 1); } if (options.eulerAngles) { node.setRotationFromEuler( options.eulerAngles.x || 0, options.eulerAngles.y || 0, options.eulerAngles.z || 0 ); } if (typeof options.active === 'boolean') { node.active = options.active; } } function configureUITransform(node, options = {}) { const transform = getOrAddComponent(node, UITransform); const width = Number.isFinite(options.width) ? options.width : 160; const height = Number.isFinite(options.height) ? options.height : 60; transform.setContentSize(width, height); if (options.anchor) { transform.setAnchorPoint(options.anchor.x ?? 0.5, options.anchor.y ?? 0.5); } return transform; } function parseColor(value, fallback = Color.WHITE) { if (!value) { return fallback.clone ? fallback.clone() : fallback; } if (typeof value === 'string') { const normalized = value.startsWith('#') ? value.slice(1) : value; const number = Number.parseInt(normalized.length === 6 ? `${normalized}ff` : normalized, 16); if (Number.isFinite(number)) { return new Color( (number >> 24) & 255, (number >> 16) & 255, (number >> 8) & 255, number & 255 ); } } return new Color(value.r ?? 255, value.g ?? 255, value.b ?? 255, value.a ?? 255); } function findComponentsByClass(componentClass) { const results = []; walkNodes((node) => { if (node === getScene()) { return; } const component = node.getComponent(componentClass); if (component) { results.push(component); } }, getScene()); return results; } function getPrefabInfo(node) { const prefab = node && node._prefab; if (!prefab) { return { linked: false, }; } const asset = prefab.asset || prefab._asset || null; return { linked: Boolean(asset || prefab.fileId || prefab.root), fileId: prefab.fileId || '', asset: asset ? { name: asset.name || '', uuid: asset.uuid || asset._uuid || '', } : null, instance: prefab.instance ? plain(prefab.instance) : null, sync: prefab.sync, rawKeys: Object.keys(prefab).slice(0, 50), }; } function collectSceneStats() { const stats = { nodeCount: 0, activeNodeCount: 0, inactiveNodeCount: 0, maxDepth: 0, componentCount: 0, componentsByType: {}, prefabInstanceCount: 0, uiTransformCount: 0, canvasCount: 0, cameraCount: 0, labelCount: 0, spriteCount: 0, buttonCount: 0, }; function visit(node, depth) { if (node !== getScene()) { stats.nodeCount += 1; stats.maxDepth = Math.max(stats.maxDepth, depth); if (node.active) stats.activeNodeCount += 1; else stats.inactiveNodeCount += 1; if (node._prefab) stats.prefabInstanceCount += 1; } for (const component of node.components || []) { const name = component && component.constructor ? component.constructor.name : 'UnknownComponent'; stats.componentCount += 1; stats.componentsByType[name] = (stats.componentsByType[name] || 0) + 1; if (component instanceof UITransform) stats.uiTransformCount += 1; if (component instanceof Canvas) stats.canvasCount += 1; if (component instanceof Camera) stats.cameraCount += 1; if (component instanceof Label) stats.labelCount += 1; if (component instanceof Sprite) stats.spriteCount += 1; if (component instanceof Button) stats.buttonCount += 1; } for (const child of node.children) { visit(child, depth + 1); } } visit(getScene(), 0); return stats; } function buildSceneWarnings(stats) { const warnings = []; if (stats.nodeCount === 0) { warnings.push({ severity: 'warn', code: 'empty_scene', message: 'The active scene has no child nodes.' }); } if (stats.cameraCount === 0) { warnings.push({ severity: 'warn', code: 'missing_camera', message: 'No Camera component was found in the active scene.' }); } if (stats.nodeCount > 500) { warnings.push({ severity: 'info', code: 'large_node_count', message: `Scene has ${stats.nodeCount} nodes.` }); } if (stats.maxDepth > 12) { warnings.push({ severity: 'info', code: 'deep_hierarchy', message: `Scene hierarchy depth is ${stats.maxDepth}.` }); } if (stats.labelCount > 80) { warnings.push({ severity: 'info', code: 'many_labels', message: `Scene has ${stats.labelCount} Label components.` }); } return warnings; } function getScheduler() { return typeof director.getScheduler === 'function' ? director.getScheduler() : null; } function loadAnimationClipByUuid(uuid) { return new Promise((resolve, reject) => { assetManager.loadAny(uuid, (error, asset) => { if (error) { reject(error); return; } if (!(asset instanceof AnimationClip)) { reject(new Error(`Asset '${uuid}' is not an AnimationClip.`)); return; } resolve(asset); }); }); } function getValueByPath(target, propertyPath) { const segments = String(propertyPath || '') .split('.') .map((segment) => segment.trim()) .filter(Boolean); let current = target; for (const segment of segments) { if (current == null) { return undefined; } current = current[segment]; } return current; } function setValueByPath(target, propertyPath, value) { const segments = String(propertyPath || '') .split('.') .map((segment) => segment.trim()) .filter(Boolean); if (!segments.length) { throw new Error('propertyPath is required.'); } let current = target; for (let index = 0; index < segments.length - 1; index += 1) { const segment = segments[index]; if (current[segment] == null || typeof current[segment] !== 'object') { current[segment] = {}; } current = current[segment]; } current[segments[segments.length - 1]] = value; } function resetValueByPath(target, propertyPath) { const segments = String(propertyPath || '') .split('.') .map((segment) => segment.trim()) .filter(Boolean); if (!segments.length) { throw new Error('propertyPath is required.'); } let current = target; for (let index = 0; index < segments.length - 1; index += 1) { current = current[segments[index]]; if (current == null) { return; } } const key = segments[segments.length - 1]; if (current && Object.prototype.hasOwnProperty.call(current, key)) { delete current[key]; } else if (current) { current[key] = undefined; } } function loadAssetByUuid(uuid) { return new Promise((resolve, reject) => { assetManager.loadAny(uuid, (error, asset) => { if (error) { reject(error); return; } resolve(asset); }); }); } function plain(value, depth = 0, seen = new WeakSet()) { if (value == null) { return value; } if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { return value; } if (value instanceof Node) { return { name: value.name, path: getNodePath(value), uuid: value.uuid, active: Boolean(value.active), components: getComponentNames(value), }; } if (value instanceof Vec3) { return vectorToObject(value); } if (value instanceof Quat) { return quatToObject(value); } if (value instanceof Color) { return colorToObject(value); } if (Array.isArray(value)) { if (depth >= 5) { return `[Array(${value.length})]`; } return value.map((item) => plain(item, depth + 1, seen)); } if (typeof value === 'object') { if (seen.has(value)) { return '[Circular]'; } seen.add(value); if (depth >= 5) { return `[${value.constructor && value.constructor.name ? value.constructor.name : 'Object'}]`; } const output = {}; for (const key of Object.keys(value)) { try { output[key] = plain(value[key], depth + 1, seen); } catch (error) { output[key] = `[Unserializable: ${error.message}]`; } } return output; } return String(value); } async function executeUserCode(code, args) { const scene = getScene(); const runner = new AsyncFunction('require', 'cc', 'Editor', 'scene', 'director', 'args', ` const module = { exports: {} }; const exports = module.exports; ${code} if (typeof run === 'function') { return await run({ cc, Editor, scene, director, args }); } if (typeof module.exports === 'function') { return await module.exports({ cc, Editor, scene, director, args }); } if (module.exports && typeof module.exports.run === 'function') { return await module.exports.run({ cc, Editor, scene, director, args }); } `); return await runner(require, cc, global.Editor, scene, director, args || {}); } exports.methods = { async getSceneInfo(options = {}) { const maxDepth = Number.isFinite(options.maxDepth) ? options.maxDepth : 2; const includeComponents = options.includeComponents !== false; const scene = getScene(); return { sceneName: scene.name, uuid: scene.uuid, childCount: scene.children.length, nodes: scene.children .map((child) => summarizeNode(child, 1, Math.max(1, maxDepth), includeComponents, true)) .filter(Boolean), }; }, async getHierarchy(options = {}) { const root = options.rootPath ? findNodeByPath(options.rootPath) : getScene(); if (!root) { throw new Error(`Node not found: ${options.rootPath}`); } const maxDepth = Number.isFinite(options.maxDepth) ? options.maxDepth : 3; const includeComponents = options.includeComponents !== false; const includeInactive = options.includeInactive !== false; if (root === getScene()) { return { sceneName: root.name, nodes: root.children .map((child) => summarizeNode(child, 1, Math.max(1, maxDepth), includeComponents, includeInactive)) .filter(Boolean), }; } return summarizeNode(root, 0, Math.max(1, maxDepth), includeComponents, includeInactive); }, async inspectNode(options = {}) { const node = findNode(options); if (!node) { throw new Error('Target node was not found.'); } return { name: node.name, path: getNodePath(node), uuid: node.uuid, active: Boolean(node.active), layer: node.layer, siblingIndex: node.getSiblingIndex(), position: vectorToObject(node.position), rotation: quatToObject(node.rotation), scale: vectorToObject(node.scale), children: node.children.map((child) => ({ name: child.name, path: getNodePath(child), uuid: child.uuid, })), components: getComponentNames(node), }; }, async findNodes(options = {}) { const name = options.name ? String(options.name) : ''; const pathContains = options.pathContains ? String(options.pathContains) : ''; const component = options.component ? String(options.component) : ''; const includeInactive = options.includeInactive !== false; const results = []; walkNodes((node) => { if (node === getScene()) { return; } if (!includeInactive && !node.active) { return; } const nodePath = getNodePath(node); const components = getComponentNames(node); if (name && node.name !== name) { return; } if (pathContains && !nodePath.includes(pathContains)) { return; } if (component && !components.includes(component)) { return; } results.push({ name: node.name, path: nodePath, uuid: node.uuid, active: Boolean(node.active), components, }); }, getScene()); return { count: results.length, nodes: results.slice(0, 200), }; }, async createNode(options = {}) { const name = String(options.name || '').trim(); if (!name) { throw new Error('name is required.'); } const parent = options.parentPath ? findNodeByPath(options.parentPath) : getScene(); if (!parent) { throw new Error(`Parent not found: ${options.parentPath}`); } const node = new Node(name); node.parent = parent; if (options.position) { node.setPosition(options.position.x || 0, options.position.y || 0, options.position.z || 0); } if (options.scale) { node.setScale(options.scale.x || 1, options.scale.y || 1, options.scale.z || 1); } if (options.eulerAngles) { node.setRotationFromEuler( options.eulerAngles.x || 0, options.eulerAngles.y || 0, options.eulerAngles.z || 0 ); } if (typeof options.active === 'boolean') { node.active = options.active; } return { created: true, name: node.name, path: getNodePath(node), uuid: node.uuid, }; }, async deleteNode(options = {}) { const node = findNode(options); if (!node) { throw new Error('Target node was not found.'); } const targetPath = getNodePath(node); node.removeFromParent(); node.destroy(); return { deleted: true, path: targetPath, uuid: node.uuid, }; }, async renameNode(options = {}) { const node = findNode(options); if (!node) { throw new Error('Target node was not found.'); } const oldName = node.name; const oldPath = getNodePath(node); const newName = String(options.newName || '').trim(); if (!newName) { throw new Error('newName is required.'); } node.name = newName; return { renamed: true, oldName, newName, oldPath, newPath: getNodePath(node), uuid: node.uuid, }; }, async reparentNode(options = {}) { const node = findNode(options); if (!node) { throw new Error('Target node was not found.'); } const oldPath = getNodePath(node); const oldParent = node.parent; const keepWorldTransform = options.keepWorldTransform === true; const oldWorldPos = keepWorldTransform ? node.worldPosition.clone() : null; const oldWorldRot = keepWorldTransform ? node.worldRotation.clone() : null; const oldWorldScale = keepWorldTransform ? node.worldScale.clone() : null; let newParent = null; if (options.targetUuid) { newParent = findNodeByUuid(options.targetUuid); } else if (options.targetPath) { newParent = findNodeByPath(options.targetPath); } else if (options.targetName) { newParent = findNodeByName(options.targetName); } if (!newParent) { throw new Error('Target parent node was not found.'); } const siblingIndex = Number.isFinite(options.siblingIndex) ? options.siblingIndex : -1; if (siblingIndex >= 0 && siblingIndex <= newParent.children.length) { newParent.insertChild(node, siblingIndex); } else { node.parent = newParent; } if (keepWorldTransform) { node.worldPosition = oldWorldPos; node.worldRotation = oldWorldRot; node.worldScale = oldWorldScale; } return { reparented: true, uuid: node.uuid, oldPath, newPath: getNodePath(node), oldParent: oldParent ? oldParent.name : null, newParent: newParent.name, siblingIndex: node.getSiblingIndex(), keepWorldTransform, }; }, async setNodeTransform(options = {}) { const node = findNode(options); if (!node) { throw new Error('Target node was not found.'); } if (options.position) { node.setPosition(options.position.x || 0, options.position.y || 0, options.position.z || 0); } if (options.scale) { node.setScale(options.scale.x || 1, options.scale.y || 1, options.scale.z || 1); } if (options.eulerAngles) { node.setRotationFromEuler( options.eulerAngles.x || 0, options.eulerAngles.y || 0, options.eulerAngles.z || 0 ); } if (typeof options.active === 'boolean') { node.active = options.active; } return { updated: true, name: node.name, path: getNodePath(node), active: Boolean(node.active), position: vectorToObject(node.position), rotation: quatToObject(node.rotation), scale: vectorToObject(node.scale), }; }, async listComponents(options = {}) { const node = findNode(options); if (!node) { throw new Error('Target node was not found.'); } return { node: { name: node.name, path: getNodePath(node), uuid: node.uuid, }, components: node.components.map((component, index) => ({ index, name: component && component.constructor ? component.constructor.name : 'UnknownComponent', enabled: typeof component.enabled === 'boolean' ? component.enabled : undefined, keys: getSerializableKeys(component).slice(0, 50), })), }; }, async addComponent(options = {}) { const node = findNode(options); if (!node) { throw new Error('Target node was not found.'); } const componentClass = resolveComponentClass(options.componentName); if (!componentClass) { throw new Error(`Component class not found: ${options.componentName}`); } const component = node.addComponent(componentClass); return { added: true, node: getNodePath(node), component: component.constructor ? component.constructor.name : options.componentName, index: node.components.indexOf(component), }; }, async removeComponent(options = {}) { const node = findNode(options); if (!node) { throw new Error('Target node was not found.'); } const component = findComponent(node, options); if (!component) { throw new Error(`Target component was not found${options.componentName ? ': ' + options.componentName : ''}. Ensure the script is compiled and the component is attached to the node.`); } const componentName = component.constructor ? component.constructor.name : 'UnknownComponent'; node.removeComponent(component); return { removed: true, node: getNodePath(node), component: componentName, }; }, async inspectComponent(options = {}) { const node = findNode(options); if (!node) { throw new Error('Target node was not found.'); } const component = findComponent(node, options); if (!component) { throw new Error(`Target component was not found${options.componentName ? ': ' + options.componentName : ''}. Ensure the script is compiled and the component is attached to the node.`); } return { node: { name: node.name, path: getNodePath(node), uuid: node.uuid, }, component: { name: component.constructor ? component.constructor.name : 'UnknownComponent', enabled: typeof component.enabled === 'boolean' ? component.enabled : undefined, data: plain(component), }, }; }, async setComponentProperty(options = {}) { const node = findNode(options); if (!node) { throw new Error('Target node was not found.'); } const component = findComponent(node, options); if (!component) { throw new Error(`Target component was not found${options.componentName ? ': ' + options.componentName : ''}. Ensure the script is compiled and the component is attached to the node.`); } setValueByPath(component, options.propertyPath, options.value); return { updated: true, node: getNodePath(node), component: component.constructor ? component.constructor.name : 'UnknownComponent', propertyPath: options.propertyPath, value: plain(getValueByPath(component, options.propertyPath)), }; }, async resetComponentProperty(options = {}) { const node = findNode(options); if (!node) { throw new Error('Target node was not found.'); } const component = findComponent(node, options); if (!component) { throw new Error(`Target component was not found${options.componentName ? ': ' + options.componentName : ''}. Ensure the script is compiled and the component is attached to the node.`); } resetValueByPath(component, options.propertyPath); return { reset: true, node: getNodePath(node), component: component.constructor ? component.constructor.name : 'UnknownComponent', propertyPath: options.propertyPath, value: plain(getValueByPath(component, options.propertyPath)), }; }, async instantiatePrefab(options = {}) { const prefabUuid = String(options.prefabUuid || '').trim(); if (!prefabUuid) { throw new Error('prefabUuid is required.'); } const parent = options.parentPath ? findNodeByPath(options.parentPath) : getScene(); if (!parent) { throw new Error(`Parent not found: ${options.parentPath}`); } const asset = await loadAssetByUuid(prefabUuid); if (!(asset instanceof Prefab)) { throw new Error(`Asset '${prefabUuid}' is not a Prefab.`); } const node = instantiate(asset); node.parent = parent; if (options.name) { node.name = options.name; } if (options.position) { node.setPosition(options.position.x || 0, options.position.y || 0, options.position.z || 0); } return { instantiated: true, prefabUuid, node: { name: node.name, path: getNodePath(node), uuid: node.uuid, }, }; }, async runSceneAsset(options = {}) { const sceneUuid = String(options.sceneUuid || '').trim(); if (!sceneUuid) { throw new Error('sceneUuid is required.'); } const asset = await loadAssetByUuid(sceneUuid); if (!(asset instanceof SceneAsset)) { throw new Error(`Asset '${sceneUuid}' is not a SceneAsset.`); } director.runSceneImmediate(asset); const scene = getScene(); return { loaded: true, sceneUuid, sceneName: scene.name, childCount: scene.children.length, }; }, async createCanvas(options = {}) { const parent = options.parentPath ? findNodeByPath(options.parentPath) : getScene(); if (!parent) { throw new Error(`Parent not found: ${options.parentPath}`); } const node = new Node(options.name || 'Canvas'); node.parent = parent; configureNodeBasics(node, options); getOrAddComponent(node, Canvas); configureUITransform(node, { width: Number.isFinite(options.width) ? options.width : 1280, height: Number.isFinite(options.height) ? options.height : 720, }); return { created: true, name: node.name, path: getNodePath(node), uuid: node.uuid, components: getComponentNames(node), }; }, async createLabel(options = {}) { const parent = options.parentPath ? findNodeByPath(options.parentPath) : getScene(); if (!parent) { throw new Error(`Parent not found: ${options.parentPath}`); } const node = new Node(options.name || 'Label'); node.parent = parent; configureNodeBasics(node, options); configureUITransform(node, options); const label = getOrAddComponent(node, Label); label.string = options.text || 'Label'; label.fontSize = Number.isFinite(options.fontSize) ? options.fontSize : 32; label.lineHeight = Number.isFinite(options.lineHeight) ? options.lineHeight : label.fontSize + 8; label.color = parseColor(options.color, Color.WHITE); return { created: true, path: getNodePath(node), uuid: node.uuid, text: label.string, }; }, async createButton(options = {}) { const parent = options.parentPath ? findNodeByPath(options.parentPath) : getScene(); if (!parent) { throw new Error(`Parent not found: ${options.parentPath}`); } const node = new Node(options.name || 'Button'); node.parent = parent; configureNodeBasics(node, options); configureUITransform(node, { ...options, width: Number.isFinite(options.width) ? options.width : 180, height: Number.isFinite(options.height) ? options.height : 64, }); const sprite = getOrAddComponent(node, Sprite); sprite.color = parseColor(options.backgroundColor, new Color(64, 96, 255, 255)); const button = getOrAddComponent(node, Button); button.target = node; const labelNode = new Node(options.labelName || 'Label'); labelNode.parent = node; configureUITransform(labelNode, { width: Number.isFinite(options.width) ? options.width : 180, height: Number.isFinite(options.height) ? options.height : 64, }); const label = getOrAddComponent(labelNode, Label); label.string = options.text || 'Button'; label.fontSize = Number.isFinite(options.fontSize) ? options.fontSize : 28; label.lineHeight = Number.isFinite(options.lineHeight) ? options.lineHeight : label.fontSize + 8; label.color = parseColor(options.textColor, Color.WHITE); return { created: true, path: getNodePath(node), uuid: node.uuid, labelPath: getNodePath(labelNode), components: getComponentNames(node), }; }, async createSprite(options = {}) { const parent = options.parentPath ? findNodeByPath(options.parentPath) : getScene(); if (!parent) { throw new Error(`Parent not found: ${options.parentPath}`); } const node = new Node(options.name || 'Sprite'); node.parent = parent; configureNodeBasics(node, options); configureUITransform(node, options); const sprite = getOrAddComponent(node, Sprite); sprite.color = parseColor(options.color, Color.WHITE); if (options.spriteFrameUuid) { sprite.spriteFrame = await loadAssetByUuid(options.spriteFrameUuid); } return { created: true, path: getNodePath(node), uuid: node.uuid, components: getComponentNames(node), }; }, async listCameras() { const cameras = findComponentsByClass(Camera); return { count: cameras.length, cameras: cameras.map((camera) => ({ node: camera.node ? getNodePath(camera.node) : '', uuid: camera.node ? camera.node.uuid : '', enabled: Boolean(camera.enabled), priority: camera.priority, projection: camera.projection, visibility: camera.visibility, clearFlags: camera.clearFlags, })), }; }, async createCamera(options = {}) { const parent = options.parentPath ? findNodeByPath(options.parentPath) : getScene(); if (!parent) { throw new Error(`Parent not found: ${options.parentPath}`); } const node = new Node(options.name || 'Camera'); node.parent = parent; configureNodeBasics(node, options); const camera = getOrAddComponent(node, Camera); if (Number.isFinite(options.priority)) { camera.priority = options.priority; } if (Number.isFinite(options.visibility)) { camera.visibility = options.visibility; } if (Number.isFinite(options.clearFlags)) { camera.clearFlags = options.clearFlags; } return { created: true, path: getNodePath(node), uuid: node.uuid, camera: plain(camera), }; }, async setCameraProperties(options = {}) { const node = findNode(options); if (!node) { throw new Error('Target camera node was not found.'); } const camera = node.getComponent(Camera); if (!camera) { throw new Error('Camera component was not found on target node.'); } for (const key of ['priority', 'visibility', 'clearFlags', 'projection', 'orthoHeight', 'fov', 'near', 'far']) { if (options[key] !== undefined) { camera[key] = options[key]; } } return { updated: true, node: getNodePath(node), camera: plain(camera), }; }, async listAnimations(options = {}) { const animations = options.path || options.uuid || options.name ? [findNode(options)].filter(Boolean).map((node) => node.getComponent(Animation)).filter(Boolean) : findComponentsByClass(Animation); return { count: animations.length, animations: animations.map((animation) => ({ node: animation.node ? getNodePath(animation.node) : '', uuid: animation.node ? animation.node.uuid : '', enabled: Boolean(animation.enabled), defaultClip: animation.defaultClip ? animation.defaultClip.name : '', clips: Array.isArray(animation.clips) ? animation.clips.map((clip) => clip && clip.name).filter(Boolean) : [], })), }; }, async addAnimationClip(options = {}) { const node = findNode(options); if (!node) { throw new Error('Target node was not found.'); } const clipUuid = String(options.clipUuid || '').trim(); if (!clipUuid) { throw new Error('clipUuid is required.'); } const clip = await loadAnimationClipByUuid(clipUuid); const animation = getOrAddComponent(node, Animation); const clips = Array.isArray(animation.clips) ? animation.clips.slice() : []; if (!clips.includes(clip)) { clips.push(clip); animation.clips = clips; } if (options.makeDefault !== false) { animation.defaultClip = clip; } return { added: true, node: getNodePath(node), clip: clip.name, clipUuid, clips: animation.clips.map((item) => item && item.name).filter(Boolean), }; }, async playAnimation(options = {}) { const node = findNode(options); if (!node) { throw new Error('Target node was not found.'); } const animation = node.getComponent(Animation); if (!animation) { throw new Error('Animation component was not found on target node.'); } const state = options.clipName ? animation.play(options.clipName) : animation.play(); return { playing: true, node: getNodePath(node), clip: state && state.clip ? state.clip.name : options.clipName || '', }; }, async stopAnimation(options = {}) { const node = findNode(options); if (!node) { throw new Error('Target node was not found.'); } const animation = node.getComponent(Animation); if (!animation) { throw new Error('Animation component was not found on target node.'); } if (options.clipName) { animation.stop(options.clipName); } else { animation.stop(); } return { stopped: true, node: getNodePath(node), clip: options.clipName || '(all)', }; }, async getRuntimeState() { const scheduler = getScheduler(); return { sceneName: getScene().name, paused: typeof director.isPaused === 'function' ? director.isPaused() : false, timeScale: scheduler && typeof scheduler.getTimeScale === 'function' ? scheduler.getTimeScale() : 1, totalFrames: typeof director.getTotalFrames === 'function' ? director.getTotalFrames() : undefined, }; }, async getPerformanceSnapshot() { const scheduler = getScheduler(); const stats = collectSceneStats(); const memory = typeof performance !== 'undefined' && performance.memory ? { jsHeapSizeLimit: performance.memory.jsHeapSizeLimit, totalJSHeapSize: performance.memory.totalJSHeapSize, usedJSHeapSize: performance.memory.usedJSHeapSize, } : null; return { sceneName: getScene().name, runtime: { paused: typeof director.isPaused === 'function' ? director.isPaused() : false, timeScale: scheduler && typeof scheduler.getTimeScale === 'function' ? scheduler.getTimeScale() : 1, totalFrames: typeof director.getTotalFrames === 'function' ? director.getTotalFrames() : undefined, }, stats, memory, warnings: buildSceneWarnings(stats), }; }, async getPrefabInstanceInfo(options = {}) { const node = findNode(options); if (!node) { throw new Error('Target node was not found.'); } return { node: { name: node.name, path: getNodePath(node), uuid: node.uuid, }, prefab: getPrefabInfo(node), }; }, async pauseRuntime() { if (typeof director.pause === 'function') { director.pause(); } return await exports.methods.getRuntimeState(); }, async resumeRuntime() { if (typeof director.resume === 'function') { director.resume(); } return await exports.methods.getRuntimeState(); }, async setTimeScale(options = {}) { const scale = Number(options.scale); if (!Number.isFinite(scale) || scale < 0 || scale > 100) { throw new Error('scale must be a number between 0 and 100.'); } const scheduler = getScheduler(); if (!scheduler || typeof scheduler.setTimeScale !== 'function') { throw new Error('director scheduler time scale API is unavailable.'); } scheduler.setTimeScale(scale); return await exports.methods.getRuntimeState(); }, async emitNodeEvent(options = {}) { const node = findNode(options); if (!node) { throw new Error('Target node was not found.'); } const eventName = String(options.eventName || '').trim(); if (!eventName) { throw new Error('eventName is required.'); } node.emit(eventName, options.payload || {}); return { emitted: true, node: getNodePath(node), eventName, }; }, async simulateButtonClick(options = {}) { const node = findNode(options); if (!node) { throw new Error('Target node was not found.'); } const button = node.getComponent(Button); if (!button) { throw new Error('Button component was not found on target node.'); } if (Component && Component.EventHandler && typeof Component.EventHandler.emitEvents === 'function') { Component.EventHandler.emitEvents(button.clickEvents, button); } node.emit(Button.EventType ? Button.EventType.CLICK : 'click', button); return { clicked: true, node: getNodePath(node), clickEventCount: Array.isArray(button.clickEvents) ? button.clickEvents.length : 0, }; }, async listButtonClickEvents(options = {}) { const node = findNode(options); if (!node) { throw new Error('Target node was not found.'); } const button = node.getComponent(Button); if (!button) { throw new Error('Button component was not found on target node.'); } return { node: getNodePath(node), uuid: node.uuid, clickEventCount: Array.isArray(button.clickEvents) ? button.clickEvents.length : 0, clickEvents: Array.isArray(button.clickEvents) ? button.clickEvents.map(serializeEventHandler).filter(Boolean) : [], }; }, async bindButtonClickEvent(options = {}) { const node = findNode(options); if (!node) { throw new Error('Button node was not found.'); } const button = node.getComponent(Button); if (!button) { throw new Error('Button component was not found on target node.'); } const target = findNode({ path: options.targetPath, uuid: options.targetUuid, name: options.targetName, }); if (!target) { throw new Error('Event target node was not found.'); } const componentName = String(options.componentName || '').trim(); const handlerName = String(options.handler || options.handlerName || '').trim(); if (!componentName || !handlerName) { throw new Error('componentName and handler are required.'); } const component = findComponent(target, { componentName }); if (!component) { throw new Error(`Target component was not found: ${componentName}. Ensure the script is compiled and the component is attached to the target node.`); } if (typeof component[handlerName] !== 'function') { throw new Error(`Target component method was not found: ${componentName}.${handlerName}`); } const HandlerClass = getEventHandlerClass(); if (!HandlerClass) { throw new Error('Cocos EventHandler class is unavailable.'); } const existing = Array.isArray(button.clickEvents) ? button.clickEvents : []; const duplicate = existing.find((event) => ( event && event.target === target && event.component === componentName && event.handler === handlerName && String(event.customEventData || '') === String(options.customEventData || '') )); if (duplicate && options.replace !== true) { return { bound: false, duplicate: true, node: getNodePath(node), event: serializeEventHandler(duplicate), clickEventCount: existing.length, }; } const event = new HandlerClass(); event.target = target; event.component = componentName; event.handler = handlerName; event.customEventData = String(options.customEventData || ''); button.clickEvents = options.replace === true ? existing.filter((item) => item !== duplicate).concat(event) : existing.concat(event); return { bound: true, node: getNodePath(node), uuid: node.uuid, event: serializeEventHandler(event), clickEventCount: button.clickEvents.length, }; }, async invokeComponentMethod(options = {}) { const node = findNode(options); if (!node) { throw new Error('Target node was not found.'); } const component = findComponent(node, options); if (!component) { throw new Error(`Target component was not found${options.componentName ? ': ' + options.componentName : ''}. Ensure the script is compiled and the component is attached to the node.`); } const methodName = String(options.methodName || '').trim(); if (!methodName || typeof component[methodName] !== 'function') { throw new Error(`Component method not found: ${methodName}`); } const result = component[methodName](...(Array.isArray(options.args) ? options.args : [])); return { invoked: true, node: getNodePath(node), component: component.constructor ? component.constructor.name : 'UnknownComponent', methodName, result: plain(result), }; }, async executeCode(options = {}) { const code = String(options.code || ''); if (!code.trim()) { throw new Error('code is required.'); } const result = await executeUserCode(code, options.args || {}); return { ok: true, result: plain(result), sceneName: getScene().name, }; }, };