Files
tactical-shooter/package/dist/tools/node-tools.js
T
shawn 91b878f347 Quaternius weapons, input fixes, sound & path fixes
- Added 6 Quaternius weapon models (Pistol, Revolver, Rifle, Shotgun, P90, SniperRifle)
- Created weapon resource files with balanced stats
- Wired Pistol + Rifle into player.tscn replacing Kenney blasters
- Fixed WASD input (matched input map action names)
- Fixed Escape key toggles mouse capture
- Added Audio autoload singleton
- Fixed broken sound asset paths across all scripts
- Fixed all scene ext_resource text paths (nested under assets/kenney-fps/)
- Fixed all .import source_file paths
- Deleted stale .import files (will be regenerated by editor on open)
2026-07-05 22:53:27 -04:00

473 lines
23 KiB
JavaScript

import { existsSync } from 'fs';
import { join } from 'path';
import { normalizeParameters, validateSubPath, validateNodePath, createErrorResponse, validateSceneArgs, } from '../utils/godot-runner.js';
import { executeSceneOp } from '../utils/handler-helpers.js';
// --- Tool definitions ---
export const nodeToolDefinitions = [
{
name: 'delete_nodes',
description: 'Remove one or more nodes (and their descendants) from a scene file. Always-array: pass a single-element nodePaths array for one-off deletes. Saves once at the end. Cannot delete the scene root — that entry returns an error and the rest still process. Returns: results array with one entry per nodePath in input order (success or error message).',
annotations: { destructiveHint: true },
inputSchema: {
type: 'object',
properties: {
projectPath: { type: 'string', description: 'Path to the Godot project directory' },
scenePath: {
type: 'string',
description: 'Scene file path relative to the project (e.g. "scenes/main.tscn")',
},
nodePaths: {
type: 'array',
items: { type: 'string' },
description: 'Node paths from scene root to delete (e.g. ["root/Player/Sprite2D"])',
},
},
required: ['projectPath', 'scenePath', 'nodePaths'],
},
outputSchema: {
type: 'object',
properties: {
results: {
type: 'array',
items: {
type: 'object',
properties: {
nodePath: { type: 'string' },
success: { type: 'boolean' },
error: { type: 'string' },
},
},
},
},
},
},
{
name: 'set_node_properties',
description: 'Set one or more node properties on a scene in a single Godot process. Always-array: pass a single-element updates array for one-off edits. Vector2 ({x,y}), Vector3 ({x,y,z}), and Color ({r,g,b,a}) auto-convert; primitives pass through. For other complex GDScript types (Resource, NodePath, etc.), use run_script. abortOnError stops on first failure (default false continues). Saves once at the end. Returns: results[] with one entry per update in input order (success or error).',
annotations: { idempotentHint: true },
inputSchema: {
type: 'object',
properties: {
projectPath: { type: 'string', description: 'Path to the Godot project directory' },
scenePath: { type: 'string', description: 'Scene file path relative to the project' },
updates: {
type: 'array',
description: 'Property updates to apply',
items: {
type: 'object',
properties: {
nodePath: {
type: 'string',
description: 'Node path from scene root (e.g. "root/Player")',
},
property: {
type: 'string',
description: 'GDScript property name in snake_case (e.g. "position", "modulate", "collision_layer")',
},
value: { description: 'New property value' },
},
required: ['nodePath', 'property', 'value'],
},
},
abortOnError: {
type: 'boolean',
description: 'Stop processing on first error (default: false)',
},
},
required: ['projectPath', 'scenePath', 'updates'],
},
outputSchema: {
type: 'object',
properties: {
results: {
type: 'array',
items: {
type: 'object',
properties: {
nodePath: { type: 'string' },
property: { type: 'string' },
success: { type: 'boolean' },
error: { type: 'string' },
},
},
},
},
},
},
{
name: 'get_node_properties',
description: "Read one or more nodes' current property values from a scene file in a single Godot process. Always-array: pass a single-element nodes array for one-off reads. Per-node changedOnly:true filters out properties matching class defaults (useful for compact diffs). Returns: { results: [{ nodePath, nodeType, properties?, error? }] }; failed reads include error and omit properties.",
annotations: { readOnlyHint: true },
inputSchema: {
type: 'object',
properties: {
projectPath: { type: 'string', description: 'Path to the Godot project directory' },
scenePath: { type: 'string', description: 'Scene file path relative to the project' },
nodes: {
type: 'array',
description: 'Nodes to read properties from',
items: {
type: 'object',
properties: {
nodePath: {
type: 'string',
description: 'Node path from scene root (e.g. "root/Player")',
},
changedOnly: {
type: 'boolean',
description: 'Only return properties differing from defaults (default: false)',
},
},
required: ['nodePath'],
},
},
},
required: ['projectPath', 'scenePath', 'nodes'],
},
},
{
name: 'attach_script',
description: 'Attach an existing GDScript file to a node in a scene. Use after writing the script with the standard file tools and validating it via the validate tool. Replaces any previously attached script. Saves automatically. Returns: success with the resolved nodePath and scriptPath that were attached. Errors if scriptPath does not exist or nodePath is not found.',
annotations: { idempotentHint: true },
inputSchema: {
type: 'object',
properties: {
projectPath: { type: 'string', description: 'Path to the Godot project directory' },
scenePath: { type: 'string', description: 'Scene file path relative to the project' },
nodePath: { type: 'string', description: 'Node path from scene root (e.g. "root/Player")' },
scriptPath: {
type: 'string',
description: 'Path to the GDScript file relative to the project (e.g. "scripts/player.gd")',
},
},
required: ['projectPath', 'scenePath', 'nodePath', 'scriptPath'],
},
outputSchema: {
type: 'object',
properties: {
success: { type: 'boolean' },
nodePath: { type: 'string' },
scriptPath: { type: 'string' },
},
},
},
{
name: 'get_scene_tree',
description: 'Get the scene hierarchy as a nested tree of { name, type, path, script, children }. Use maxDepth:1 for a shallow listing of direct children only; default -1 returns the full tree. parentPath scopes the result to a subtree. Returns the nested tree as JSON text. Errors if scene does not exist or parentPath is not found.',
annotations: { readOnlyHint: true },
inputSchema: {
type: 'object',
properties: {
projectPath: { type: 'string', description: 'Path to the Godot project directory' },
scenePath: { type: 'string', description: 'Scene file path relative to the project' },
parentPath: {
type: 'string',
description: 'Scope to a subtree starting at this node path (e.g. "root/Player")',
},
maxDepth: {
type: 'number',
description: 'Maximum recursion depth. -1 for unlimited (default: -1). 1 returns only direct children.',
},
},
required: ['projectPath', 'scenePath'],
},
},
{
name: 'duplicate_node',
description: 'Duplicate a node and its descendants in a Godot scene. Use to clone a configured subtree without re-creating it node-by-node via add_node. newName defaults to the original name + "2"; targetParentPath defaults to the original parent. Saves automatically. Returns: success with originalPath and the newPath where the duplicate now lives — use newPath for follow-up edits. Errors if nodePath does not exist or targetParentPath cannot accept children.',
inputSchema: {
type: 'object',
properties: {
projectPath: { type: 'string', description: 'Path to the Godot project directory' },
scenePath: { type: 'string', description: 'Scene file path relative to the project' },
nodePath: { type: 'string', description: 'Node path from scene root to duplicate' },
newName: {
type: 'string',
description: 'Name for the duplicated node (default: original name + "2")',
},
targetParentPath: {
type: 'string',
description: 'Parent node path for the duplicate (default: same parent as original)',
},
},
required: ['projectPath', 'scenePath', 'nodePath'],
},
outputSchema: {
type: 'object',
properties: {
success: { type: 'boolean' },
originalPath: { type: 'string' },
newPath: { type: 'string' },
},
},
},
{
name: 'get_node_signals',
description: 'List all signals defined on a node and their current connections. Use before connect_signal/disconnect_signal to verify signal/method names. The connections[].target field uses Godot absolute path format (/root/Scene/Node) — convert to scene-root-relative (root/Node) before passing to connect/disconnect_signal. Returns: nodeType and signals[], each with name and current connections (signal/target/method). Errors if node not found.',
annotations: { readOnlyHint: true },
inputSchema: {
type: 'object',
properties: {
projectPath: { type: 'string', description: 'Path to the Godot project directory' },
scenePath: { type: 'string', description: 'Scene file path relative to the project' },
nodePath: { type: 'string', description: 'Node path from scene root (e.g. "root/Button")' },
},
required: ['projectPath', 'scenePath', 'nodePath'],
},
outputSchema: {
type: 'object',
properties: {
nodePath: { type: 'string' },
nodeType: { type: 'string' },
signals: {
type: 'array',
items: {
type: 'object',
properties: {
name: { type: 'string' },
connections: {
type: 'array',
items: {
type: 'object',
properties: {
signal: { type: 'string' },
target: { type: 'string' },
method: { type: 'string' },
},
},
},
},
},
},
},
},
},
{
name: 'connect_signal',
description: 'Connect a signal on a source node to a method on a target node, persisting the connection in the .tscn. Use after get_node_signals to confirm the signal name on the source and the method name on the target. Connecting the same signal+method pair twice creates a duplicate connection — call get_node_signals first if uncertain. Saves automatically. Returns a plain-text confirmation naming the source, signal, target, and method. Errors if the signal does not exist on the source node or the method does not exist on the target node.',
inputSchema: {
type: 'object',
properties: {
projectPath: { type: 'string', description: 'Path to the Godot project directory' },
scenePath: { type: 'string', description: 'Scene file path relative to the project' },
nodePath: { type: 'string', description: 'Source node path from scene root' },
signal: {
type: 'string',
description: 'Signal name on the source node (e.g. "pressed", "body_entered")',
},
targetNodePath: {
type: 'string',
description: 'Target node path from scene root that receives the signal',
},
method: {
type: 'string',
description: 'Method name on the target node to call when the signal fires',
},
},
required: ['projectPath', 'scenePath', 'nodePath', 'signal', 'targetNodePath', 'method'],
},
},
{
name: 'disconnect_signal',
description: 'Remove an existing signal connection between two nodes, persisting the change in the .tscn. Use get_node_signals first to confirm the connection exists; recovery requires reconnecting via connect_signal. Saves automatically. Returns a plain-text confirmation naming the disconnected signal and target. Errors if the connection does not exist.',
annotations: { destructiveHint: true },
inputSchema: {
type: 'object',
properties: {
projectPath: { type: 'string', description: 'Path to the Godot project directory' },
scenePath: { type: 'string', description: 'Scene file path relative to the project' },
nodePath: { type: 'string', description: 'Source node path from scene root' },
signal: { type: 'string', description: 'Signal name on the source node' },
targetNodePath: { type: 'string', description: 'Target node path from scene root' },
method: { type: 'string', description: 'Method name on the target node' },
},
required: ['projectPath', 'scenePath', 'nodePath', 'signal', 'targetNodePath', 'method'],
},
},
];
// --- Handlers ---
export async function handleDeleteNodes(runner, args) {
args = normalizeParameters(args);
const v = validateSceneArgs(args);
if ('isError' in v)
return v;
if (!args.nodePaths || !Array.isArray(args.nodePaths) || args.nodePaths.length === 0) {
return createErrorResponse('nodePaths array is required', [
'Provide a non-empty array of node paths (e.g. ["root/Player"])',
]);
}
for (const p of args.nodePaths) {
if (typeof p !== 'string' || !validateNodePath(p)) {
return createErrorResponse('Invalid nodePath in nodePaths', [
'Provide a scene-tree path without ".." (e.g. "root/Player")',
]);
}
}
const params = { scenePath: args.scenePath, nodePaths: args.nodePaths };
return executeSceneOp(runner, 'delete_nodes', params, v.projectPath, 'Failed to delete nodes', ['Check if the node paths are correct'], undefined, { parseStdoutAsJson: true });
}
export async function handleSetNodeProperties(runner, args) {
args = normalizeParameters(args);
const v = validateSceneArgs(args);
if ('isError' in v)
return v;
if (!args.updates || !Array.isArray(args.updates)) {
return createErrorResponse('updates array is required', [
'Provide an array of { nodePath, property, value }',
]);
}
const params = {
scenePath: args.scenePath,
updates: args.updates,
abortOnError: args.abortOnError ?? false,
};
return executeSceneOp(runner, 'set_node_properties', params, v.projectPath, 'Failed to set node properties', ['Check node paths and property names'], undefined, { parseStdoutAsJson: true });
}
export async function handleGetNodeProperties(runner, args) {
args = normalizeParameters(args);
const v = validateSceneArgs(args);
if ('isError' in v)
return v;
if (!args.nodes || !Array.isArray(args.nodes)) {
return createErrorResponse('nodes array is required', [
'Provide an array of { nodePath, changedOnly? }',
]);
}
const params = { scenePath: args.scenePath, nodes: args.nodes };
return executeSceneOp(runner, 'get_node_properties', params, v.projectPath, 'Failed to get node properties', ['Check node paths']);
}
export async function handleAttachScript(runner, args) {
args = normalizeParameters(args);
const v = validateSceneArgs(args);
if ('isError' in v)
return v;
if (!args.nodePath || !validateNodePath(args.nodePath)) {
return createErrorResponse('Valid nodePath is required', [
'Provide the node path (e.g. "root/Player")',
]);
}
if (!args.scriptPath || !validateSubPath(v.projectPath, args.scriptPath)) {
return createErrorResponse('Valid scriptPath is required', [
'Provide a relative script path that stays inside the project directory',
]);
}
const scriptFullPath = join(v.projectPath, args.scriptPath);
if (!existsSync(scriptFullPath)) {
return createErrorResponse(`Script file does not exist: ${args.scriptPath}`, [
'Create the script file first',
]);
}
const params = {
scenePath: args.scenePath,
nodePath: args.nodePath,
scriptPath: args.scriptPath,
};
return executeSceneOp(runner, 'attach_script', params, v.projectPath, 'Failed to attach script', ['Ensure the script is valid for this node type'], undefined, { parseStdoutAsJson: true });
}
export async function handleGetSceneTree(runner, args) {
args = normalizeParameters(args);
const v = validateSceneArgs(args);
if ('isError' in v)
return v;
if (args.parentPath && !validateNodePath(args.parentPath)) {
return createErrorResponse('Invalid parentPath', [
'Provide a scene-tree path without ".." (e.g. "root/Player")',
]);
}
const params = { scenePath: args.scenePath };
if (args.parentPath)
params.parentPath = args.parentPath;
if (typeof args.maxDepth === 'number')
params.maxDepth = args.maxDepth;
return executeSceneOp(runner, 'get_scene_tree', params, v.projectPath, 'Failed to get scene tree', ['Ensure the scene is valid']);
}
export async function handleDuplicateNode(runner, args) {
args = normalizeParameters(args);
const v = validateSceneArgs(args);
if ('isError' in v)
return v;
if (!args.nodePath || !validateNodePath(args.nodePath)) {
return createErrorResponse('Valid nodePath is required', [
'Provide the node path to duplicate',
]);
}
if (args.targetParentPath && !validateNodePath(args.targetParentPath)) {
return createErrorResponse('Invalid targetParentPath', [
'Provide a scene-tree path without ".." (e.g. "root/Player")',
]);
}
const params = { scenePath: args.scenePath, nodePath: args.nodePath };
if (args.newName)
params.newName = args.newName;
if (args.targetParentPath)
params.targetParentPath = args.targetParentPath;
return executeSceneOp(runner, 'duplicate_node', params, v.projectPath, 'Failed to duplicate node', ['Check if the node path and target parent path are correct'], undefined, { parseStdoutAsJson: true });
}
export async function handleGetNodeSignals(runner, args) {
args = normalizeParameters(args);
const v = validateSceneArgs(args);
if ('isError' in v)
return v;
if (!args.nodePath || !validateNodePath(args.nodePath)) {
return createErrorResponse('Valid nodePath is required', [
'Provide the node path (e.g. "root/Button")',
]);
}
const params = { scenePath: args.scenePath, nodePath: args.nodePath };
return executeSceneOp(runner, 'get_node_signals', params, v.projectPath, 'Failed to get node signals', ['Check if the node path is correct'], undefined, { parseStdoutAsJson: true });
}
function validateSignalArgs(args) {
const v = validateSceneArgs(args);
if ('isError' in v)
return v;
if (!args.nodePath || !validateNodePath(args.nodePath)) {
return createErrorResponse('Valid nodePath is required', ['Provide the source node path']);
}
if (!args.signal || !args.targetNodePath || !args.method) {
return createErrorResponse('signal, targetNodePath, and method are required', [
'Provide all three parameters',
]);
}
if (!validateNodePath(args.targetNodePath)) {
return createErrorResponse('Invalid targetNodePath', [
'Provide a scene-tree path without ".." (e.g. "root/Player")',
]);
}
return {
projectPath: v.projectPath,
scenePath: v.scenePath,
nodePath: args.nodePath,
signal: args.signal,
targetNodePath: args.targetNodePath,
method: args.method,
};
}
export async function handleConnectSignal(runner, args) {
args = normalizeParameters(args);
const v = validateSignalArgs(args);
if ('isError' in v)
return v;
const params = {
scenePath: v.scenePath,
nodePath: v.nodePath,
signal: v.signal,
targetNodePath: v.targetNodePath,
method: v.method,
};
return executeSceneOp(runner, 'connect_signal', params, v.projectPath, 'Failed to connect signal', ['Ensure the signal exists on the source node and the method exists on the target node']);
}
export async function handleDisconnectSignal(runner, args) {
args = normalizeParameters(args);
const v = validateSignalArgs(args);
if ('isError' in v)
return v;
const params = {
scenePath: v.scenePath,
nodePath: v.nodePath,
signal: v.signal,
targetNodePath: v.targetNodePath,
method: v.method,
};
return executeSceneOp(runner, 'disconnect_signal', params, v.projectPath, 'Failed to disconnect signal', ['Ensure the signal connection exists before trying to disconnect it']);
}
//# sourceMappingURL=node-tools.js.map