@gripforgeai/mcp 0.1.10 → 0.1.12
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/LICENSE +21 -0
- package/README.md +82 -2
- package/dist/ability-tools.js +21 -0
- package/dist/architecture-tools.js +83 -0
- package/dist/asset-production-tools.js +101 -0
- package/dist/creature-rig-tools.js +41 -0
- package/dist/game-concept-tools.js +56 -0
- package/dist/game-creation-tools.js +45 -0
- package/dist/gamekit-deliver-local.js +5 -7
- package/dist/gamekit-tools.js +133 -24
- package/dist/hero-ability-tools.js +48 -0
- package/dist/joystick-kit/godot.js +146 -0
- package/dist/joystick-kit/index.js +32 -0
- package/dist/joystick-kit/threejs.js +93 -0
- package/dist/joystick-kit/unity.js +182 -0
- package/dist/joystick-kit/unreal.js +231 -0
- package/dist/joystick-tools.js +26 -0
- package/dist/look.js +6 -0
- package/dist/map-unreal-export-local.js +285 -0
- package/dist/map-unreal-import-local.js +421 -0
- package/dist/map-unreal-local.js +75 -0
- package/dist/motion-tools.js +50 -0
- package/dist/projectile-tools.js +28 -0
- package/dist/scene-tools.js +79 -13
- package/dist/server-tools.js +3 -3
- package/dist/server.js +139 -13
- package/dist/shield-vfx-tools.js +29 -0
- package/dist/studio-tools-tools.js +106 -0
- package/dist/unreal-export-package.js +212 -0
- package/dist/vfx-project-tools.js +2 -1
- package/dist/weapon-motion-tools.js +34 -0
- package/package.json +6 -1
- package/runtime/unreal-scene-export.py +341 -0
- package/runtime/unreal-scene-import.py +622 -0
- package/runtime/unreal-scene-upload.mjs +7 -0
- package/runtime/unreal_scene_math.py +96 -0
- package/runtime/unreal_spawn_review.py +135 -0
package/dist/gamekit-tools.js
CHANGED
|
@@ -3,6 +3,7 @@ import { z } from 'zod/v4';
|
|
|
3
3
|
export const GAMEKIT_TOOL_NAMES = [
|
|
4
4
|
'gripforge_gamekit_search',
|
|
5
5
|
'gripforge_gamekit_get',
|
|
6
|
+
'gripforge_gamekit_installed',
|
|
6
7
|
'gripforge_gamekit_install',
|
|
7
8
|
'gripforge_gamekit_remove',
|
|
8
9
|
'gripforge_gamekit_configure',
|
|
@@ -11,7 +12,13 @@ export const GAMEKIT_TOOL_NAMES = [
|
|
|
11
12
|
'gripforge_gamekit_deliver',
|
|
12
13
|
'gripforge_game_capabilities',
|
|
13
14
|
'gripforge_game_project',
|
|
15
|
+
'gripforge_game_engine',
|
|
16
|
+
'gripforge_game_content',
|
|
14
17
|
'gripforge_game_play_url',
|
|
18
|
+
'gripforge_moba_roster',
|
|
19
|
+
'gripforge_ability_vfx',
|
|
20
|
+
'gripforge_moba_map',
|
|
21
|
+
'gripforge_terrain_map_use',
|
|
15
22
|
];
|
|
16
23
|
const enc = (v) => encodeURIComponent(String(v));
|
|
17
24
|
const HOSTED_DELIVERY_NOTE = 'This hosted endpoint cannot write into your project: write bundle.files following plan.actions in order, or use npx @gripforgeai/mcp with gripforge_gamekit_deliver_local { project_dir }, or the editor bridge.';
|
|
@@ -30,7 +37,6 @@ export function registerGameKitTools(register, options, schema = z) {
|
|
|
30
37
|
.max(120)
|
|
31
38
|
.regex(/^[a-z0-9][a-z0-9._-]*$/i)
|
|
32
39
|
.describe('Kit id from the catalogue, dotted (e.g. vehicle.driveable, mission.objectives).');
|
|
33
|
-
const range = schema.string().max(80).optional().describe('Semver range to satisfy (e.g. ^1.2.0, 1.x). Default: latest compatible version.');
|
|
34
40
|
const config = schema
|
|
35
41
|
.record(schema.string(), schema.unknown())
|
|
36
42
|
.optional()
|
|
@@ -92,11 +98,11 @@ export function registerGameKitTools(register, options, schema = z) {
|
|
|
92
98
|
delete out[k];
|
|
93
99
|
return out;
|
|
94
100
|
};
|
|
95
|
-
tool('gripforge_gamekit_search', 'Search the Game Kit catalogue', 'Start here. Search the modular Game Kit catalogue (vehicle.driveable, mission.objectives, npc.wanted, …) by free text, capability, tag or engine target. Pass project_id to score kits against that project: each hit then carries state { installed,
|
|
101
|
+
tool('gripforge_gamekit_search', 'Search the Game Kit catalogue', 'Start here. Search the modular Game Kit catalogue (vehicle.driveable, mission.objectives, npc.wanted, …) by free text, capability, tag or engine target. Pass project_id to score kits against that project: each hit then carries state { installed, enabled, fills, reason } and kits that fill one of its missing capabilities rank first. Kits are bricks that go together: a kit handling the same thing as an installed one is a same-scene overlap (only one active in a scene), never a reason to skip it. Returns { kits, presets }; each kit carries its presentation: name, tagline (one-line hook), tags, targets and status per engine (web / godot / unity / unreal), and cover / coverUrl, the featured landscape image (WebP) to show the user when proposing kits. legacy.* kits play through their existing client. Kart Racing is native: compose world.racetrack + vehicle.driveable + race.kart; edit race_tracks through project data. world.lighting adds shared sun/fill, point lights and spots, presets, bounded light budgets and bloom; edit world_lights through project data. world.fluids adds interactive mud / blood surfaces, displaced 3D wheel ruts and footprints; edit fluid_surfaces through project data (web renderer). world.terrain adds a seeded large landscape (terrain_edits data), movement.traversal climbing / swimming / gliding with stamina, world.elements data-driven fire, water, ice, electricity and poison (element_rules, element_materials, element_climates data). Call this BEFORE gripforge_gamekit_install. 0 credits.', {
|
|
96
102
|
q: schema.string().max(200).optional().describe('Free text matched on id, name, description and tags (e.g. "drive a car", "wanted level").'),
|
|
97
103
|
capability: schema.string().max(120).optional().describe('Capability the kit must provide (exact id or prefix, e.g. vehicle.drive).'),
|
|
98
104
|
tag: schema.string().max(60).optional().describe('Tag filter (e.g. vehicle, mission, npc, legacy).'),
|
|
99
|
-
target: schema.enum(['web', 'godot', 'unity', 'unreal']).optional().describe('Engine target the kit must support.
|
|
105
|
+
target: schema.enum(['web', 'godot', 'unity', 'unreal']).optional().describe('Engine target the kit must support. For game creation, use the engine chosen by the user or identified in their project; ask Unity / Godot / Unreal Engine / Three.js if unknown. The API web default is not user consent.'),
|
|
100
106
|
project_id: projectId.optional().describe('Game Kit project id (gkp_…) to score results against. Default: the workspace’s most recently updated project.'),
|
|
101
107
|
limit: schema.number().int().min(1).max(100).optional().describe('Max kits returned (default 20).'),
|
|
102
108
|
}, { readOnly: true }, (args, extra) => api('gamekits', args, 'GET', extra?.signal, {
|
|
@@ -107,7 +113,7 @@ export function registerGameKitTools(register, options, schema = z) {
|
|
|
107
113
|
project: typeof args.project_id === 'string' ? args.project_id : undefined,
|
|
108
114
|
limit: typeof args.limit === 'number' ? args.limit : undefined,
|
|
109
115
|
}));
|
|
110
|
-
tool('gripforge_gamekit_get', 'Read one Game Kit', 'Read one Game Kit: its manifest (provides / requires capabilities, asset slots, data collections, events), README docs, config JSON schema with defaults, and
|
|
116
|
+
tool('gripforge_gamekit_get', 'Read one Game Kit', 'Read one Game Kit: its manifest (provides / requires capabilities, asset slots, data collections, events), README docs, config JSON schema with defaults, version and changelog (information only: projects always run the latest version), and summary { tagline, tags, targets, status, cover, coverUrl } with the featured image URL. Pass with_usage=true to also list the workspace projects that have it installed. Call this BEFORE gripforge_gamekit_configure to know which config keys exist. 0 credits.', {
|
|
111
117
|
id: kitId,
|
|
112
118
|
version: schema.string().max(40).optional().describe('Exact version to read (e.g. 1.0.0). Default: latest.'),
|
|
113
119
|
with_usage: schema.boolean().optional().describe('true to include usedBy: the workspace projects where this kit is installed.'),
|
|
@@ -115,30 +121,41 @@ export function registerGameKitTools(register, options, schema = z) {
|
|
|
115
121
|
version: typeof args.version === 'string' ? args.version : undefined,
|
|
116
122
|
with: args.with_usage === true ? 'usage' : undefined,
|
|
117
123
|
}));
|
|
118
|
-
tool('
|
|
124
|
+
tool('gripforge_gamekit_installed', 'Installed kits of a project (use these first)', 'Start here for an existing project. Lists the kits installed in a Game Kit project (gkp_…), first and marked priority: id, name, enabled, addedBy (user / agent / auto / preset / default — the interface kits every new game gets), provides, requires and config, plus the rule that goes with them. Versions are for your information (projects always follow the latest): each kit also has version (in the project), latest, delivered { godot | unity | unreal } (the version the last engine delivery carries — a delivered game can run an older one) and pendingMigrations (manual migrations left to you). Installed kits are choices already made for this game: build on them for what they cover and configure them as needed; search the catalogue only for what they do not cover. 0 credits.', { project_id: projectId }, { readOnly: true }, (args, extra) => api(`gamekit-projects/${enc(args.project_id)}/kits`, args, 'GET', extra?.signal));
|
|
125
|
+
tool('gripforge_gamekit_install', 'Install a Game Kit into a project', 'Add a gameplay kit (a brick) to a Game Kit project (gkp_…); installing marks that the game uses it. Decide it yourself, no confirmation needed. Always the latest version; dependencies are added automatically and listed in added [{ kit, version, requiredBy, capability }] (e.g. game.engine for a kit that requires it). Kits are bricks that go together: two kits handling the same thing (e.g. two lightings) are a same-scene overlap, never a game-level conflict — only one is active in a scene. Returns { plan, applied, added, project }; dry_run=true previews without writing. Check gripforge_gamekit_installed first, then gripforge_gamekit_search; tune with gripforge_gamekit_configure. Legacy genre kits (legacy.*) use gripforge_kit. 0 credits.', {
|
|
119
126
|
project_id: projectId,
|
|
120
127
|
id: kitId,
|
|
121
|
-
range,
|
|
122
128
|
config,
|
|
123
129
|
dry_run: schema.boolean().optional().describe('true to return the install plan without writing the project.'),
|
|
124
130
|
allow_planned: schema.boolean().optional().describe('true to accept kits still marked planned (0.x, no runtime yet). Default false.'),
|
|
131
|
+
reason: schema.string().max(300).optional().describe('One sentence: why you do this, recorded in the project history (required in spirit when you go against the installed-kits rule).'),
|
|
125
132
|
}, { readOnly: false }, (args, extra) => api(`gamekit-projects/${enc(args.project_id)}/kits`, { ...body(args), id: args.id }, 'POST', extra?.signal));
|
|
126
|
-
tool('gripforge_gamekit_remove', 'Uninstall a Game Kit from a project', 'Uninstall a kit from a Game Kit project (gkp_…).
|
|
133
|
+
tool('gripforge_gamekit_remove', 'Uninstall a Game Kit from a project', 'Uninstall a kit from a Game Kit project (gkp_…). Remove a kit the user installed (addedBy user in gripforge_gamekit_installed) only when the user asks. game.engine (the engine of the game) and the default kits (ui.menu, input.remap, settings.graphics, ui.prompts, ui.endscreen, save.persistence, audio.core, i18n.text, ui.theme, input.virtualpad, addedBy default) are removed only when the user explicitly asks — never to "clean up". A kit other kits depend on, and game.engine always, is not removed by a first call: it answers 409 (kit_in_use, or confirm_required) with details { dependents, confirm } — nothing applied; show the user the dependents, then call again with with_dependents=true and confirm=<token> to remove the kit and its dependents together (the token is bound to the project revision). dry_run=true returns the plan, the dependents and the token without writing. force=true (legacy) removes it and disables its dependents instead; prune=true also drops the dependencies nothing else needs (never game.engine nor a default kit). Returns { applied, removed[], dependents, project }. 0 credits.', {
|
|
127
134
|
project_id: projectId,
|
|
128
135
|
id: kitId,
|
|
129
|
-
|
|
130
|
-
|
|
136
|
+
dry_run: schema.boolean().optional().describe('true to return the removal plan (with the dependents and the confirm token) without writing.'),
|
|
137
|
+
with_dependents: schema.boolean().optional().describe('true to remove the kit AND the kits that depend on it (needs confirm).'),
|
|
138
|
+
confirm: schema.string().max(80).optional().describe('The confirm token of the previous answer, after the user saw the dependents.'),
|
|
139
|
+
force: schema.boolean().optional().describe('Legacy: remove even when other kits depend on it; they are disabled, not removed.'),
|
|
140
|
+
prune: schema.boolean().optional().describe('true to also remove dependencies that no remaining kit requires (never game.engine nor a default kit).'),
|
|
141
|
+
reason: schema.string().max(300).optional().describe('One sentence: why you do this, recorded in the project history (required in spirit when you go against the installed-kits rule).'),
|
|
131
142
|
}, { readOnly: false, destructive: true }, (args, extra) => api(`gamekit-projects/${enc(args.project_id)}/kits/${enc(args.id)}`, args, 'DELETE', extra?.signal, {
|
|
132
143
|
force: args.force === true ? 1 : undefined,
|
|
133
144
|
prune: args.prune === true ? 1 : undefined,
|
|
145
|
+
dry_run: args.dry_run === true ? 1 : undefined,
|
|
146
|
+
with_dependents: args.with_dependents === true ? 1 : undefined,
|
|
147
|
+
confirm: typeof args.confirm === 'string' && args.confirm ? args.confirm : undefined,
|
|
148
|
+
reason: typeof args.reason === 'string' && args.reason.trim() ? args.reason : undefined,
|
|
134
149
|
}));
|
|
135
|
-
tool('gripforge_gamekit_configure', 'Configure an installed Game Kit', 'Tune an installed kit: merge config values validated against the kit configSchema (read it with gripforge_gamekit_get), or toggle enabled to switch the kit off without uninstalling it. Returns { kit, project } with the new project revision. Call this AFTER gripforge_gamekit_install. 0 credits.', {
|
|
150
|
+
tool('gripforge_gamekit_configure', 'Configure an installed Game Kit', 'Tune an installed kit: merge config values validated against the kit configSchema (read it with gripforge_gamekit_get), or toggle enabled to switch the kit off without uninstalling it. Configure installed kits freely rather than recoding what they do (game.engine: startScene, startEntry, flow.title, story flags). Switching game.engine off follows the confirmation of a removal: the first call answers 409 confirm_required with details.confirm; send it back with enabled=false after the user agreed — never on your own. Returns { kit, project } with the new project revision. Call this AFTER gripforge_gamekit_install. 0 credits.', {
|
|
136
151
|
project_id: projectId,
|
|
137
152
|
id: kitId,
|
|
138
153
|
config,
|
|
139
154
|
enabled: schema.boolean().optional().describe('false to disable the kit at runtime while keeping it installed; true to re-enable.'),
|
|
140
|
-
|
|
141
|
-
|
|
155
|
+
confirm: schema.string().max(80).optional().describe('game.engine enabled=false: the confirm token of the previous answer.'),
|
|
156
|
+
reason: schema.string().max(300).optional().describe('One sentence: why you do this, recorded in the project history (required in spirit when you go against the installed-kits rule).'),
|
|
157
|
+
}, { readOnly: false }, (args, extra) => api(`gamekit-projects/${enc(args.project_id)}/kits/${enc(args.id)}`, { ...body(args), ...(typeof args.confirm === 'string' ? { confirm: args.confirm } : {}) }, 'PATCH', extra?.signal));
|
|
158
|
+
tool('gripforge_gamekit_dependencies', 'Dependency graph of a project or kit', 'Dependency graph of a Game Kit project: nodes (installed kits with version, provides, requires), edges labelled by capability, same-scene overlaps (conflicts: several enabled providers of one exclusive capability) and unresolved requirements. Pass project_id for the installed graph; pass a kit id instead to read the declared requires / provides tree of a catalogue kit before installing it. Call this BEFORE gripforge_gamekit_remove with force. 0 credits.', {
|
|
142
159
|
project_id: projectId.optional().describe('Game Kit project id (gkp_…). Default: the workspace’s most recently updated project when id is not given.'),
|
|
143
160
|
id: kitId.optional().describe('Catalogue kit id to inspect instead of a project (returns its manifest tree).'),
|
|
144
161
|
}, { readOnly: true }, (args, extra) => {
|
|
@@ -148,11 +165,10 @@ export function registerGameKitTools(register, options, schema = z) {
|
|
|
148
165
|
return Promise.resolve(fail('Pass project_id (gkp_…) or a kit id.'));
|
|
149
166
|
return api(`gamekit-projects/${enc(args.project_id)}/dependencies`, args, 'GET', extra?.signal);
|
|
150
167
|
});
|
|
151
|
-
tool('gripforge_gamekit_update', '
|
|
168
|
+
tool('gripforge_gamekit_update', 'Bring Game Kits to their latest version', 'Projects always follow the latest version of their kits: the studio updates them automatically when a project is opened. Use this to do it now. Dry-run by default: returns the plan (from / to version, migrations). Pass apply=true to apply it. A migration with auto=false is never applied by itself: it is your task — apply its note (config, data) then call again. Omit id to check every installed kit. 0 credits.', {
|
|
152
169
|
project_id: projectId,
|
|
153
170
|
id: kitId.optional().describe('Installed kit to update. Omit to check every installed kit.'),
|
|
154
|
-
|
|
155
|
-
apply: schema.boolean().optional().describe('true to apply the update (new lockfile + migrations). Default false = plan only.'),
|
|
171
|
+
apply: schema.boolean().optional().describe('true to apply the update to the latest version (automatic migrations run). Default false = plan only.'),
|
|
156
172
|
}, { readOnly: false }, async (args, extra) => {
|
|
157
173
|
const project = enc(args.project_id);
|
|
158
174
|
const payload = body(args);
|
|
@@ -178,12 +194,11 @@ export function registerGameKitTools(register, options, schema = z) {
|
|
|
178
194
|
const data = { project_id: args.project_id, kits };
|
|
179
195
|
return { ...(isError ? { isError: true } : {}), structuredContent: data, content: [{ type: 'text', text: JSON.stringify(data) }] };
|
|
180
196
|
});
|
|
181
|
-
tool('gripforge_gamekit_deliver', 'Deliver Game Kits into an engine project', 'Turn catalogue kits, or the enabled kits of a Game Kit project, into engine files: Godot gets GDScript, scenes and config under gripforge/kits/<kit>/, Unity and Unreal
|
|
182
|
-
target: schema.enum(['godot', 'unity', 'unreal', 'web']).describe('Engine to deliver to: godot (files), unity
|
|
197
|
+
tool('gripforge_gamekit_deliver', 'Deliver Game Kits into an engine project', 'Turn catalogue kits, or the enabled kits of a Game Kit project, into engine files: Godot gets GDScript, scenes and config under gripforge/kits/<kit>/, Unity gets an embedded UPM package plus C# files and Unreal a Blueprint-first plugin for every kit with an engine delivery (gameplay and net.* kits; recipes otherwise), web needs nothing. Kits always deliver at their latest version; a kit the project copied locally is delivered from its copy. Call with dry_run=true FIRST: it returns the plan (files to add / keep / modify / delete, conflicts, dependencies, post-install steps) and never charges. Pass project_state (the current gripforge/gamekits.lock.json, path → sha256 of the files under gripforge/, engineVersion) so updates keep your edits and detect conflicts. Then call without dry_run to get the bundle. The hosted endpoint cannot write into your project: write bundle.files following plan.actions in order (backup, write, writeBin from bundle.binaries, delete with .uid / .import siblings, writeLock with bundle.lock), or use the npm client with project_dir (gripforge_gamekit_deliver_local), or the editor bridge. A blocked plan answers 409 with the plan: a file edited in the engine project is reported (modified_file), never overwritten silently; force backs it up then overwrites it. Credits: the first delivery of a kit major version to an engine costs 1 credit per workspace; re-deliveries, updates within a major, dry runs and the web target are free.', {
|
|
198
|
+
target: schema.enum(['godot', 'unity', 'unreal', 'web']).describe('Engine to deliver to: godot (addon + kit files), unity (UPM package + C# files), unreal (plugins), web (native, nothing to write). Kits without an engine delivery get recipes.'),
|
|
183
199
|
kits: schema
|
|
184
200
|
.array(schema.object({
|
|
185
201
|
id: kitId,
|
|
186
|
-
version: schema.string().max(40).optional().describe('Exact version; only the latest is deliverable. Default latest.'),
|
|
187
202
|
config: schema.record(schema.string(), schema.unknown()).optional().describe('Kit config, validated against its configSchema.'),
|
|
188
203
|
bindings: schema.record(schema.string(), schema.string()).optional().describe('Asset slot → Library item id (lib_…).'),
|
|
189
204
|
}))
|
|
@@ -202,7 +217,6 @@ export function registerGameKitTools(register, options, schema = z) {
|
|
|
202
217
|
.describe('What the engine project contains now. Without it the plan assumes an empty project (add-only).'),
|
|
203
218
|
dry_run: schema.boolean().optional().describe('true → plan only: no bundle, never charged. Do this first.'),
|
|
204
219
|
force: schema.boolean().optional().describe('true → back up then overwrite edited managed files and existing seed files.'),
|
|
205
|
-
accept_breaking: schema.boolean().optional().describe('true → allow a breaking (major) update.'),
|
|
206
220
|
}, { readOnly: false }, async (args, extra) => {
|
|
207
221
|
const kits = Array.isArray(args.kits) ? args.kits : [];
|
|
208
222
|
const state = args.project_state;
|
|
@@ -217,7 +231,6 @@ export function registerGameKitTools(register, options, schema = z) {
|
|
|
217
231
|
project: args.project_state,
|
|
218
232
|
dryRun: args.dry_run === true,
|
|
219
233
|
force: args.force === true,
|
|
220
|
-
acceptBreaking: args.accept_breaking === true,
|
|
221
234
|
}, 'POST', extra?.signal);
|
|
222
235
|
const data = result.structuredContent;
|
|
223
236
|
if (result.isError || !data?.bundle)
|
|
@@ -225,19 +238,19 @@ export function registerGameKitTools(register, options, schema = z) {
|
|
|
225
238
|
const withNote = { ...data, note: HOSTED_DELIVERY_NOTE };
|
|
226
239
|
return { structuredContent: withNote, content: [{ type: 'text', text: JSON.stringify(withNote) }] };
|
|
227
240
|
});
|
|
228
|
-
tool('gripforge_game_capabilities', 'Capabilities report of a project', '
|
|
241
|
+
tool('gripforge_game_capabilities', 'Capabilities report of a project', 'Report the capabilities a Game Kit project provides, what is still missing for a goal (a preset id from gripforge_gamekit_search or a comma-separated list of capabilities) and which catalogue kits would fill each gap. The installed kits (gripforge_gamekit_installed) come first; call this BEFORE gripforge_gamekit_install to pick the next kit. 0 credits.', {
|
|
229
242
|
project_id: projectId,
|
|
230
243
|
goal: schema.string().max(400).optional().describe('Preset id (e.g. from gripforge_gamekit_search presets) or comma-separated capabilities to reach. Default: the project’s own preset.'),
|
|
231
244
|
}, { readOnly: true }, (args, extra) => api(`gamekit-projects/${enc(args.project_id)}/capabilities`, args, 'GET', extra?.signal, {
|
|
232
245
|
goal: typeof args.goal === 'string' ? args.goal : undefined,
|
|
233
246
|
}));
|
|
234
|
-
tool('gripforge_game_project', 'List, create, read, bind, feed or delete Game Kit projects', 'Manage Game Kit projects (gkp_…): the container holding installed kits, their lockfile, asset bindings and data collections. action=list lists the workspace projects; create needs name and takes a preset (see gripforge_gamekit_search) or an explicit kits map; get returns the full ProjectDetail (installed kits, bindings, capabilities, missing, playUrl); bind maps asset slots to Library items (lib_…, null to clear); data reads a collection or upserts / removes documents validated against the kits’ schemas (replace=true swaps the whole collection); delete needs confirm=true. Call this first to create or pick the project, then gripforge_gamekit_install. 0 credits.', {
|
|
247
|
+
tool('gripforge_game_project', 'List, create, read, bind, feed or delete Game Kit projects', 'Manage Game Kit projects (gkp_…): the container holding installed kits, their lockfile, asset bindings and data collections. action=list lists the workspace projects; create needs name and takes a preset (see gripforge_gamekit_search; adventure is the reference game of game.engine) or an explicit kits map — every new project gets game.engine first (the engine of the game: scenes, archetypes, scripts, life cycle; its answer carries the engine start pack { structure, scene, blocks, order, next, rules } and added, the kits installed for dependencies), then the default kits ui.menu (title / pause / options / load and save screen), input.remap (key remapping), settings.graphics (graphics options), ui.prompts (on-screen key prompts that follow remapping and the gamepad), ui.endscreen (end screen and credits), save.persistence (save slots, quick save, autosave), audio.core (game audio: mixer, music, effects, event → sound table), i18n.text (game texts in the player’s language: translation tables in the translations collection, ICU plurals) ui.theme (the design of every interface as kit config: colours, font, shapes, key glyphs; unset = built-in look) and input.virtualpad (the on-screen gamepad of touch screens, mode auto: shown on a touch screen without a gamepad, Touch controls tab), addedBy default: keep and configure them, leave one out ({ "ui.menu": false }) or remove it only if the user asks; get returns the full ProjectDetail (installed kits, bindings, capabilities, missing, playUrl); bind maps asset slots to Library items (lib_…, null to clear); data reads a collection or upserts / removes documents validated against the kits’ schemas (replace=true swaps the whole collection); delete needs confirm=true. Call this first to create or pick the project, then gripforge_gamekit_installed (existing project) or gripforge_gamekit_install. 0 credits.', {
|
|
235
248
|
action: schema.enum(['list', 'create', 'get', 'bind', 'data', 'delete']).describe('list | create | get | bind | data | delete.'),
|
|
236
249
|
project_id: projectId.optional().describe('Game Kit project id (gkp_…). Required for get, bind, data and delete.'),
|
|
237
250
|
name: schema.string().min(1).max(120).optional().describe('create: project name.'),
|
|
238
251
|
preset: schema.string().max(80).optional().describe('create: preset id whose default kits are installed (gripforge_gamekit_search returns presets).'),
|
|
239
|
-
kits: schema.record(schema.string(), schema.boolean()).optional().describe('create: explicit kit toggles { "vehicle.driveable": true, … } on top of the preset. Unchecking a required kit fails with toggle_requires.'),
|
|
240
|
-
target: schema.enum(['web', 'godot', 'unity', 'unreal']).optional().describe('create: engine
|
|
252
|
+
kits: schema.record(schema.string(), schema.boolean()).optional().describe('create: explicit kit toggles { "vehicle.driveable": true, … } on top of the preset. Unchecking a required kit fails with toggle_requires. The default kits (ui.menu, input.remap, settings.graphics, ui.prompts, ui.endscreen, save.persistence, audio.core, i18n.text, ui.theme, input.virtualpad) are included unless set to false here — only when the user asked for it.'),
|
|
253
|
+
target: schema.enum(['web', 'godot', 'unity', 'unreal']).optional().describe('create: pass the confirmed engine explicitly (Three.js=web). Ask the user to choose Unity / Godot / Unreal Engine / Three.js before creating if unknown. API default web is for compatibility, not an engine choice.'),
|
|
241
254
|
bindings: schema.record(schema.string(), schema.string().nullable()).optional().describe('bind: { slot: lib_… | null } — Library item per asset slot declared by the installed kits, null clears.'),
|
|
242
255
|
collection: schema.string().max(80).optional().describe('data: collection name declared by an installed kit (e.g. missions, npcs, zones).'),
|
|
243
256
|
documents: schema.array(schema.record(schema.string(), schema.unknown())).max(500).optional().describe('data: documents to upsert (each with its id), validated against the kit schema.'),
|
|
@@ -278,6 +291,51 @@ export function registerGameKitTools(register, options, schema = z) {
|
|
|
278
291
|
}
|
|
279
292
|
return Promise.resolve(fail(`Unknown action ${action}.`));
|
|
280
293
|
});
|
|
294
|
+
tool('gripforge_game_engine', 'The engine of a game: structure, bricks, report, next steps', 'Read game.engine of a Game Kit project (gkp_…) — a direction, never a gate. action=start (default): the start pack { version, active, structure { engine, kits, content counts, code }, scene, blocks digest, order (scene → playable → assets → archetypes → story → scripts → ui → validate → deliver → code), next [{ step, tool, args, why }], code (where game code goes per engine), rules } and the full content report. action=blocks: the brick catalogue computed from the manifests — entity kinds, archetype components (kit, schema, example), script verbs (conditions / actions with arguments and shorthand), script sugars (interact, enter, leave, scene, talked, quest, state, gives), quest step types, NPC behaviours, worlds, layers, events, variables; scope=catalog adds the bricks of kits not installed (install <kit>). action=report: { ok, errors, warnings, stats } — an error means that piece is left out of the game (the rest plays), a warning that it loads with something missing; each issue may carry a fix { tool, args }. action=next: the next steps only. A game with everything in code is valid: content first, kits second, code for the rest. 0 credits.', {
|
|
295
|
+
project_id: projectId,
|
|
296
|
+
action: schema.enum(['start', 'structure', 'blocks', 'report', 'next']).optional().describe('start (default) | structure | blocks | report | next.'),
|
|
297
|
+
scope: schema.enum(['installed', 'catalog']).optional().describe('blocks: installed (default) or catalog.'),
|
|
298
|
+
scene: schema.string().max(64).optional().describe('start: also return this scene document.'),
|
|
299
|
+
}, { readOnly: true }, async (args, extra) => {
|
|
300
|
+
const action = typeof args.action === 'string' ? args.action : 'start';
|
|
301
|
+
const project = enc(args.project_id);
|
|
302
|
+
if (action === 'blocks')
|
|
303
|
+
return api(`gamekit-projects/${project}/engine/blocks`, args, 'GET', extra?.signal, { scope: typeof args.scope === 'string' ? args.scope : undefined });
|
|
304
|
+
const res = await api(`gamekit-projects/${project}/engine`, args, 'GET', extra?.signal, { scene: typeof args.scene === 'string' ? args.scene : undefined });
|
|
305
|
+
if (res.isError || action === 'start')
|
|
306
|
+
return res;
|
|
307
|
+
const data = res.structuredContent;
|
|
308
|
+
const picked = action === 'report' ? { report: data?.report } : action === 'next' ? { next: data?.engine?.next, order: data?.engine?.order } : { structure: data?.engine?.structure, scene: data?.engine?.scene, active: data?.engine?.active };
|
|
309
|
+
return { structuredContent: picked, content: [{ type: 'text', text: JSON.stringify(picked) }] };
|
|
310
|
+
});
|
|
311
|
+
tool('gripforge_game_content', 'Drop content into a game: scenes, archetypes, scripts, quests…', 'Read and write the content of a Game Kit project (gkp_…) — what game.engine builds the game from, no engine code needed. kind: archetype (an entity template: kind prop|npc|…, asset { item: lib_… } bound to content:<id> | { slot }, components claimed by kits — see gripforge_game_engine blocks — and gives { quest, offer, progress?, thanks? } for "this NPC gives this quest"), scene (world { kit, config?, doc? }, entries, instances [{ id, archetype, at, yaw?, place?, set? }], zones, layers, ui), script (gripforge.script/1: { id, on: <instance> | archetype:<id> | scene:<id>, rules: [{ when, if?, then, else?, once? }] } with sugars { "interact": "self" }, { "enter": zone }, { "state": { open: false } }, { "quest": id, "is": "completed" }, { "talked": npc }, engine verbs set_state, play_clip, set_solid, show, hide, enable_script, disable_script, load_scene, end_game, call, set_talk and kit verbs as shorthand { "give_item": "key" }), mission (alias quest), dialogue, npc, item, translations, code_module (what game code registers: verbs, components, services, entity kinds), zone, trigger. action=list (kind optional: counts), get (kind, id), put (kind, doc or docs: upsert, validated against the declaring kit schema), remove (kind, id), validate (dry run of a put: the report it would give), place (scene, archetype or asset lib_…, at, yaw?, id?, place ground|none: the placement on the ground is baked once on the server, the same on every engine), bake (re-bake the placements of a scene after its world changed). Writes return the content report after the change; the report never refuses a write — only a document invalid for its schema is. 0 credits.', {
|
|
312
|
+
project_id: projectId,
|
|
313
|
+
action: schema.enum(['list', 'get', 'put', 'remove', 'validate', 'place', 'bake']).describe('list | get | put | remove | validate | place | bake.'),
|
|
314
|
+
kind: schema.enum(['archetype', 'scene', 'script', 'mission', 'quest', 'dialogue', 'npc', 'item', 'translations', 'code_module', 'zone', 'trigger']).optional().describe('The content kind (list without kind: counts per kind).'),
|
|
315
|
+
id: schema.string().max(120).optional().describe('get / remove: the document id; place: the instance id (default <archetype>_<n>).'),
|
|
316
|
+
doc: schema.record(schema.string(), schema.unknown()).optional().describe('put / validate: the document (with its id).'),
|
|
317
|
+
docs: schema.array(schema.record(schema.string(), schema.unknown())).max(200).optional().describe('put / validate: several documents of the same kind.'),
|
|
318
|
+
scene: schema.string().max(64).optional().describe('place / bake: the scene id (place default: the first scene).'),
|
|
319
|
+
archetype: schema.string().max(64).optional().describe('place: the archetype of the instance.'),
|
|
320
|
+
asset: schema.string().max(64).optional().describe('place: a Library asset (lib_…) instead of an archetype: its archetype is the one using it, or a new prop.'),
|
|
321
|
+
at: schema.array(schema.number()).length(3).optional().describe('place: position [x, y, z] in metres (+Y up).'),
|
|
322
|
+
yaw: schema.number().optional().describe('place: rotation around +Y in radians.'),
|
|
323
|
+
place: schema.enum(['ground', 'none']).optional().describe('place: ground (default) snaps to the scene ground; none keeps y.'),
|
|
324
|
+
set: schema.record(schema.string(), schema.unknown()).optional().describe('place: component overrides of this instance.'),
|
|
325
|
+
dry_run: schema.boolean().optional().describe('put / remove: validate and report without writing.'),
|
|
326
|
+
}, { readOnly: false }, (args, extra) => {
|
|
327
|
+
const action = String(args.action);
|
|
328
|
+
const project = enc(args.project_id);
|
|
329
|
+
if (action === 'list' || action === 'get') {
|
|
330
|
+
if (action === 'get' && (typeof args.kind !== 'string' || typeof args.id !== 'string'))
|
|
331
|
+
return Promise.resolve(fail('get needs kind and id.'));
|
|
332
|
+
return api(`gamekit-projects/${project}/content`, args, 'GET', extra?.signal, { kind: typeof args.kind === 'string' ? args.kind : undefined, id: typeof args.id === 'string' ? args.id : undefined });
|
|
333
|
+
}
|
|
334
|
+
const { project_id: _p, ...payload } = args;
|
|
335
|
+
if (action === 'place')
|
|
336
|
+
return api(`gamekit-projects/${project}/content/place`, payload, 'POST', extra?.signal);
|
|
337
|
+
return api(`gamekit-projects/${project}/content`, payload, 'POST', extra?.signal);
|
|
338
|
+
});
|
|
281
339
|
tool('gripforge_game_play_url', 'Play URL of a Game Kit project', 'Get the browser play URL of a Game Kit project so a human (or a screenshot step) can run its current revision in the web runtime. Reads the project bundle and returns { url, absolute }; absolute is built on the GripForge origin of this key. Call it AFTER gripforge_gamekit_install or gripforge_game_project bind to check the result live. 0 credits.', { project_id: projectId }, { readOnly: true }, async (args, extra) => {
|
|
282
340
|
const res = await api(`gamekit-projects/${enc(args.project_id)}/bundle`, args, 'GET', extra?.signal);
|
|
283
341
|
if (res.isError)
|
|
@@ -295,4 +353,55 @@ export function registerGameKitTools(register, options, schema = z) {
|
|
|
295
353
|
const data = { url: playUrl, absolute };
|
|
296
354
|
return { structuredContent: data, content: [{ type: 'text', text: JSON.stringify(data) }] };
|
|
297
355
|
});
|
|
356
|
+
tool('gripforge_moba_roster', 'Set the champions of a MOBA project', 'Turn champions into the playable roster of a moba project (preset "moba"). Pass the Library ids of 1 to 10 champions that carry an ability pack (the items made by gripforge_abilities_generate: rigged model + basic_attack and ability_q…r clips). Writes the moba_heroes and abilities collections (ids <hero>__ability_q), binds each champion to moba_hero_<n> and the player\'s champion (player, else the first) to player_character. Bots play the other champions. Returns the heroes with their attack and Q/W/E/R. Then gripforge_game_play_url to play. 0 credits.', {
|
|
357
|
+
project_id: projectId,
|
|
358
|
+
heroes: schema.array(schema.string()).min(1).max(10).describe('Library ids (lib_…) of champions with an ability pack, in roster order.'),
|
|
359
|
+
player: schema.string().optional().describe('The champion the player controls (one of heroes); default the first.'),
|
|
360
|
+
}, { readOnly: false }, async (args, extra) => {
|
|
361
|
+
const { project_id, ...body } = args;
|
|
362
|
+
return api(`gamekit-projects/${enc(project_id)}/moba-roster`, body, 'PUT', extra?.signal);
|
|
363
|
+
});
|
|
364
|
+
tool('gripforge_ability_vfx', 'Give every ability of a game its cast effect', 'Write and bind a cast effect for each ability of a Game Kit project that has none: an area gets a ground shockwave, a line a bolt and its trail, an arc a sweeping crescent, a self cast a rising aura; colour from the ability\'s words (fire, frost, wind, holy…), size from its range, ultimates larger. The project needs the fx.ability_vfx kit (the moba preset installs it) and an abilities collection (gripforge_moba_roster writes it). Effects are procedural sources saved as VFX items and bound to ability_vfx:<ability id>, immediately. replace: true rewrites the existing ones; abilities limits to some ids. A hand-made or generated VFX bound to the same slot takes over. 0 credits.', {
|
|
365
|
+
project_id: projectId,
|
|
366
|
+
replace: schema.boolean().optional().describe('Rewrite abilities that already have an effect (default: keep them).'),
|
|
367
|
+
abilities: schema.array(schema.string()).max(80).optional().describe('Only these ability ids (default: all).'),
|
|
368
|
+
}, { readOnly: false }, async (args, extra) => {
|
|
369
|
+
const { project_id, ...body } = args;
|
|
370
|
+
return api(`gamekit-projects/${enc(project_id)}/ability-vfx`, body, 'POST', extra?.signal);
|
|
371
|
+
});
|
|
372
|
+
tool('gripforge_moba_map', 'Generate, dress, check and set the map of a MOBA project', 'The map of a moba project as a gameplay plan: bounds, team bases and spawns, 1 to 5 lanes (the minions\' paths), structure slots (towers, inhibitors, nexus), jungle zones and camp slots, objective zones, the divider (river: walkable; chasm, lava, void: crossed only at bridges), crossings, bush zones, walls, camera limits and symmetry. action "generate" builds a fair map (mirrored through the centre) from lanes, divider, size, team_size, camps_per_jungle, objectives, bushes, walls and returns it with a playability report; apply: true puts it in the project (moba_maps, the match\'s map and team size, the camera turn). action "validate" checks a map you pass, or the project\'s: every lane open end to end, spawns, structures and camps reachable, nothing out of bounds, the declared symmetry respected. action "set" stores an edited map document (refused while it has errors, unless force). action "get" returns the current map and its report. New maps default to style {theme: "sanctum", detail: "high", seed: 11, relief: "landscape"}. action "dress" previews a reusable procedural finish for the current map: jungle plateaus, river banks and paved fords, terraced bases, walkable staircases, carved structures, clustered forests, stone guardians and lilies; apply: true saves only the map document, keeping its lane layout and camera configuration; relief updates the shared terrain mesh, ground queries and stair collisions; landscape is the default, terraced keeps base terraces only, flat keeps a level arena. Themes: sanctum, wildwood, ashen; detail: low, medium, high; seed makes the finish repeatable. Bound Library models and textures take precedence. The scenery kit dresses walls and bushes with the bound models. 0 credits.', {
|
|
373
|
+
project_id: projectId,
|
|
374
|
+
action: schema.enum(['get', 'generate', 'dress', 'validate', 'set']).optional().describe('Default: get. dress previews a finish for the existing map, without rebuilding its layout; apply: true saves it.'),
|
|
375
|
+
style: schema.object({
|
|
376
|
+
relief: schema.enum(['landscape', 'terraced', 'flat']).optional().describe('Default landscape: jungle plateaus, river banks and terraced gardens; shared terrain mesh, physics and unit heights. terraced keeps only the gardens; flat keeps a level map.'),
|
|
377
|
+
theme: schema.enum(['sanctum', 'wildwood', 'ashen']).optional().describe('sanctum: pale carved stone, gold, contrasting groves and lilies; wildwood: mossy forest; ashen: weathered volcanic stone. Default: sanctum.'),
|
|
378
|
+
detail: schema.enum(['low', 'medium', 'high']).optional().describe('Procedural decoration budget, default high. Navigation is identical at all levels.'),
|
|
379
|
+
seed: schema.number().int().min(0).max(2147483646).optional().describe('Repeatable finish, default 11.'),
|
|
380
|
+
}).strict().optional().describe('generate / dress: art direction stored with the map. Paved lanes, inset seals, bevelled stone borders, banks, plants, lilies and entrance lights. Bound Library models and textures take precedence.'),
|
|
381
|
+
lanes: schema.number().int().min(1).max(5).optional().describe('generate: number of lanes (default 3).'),
|
|
382
|
+
divider: schema.enum(['river', 'chasm', 'lava', 'void', 'none']).optional().describe('generate: what separates the halves (default river).'),
|
|
383
|
+
size: schema.number().min(100).max(400).optional().describe('generate: side of the square map in metres.'),
|
|
384
|
+
team_size: schema.number().int().min(1).max(5).optional().describe('generate: heroes per team (default 3 for one lane, 5 otherwise).'),
|
|
385
|
+
camps_per_jungle: schema.number().int().min(0).max(3).optional().describe('generate: small camps in each jungle (default 2).'),
|
|
386
|
+
objectives: schema.number().int().min(0).max(4).optional().describe('generate: epic objectives on the divider.'),
|
|
387
|
+
bushes: schema.boolean().optional().describe('generate: stealth bushes along the lanes (default true).'),
|
|
388
|
+
walls: schema.boolean().optional().describe('generate: rock walls behind the camps (default true).'),
|
|
389
|
+
id: schema.string().max(41).optional().describe('generate: id of the new map (lowercase, digits, _).'),
|
|
390
|
+
name: schema.string().max(80).optional().describe('generate: display name of the map.'),
|
|
391
|
+
apply: schema.boolean().optional().describe('generate / dress: store the result in the project (default: preview only).'),
|
|
392
|
+
map: schema.record(schema.string(), schema.unknown()).optional().describe('validate / set: a full map document.'),
|
|
393
|
+
force: schema.boolean().optional().describe('set: store a map that has errors.'),
|
|
394
|
+
}, { readOnly: false }, async (args, extra) => {
|
|
395
|
+
const { project_id, ...body } = args;
|
|
396
|
+
return api(`gamekit-projects/${enc(project_id)}/moba-map`, body, 'POST', extra?.signal);
|
|
397
|
+
});
|
|
398
|
+
tool('gripforge_terrain_map_use', 'Play a terrain studio map in a game', 'Make a map of the terrain studio the world of a game that has the world.terrain kit. Pass map (a terrain studio map id, lvl_…) or scene (a scene id, with terrain when the scene has several terrain nodes). The map is baked once into a file (heights, ground roles, lakes and rivers, vegetation, roads, spawn and objective points) stored in the Library and bound to the terrain_map slot; the project data keeps its reference in terrain_maps. The same file is delivered to Godot, Unity and Unreal with the kit. Buildings and other non-vegetation props, and terrain holes, are not carried yet (counted in map.notCarried). Without a bound map world.terrain keeps generating its world from the seed. 0 credits.', {
|
|
399
|
+
project_id: projectId,
|
|
400
|
+
map: schema.string().optional().describe('Terrain studio map id (lvl_…).'),
|
|
401
|
+
scene: schema.string().optional().describe('Scene id whose terrain node becomes the world (instead of map).'),
|
|
402
|
+
terrain: schema.string().optional().describe('Terrain node id in the scene (default: its first terrain).'),
|
|
403
|
+
}, { readOnly: false }, async (args, extra) => {
|
|
404
|
+
const { project_id, ...body } = args;
|
|
405
|
+
return api(`gamekit-projects/${enc(project_id)}/terrain-map`, body, 'PUT', extra?.signal);
|
|
406
|
+
});
|
|
298
407
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
|
+
export const HERO_ABILITY_TOOL_NAMES = ['gripforge_abilities_generate'];
|
|
3
|
+
/**
|
|
4
|
+
* A hero's abilities ready in Character Studio: kit (role, basic attack, passive, Q/W/E/R), the
|
|
5
|
+
* Kimodo clip of each cast on the character, and the gripforge.abilities/1 pack bound to them.
|
|
6
|
+
* Hosted and local MCP share the same REST workflow: stage=plan (free), then stage=build.
|
|
7
|
+
*/
|
|
8
|
+
export function registerHeroAbilityTools(register, options, schema = z) {
|
|
9
|
+
const shape = {
|
|
10
|
+
stage: schema.enum(['plan', 'build']).optional().describe('plan (default, free): the kit, its abilities and clips, and the price. build: queue the GPU job.'),
|
|
11
|
+
prompt: schema.string().min(8).max(600).optional().describe('The hero in one sentence when there is no concept, e.g. "a frost archer who slows enemies, ultimate freezes an area". Its kit is written at plan.'),
|
|
12
|
+
hero_concept_id: schema.string().optional().describe('A hero concept with an ability kit (gripforge_game_concepts with hero_kits).'),
|
|
13
|
+
kit: schema.record(schema.string(), schema.unknown()).optional().describe('The kit returned by plan: pass it to build to keep exactly what was shown.'),
|
|
14
|
+
name: schema.string().min(2).max(80).optional(),
|
|
15
|
+
character_id: schema.string().optional().describe('Rigged Library character or enemy the clips and abilities go on. Default: the UAL mannequin.'),
|
|
16
|
+
workspace_id: schema.string().max(100).optional(),
|
|
17
|
+
idempotency_key: schema.string().regex(/^[a-zA-Z0-9_.:-]{8,160}$/).optional(),
|
|
18
|
+
};
|
|
19
|
+
register(HERO_ABILITY_TOOL_NAMES[0], {
|
|
20
|
+
title: 'Generate hero abilities',
|
|
21
|
+
description: 'A hero\'s abilities ready to play in Character Studio: the kit (role, lane, basic attack, passive + Q/W/E/R with type, range, cooldown, cost, effect, VFX brief, cast animation) from a hero concept or written from one sentence; the basic attack and Q/W/E/R casts animated with NVIDIA Kimodo on the rigged character; and the gripforge.abilities/1 pack (targeting, damage/heal, timings) bound to those clips and stored on the result. Call stage=plan first (free) and show the abilities and price (1 credit per clip, 5 for a kit); then stage=build with the returned kit. Poll gripforge_generation_read with job_id; result.studio_url opens the Abilities tab. Numbers are starting values, not a balance pass; VFX lines are briefs for gripforge_vfx_generate.',
|
|
22
|
+
inputSchema: shape,
|
|
23
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
24
|
+
}, async (args, extra) => {
|
|
25
|
+
const parsed = schema.object(shape).strict().safeParse(args);
|
|
26
|
+
if (!parsed.success)
|
|
27
|
+
return { isError: true, content: [{ type: 'text', text: parsed.error.message }] };
|
|
28
|
+
const key = options.getApiKey();
|
|
29
|
+
if (!key)
|
|
30
|
+
return { isError: true, content: [{ type: 'text', text: 'GripForge API key required.' }] };
|
|
31
|
+
const { workspace_id, ...body } = parsed.data;
|
|
32
|
+
try {
|
|
33
|
+
const response = await fetch(options.apiUrl.replace(/\/$/, '') + '/api/v1/abilities/generate', {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
headers: { 'content-type': 'application/json', 'x-gripforge-client': 'mcp', 'x-api-key': key, ...(typeof workspace_id === 'string' ? { 'x-workspace-id': workspace_id } : {}) },
|
|
36
|
+
body: JSON.stringify(body),
|
|
37
|
+
signal: AbortSignal.any([AbortSignal.timeout(120_000), ...(extra?.signal ? [extra.signal] : [])]),
|
|
38
|
+
});
|
|
39
|
+
const data = await response.json();
|
|
40
|
+
if (typeof data.status_url === 'string' && data.status_url.startsWith('/'))
|
|
41
|
+
data.status_url = new URL(data.status_url, options.apiUrl).href;
|
|
42
|
+
return { ...(!response.ok ? { isError: true } : {}), structuredContent: data, content: [{ type: 'text', text: JSON.stringify(data) }] };
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
return { isError: true, content: [{ type: 'text', text: error instanceof Error ? error.message : 'Ability request failed.' }] };
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
export function godotJoystickFiles(c) {
|
|
2
|
+
return [{ path: 'gripforge/joystick/virtual_stick.gd', content: `extends Control
|
|
3
|
+
## GripForge input.joystick: persistent analog state, one pointer per stick.
|
|
4
|
+
signal axis_changed(value: Vector2)
|
|
5
|
+
@export_range(0.0, 0.5) var deadzone: float = ${c.deadzone}
|
|
6
|
+
@export var accent: Color = Color("${c.accent}")
|
|
7
|
+
var axis := Vector2.ZERO
|
|
8
|
+
var visual := Vector2.ZERO
|
|
9
|
+
var pointer := -1
|
|
10
|
+
|
|
11
|
+
func _ready() -> void:
|
|
12
|
+
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
13
|
+
resized.connect(reset)
|
|
14
|
+
|
|
15
|
+
static func radial(value: Vector2, zone: float) -> Vector2:
|
|
16
|
+
var magnitude := value.length()
|
|
17
|
+
if magnitude <= zone: return Vector2.ZERO
|
|
18
|
+
return value / magnitude * minf(1.0, (magnitude - zone) / (1.0 - zone))
|
|
19
|
+
|
|
20
|
+
func update_pointer(screen_position: Vector2) -> void:
|
|
21
|
+
var local := get_global_transform_with_canvas().affine_inverse() * screen_position
|
|
22
|
+
var travel := minf(size.x, size.y) * 0.325
|
|
23
|
+
if travel <= 0.0: return
|
|
24
|
+
var raw := (local - size * 0.5) / travel
|
|
25
|
+
visual = raw.limit_length()
|
|
26
|
+
axis = radial(Vector2(raw.x, -raw.y), deadzone)
|
|
27
|
+
axis_changed.emit(axis)
|
|
28
|
+
queue_redraw()
|
|
29
|
+
|
|
30
|
+
func _input(event: InputEvent) -> void:
|
|
31
|
+
if not is_visible_in_tree():
|
|
32
|
+
reset()
|
|
33
|
+
return
|
|
34
|
+
if event is InputEventScreenTouch:
|
|
35
|
+
if event.canceled and pointer == event.index:
|
|
36
|
+
reset()
|
|
37
|
+
get_viewport().set_input_as_handled()
|
|
38
|
+
elif event.pressed and not event.canceled and pointer == -1 and get_global_rect().has_point(event.position):
|
|
39
|
+
pointer = event.index
|
|
40
|
+
update_pointer(event.position)
|
|
41
|
+
get_viewport().set_input_as_handled()
|
|
42
|
+
elif not event.pressed and pointer == event.index:
|
|
43
|
+
reset()
|
|
44
|
+
get_viewport().set_input_as_handled()
|
|
45
|
+
elif event is InputEventScreenDrag and pointer == event.index:
|
|
46
|
+
update_pointer(event.position)
|
|
47
|
+
get_viewport().set_input_as_handled()
|
|
48
|
+
elif event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
|
|
49
|
+
if event.pressed and pointer == -1 and get_global_rect().has_point(event.position):
|
|
50
|
+
pointer = -2
|
|
51
|
+
update_pointer(event.position)
|
|
52
|
+
get_viewport().set_input_as_handled()
|
|
53
|
+
elif not event.pressed and pointer == -2:
|
|
54
|
+
reset()
|
|
55
|
+
get_viewport().set_input_as_handled()
|
|
56
|
+
elif event is InputEventMouseMotion and pointer == -2:
|
|
57
|
+
update_pointer(event.position)
|
|
58
|
+
get_viewport().set_input_as_handled()
|
|
59
|
+
|
|
60
|
+
func reset() -> void:
|
|
61
|
+
pointer = -1
|
|
62
|
+
axis = Vector2.ZERO
|
|
63
|
+
visual = Vector2.ZERO
|
|
64
|
+
axis_changed.emit(axis)
|
|
65
|
+
queue_redraw()
|
|
66
|
+
|
|
67
|
+
func _notification(what: int) -> void:
|
|
68
|
+
if what == NOTIFICATION_APPLICATION_FOCUS_OUT or what == NOTIFICATION_EXIT_TREE:
|
|
69
|
+
reset()
|
|
70
|
+
|
|
71
|
+
func _draw() -> void:
|
|
72
|
+
var radius := minf(size.x, size.y) * 0.5
|
|
73
|
+
draw_circle(size * 0.5, radius, Color(0.06, 0.08, 0.11, 0.72))
|
|
74
|
+
draw_arc(size * 0.5, maxf(0, radius - 1), 0, TAU, 64, Color(1, 1, 1, 0.22), 2, true)
|
|
75
|
+
draw_circle(size * 0.5 + visual * radius * 0.65, radius * 0.28, accent)
|
|
76
|
+
` }, { path: 'gripforge/joystick/joystick_rig.gd', content: `extends CanvasLayer
|
|
77
|
+
## Add this script to a CanvasLayer. No InputMap mutation or autoload required.
|
|
78
|
+
const Stick = preload("res://gripforge/joystick/virtual_stick.gd")
|
|
79
|
+
@export var dual: bool = ${c.layout === 'dual' ? 'true' : 'false'}
|
|
80
|
+
@export_range(32, 160) var radius: float = ${c.radius}
|
|
81
|
+
@export_range(0, 0.5) var deadzone: float = ${c.deadzone}
|
|
82
|
+
var left: Control
|
|
83
|
+
var right: Control
|
|
84
|
+
|
|
85
|
+
func _ready() -> void:
|
|
86
|
+
layer = 20
|
|
87
|
+
left = Stick.new()
|
|
88
|
+
left.deadzone = deadzone
|
|
89
|
+
add_child(left)
|
|
90
|
+
if dual:
|
|
91
|
+
right = Stick.new()
|
|
92
|
+
right.deadzone = deadzone
|
|
93
|
+
add_child(right)
|
|
94
|
+
get_viewport().size_changed.connect(_layout)
|
|
95
|
+
_layout()
|
|
96
|
+
|
|
97
|
+
func _layout() -> void:
|
|
98
|
+
var view := get_viewport().get_visible_rect().size
|
|
99
|
+
var r := minf(radius, maxf(24, (view.x - 72) / (4 if dual else 2)))
|
|
100
|
+
left.size = Vector2.ONE * r * 2
|
|
101
|
+
left.position = Vector2(24, maxf(0, view.y - 24 - r * 2))
|
|
102
|
+
left.reset()
|
|
103
|
+
if right:
|
|
104
|
+
right.size = left.size
|
|
105
|
+
right.position = Vector2(view.x - 24 - r * 2, left.position.y)
|
|
106
|
+
right.reset()
|
|
107
|
+
|
|
108
|
+
func movement() -> Vector2:
|
|
109
|
+
return left.axis if is_instance_valid(left) else Vector2.ZERO
|
|
110
|
+
|
|
111
|
+
func look() -> Vector2:
|
|
112
|
+
return right.axis if is_instance_valid(right) else Vector2.ZERO
|
|
113
|
+
` }, { path: 'gripforge/joystick/joystick.tscn', content: `[gd_scene load_steps=2 format=3]
|
|
114
|
+
[ext_resource type="Script" path="res://gripforge/joystick/joystick_rig.gd" id="1"]
|
|
115
|
+
[node name="GripForgeJoystick" type="CanvasLayer"]
|
|
116
|
+
script = ExtResource("1")
|
|
117
|
+
` }, { path: 'gripforge/joystick/character_example.gd', content: `extends CharacterBody3D
|
|
118
|
+
## Optional example; merge input reads into your own motor instead of running two motors.
|
|
119
|
+
@export var joystick: CanvasLayer
|
|
120
|
+
@export var camera: Node3D
|
|
121
|
+
@export var speed: float = 5.0
|
|
122
|
+
@export var gravity: float = 9.8
|
|
123
|
+
func _physics_process(delta: float) -> void:
|
|
124
|
+
var axes: Vector2 = joystick.movement() if joystick else Vector2.ZERO
|
|
125
|
+
var yaw: float = camera.global_rotation.y if camera else global_rotation.y
|
|
126
|
+
var direction := Basis(Vector3.UP, yaw) * Vector3(axes.x, 0, -axes.y)
|
|
127
|
+
velocity.x = direction.x * speed
|
|
128
|
+
velocity.z = direction.z * speed
|
|
129
|
+
if not is_on_floor(): velocity.y -= gravity * delta
|
|
130
|
+
else: velocity.y = 0
|
|
131
|
+
move_and_slide()
|
|
132
|
+
` }, { path: 'README.md', content: `# GripForge joystick · Godot 4.2+
|
|
133
|
+
|
|
134
|
+
Copy gripforge/ into res://. Instance joystick.tscn in the gameplay scene.
|
|
135
|
+
Read movement() and look() every physics frame. X is right; Y is forward/up.
|
|
136
|
+
Assign the rig and camera in the optional CharacterBody3D example. A collider and
|
|
137
|
+
floor are still supplied by your game; the joystick does not replace collision.
|
|
138
|
+
|
|
139
|
+
Touch IDs are isolated; both sticks can be held simultaneously. Mouse testing is
|
|
140
|
+
built in: you do not need to enable Emulate Touch From Mouse. Keep only one such
|
|
141
|
+
emulation path active. Release, focus loss, resizing and removal reset the axes.
|
|
142
|
+
The rig does not erase keyboard/gamepad bindings. Combine its movement vector with
|
|
143
|
+
your existing input source in your motor (choose one source per frame, then clamp
|
|
144
|
+
the combined length to one). Apply look as angular velocity times delta once.
|
|
145
|
+
` }];
|
|
146
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { threeJoystickFiles } from './threejs.js';
|
|
2
|
+
import { godotJoystickFiles } from './godot.js';
|
|
3
|
+
import { unityJoystickFiles } from './unity.js';
|
|
4
|
+
import { unrealJoystickFiles } from './unreal.js';
|
|
5
|
+
export const joystickDefaults = { layout: 'dual', deadzone: .15, radius: 72, accent: '#ff681f' };
|
|
6
|
+
/** Radial, continuous, unit-disc axes. Y is up/forward on every target. */
|
|
7
|
+
export function joystickAxis(x, y, deadzone = .15) {
|
|
8
|
+
if (![x, y, deadzone].every(Number.isFinite) || deadzone < 0 || deadzone >= 1)
|
|
9
|
+
return { x: 0, y: 0 };
|
|
10
|
+
const length = Math.hypot(x, y);
|
|
11
|
+
if (length <= deadzone)
|
|
12
|
+
return { x: 0, y: 0 };
|
|
13
|
+
const scale = Math.min(1, (length - deadzone) / (1 - deadzone)) / length;
|
|
14
|
+
return { x: x * scale, y: y * scale };
|
|
15
|
+
}
|
|
16
|
+
export function createJoystickKit(target, options = {}) {
|
|
17
|
+
const config = { ...joystickDefaults, ...options };
|
|
18
|
+
if (!['threejs', 'godot', 'unity', 'unreal', 'all'].includes(target))
|
|
19
|
+
throw Error('Unknown joystick target.');
|
|
20
|
+
if (!['move', 'dual'].includes(config.layout) || !Number.isFinite(config.deadzone) || config.deadzone < 0 || config.deadzone > .5 ||
|
|
21
|
+
!Number.isFinite(config.radius) || config.radius < 32 || config.radius > 160 || !/^#[\da-f]{6}$/i.test(config.accent))
|
|
22
|
+
throw Error('Invalid joystick configuration.');
|
|
23
|
+
const targets = target === 'all' ? ['threejs', 'godot', 'unity', 'unreal'] : [target];
|
|
24
|
+
const factories = { threejs: threeJoystickFiles, godot: godotJoystickFiles, unity: unityJoystickFiles, unreal: unrealJoystickFiles };
|
|
25
|
+
return {
|
|
26
|
+
schema: 'gripforge.joystick-kit.v1', module: 'input.joystick', version: '1.0.0', credits: 0, config,
|
|
27
|
+
conventions: { axes: 'x right, y up/forward', range: 'unit disc; diagonals never exceed magnitude 1', deadzone: 'radial, rescaled continuously outside the dead zone', holding: 'value persists every frame until release/cancel/focus loss', ownership: 'one pointer per stick; movement and look can be held simultaneously', physics: 'Feeds the existing pawn/controller; examples are optional.' },
|
|
28
|
+
deliveries: targets.map(engine => ({ engine, files: factories[engine](config) })),
|
|
29
|
+
status: 'files_generated',
|
|
30
|
+
next: 'Write each delivery file at its relative path, then follow that target README. Install only one movement driver. Generation does not modify your project or claim an in-game verification.',
|
|
31
|
+
};
|
|
32
|
+
}
|