@satelliteoflove/godot-mcp 1.3.0 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -4
- package/dist/__tests__/tools/animation.test.js +319 -306
- package/dist/__tests__/tools/animation.test.js.map +1 -1
- package/dist/__tests__/tools/editor.test.d.ts +2 -0
- package/dist/__tests__/tools/editor.test.d.ts.map +1 -0
- package/dist/__tests__/tools/editor.test.js +225 -0
- package/dist/__tests__/tools/editor.test.js.map +1 -0
- package/dist/__tests__/tools/node.test.js +151 -44
- package/dist/__tests__/tools/node.test.js.map +1 -1
- package/dist/__tests__/tools/resource.test.js +32 -20
- package/dist/__tests__/tools/resource.test.js.map +1 -1
- package/dist/__tests__/tools/scene.test.js +28 -51
- package/dist/__tests__/tools/scene.test.js.map +1 -1
- package/dist/__tests__/tools/tilemap.test.js +336 -323
- package/dist/__tests__/tools/tilemap.test.js.map +1 -1
- package/dist/tools/animation.d.ts +46 -118
- package/dist/tools/animation.d.ts.map +1 -1
- package/dist/tools/animation.js +95 -150
- package/dist/tools/animation.js.map +1 -1
- package/dist/tools/editor.d.ts +27 -15
- package/dist/tools/editor.d.ts.map +1 -1
- package/dist/tools/editor.js +86 -77
- package/dist/tools/editor.js.map +1 -1
- package/dist/tools/index.d.ts +0 -2
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +0 -6
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/node.d.ts +46 -45
- package/dist/tools/node.d.ts.map +1 -1
- package/dist/tools/node.js +135 -88
- package/dist/tools/node.js.map +1 -1
- package/dist/tools/project.d.ts +4 -25
- package/dist/tools/project.d.ts.map +1 -1
- package/dist/tools/project.js +30 -75
- package/dist/tools/project.js.map +1 -1
- package/dist/tools/resource.d.ts +17 -4
- package/dist/tools/resource.d.ts.map +1 -1
- package/dist/tools/resource.js +42 -24
- package/dist/tools/resource.js.map +1 -1
- package/dist/tools/scene.d.ts +20 -22
- package/dist/tools/scene.d.ts.map +1 -1
- package/dist/tools/scene.js +57 -62
- package/dist/tools/scene.js.map +1 -1
- package/dist/tools/tilemap.d.ts +92 -188
- package/dist/tools/tilemap.d.ts.map +1 -1
- package/dist/tools/tilemap.js +57 -96
- package/dist/tools/tilemap.js.map +1 -1
- package/package.json +1 -1
- package/dist/__tests__/screenshot.test.d.ts +0 -2
- package/dist/__tests__/screenshot.test.d.ts.map +0 -1
- package/dist/__tests__/screenshot.test.js +0 -42
- package/dist/__tests__/screenshot.test.js.map +0 -1
- package/dist/tools/screenshot.d.ts +0 -21
- package/dist/tools/screenshot.d.ts.map +0 -1
- package/dist/tools/screenshot.js +0 -46
- package/dist/tools/screenshot.js.map +0 -1
- package/dist/tools/script.d.ts +0 -51
- package/dist/tools/script.d.ts.map +0 -1
- package/dist/tools/script.js +0 -86
- package/dist/tools/script.js.map +0 -1
package/README.md
CHANGED
|
@@ -4,12 +4,16 @@ MCP (Model Context Protocol) server for Godot Engine integration. Enables AI ass
|
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
<!-- NPM_FEATURES_START -->
|
|
8
|
+
- **8 MCP tools** for scene, node, editor, project, animation, tilemap, resource operations
|
|
8
9
|
- **3 MCP resources** for reading scene trees, scripts, and project files
|
|
9
10
|
- Real-time bidirectional communication via WebSocket
|
|
10
|
-
-
|
|
11
|
-
- Full
|
|
12
|
-
-
|
|
11
|
+
- Screenshot capture from editor viewports and running games
|
|
12
|
+
- Full animation support (query, playback, editing)
|
|
13
|
+
- TileMapLayer and GridMap editing
|
|
14
|
+
- Resource inspection for SpriteFrames, TileSets, Materials, and Textures
|
|
15
|
+
- Debug output capture from running games
|
|
16
|
+
<!-- NPM_FEATURES_END -->
|
|
13
17
|
|
|
14
18
|
## Requirements
|
|
15
19
|
|
|
@@ -1,315 +1,328 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
2
|
import { createMockGodot, createToolContext } from '../helpers/mock-godot.js';
|
|
3
|
-
import {
|
|
4
|
-
describe('
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
expect(result).toBe('No AnimationPlayer nodes found in scene');
|
|
15
|
-
});
|
|
16
|
-
it('list_players formats found players', async () => {
|
|
17
|
-
mock.mockResponse({
|
|
18
|
-
animation_players: [
|
|
19
|
-
{ path: '/root/Player/AnimPlayer', name: 'AnimPlayer' },
|
|
20
|
-
{ path: '/root/Enemy/AnimPlayer', name: 'AnimPlayer' },
|
|
21
|
-
],
|
|
3
|
+
import { animation, animationTools } from '../../tools/animation.js';
|
|
4
|
+
describe('animation tool', () => {
|
|
5
|
+
describe('tool definitions', () => {
|
|
6
|
+
it('exports one tool', () => {
|
|
7
|
+
expect(animationTools).toHaveLength(1);
|
|
8
|
+
});
|
|
9
|
+
it('has animation tool with all action types', () => {
|
|
10
|
+
expect(animation.name).toBe('animation');
|
|
11
|
+
expect(animation.description).toContain('Query');
|
|
12
|
+
expect(animation.description).toContain('Playback');
|
|
13
|
+
expect(animation.description).toContain('Edit');
|
|
22
14
|
});
|
|
23
|
-
const ctx = createToolContext(mock);
|
|
24
|
-
const result = await animationQuery.execute({ action: 'list_players' }, ctx);
|
|
25
|
-
expect(result).toContain('Found 2 AnimationPlayer(s)');
|
|
26
|
-
expect(result).toContain('/root/Player/AnimPlayer');
|
|
27
|
-
});
|
|
28
|
-
it('get_info sends command and returns JSON', async () => {
|
|
29
|
-
const info = { current_animation: 'idle', is_playing: true, current_position: 0.5 };
|
|
30
|
-
mock.mockResponse(info);
|
|
31
|
-
const ctx = createToolContext(mock);
|
|
32
|
-
const result = await animationQuery.execute({
|
|
33
|
-
action: 'get_info',
|
|
34
|
-
node_path: '/root/AnimPlayer',
|
|
35
|
-
}, ctx);
|
|
36
|
-
expect(mock.calls[0].command).toBe('get_animation_player_info');
|
|
37
|
-
expect(mock.calls[0].params.node_path).toBe('/root/AnimPlayer');
|
|
38
|
-
expect(result).toBe(JSON.stringify(info, null, 2));
|
|
39
|
-
});
|
|
40
|
-
it('get_details sends command with animation name', async () => {
|
|
41
|
-
const details = { name: 'walk', length: 1.5, track_count: 3 };
|
|
42
|
-
mock.mockResponse(details);
|
|
43
|
-
const ctx = createToolContext(mock);
|
|
44
|
-
const result = await animationQuery.execute({
|
|
45
|
-
action: 'get_details',
|
|
46
|
-
node_path: '/root/AnimPlayer',
|
|
47
|
-
animation_name: 'walk',
|
|
48
|
-
}, ctx);
|
|
49
|
-
expect(mock.calls[0].command).toBe('get_animation_details');
|
|
50
|
-
expect(mock.calls[0].params.animation_name).toBe('walk');
|
|
51
|
-
expect(result).toBe(JSON.stringify(details, null, 2));
|
|
52
|
-
});
|
|
53
|
-
it('get_keyframes sends command with track index', async () => {
|
|
54
|
-
const keyframes = { track_path: 'Sprite:frame', keyframes: [{ time: 0, value: 0 }] };
|
|
55
|
-
mock.mockResponse(keyframes);
|
|
56
|
-
const ctx = createToolContext(mock);
|
|
57
|
-
const result = await animationQuery.execute({
|
|
58
|
-
action: 'get_keyframes',
|
|
59
|
-
node_path: '/root/AnimPlayer',
|
|
60
|
-
animation_name: 'walk',
|
|
61
|
-
track_index: 0,
|
|
62
|
-
}, ctx);
|
|
63
|
-
expect(mock.calls[0].command).toBe('get_track_keyframes');
|
|
64
|
-
expect(mock.calls[0].params.track_index).toBe(0);
|
|
65
|
-
expect(result).toBe(JSON.stringify(keyframes, null, 2));
|
|
66
|
-
});
|
|
67
|
-
it('throws on error from Godot', async () => {
|
|
68
|
-
mock.mockError(new Error('Node not found'));
|
|
69
|
-
const ctx = createToolContext(mock);
|
|
70
|
-
await expect(animationQuery.execute({
|
|
71
|
-
action: 'get_info',
|
|
72
|
-
node_path: '/root/Missing',
|
|
73
|
-
}, ctx)).rejects.toThrow('Node not found');
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
describe('animation_playback', () => {
|
|
77
|
-
let mock;
|
|
78
|
-
beforeEach(() => {
|
|
79
|
-
mock = createMockGodot();
|
|
80
|
-
});
|
|
81
|
-
it('play sends command and returns confirmation', async () => {
|
|
82
|
-
mock.mockResponse({ playing: 'run', from_position: 0 });
|
|
83
|
-
const ctx = createToolContext(mock);
|
|
84
|
-
const result = await animationPlayback.execute({
|
|
85
|
-
action: 'play',
|
|
86
|
-
node_path: '/root/AnimPlayer',
|
|
87
|
-
animation_name: 'run',
|
|
88
|
-
}, ctx);
|
|
89
|
-
expect(mock.calls[0].command).toBe('play_animation');
|
|
90
|
-
expect(mock.calls[0].params.animation_name).toBe('run');
|
|
91
|
-
expect(result).toBe('Playing animation: run');
|
|
92
|
-
});
|
|
93
|
-
it('play passes optional params', async () => {
|
|
94
|
-
mock.mockResponse({ playing: 'walk', from_position: 0 });
|
|
95
|
-
const ctx = createToolContext(mock);
|
|
96
|
-
await animationPlayback.execute({
|
|
97
|
-
action: 'play',
|
|
98
|
-
node_path: '/root/AnimPlayer',
|
|
99
|
-
animation_name: 'walk',
|
|
100
|
-
custom_speed: 2.0,
|
|
101
|
-
custom_blend: 0.5,
|
|
102
|
-
from_end: true,
|
|
103
|
-
}, ctx);
|
|
104
|
-
expect(mock.calls[0].params.custom_speed).toBe(2.0);
|
|
105
|
-
expect(mock.calls[0].params.custom_blend).toBe(0.5);
|
|
106
|
-
expect(mock.calls[0].params.from_end).toBe(true);
|
|
107
|
-
});
|
|
108
|
-
it('stop sends command and returns confirmation', async () => {
|
|
109
|
-
mock.mockResponse({});
|
|
110
|
-
const ctx = createToolContext(mock);
|
|
111
|
-
const result = await animationPlayback.execute({
|
|
112
|
-
action: 'stop',
|
|
113
|
-
node_path: '/root/AnimPlayer',
|
|
114
|
-
}, ctx);
|
|
115
|
-
expect(mock.calls[0].command).toBe('stop_animation');
|
|
116
|
-
expect(result).toBe('Animation stopped');
|
|
117
|
-
});
|
|
118
|
-
it('pause sends command with paused=true', async () => {
|
|
119
|
-
mock.mockResponse({});
|
|
120
|
-
const ctx = createToolContext(mock);
|
|
121
|
-
const result = await animationPlayback.execute({
|
|
122
|
-
action: 'pause',
|
|
123
|
-
node_path: '/root/AnimPlayer',
|
|
124
|
-
paused: true,
|
|
125
|
-
}, ctx);
|
|
126
|
-
expect(mock.calls[0].command).toBe('pause_animation');
|
|
127
|
-
expect(mock.calls[0].params.paused).toBe(true);
|
|
128
|
-
expect(result).toBe('Animation paused');
|
|
129
|
-
});
|
|
130
|
-
it('pause sends command with paused=false', async () => {
|
|
131
|
-
mock.mockResponse({});
|
|
132
|
-
const ctx = createToolContext(mock);
|
|
133
|
-
const result = await animationPlayback.execute({
|
|
134
|
-
action: 'pause',
|
|
135
|
-
node_path: '/root/AnimPlayer',
|
|
136
|
-
paused: false,
|
|
137
|
-
}, ctx);
|
|
138
|
-
expect(mock.calls[0].params.paused).toBe(false);
|
|
139
|
-
expect(result).toBe('Animation unpaused');
|
|
140
|
-
});
|
|
141
|
-
it('seek sends command and returns position', async () => {
|
|
142
|
-
mock.mockResponse({ position: 1.5 });
|
|
143
|
-
const ctx = createToolContext(mock);
|
|
144
|
-
const result = await animationPlayback.execute({
|
|
145
|
-
action: 'seek',
|
|
146
|
-
node_path: '/root/AnimPlayer',
|
|
147
|
-
seconds: 1.5,
|
|
148
|
-
}, ctx);
|
|
149
|
-
expect(mock.calls[0].command).toBe('seek_animation');
|
|
150
|
-
expect(mock.calls[0].params.seconds).toBe(1.5);
|
|
151
|
-
expect(result).toBe('Seeked to position: 1.5');
|
|
152
|
-
});
|
|
153
|
-
it('queue sends command and returns queue info', async () => {
|
|
154
|
-
mock.mockResponse({ queued: 'attack', queue_length: 3 });
|
|
155
|
-
const ctx = createToolContext(mock);
|
|
156
|
-
const result = await animationPlayback.execute({
|
|
157
|
-
action: 'queue',
|
|
158
|
-
node_path: '/root/AnimPlayer',
|
|
159
|
-
animation_name: 'attack',
|
|
160
|
-
}, ctx);
|
|
161
|
-
expect(mock.calls[0].command).toBe('queue_animation');
|
|
162
|
-
expect(result).toBe('Queued animation: attack (queue length: 3)');
|
|
163
|
-
});
|
|
164
|
-
it('clear_queue sends command and returns confirmation', async () => {
|
|
165
|
-
mock.mockResponse({});
|
|
166
|
-
const ctx = createToolContext(mock);
|
|
167
|
-
const result = await animationPlayback.execute({
|
|
168
|
-
action: 'clear_queue',
|
|
169
|
-
node_path: '/root/AnimPlayer',
|
|
170
|
-
}, ctx);
|
|
171
|
-
expect(mock.calls[0].command).toBe('clear_animation_queue');
|
|
172
|
-
expect(result).toBe('Animation queue cleared');
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
|
-
describe('animation_edit', () => {
|
|
176
|
-
let mock;
|
|
177
|
-
beforeEach(() => {
|
|
178
|
-
mock = createMockGodot();
|
|
179
|
-
});
|
|
180
|
-
it('create sends command and returns confirmation', async () => {
|
|
181
|
-
mock.mockResponse({ created: 'new_anim', library: '' });
|
|
182
|
-
const ctx = createToolContext(mock);
|
|
183
|
-
const result = await animationEdit.execute({
|
|
184
|
-
action: 'create',
|
|
185
|
-
node_path: '/root/AnimPlayer',
|
|
186
|
-
animation_name: 'new_anim',
|
|
187
|
-
length: 2.0,
|
|
188
|
-
}, ctx);
|
|
189
|
-
expect(mock.calls[0].command).toBe('create_animation');
|
|
190
|
-
expect(mock.calls[0].params.animation_name).toBe('new_anim');
|
|
191
|
-
expect(mock.calls[0].params.length).toBe(2.0);
|
|
192
|
-
expect(result).toBe('Created animation: new_anim');
|
|
193
|
-
});
|
|
194
|
-
it('create includes library in result when provided', async () => {
|
|
195
|
-
mock.mockResponse({ created: 'walk', library: 'movement' });
|
|
196
|
-
const ctx = createToolContext(mock);
|
|
197
|
-
const result = await animationEdit.execute({
|
|
198
|
-
action: 'create',
|
|
199
|
-
node_path: '/root/AnimPlayer',
|
|
200
|
-
animation_name: 'walk',
|
|
201
|
-
library_name: 'movement',
|
|
202
|
-
}, ctx);
|
|
203
|
-
expect(result).toBe('Created animation: walk in library: movement');
|
|
204
|
-
});
|
|
205
|
-
it('delete sends command and returns confirmation', async () => {
|
|
206
|
-
mock.mockResponse({ deleted: 'old_anim' });
|
|
207
|
-
const ctx = createToolContext(mock);
|
|
208
|
-
const result = await animationEdit.execute({
|
|
209
|
-
action: 'delete',
|
|
210
|
-
node_path: '/root/AnimPlayer',
|
|
211
|
-
animation_name: 'old_anim',
|
|
212
|
-
}, ctx);
|
|
213
|
-
expect(mock.calls[0].command).toBe('delete_animation');
|
|
214
|
-
expect(result).toBe('Deleted animation: old_anim');
|
|
215
|
-
});
|
|
216
|
-
it('rename sends command with old and new names', async () => {
|
|
217
|
-
mock.mockResponse({ renamed: { from: 'walk', to: 'walk_slow' } });
|
|
218
|
-
const ctx = createToolContext(mock);
|
|
219
|
-
const result = await animationEdit.execute({
|
|
220
|
-
action: 'rename',
|
|
221
|
-
node_path: '/root/AnimPlayer',
|
|
222
|
-
old_name: 'walk',
|
|
223
|
-
new_name: 'walk_slow',
|
|
224
|
-
}, ctx);
|
|
225
|
-
expect(mock.calls[0].command).toBe('rename_animation');
|
|
226
|
-
expect(mock.calls[0].params.old_name).toBe('walk');
|
|
227
|
-
expect(mock.calls[0].params.new_name).toBe('walk_slow');
|
|
228
|
-
expect(result).toBe('Renamed animation: walk -> walk_slow');
|
|
229
|
-
});
|
|
230
|
-
it('update_props sends command and returns updated properties', async () => {
|
|
231
|
-
mock.mockResponse({ updated: 'walk', properties: { length: 2.0, loop_mode: 'linear' } });
|
|
232
|
-
const ctx = createToolContext(mock);
|
|
233
|
-
const result = await animationEdit.execute({
|
|
234
|
-
action: 'update_props',
|
|
235
|
-
node_path: '/root/AnimPlayer',
|
|
236
|
-
animation_name: 'walk',
|
|
237
|
-
length: 2.0,
|
|
238
|
-
loop_mode: 'linear',
|
|
239
|
-
}, ctx);
|
|
240
|
-
expect(mock.calls[0].command).toBe('update_animation_properties');
|
|
241
|
-
expect(result).toContain('Updated animation: walk');
|
|
242
|
-
expect(result).toContain('"length":2');
|
|
243
|
-
});
|
|
244
|
-
it('add_track sends command and returns track info', async () => {
|
|
245
|
-
mock.mockResponse({ track_index: 0, track_path: 'Sprite2D:frame', track_type: 'value' });
|
|
246
|
-
const ctx = createToolContext(mock);
|
|
247
|
-
const result = await animationEdit.execute({
|
|
248
|
-
action: 'add_track',
|
|
249
|
-
node_path: '/root/AnimPlayer',
|
|
250
|
-
animation_name: 'walk',
|
|
251
|
-
track_type: 'value',
|
|
252
|
-
track_path: 'Sprite2D:frame',
|
|
253
|
-
}, ctx);
|
|
254
|
-
expect(mock.calls[0].command).toBe('add_animation_track');
|
|
255
|
-
expect(result).toBe('Added track 0: value -> Sprite2D:frame');
|
|
256
|
-
});
|
|
257
|
-
it('remove_track sends command and returns confirmation', async () => {
|
|
258
|
-
mock.mockResponse({ removed_track: 2 });
|
|
259
|
-
const ctx = createToolContext(mock);
|
|
260
|
-
const result = await animationEdit.execute({
|
|
261
|
-
action: 'remove_track',
|
|
262
|
-
node_path: '/root/AnimPlayer',
|
|
263
|
-
animation_name: 'walk',
|
|
264
|
-
track_index: 2,
|
|
265
|
-
}, ctx);
|
|
266
|
-
expect(mock.calls[0].command).toBe('remove_animation_track');
|
|
267
|
-
expect(result).toBe('Removed track: 2');
|
|
268
15
|
});
|
|
269
|
-
|
|
270
|
-
mock
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
16
|
+
describe('query actions', () => {
|
|
17
|
+
let mock;
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
mock = createMockGodot();
|
|
20
|
+
});
|
|
21
|
+
it('list_players sends command and formats empty result', async () => {
|
|
22
|
+
mock.mockResponse({ animation_players: [] });
|
|
23
|
+
const ctx = createToolContext(mock);
|
|
24
|
+
const result = await animation.execute({ action: 'list_players' }, ctx);
|
|
25
|
+
expect(mock.calls[0].command).toBe('list_animation_players');
|
|
26
|
+
expect(result).toBe('No AnimationPlayer nodes found in scene');
|
|
27
|
+
});
|
|
28
|
+
it('list_players formats found players', async () => {
|
|
29
|
+
mock.mockResponse({
|
|
30
|
+
animation_players: [
|
|
31
|
+
{ path: '/root/Player/AnimPlayer', name: 'AnimPlayer' },
|
|
32
|
+
{ path: '/root/Enemy/AnimPlayer', name: 'AnimPlayer' },
|
|
33
|
+
],
|
|
34
|
+
});
|
|
35
|
+
const ctx = createToolContext(mock);
|
|
36
|
+
const result = await animation.execute({ action: 'list_players' }, ctx);
|
|
37
|
+
expect(result).toContain('Found 2 AnimationPlayer(s)');
|
|
38
|
+
expect(result).toContain('/root/Player/AnimPlayer');
|
|
39
|
+
});
|
|
40
|
+
it('get_info sends command and returns JSON', async () => {
|
|
41
|
+
const info = { current_animation: 'idle', is_playing: true, current_position: 0.5 };
|
|
42
|
+
mock.mockResponse(info);
|
|
43
|
+
const ctx = createToolContext(mock);
|
|
44
|
+
const result = await animation.execute({
|
|
45
|
+
action: 'get_info',
|
|
46
|
+
node_path: '/root/AnimPlayer',
|
|
47
|
+
}, ctx);
|
|
48
|
+
expect(mock.calls[0].command).toBe('get_animation_player_info');
|
|
49
|
+
expect(mock.calls[0].params.node_path).toBe('/root/AnimPlayer');
|
|
50
|
+
expect(result).toBe(JSON.stringify(info, null, 2));
|
|
51
|
+
});
|
|
52
|
+
it('get_details sends command with animation name', async () => {
|
|
53
|
+
const details = { name: 'walk', length: 1.5, track_count: 3 };
|
|
54
|
+
mock.mockResponse(details);
|
|
55
|
+
const ctx = createToolContext(mock);
|
|
56
|
+
const result = await animation.execute({
|
|
57
|
+
action: 'get_details',
|
|
58
|
+
node_path: '/root/AnimPlayer',
|
|
59
|
+
animation_name: 'walk',
|
|
60
|
+
}, ctx);
|
|
61
|
+
expect(mock.calls[0].command).toBe('get_animation_details');
|
|
62
|
+
expect(mock.calls[0].params.animation_name).toBe('walk');
|
|
63
|
+
expect(result).toBe(JSON.stringify(details, null, 2));
|
|
64
|
+
});
|
|
65
|
+
it('get_keyframes sends command with track index', async () => {
|
|
66
|
+
const keyframes = { track_path: 'Sprite:frame', keyframes: [{ time: 0, value: 0 }] };
|
|
67
|
+
mock.mockResponse(keyframes);
|
|
68
|
+
const ctx = createToolContext(mock);
|
|
69
|
+
const result = await animation.execute({
|
|
70
|
+
action: 'get_keyframes',
|
|
71
|
+
node_path: '/root/AnimPlayer',
|
|
72
|
+
animation_name: 'walk',
|
|
73
|
+
track_index: 0,
|
|
74
|
+
}, ctx);
|
|
75
|
+
expect(mock.calls[0].command).toBe('get_track_keyframes');
|
|
76
|
+
expect(mock.calls[0].params.track_index).toBe(0);
|
|
77
|
+
expect(result).toBe(JSON.stringify(keyframes, null, 2));
|
|
78
|
+
});
|
|
79
|
+
it('throws on error from Godot', async () => {
|
|
80
|
+
mock.mockError(new Error('Node not found'));
|
|
81
|
+
const ctx = createToolContext(mock);
|
|
82
|
+
await expect(animation.execute({
|
|
83
|
+
action: 'get_info',
|
|
84
|
+
node_path: '/root/Missing',
|
|
85
|
+
}, ctx)).rejects.toThrow('Node not found');
|
|
86
|
+
});
|
|
284
87
|
});
|
|
285
|
-
|
|
286
|
-
mock
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
88
|
+
describe('playback actions', () => {
|
|
89
|
+
let mock;
|
|
90
|
+
beforeEach(() => {
|
|
91
|
+
mock = createMockGodot();
|
|
92
|
+
});
|
|
93
|
+
it('play sends command and returns confirmation', async () => {
|
|
94
|
+
mock.mockResponse({ playing: 'run', from_position: 0 });
|
|
95
|
+
const ctx = createToolContext(mock);
|
|
96
|
+
const result = await animation.execute({
|
|
97
|
+
action: 'play',
|
|
98
|
+
node_path: '/root/AnimPlayer',
|
|
99
|
+
animation_name: 'run',
|
|
100
|
+
}, ctx);
|
|
101
|
+
expect(mock.calls[0].command).toBe('play_animation');
|
|
102
|
+
expect(mock.calls[0].params.animation_name).toBe('run');
|
|
103
|
+
expect(result).toBe('Playing animation: run');
|
|
104
|
+
});
|
|
105
|
+
it('play passes optional params', async () => {
|
|
106
|
+
mock.mockResponse({ playing: 'walk', from_position: 0 });
|
|
107
|
+
const ctx = createToolContext(mock);
|
|
108
|
+
await animation.execute({
|
|
109
|
+
action: 'play',
|
|
110
|
+
node_path: '/root/AnimPlayer',
|
|
111
|
+
animation_name: 'walk',
|
|
112
|
+
custom_speed: 2.0,
|
|
113
|
+
custom_blend: 0.5,
|
|
114
|
+
from_end: true,
|
|
115
|
+
}, ctx);
|
|
116
|
+
expect(mock.calls[0].params.custom_speed).toBe(2.0);
|
|
117
|
+
expect(mock.calls[0].params.custom_blend).toBe(0.5);
|
|
118
|
+
expect(mock.calls[0].params.from_end).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
it('stop sends command and returns confirmation', async () => {
|
|
121
|
+
mock.mockResponse({});
|
|
122
|
+
const ctx = createToolContext(mock);
|
|
123
|
+
const result = await animation.execute({
|
|
124
|
+
action: 'stop',
|
|
125
|
+
node_path: '/root/AnimPlayer',
|
|
126
|
+
}, ctx);
|
|
127
|
+
expect(mock.calls[0].command).toBe('stop_animation');
|
|
128
|
+
expect(result).toBe('Animation stopped');
|
|
129
|
+
});
|
|
130
|
+
it('pause sends command with paused=true', async () => {
|
|
131
|
+
mock.mockResponse({});
|
|
132
|
+
const ctx = createToolContext(mock);
|
|
133
|
+
const result = await animation.execute({
|
|
134
|
+
action: 'pause',
|
|
135
|
+
node_path: '/root/AnimPlayer',
|
|
136
|
+
paused: true,
|
|
137
|
+
}, ctx);
|
|
138
|
+
expect(mock.calls[0].command).toBe('pause_animation');
|
|
139
|
+
expect(mock.calls[0].params.paused).toBe(true);
|
|
140
|
+
expect(result).toBe('Animation paused');
|
|
141
|
+
});
|
|
142
|
+
it('pause sends command with paused=false', async () => {
|
|
143
|
+
mock.mockResponse({});
|
|
144
|
+
const ctx = createToolContext(mock);
|
|
145
|
+
const result = await animation.execute({
|
|
146
|
+
action: 'pause',
|
|
147
|
+
node_path: '/root/AnimPlayer',
|
|
148
|
+
paused: false,
|
|
149
|
+
}, ctx);
|
|
150
|
+
expect(mock.calls[0].params.paused).toBe(false);
|
|
151
|
+
expect(result).toBe('Animation unpaused');
|
|
152
|
+
});
|
|
153
|
+
it('seek sends command and returns position', async () => {
|
|
154
|
+
mock.mockResponse({ position: 1.5 });
|
|
155
|
+
const ctx = createToolContext(mock);
|
|
156
|
+
const result = await animation.execute({
|
|
157
|
+
action: 'seek',
|
|
158
|
+
node_path: '/root/AnimPlayer',
|
|
159
|
+
seconds: 1.5,
|
|
160
|
+
}, ctx);
|
|
161
|
+
expect(mock.calls[0].command).toBe('seek_animation');
|
|
162
|
+
expect(mock.calls[0].params.seconds).toBe(1.5);
|
|
163
|
+
expect(result).toBe('Seeked to position: 1.5');
|
|
164
|
+
});
|
|
165
|
+
it('queue sends command and returns queue info', async () => {
|
|
166
|
+
mock.mockResponse({ queued: 'attack', queue_length: 3 });
|
|
167
|
+
const ctx = createToolContext(mock);
|
|
168
|
+
const result = await animation.execute({
|
|
169
|
+
action: 'queue',
|
|
170
|
+
node_path: '/root/AnimPlayer',
|
|
171
|
+
animation_name: 'attack',
|
|
172
|
+
}, ctx);
|
|
173
|
+
expect(mock.calls[0].command).toBe('queue_animation');
|
|
174
|
+
expect(result).toBe('Queued animation: attack (queue length: 3)');
|
|
175
|
+
});
|
|
176
|
+
it('clear_queue sends command and returns confirmation', async () => {
|
|
177
|
+
mock.mockResponse({});
|
|
178
|
+
const ctx = createToolContext(mock);
|
|
179
|
+
const result = await animation.execute({
|
|
180
|
+
action: 'clear_queue',
|
|
181
|
+
node_path: '/root/AnimPlayer',
|
|
182
|
+
}, ctx);
|
|
183
|
+
expect(mock.calls[0].command).toBe('clear_animation_queue');
|
|
184
|
+
expect(result).toBe('Animation queue cleared');
|
|
185
|
+
});
|
|
297
186
|
});
|
|
298
|
-
|
|
299
|
-
mock
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
187
|
+
describe('edit actions', () => {
|
|
188
|
+
let mock;
|
|
189
|
+
beforeEach(() => {
|
|
190
|
+
mock = createMockGodot();
|
|
191
|
+
});
|
|
192
|
+
it('create sends command and returns confirmation', async () => {
|
|
193
|
+
mock.mockResponse({ created: 'new_anim', library: '' });
|
|
194
|
+
const ctx = createToolContext(mock);
|
|
195
|
+
const result = await animation.execute({
|
|
196
|
+
action: 'create',
|
|
197
|
+
node_path: '/root/AnimPlayer',
|
|
198
|
+
animation_name: 'new_anim',
|
|
199
|
+
length: 2.0,
|
|
200
|
+
}, ctx);
|
|
201
|
+
expect(mock.calls[0].command).toBe('create_animation');
|
|
202
|
+
expect(mock.calls[0].params.animation_name).toBe('new_anim');
|
|
203
|
+
expect(mock.calls[0].params.length).toBe(2.0);
|
|
204
|
+
expect(result).toBe('Created animation: new_anim');
|
|
205
|
+
});
|
|
206
|
+
it('create includes library in result when provided', async () => {
|
|
207
|
+
mock.mockResponse({ created: 'walk', library: 'movement' });
|
|
208
|
+
const ctx = createToolContext(mock);
|
|
209
|
+
const result = await animation.execute({
|
|
210
|
+
action: 'create',
|
|
211
|
+
node_path: '/root/AnimPlayer',
|
|
212
|
+
animation_name: 'walk',
|
|
213
|
+
library_name: 'movement',
|
|
214
|
+
}, ctx);
|
|
215
|
+
expect(result).toBe('Created animation: walk in library: movement');
|
|
216
|
+
});
|
|
217
|
+
it('delete sends command and returns confirmation', async () => {
|
|
218
|
+
mock.mockResponse({ deleted: 'old_anim' });
|
|
219
|
+
const ctx = createToolContext(mock);
|
|
220
|
+
const result = await animation.execute({
|
|
221
|
+
action: 'delete',
|
|
222
|
+
node_path: '/root/AnimPlayer',
|
|
223
|
+
animation_name: 'old_anim',
|
|
224
|
+
}, ctx);
|
|
225
|
+
expect(mock.calls[0].command).toBe('delete_animation');
|
|
226
|
+
expect(result).toBe('Deleted animation: old_anim');
|
|
227
|
+
});
|
|
228
|
+
it('rename sends command with old and new names', async () => {
|
|
229
|
+
mock.mockResponse({ renamed: { from: 'walk', to: 'walk_slow' } });
|
|
230
|
+
const ctx = createToolContext(mock);
|
|
231
|
+
const result = await animation.execute({
|
|
232
|
+
action: 'rename',
|
|
233
|
+
node_path: '/root/AnimPlayer',
|
|
234
|
+
old_name: 'walk',
|
|
235
|
+
new_name: 'walk_slow',
|
|
236
|
+
}, ctx);
|
|
237
|
+
expect(mock.calls[0].command).toBe('rename_animation');
|
|
238
|
+
expect(mock.calls[0].params.old_name).toBe('walk');
|
|
239
|
+
expect(mock.calls[0].params.new_name).toBe('walk_slow');
|
|
240
|
+
expect(result).toBe('Renamed animation: walk -> walk_slow');
|
|
241
|
+
});
|
|
242
|
+
it('update_props sends command and returns updated properties', async () => {
|
|
243
|
+
mock.mockResponse({ updated: 'walk', properties: { length: 2.0, loop_mode: 'linear' } });
|
|
244
|
+
const ctx = createToolContext(mock);
|
|
245
|
+
const result = await animation.execute({
|
|
246
|
+
action: 'update_props',
|
|
247
|
+
node_path: '/root/AnimPlayer',
|
|
248
|
+
animation_name: 'walk',
|
|
249
|
+
length: 2.0,
|
|
250
|
+
loop_mode: 'linear',
|
|
251
|
+
}, ctx);
|
|
252
|
+
expect(mock.calls[0].command).toBe('update_animation_properties');
|
|
253
|
+
expect(result).toContain('Updated animation: walk');
|
|
254
|
+
expect(result).toContain('"length":2');
|
|
255
|
+
});
|
|
256
|
+
it('add_track sends command and returns track info', async () => {
|
|
257
|
+
mock.mockResponse({ track_index: 0, track_path: 'Sprite2D:frame', track_type: 'value' });
|
|
258
|
+
const ctx = createToolContext(mock);
|
|
259
|
+
const result = await animation.execute({
|
|
260
|
+
action: 'add_track',
|
|
261
|
+
node_path: '/root/AnimPlayer',
|
|
262
|
+
animation_name: 'walk',
|
|
263
|
+
track_type: 'value',
|
|
264
|
+
track_path: 'Sprite2D:frame',
|
|
265
|
+
}, ctx);
|
|
266
|
+
expect(mock.calls[0].command).toBe('add_animation_track');
|
|
267
|
+
expect(result).toBe('Added track 0: value -> Sprite2D:frame');
|
|
268
|
+
});
|
|
269
|
+
it('remove_track sends command and returns confirmation', async () => {
|
|
270
|
+
mock.mockResponse({ removed_track: 2 });
|
|
271
|
+
const ctx = createToolContext(mock);
|
|
272
|
+
const result = await animation.execute({
|
|
273
|
+
action: 'remove_track',
|
|
274
|
+
node_path: '/root/AnimPlayer',
|
|
275
|
+
animation_name: 'walk',
|
|
276
|
+
track_index: 2,
|
|
277
|
+
}, ctx);
|
|
278
|
+
expect(mock.calls[0].command).toBe('remove_animation_track');
|
|
279
|
+
expect(result).toBe('Removed track: 2');
|
|
280
|
+
});
|
|
281
|
+
it('add_keyframe sends command and returns keyframe info', async () => {
|
|
282
|
+
mock.mockResponse({ keyframe_index: 0, time: 0.5, value: 3 });
|
|
283
|
+
const ctx = createToolContext(mock);
|
|
284
|
+
const result = await animation.execute({
|
|
285
|
+
action: 'add_keyframe',
|
|
286
|
+
node_path: '/root/AnimPlayer',
|
|
287
|
+
animation_name: 'walk',
|
|
288
|
+
track_index: 0,
|
|
289
|
+
time: 0.5,
|
|
290
|
+
value: 3,
|
|
291
|
+
}, ctx);
|
|
292
|
+
expect(mock.calls[0].command).toBe('add_keyframe');
|
|
293
|
+
expect(mock.calls[0].params.time).toBe(0.5);
|
|
294
|
+
expect(mock.calls[0].params.value).toBe(3);
|
|
295
|
+
expect(result).toBe('Added keyframe 0 at 0.5s');
|
|
296
|
+
});
|
|
297
|
+
it('remove_keyframe sends command and returns confirmation', async () => {
|
|
298
|
+
mock.mockResponse({ removed_keyframe: 1, track_index: 0 });
|
|
299
|
+
const ctx = createToolContext(mock);
|
|
300
|
+
const result = await animation.execute({
|
|
301
|
+
action: 'remove_keyframe',
|
|
302
|
+
node_path: '/root/AnimPlayer',
|
|
303
|
+
animation_name: 'walk',
|
|
304
|
+
track_index: 0,
|
|
305
|
+
keyframe_index: 1,
|
|
306
|
+
}, ctx);
|
|
307
|
+
expect(mock.calls[0].command).toBe('remove_keyframe');
|
|
308
|
+
expect(result).toBe('Removed keyframe 1 from track 0');
|
|
309
|
+
});
|
|
310
|
+
it('update_keyframe sends command and returns changes', async () => {
|
|
311
|
+
mock.mockResponse({ updated_keyframe: 0, changes: { time: 0.75, value: 5 } });
|
|
312
|
+
const ctx = createToolContext(mock);
|
|
313
|
+
const result = await animation.execute({
|
|
314
|
+
action: 'update_keyframe',
|
|
315
|
+
node_path: '/root/AnimPlayer',
|
|
316
|
+
animation_name: 'walk',
|
|
317
|
+
track_index: 0,
|
|
318
|
+
keyframe_index: 0,
|
|
319
|
+
time: 0.75,
|
|
320
|
+
value: 5,
|
|
321
|
+
}, ctx);
|
|
322
|
+
expect(mock.calls[0].command).toBe('update_keyframe');
|
|
323
|
+
expect(result).toContain('Updated keyframe 0');
|
|
324
|
+
expect(result).toContain('"time":0.75');
|
|
325
|
+
});
|
|
313
326
|
});
|
|
314
327
|
});
|
|
315
328
|
//# sourceMappingURL=animation.test.js.map
|