@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.
Files changed (37) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +82 -2
  3. package/dist/ability-tools.js +21 -0
  4. package/dist/architecture-tools.js +83 -0
  5. package/dist/asset-production-tools.js +101 -0
  6. package/dist/creature-rig-tools.js +41 -0
  7. package/dist/game-concept-tools.js +56 -0
  8. package/dist/game-creation-tools.js +45 -0
  9. package/dist/gamekit-deliver-local.js +5 -7
  10. package/dist/gamekit-tools.js +133 -24
  11. package/dist/hero-ability-tools.js +48 -0
  12. package/dist/joystick-kit/godot.js +146 -0
  13. package/dist/joystick-kit/index.js +32 -0
  14. package/dist/joystick-kit/threejs.js +93 -0
  15. package/dist/joystick-kit/unity.js +182 -0
  16. package/dist/joystick-kit/unreal.js +231 -0
  17. package/dist/joystick-tools.js +26 -0
  18. package/dist/look.js +6 -0
  19. package/dist/map-unreal-export-local.js +285 -0
  20. package/dist/map-unreal-import-local.js +421 -0
  21. package/dist/map-unreal-local.js +75 -0
  22. package/dist/motion-tools.js +50 -0
  23. package/dist/projectile-tools.js +28 -0
  24. package/dist/scene-tools.js +79 -13
  25. package/dist/server-tools.js +3 -3
  26. package/dist/server.js +139 -13
  27. package/dist/shield-vfx-tools.js +29 -0
  28. package/dist/studio-tools-tools.js +106 -0
  29. package/dist/unreal-export-package.js +212 -0
  30. package/dist/vfx-project-tools.js +2 -1
  31. package/dist/weapon-motion-tools.js +34 -0
  32. package/package.json +6 -1
  33. package/runtime/unreal-scene-export.py +341 -0
  34. package/runtime/unreal-scene-import.py +622 -0
  35. package/runtime/unreal-scene-upload.mjs +7 -0
  36. package/runtime/unreal_scene_math.py +96 -0
  37. package/runtime/unreal_spawn_review.py +135 -0
@@ -0,0 +1,96 @@
1
+ """Renderer-independent scene transforms used by the Unreal scene worker (metres, Y up)."""
2
+ import math
3
+
4
+
5
+ def exposure_brightness(ev100, extended_range):
6
+ """UE's same property means EV100 in extended projects, luminance in legacy projects."""
7
+ return ev100 if extended_range else 2.0 ** ev100
8
+
9
+
10
+ def identity():
11
+ return [[1 if i == j else 0 for j in range(4)] for i in range(4)]
12
+
13
+
14
+ def multiply(a, b):
15
+ return [[sum(a[i][k] * b[k][j] for k in range(4)) for j in range(4)] for i in range(4)]
16
+
17
+
18
+ def matrix(transform):
19
+ t, q, s = (transform[k] for k in ('translation', 'rotation', 'scale'))
20
+ x, y, z, w = (q[k] for k in 'xyzw')
21
+ length = math.sqrt(x*x + y*y + z*z + w*w)
22
+ if length < 1e-9:
23
+ raise ValueError('Zero scene quaternion')
24
+ x, y, z, w = (v / length for v in (x, y, z, w))
25
+ r = [[1-2*(y*y+z*z), 2*(x*y-z*w), 2*(x*z+y*w), t['x']],
26
+ [2*(x*y+z*w), 1-2*(x*x+z*z), 2*(y*z-x*w), t['y']],
27
+ [2*(x*z-y*w), 2*(y*z+x*w), 1-2*(x*x+y*y), t['z']], [0, 0, 0, 1]]
28
+ for i in range(3):
29
+ for j, key in enumerate('xyz'):
30
+ r[i][j] *= s[key]
31
+ return r
32
+
33
+
34
+ def world_matrices(nodes):
35
+ by_id, done, visiting = {n['id']: n for n in nodes}, {}, set()
36
+ def visit(key):
37
+ if key in done:
38
+ return done[key]
39
+ if key in visiting:
40
+ raise ValueError('Cyclic scene hierarchy at ' + key)
41
+ visiting.add(key)
42
+ node = by_id[key]
43
+ parent = visit(node['parentId']) if node.get('parentId') else identity()
44
+ done[key] = multiply(parent, matrix(node['transform']))
45
+ visiting.remove(key)
46
+ return done[key]
47
+ for node in nodes:
48
+ visit(node['id'])
49
+ return done
50
+
51
+
52
+ def point(m, p):
53
+ return [sum(m[i][j] * p[j] for j in range(3)) + m[i][3] for i in range(3)]
54
+
55
+
56
+ def ue_matrix(m):
57
+ # Same axis conversion as Unreal's GLTFCore importer: (x, y, z) -> (x, z, y).
58
+ order = [0, 2, 1, 3]
59
+ converted = [[m[order[i]][order[j]] for j in range(4)] for i in range(4)]
60
+ for i in range(3):
61
+ converted[i][3] *= 100
62
+ return converted
63
+
64
+
65
+ def decompose(m):
66
+ s = [math.sqrt(sum(m[i][j]**2 for i in range(3))) for j in range(3)]
67
+ if min(s) < 1e-8:
68
+ raise ValueError('Zero-scale scene instance')
69
+ det = (m[0][0]*(m[1][1]*m[2][2]-m[1][2]*m[2][1])
70
+ - m[0][1]*(m[1][0]*m[2][2]-m[1][2]*m[2][0])
71
+ + m[0][2]*(m[1][0]*m[2][1]-m[1][1]*m[2][0]))
72
+ if det < 0:
73
+ s[0] *= -1
74
+ r = [[m[i][j] / s[j] for j in range(3)] for i in range(3)]
75
+ if any(abs(sum(r[k][i]*r[k][j] for k in range(3))) > 1e-4 for i in range(3) for j in range(i+1, 3)):
76
+ raise ValueError('Sheared hierarchy requires baking geometry before Unreal import')
77
+ trace = sum(r[i][i] for i in range(3))
78
+ if trace > 0:
79
+ k = math.sqrt(trace+1)*2
80
+ q = [(r[2][1]-r[1][2])/k, (r[0][2]-r[2][0])/k, (r[1][0]-r[0][1])/k, k/4]
81
+ else:
82
+ i = max(range(3), key=lambda a: r[a][a]); j, h = (i+1)%3, (i+2)%3
83
+ k = math.sqrt(1+r[i][i]-r[j][j]-r[h][h])*2
84
+ q = [0, 0, 0, (r[h][j]-r[j][h])/k]
85
+ q[i], q[j], q[h] = k/4, (r[j][i]+r[i][j])/k, (r[h][i]+r[i][h])/k
86
+ return [m[i][3] for i in range(3)], q, s
87
+
88
+
89
+ def native_transform(scene_matrix, source=None):
90
+ m = ue_matrix(scene_matrix)
91
+ if source and source.get('pivot') == 'bounds_base':
92
+ origin, extent = source['origin'], source['extent']
93
+ anchor = [origin[0], origin[1], origin[2]-extent[2]]
94
+ for i in range(3):
95
+ m[i][3] -= sum(m[i][j] * anchor[j] for j in range(3))
96
+ return decompose(m)
@@ -0,0 +1,135 @@
1
+ """Bounded PIE spawn review. Importable without Unreal for report regression tests."""
2
+ import math
3
+ import time
4
+
5
+
6
+ def assess_spawn_samples(starts, samples, duration=6, tolerance_cm=200, require_grounded=True):
7
+ """Native centimetres, simulation seconds; never infer collision from a mesh flag."""
8
+ report = dict(status='failed', expected_starts=starts, samples=samples, issues=[],
9
+ checks=['spawn_position', 'fall_distance'] + (['character_grounding'] if require_grounded else []))
10
+ issues = report['issues']
11
+ if not starts:
12
+ issues.append('no_authored_player_start')
13
+ if not samples:
14
+ issues.append('no_player_pawn')
15
+ return report
16
+ first, last = samples[0], samples[-1]
17
+ nearest = min(starts, key=lambda s: math.dist(s['position'], first['position'])) if starts else None
18
+ if nearest:
19
+ report['matched_start'] = nearest['id']
20
+ report['initial_distance_cm'] = math.dist(nearest['position'], first['position'])
21
+ if report['initial_distance_cm'] > tolerance_cm:
22
+ issues.append('game_mode_ignored_player_start')
23
+ if min(s['position'][2] for s in samples) < nearest['position'][2] - tolerance_cm:
24
+ issues.append('fell_below_spawn')
25
+ if last['time'] - first['time'] < duration - .05:
26
+ issues.append('observation_incomplete')
27
+ if len({s['pawn'] for s in samples}) != 1:
28
+ issues.append('pawn_replaced_during_review')
29
+ if require_grounded:
30
+ tail = [s for s in samples if s['time'] >= last['time'] - 1]
31
+ if any(s.get('grounded') is None for s in samples):
32
+ issues.append('ground_check_requires_character')
33
+ elif len(tail) < 2 or tail[-1]['time'] - tail[0]['time'] < .5 or not all(s['grounded'] and not s['falling'] for s in tail):
34
+ issues.append('character_not_stably_grounded')
35
+ report['status'] = 'failed' if issues else 'passed'
36
+ return report
37
+
38
+
39
+ class SpawnReview:
40
+ """Owns only the PIE session it starts. Caller polls from a Slate tick."""
41
+
42
+ def __init__(self, starts, duration=6, tolerance_cm=200, require_grounded=True):
43
+ import unreal
44
+ self.ue = unreal
45
+ self.levels = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
46
+ self.editor = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
47
+ self.starts, self.duration, self.tolerance = starts, duration, tolerance_cm
48
+ self.require_grounded = require_grounded
49
+ self.samples, self.settings_before = [], {}
50
+ self.started_at = None
51
+ self.owns_session = False
52
+ self.result = None
53
+ self.settings = None
54
+ self.world_path = None
55
+
56
+ def start(self):
57
+ if self.levels.is_in_play_in_editor() or self.editor.get_game_world():
58
+ return False # Never take over or stop the user's play session.
59
+ ue = self.ue
60
+ # This editor-only class is not exported as unreal.LevelEditorPlaySettings
61
+ # in every UE distribution. Resolve its reflected native class explicitly.
62
+ self.settings = ue.get_default_object(ue.load_class(None, '/Script/UnrealEd.LevelEditorPlaySettings'))
63
+ # UE 5.7 does not expose EPlayNetMode to Python. Preserve that setting
64
+ # and check the resulting world's mode instead of guessing its value.
65
+ values = dict(PlayNumberOfClients=1,
66
+ RunUnderOneProcess=True, bLaunchSeparateServer=False,
67
+ GameGetsMouseControl=False, AutoRecompileBlueprints=False)
68
+ try:
69
+ for key, value in values.items():
70
+ self.settings_before[key] = self.settings.get_editor_property(key)
71
+ self.settings.set_editor_property(key, value)
72
+ # LevelEditorSubsystem requests the default PlayerStart, with no
73
+ # camera-location override. This must exercise the real GameMode.
74
+ self.levels.editor_request_begin_play()
75
+ self.owns_session = True
76
+ self.started_at = time.monotonic()
77
+ except Exception:
78
+ self.close()
79
+ raise
80
+ return True
81
+
82
+ def close(self):
83
+ if self.owns_session:
84
+ self.levels.editor_request_end_play()
85
+ self.owns_session = False
86
+ if self.settings:
87
+ for key, value in self.settings_before.items():
88
+ self.settings.set_editor_property(key, value)
89
+ self.settings_before.clear()
90
+
91
+ def finish(self, extra_issue=None):
92
+ self.result = assess_spawn_samples(self.starts, self.samples, self.duration, self.tolerance, self.require_grounded)
93
+ if extra_issue:
94
+ self.result['issues'].append(extra_issue)
95
+ self.result['status'] = 'failed'
96
+ self.result['world'] = self.world_path
97
+ self.close()
98
+ return self.result
99
+
100
+ def poll(self):
101
+ if self.result is not None:
102
+ return self.result
103
+ if self.started_at is None:
104
+ raise RuntimeError('Start the spawn review before polling.')
105
+ if time.monotonic() - self.started_at > 60:
106
+ return self.finish('pie_review_timeout')
107
+ world = self.editor.get_game_world()
108
+ if not world:
109
+ return self.finish('play_session_ended_early') if self.world_path else None
110
+ self.world_path = world.get_path_name()
111
+ ue = self.ue
112
+ if not ue.SystemLibrary.is_standalone(world):
113
+ return self.finish('standalone_play_mode_required')
114
+ pawn = ue.GameplayStatics.get_player_pawn(world, 0)
115
+ if not pawn:
116
+ return self.finish('player_pawn_disappeared') if self.samples else None
117
+ sim_time = ue.GameplayStatics.get_time_seconds(world)
118
+ if self.samples and sim_time - self.samples[-1]['time'] < .2:
119
+ return None
120
+ position = pawn.get_actor_location()
121
+ movement = pawn.get_component_by_class(ue.CharacterMovementComponent)
122
+ sample = dict(time=sim_time, pawn=pawn.get_path_name(), position=[position.x, position.y, position.z],
123
+ grounded=movement.is_moving_on_ground() if movement else None,
124
+ falling=movement.is_falling() if movement else None)
125
+ self.samples.append(sample)
126
+ if len(self.samples) == 1:
127
+ mode = ue.GameplayStatics.get_game_mode(world)
128
+ self.game_mode = mode.get_class().get_path_name() if mode else None
129
+ complete = sim_time - self.samples[0]['time'] >= self.duration
130
+ wrong_spawn = min(math.dist(s['position'], self.samples[0]['position']) for s in self.starts) > self.tolerance
131
+ if complete or wrong_spawn:
132
+ result = self.finish()
133
+ result['game_mode'] = self.game_mode
134
+ return result
135
+ return None