@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
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
export function threeJoystickFiles(c) {
|
|
2
|
+
return [{ path: 'gripforge/joystick.mjs', content: `// GripForge input.joystick 1.0.0 — no framework or renderer dependency.
|
|
3
|
+
export const defaults = ${JSON.stringify(c)};
|
|
4
|
+
export function radialAxis(x,y,deadzone=defaults.deadzone) {
|
|
5
|
+
const m=Math.hypot(x,y);
|
|
6
|
+
if(!Number.isFinite(m)||m<=deadzone) return {x:0,y:0};
|
|
7
|
+
const k=Math.min(1,(m-deadzone)/(1-deadzone))/m;
|
|
8
|
+
return {x:x*k,y:y*k};
|
|
9
|
+
}
|
|
10
|
+
export function createJoystick({parent=document.body,...options}={}) {
|
|
11
|
+
const cfg={...defaults,...options};
|
|
12
|
+
if(!['move','dual'].includes(cfg.layout)||!Number.isFinite(cfg.deadzone)||cfg.deadzone<0||cfg.deadzone>.5||!Number.isFinite(cfg.radius)||cfg.radius<32||cfg.radius>160||!/^#[a-f0-9]{6}$/i.test(cfg.accent)) throw Error('Invalid joystick options');
|
|
13
|
+
const root=document.createElement('div');
|
|
14
|
+
root.dataset.gripforgeJoystick='';
|
|
15
|
+
Object.assign(root.style,{position:'fixed',inset:'0',pointerEvents:'none',zIndex:'30'});
|
|
16
|
+
const state={move:{x:0,y:0},look:{x:0,y:0}};
|
|
17
|
+
const sticks=[];
|
|
18
|
+
function make(name,right) {
|
|
19
|
+
const base=document.createElement('div'),thumb=document.createElement('div');
|
|
20
|
+
base.dataset.stick=name; base.setAttribute('aria-label',name==='move'?'Movement joystick':'Camera joystick');
|
|
21
|
+
base.setAttribute('role','group');
|
|
22
|
+
Object.assign(base.style,{position:'absolute',bottom:'max(24px, env(safe-area-inset-bottom))',width:'min('+cfg.radius*2+'px, calc((100vw - 72px) / 2))',aspectRatio:'1',borderRadius:'50%',background:'rgba(15,20,28,.72)',boxShadow:'inset 0 0 0 2px rgba(255,255,255,.22)',touchAction:'none',userSelect:'none',pointerEvents:'auto'});
|
|
23
|
+
base.style[right?'right':'left']='max(24px, env(safe-area-inset-'+(right?'right':'left')+'))';
|
|
24
|
+
Object.assign(thumb.style,{position:'absolute',left:'36%',top:'36%',width:'28%',height:'28%',borderRadius:'50%',background:cfg.accent,boxShadow:'0 0 16px '+cfg.accent+'55',pointerEvents:'none'});
|
|
25
|
+
base.append(thumb);root.append(base);
|
|
26
|
+
const stick={base,thumb,pointer:null}; sticks.push(stick);
|
|
27
|
+
function reset(){
|
|
28
|
+
const pointer=stick.pointer;stick.pointer=null;state[name]={x:0,y:0};thumb.style.transform='';
|
|
29
|
+
if(pointer!==null&&base.hasPointerCapture(pointer))base.releasePointerCapture(pointer);
|
|
30
|
+
}
|
|
31
|
+
stick.reset=reset;
|
|
32
|
+
function update(e){
|
|
33
|
+
const r=base.getBoundingClientRect(),travel=Math.min(r.width,r.height)*.325;
|
|
34
|
+
if(travel<=0){reset();return;}
|
|
35
|
+
const x=(e.clientX-r.left-r.width/2)/travel,y=-(e.clientY-r.top-r.height/2)/travel;
|
|
36
|
+
state[name]=radialAxis(x,y,cfg.deadzone);
|
|
37
|
+
const m=Math.max(1,Math.hypot(x,y));
|
|
38
|
+
thumb.style.transform='translate('+(x/m*travel)+'px,'+(-y/m*travel)+'px)';
|
|
39
|
+
}
|
|
40
|
+
base.addEventListener('pointerdown',e=>{
|
|
41
|
+
if(stick.pointer!==null||(e.pointerType==='mouse'&&e.button!==0))return;
|
|
42
|
+
e.preventDefault();e.stopPropagation();stick.pointer=e.pointerId;base.setPointerCapture(e.pointerId);update(e);
|
|
43
|
+
});
|
|
44
|
+
base.addEventListener('pointermove',e=>{if(stick.pointer===e.pointerId){e.preventDefault();e.stopPropagation();update(e);}});
|
|
45
|
+
for(const event of ['pointerup','pointercancel','lostpointercapture'])base.addEventListener(event,e=>{if(stick.pointer===e.pointerId){e.stopPropagation();reset();}});
|
|
46
|
+
}
|
|
47
|
+
make('move',false);if(cfg.layout==='dual')make('look',true);
|
|
48
|
+
parent.append(root);
|
|
49
|
+
const reset=()=>sticks.forEach(s=>s.reset());
|
|
50
|
+
const onVisibility=()=>{if(document.hidden)reset();};
|
|
51
|
+
window.addEventListener('blur',reset);window.addEventListener('resize',reset);document.addEventListener('visibilitychange',onVisibility);
|
|
52
|
+
return {root,read:()=>({move:{...state.move},look:{...state.look}}),reset,
|
|
53
|
+
dispose(){reset();window.removeEventListener('blur',reset);window.removeEventListener('resize',reset);document.removeEventListener('visibilitychange',onVisibility);root.remove();}};
|
|
54
|
+
}
|
|
55
|
+
// Optional GripForge KitHost bridge. Does not overwrite physical gamepad state.
|
|
56
|
+
export function bindGripForgeInput(joystick,input,source='touch-joystick') {
|
|
57
|
+
if(typeof input.setVirtualAxis!=='function')throw Error('Update GripForge input.actions for named virtual axes.');
|
|
58
|
+
return {update(){const s=joystick.read();for(const [name,value] of Object.entries({move_x:s.move.x,move_y:s.move.y,look_x:s.look.x,look_y:s.look.y}))input.setVirtualAxis(name,value,source);},
|
|
59
|
+
dispose(){input.clearVirtualInput(source);joystick.reset();}};
|
|
60
|
+
}
|
|
61
|
+
` }, { path: 'gripforge/three-joystick-example.mjs', content: `import * as THREE from 'three';
|
|
62
|
+
import { createJoystick } from './joystick.mjs';
|
|
63
|
+
export const joystick=createJoystick();
|
|
64
|
+
const forward=new THREE.Vector3(),right=new THREE.Vector3();
|
|
65
|
+
// Feed this direction to your capsule/character motor's normal physics update.
|
|
66
|
+
export function readMovement(camera) {
|
|
67
|
+
const {move,look}=joystick.read();
|
|
68
|
+
camera.getWorldDirection(forward);forward.y=0;
|
|
69
|
+
if(forward.lengthSq()<.00001)forward.set(0,0,-1);else forward.normalize();
|
|
70
|
+
right.crossVectors(forward,THREE.Object3D.DEFAULT_UP).normalize();
|
|
71
|
+
return {direction:new THREE.Vector3().addScaledVector(right,move.x).addScaledVector(forward,move.y),look};
|
|
72
|
+
}
|
|
73
|
+
// Each physics tick: motor.setDesiredDirection(readMovement(camera).direction).
|
|
74
|
+
// Camera: yaw -= look.x * radiansPerSecond * dt; pitch += look.y * radiansPerSecond * dt.
|
|
75
|
+
// On unmount: joystick.dispose(). No teleporting or replacement of collision/network code.
|
|
76
|
+
` }, { path: 'README.md', content: `# GripForge joystick · Three.js
|
|
77
|
+
|
|
78
|
+
Copy gripforge/ into your browser project. Import createJoystick() from joystick.mjs;
|
|
79
|
+
call read() every simulation frame. The overlay works with touch and mouse, holds
|
|
80
|
+
each finger independently and resets on release, cancellation, blur and disposal.
|
|
81
|
+
Outputs are move/look vectors in a unit disc, X right and Y forward/up.
|
|
82
|
+
|
|
83
|
+
The Three.js example converts movement to camera-relative world space. Pass that
|
|
84
|
+
vector to your existing physics motor. Its look vector is an angular velocity;
|
|
85
|
+
multiply by dt once. Destroy the overlay when unloading the game.
|
|
86
|
+
|
|
87
|
+
For a GripForge KitHost use bindGripForgeInput(), call its update() before
|
|
88
|
+
host.tick(), and dispose both the binding and joystick. Register look_x/look_y
|
|
89
|
+
actions when using a camera kit. Virtual axes bypass the extra hardware noise
|
|
90
|
+
threshold because the joystick already applies its radial deadzone. Physical
|
|
91
|
+
gamepad state remains independent.
|
|
92
|
+
` }];
|
|
93
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
export function unityJoystickFiles(c) {
|
|
2
|
+
const dir = 'Assets/GripForge/Joystick/';
|
|
3
|
+
return [{ path: dir + 'GripForgeVirtualStick.cs', content: `using UnityEngine;
|
|
4
|
+
using UnityEngine.EventSystems;
|
|
5
|
+
|
|
6
|
+
namespace GripForge.Inputs {
|
|
7
|
+
// One owner per stick; holding a finger does not require new drag events.
|
|
8
|
+
public sealed class GripForgeVirtualStick : MonoBehaviour, IPointerDownHandler,
|
|
9
|
+
IDragHandler, IPointerUpHandler, IEndDragHandler, IInitializePotentialDragHandler {
|
|
10
|
+
[Range(0, .5f)] public float Deadzone = ${c.deadzone}f;
|
|
11
|
+
public RectTransform Thumb;
|
|
12
|
+
public Vector2 Axis { get; private set; }
|
|
13
|
+
int? owner;
|
|
14
|
+
RectTransform Rect => (RectTransform)transform;
|
|
15
|
+
|
|
16
|
+
public static Vector2 Radial(Vector2 value, float deadzone) {
|
|
17
|
+
float m = value.magnitude;
|
|
18
|
+
if (float.IsNaN(m) || float.IsInfinity(m) || m <= deadzone) return Vector2.zero;
|
|
19
|
+
return value / m * Mathf.Min(1, (m - deadzone) / (1 - deadzone));
|
|
20
|
+
}
|
|
21
|
+
public void OnInitializePotentialDrag(PointerEventData e) { e.useDragThreshold = false; }
|
|
22
|
+
public void OnPointerDown(PointerEventData e) {
|
|
23
|
+
if (owner.HasValue || e.button != PointerEventData.InputButton.Left) return;
|
|
24
|
+
owner = e.pointerId;
|
|
25
|
+
UpdatePointer(e);
|
|
26
|
+
}
|
|
27
|
+
public void OnDrag(PointerEventData e) { if (owner == e.pointerId) UpdatePointer(e); }
|
|
28
|
+
public void OnPointerUp(PointerEventData e) { if (owner == e.pointerId) ResetStick(); }
|
|
29
|
+
public void OnEndDrag(PointerEventData e) { if (owner == e.pointerId) ResetStick(); }
|
|
30
|
+
void UpdatePointer(PointerEventData e) {
|
|
31
|
+
if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(Rect, e.position, e.pressEventCamera, out Vector2 p)) return;
|
|
32
|
+
float travel = Mathf.Min(Rect.rect.width, Rect.rect.height) * .325f;
|
|
33
|
+
if (travel <= 0) { ResetStick(); return; }
|
|
34
|
+
Vector2 raw = (p - Rect.rect.center) / travel;
|
|
35
|
+
Axis = Radial(raw, Deadzone);
|
|
36
|
+
if (Thumb) Thumb.anchoredPosition = Vector2.ClampMagnitude(raw, 1) * travel;
|
|
37
|
+
}
|
|
38
|
+
public void ResetStick() { owner = null; Axis = Vector2.zero; if (Thumb) Thumb.anchoredPosition = Vector2.zero; }
|
|
39
|
+
void OnDisable() { ResetStick(); }
|
|
40
|
+
void OnApplicationFocus(bool focused) { if (!focused) ResetStick(); }
|
|
41
|
+
void OnApplicationPause(bool paused) { if (paused) ResetStick(); }
|
|
42
|
+
void OnRectTransformDimensionsChange() { ResetStick(); }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
` }, { path: dir + 'GripForgeJoystickRig.cs', content: `using UnityEngine;
|
|
46
|
+
using UnityEngine.UI;
|
|
47
|
+
using UnityEngine.EventSystems;
|
|
48
|
+
#if ENABLE_INPUT_SYSTEM
|
|
49
|
+
using UnityEngine.InputSystem.UI;
|
|
50
|
+
#endif
|
|
51
|
+
|
|
52
|
+
namespace GripForge.Inputs {
|
|
53
|
+
// Add this component to a scene GameObject. It creates its own UI only.
|
|
54
|
+
public sealed class GripForgeJoystickRig : MonoBehaviour {
|
|
55
|
+
public bool Dual = ${c.layout === 'dual' ? 'true' : 'false'};
|
|
56
|
+
[Range(32, 160)] public float Radius = ${c.radius}f;
|
|
57
|
+
[Range(0, .5f)] public float Deadzone = ${c.deadzone}f;
|
|
58
|
+
public Vector2 Move => left ? left.Axis : Vector2.zero;
|
|
59
|
+
public Vector2 Look => right ? right.Axis : Vector2.zero;
|
|
60
|
+
GripForgeVirtualStick left, right;
|
|
61
|
+
GameObject ui;
|
|
62
|
+
RectTransform safe;
|
|
63
|
+
Rect lastSafe;
|
|
64
|
+
Vector2Int lastScreen;
|
|
65
|
+
Texture2D circleTexture;
|
|
66
|
+
Sprite circle;
|
|
67
|
+
|
|
68
|
+
void Awake() {
|
|
69
|
+
circleTexture = new Texture2D(64, 64, TextureFormat.RGBA32, false);
|
|
70
|
+
circleTexture.wrapMode = TextureWrapMode.Clamp;
|
|
71
|
+
for (int y=0;y<64;y++) for (int x=0;x<64;x++) {
|
|
72
|
+
float alpha = Mathf.Clamp01(32 - Vector2.Distance(new Vector2(x+.5f,y+.5f), new Vector2(32,32)));
|
|
73
|
+
circleTexture.SetPixel(x,y,new Color(1,1,1,alpha));
|
|
74
|
+
}
|
|
75
|
+
circleTexture.Apply();
|
|
76
|
+
circle = Sprite.Create(circleTexture,new Rect(0,0,64,64),new Vector2(.5f,.5f),64);
|
|
77
|
+
ui = new GameObject("GripForge Joystick UI",typeof(RectTransform),typeof(Canvas),typeof(CanvasScaler),typeof(GraphicRaycaster));
|
|
78
|
+
ui.transform.SetParent(transform,false);
|
|
79
|
+
ui.GetComponent<Canvas>().renderMode = RenderMode.ScreenSpaceOverlay;
|
|
80
|
+
ui.GetComponent<Canvas>().sortingOrder = 20;
|
|
81
|
+
var scaler = ui.GetComponent<CanvasScaler>();
|
|
82
|
+
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
|
|
83
|
+
scaler.referenceResolution = new Vector2(1280,720); scaler.matchWidthOrHeight = .5f;
|
|
84
|
+
safe = new GameObject("Safe Area",typeof(RectTransform)).GetComponent<RectTransform>();
|
|
85
|
+
safe.SetParent(ui.transform,false);
|
|
86
|
+
left = MakeStick("Move",false);
|
|
87
|
+
if (Dual) right = MakeStick("Look",true);
|
|
88
|
+
// Reuse the game's EventSystem; do not replace configured input modules.
|
|
89
|
+
if (!FindObjectOfType<EventSystem>()) {
|
|
90
|
+
var events = new GameObject("GripForge EventSystem",typeof(EventSystem));
|
|
91
|
+
events.transform.SetParent(transform,false);
|
|
92
|
+
#if ENABLE_INPUT_SYSTEM
|
|
93
|
+
var input = events.AddComponent<InputSystemUIInputModule>();
|
|
94
|
+
input.AssignDefaultActions();
|
|
95
|
+
#else
|
|
96
|
+
events.AddComponent<StandaloneInputModule>();
|
|
97
|
+
#endif
|
|
98
|
+
}
|
|
99
|
+
UpdateLayout();
|
|
100
|
+
}
|
|
101
|
+
GripForgeVirtualStick MakeStick(string name, bool rightSide) {
|
|
102
|
+
var go = new GameObject(name,typeof(RectTransform),typeof(Image),typeof(GripForgeVirtualStick));
|
|
103
|
+
var r = (RectTransform)go.transform; r.SetParent(safe,false);
|
|
104
|
+
r.anchorMin = r.anchorMax = new Vector2(rightSide ? 1 : 0,0);
|
|
105
|
+
r.pivot = new Vector2(.5f,.5f);
|
|
106
|
+
var image = go.GetComponent<Image>(); image.sprite = circle; image.color = new Color(.06f,.08f,.11f,.72f);
|
|
107
|
+
var thumb = new GameObject("Thumb",typeof(RectTransform),typeof(Image));
|
|
108
|
+
var tr = (RectTransform)thumb.transform; tr.SetParent(r,false);
|
|
109
|
+
tr.anchorMin = tr.anchorMax = tr.pivot = new Vector2(.5f,.5f);
|
|
110
|
+
var ti = thumb.GetComponent<Image>(); ti.sprite = circle; ti.raycastTarget = false;
|
|
111
|
+
ColorUtility.TryParseHtmlString("${c.accent}",out Color accent); ti.color = accent;
|
|
112
|
+
var stick = go.GetComponent<GripForgeVirtualStick>(); stick.Thumb = tr; stick.Deadzone = Deadzone;
|
|
113
|
+
return stick;
|
|
114
|
+
}
|
|
115
|
+
void Update() {
|
|
116
|
+
if (lastSafe != Screen.safeArea || lastScreen != new Vector2Int(Screen.width,Screen.height)) UpdateLayout();
|
|
117
|
+
}
|
|
118
|
+
void UpdateLayout() {
|
|
119
|
+
if (!safe || Screen.width <= 0 || Screen.height <= 0) return;
|
|
120
|
+
lastSafe = Screen.safeArea; lastScreen = new Vector2Int(Screen.width,Screen.height);
|
|
121
|
+
safe.anchorMin = new Vector2(lastSafe.xMin/Screen.width,lastSafe.yMin/Screen.height);
|
|
122
|
+
safe.anchorMax = new Vector2(lastSafe.xMax/Screen.width,lastSafe.yMax/Screen.height);
|
|
123
|
+
safe.offsetMin = safe.offsetMax = Vector2.zero;
|
|
124
|
+
Canvas.ForceUpdateCanvases();
|
|
125
|
+
float radius = Mathf.Min(Radius, Mathf.Max(24,(safe.rect.width-72)/(Dual?4:2)));
|
|
126
|
+
Layout(left,false,radius); if(right) Layout(right,true,radius);
|
|
127
|
+
}
|
|
128
|
+
void Layout(GripForgeVirtualStick stick, bool rightSide, float radius) {
|
|
129
|
+
var r = (RectTransform)stick.transform;
|
|
130
|
+
r.sizeDelta = Vector2.one * radius * 2;
|
|
131
|
+
r.anchoredPosition = new Vector2((rightSide?-1:1)*(24+radius),24+radius);
|
|
132
|
+
stick.Thumb.sizeDelta = Vector2.one * radius * .56f;
|
|
133
|
+
stick.ResetStick();
|
|
134
|
+
}
|
|
135
|
+
void OnEnable() { if(ui) ui.SetActive(true); }
|
|
136
|
+
void OnDisable() { if(ui) ui.SetActive(false); }
|
|
137
|
+
void OnDestroy() {
|
|
138
|
+
if(ui) Destroy(ui); if(circle) Destroy(circle); if(circleTexture) Destroy(circleTexture);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
` }, { path: dir + 'GripForgeJoystickMotorExample.cs', content: `using UnityEngine;
|
|
143
|
+
namespace GripForge.Inputs {
|
|
144
|
+
// Optional example. Prefer reading Joystick.Move in the motor already in your game.
|
|
145
|
+
[RequireComponent(typeof(CharacterController))]
|
|
146
|
+
public sealed class GripForgeJoystickMotorExample : MonoBehaviour {
|
|
147
|
+
public GripForgeJoystickRig Joystick;
|
|
148
|
+
public Transform View;
|
|
149
|
+
public float Speed = 5, Gravity = -20;
|
|
150
|
+
CharacterController motor;
|
|
151
|
+
float vertical;
|
|
152
|
+
void Awake() { motor = GetComponent<CharacterController>(); }
|
|
153
|
+
void Update() {
|
|
154
|
+
Vector2 axes = Joystick ? Joystick.Move : Vector2.zero;
|
|
155
|
+
float yaw = View ? View.eulerAngles.y : transform.eulerAngles.y;
|
|
156
|
+
Vector3 velocity = Quaternion.Euler(0,yaw,0) * new Vector3(axes.x,0,axes.y) * Speed;
|
|
157
|
+
if (motor.isGrounded && vertical < 0) vertical = -2;
|
|
158
|
+
vertical += Gravity * Time.deltaTime; velocity.y = vertical;
|
|
159
|
+
motor.Move(velocity * Time.deltaTime);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
` }, { path: 'README.md', content: `# GripForge joystick · Unity 2022.3 / Unity 6
|
|
164
|
+
|
|
165
|
+
Copy Assets/GripForge/Joystick into your project. Install Unity UI (com.unity.ugui)
|
|
166
|
+
if your project does not include it. Add GripForgeJoystickRig to a scene GameObject
|
|
167
|
+
and press Play. It creates the overlay and reuses your EventSystem, or creates one
|
|
168
|
+
with the enabled input backend (legacy or Input System). An existing EventSystem
|
|
169
|
+
must have a working UI input module; do not add a second one.
|
|
170
|
+
|
|
171
|
+
Read rig.Move and rig.Look each frame. X is right; Y is forward/up. Feed Move into
|
|
172
|
+
your existing movement controller; multiply Look by your angular speed and deltaTime.
|
|
173
|
+
The optional MotorExample requires the built-in Physics module and uses
|
|
174
|
+
CharacterController.Move and gravity. Assign the
|
|
175
|
+
rig and camera in the Inspector; do not run it alongside another movement motor.
|
|
176
|
+
|
|
177
|
+
The two sticks have independent pointer ownership and a radial deadzone. Axes
|
|
178
|
+
persist while held, reset when released, disabled, resized or focus is lost. The
|
|
179
|
+
UI respects the safe area. The component does not rewrite gamepad/Input Actions.
|
|
180
|
+
Disable/remove the rig to remove its controls. Mouse testing is supported.
|
|
181
|
+
` }];
|
|
182
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
export function unrealJoystickFiles(c) {
|
|
2
|
+
const p = 'Plugins/GripForgeJoystick/';
|
|
3
|
+
const src = p + 'Source/GripForgeJoystick/';
|
|
4
|
+
return [{ path: p + 'GripForgeJoystick.uplugin', content: JSON.stringify({ FileVersion: 3, Version: 1, VersionName: '1.0.0', FriendlyName: 'GripForge Joystick', Description: 'Independent touch/mouse movement and look sticks for an existing Pawn.', Category: 'Input', CreatedBy: 'GripForge', CanContainContent: false, Modules: [{ Name: 'GripForgeJoystick', Type: 'Runtime', LoadingPhase: 'Default' }] }, null, 2) },
|
|
5
|
+
{ path: src + 'GripForgeJoystick.Build.cs', content: `using UnrealBuildTool;
|
|
6
|
+
public class GripForgeJoystick : ModuleRules {
|
|
7
|
+
public GripForgeJoystick(ReadOnlyTargetRules Target) : base(Target) {
|
|
8
|
+
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
|
|
9
|
+
PublicDependencyModuleNames.AddRange(new string[]{"Core","CoreUObject","Engine","UMG"});
|
|
10
|
+
PrivateDependencyModuleNames.AddRange(new string[]{"Slate","SlateCore","InputCore","ApplicationCore"});
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
` }, { path: src + 'Private/GripForgeJoystickModule.cpp', content: `#include "Modules/ModuleManager.h"
|
|
14
|
+
IMPLEMENT_MODULE(FDefaultModuleImpl, GripForgeJoystick)
|
|
15
|
+
` }, { path: src + 'Public/GripForgeJoystickWidget.h', content: `#pragma once
|
|
16
|
+
#include "CoreMinimal.h"
|
|
17
|
+
#include "Blueprint/UserWidget.h"
|
|
18
|
+
#include "GripForgeJoystickWidget.generated.h"
|
|
19
|
+
class SGripForgeStick;
|
|
20
|
+
|
|
21
|
+
UCLASS()
|
|
22
|
+
class GRIPFORGEJOYSTICK_API UGripForgeJoystickWidget : public UUserWidget {
|
|
23
|
+
GENERATED_BODY()
|
|
24
|
+
public:
|
|
25
|
+
bool bDual = ${c.layout === 'dual' ? 'true' : 'false'};
|
|
26
|
+
float Radius = ${c.radius.toFixed(4)}f;
|
|
27
|
+
float Deadzone = ${c.deadzone.toFixed(4)}f;
|
|
28
|
+
FLinearColor Accent = FLinearColor(FColor::FromHex(TEXT("${c.accent.slice(1)}")));
|
|
29
|
+
FVector2D GetMove() const;
|
|
30
|
+
FVector2D GetLook() const;
|
|
31
|
+
void ResetSticks();
|
|
32
|
+
virtual void ReleaseSlateResources(bool bReleaseChildren) override;
|
|
33
|
+
protected:
|
|
34
|
+
virtual TSharedRef<SWidget> RebuildWidget() override;
|
|
35
|
+
private:
|
|
36
|
+
TSharedPtr<SGripForgeStick> MoveStick, LookStick;
|
|
37
|
+
};
|
|
38
|
+
` }, { path: src + 'Private/GripForgeJoystickWidget.cpp', content: `#include "GripForgeJoystickWidget.h"
|
|
39
|
+
#include "Widgets/SLeafWidget.h"
|
|
40
|
+
#include "Widgets/SOverlay.h"
|
|
41
|
+
#include "Widgets/Layout/SSafeZone.h"
|
|
42
|
+
#include "Widgets/Layout/SBox.h"
|
|
43
|
+
#include "Brushes/SlateRoundedBoxBrush.h"
|
|
44
|
+
#include "Rendering/DrawElements.h"
|
|
45
|
+
#include "Input/Reply.h"
|
|
46
|
+
#include "InputCoreTypes.h"
|
|
47
|
+
|
|
48
|
+
class SGripForgeStick : public SLeafWidget {
|
|
49
|
+
public:
|
|
50
|
+
SLATE_BEGIN_ARGS(SGripForgeStick) : _Radius(72), _Deadzone(.15f), _Accent(FLinearColor::White) {}
|
|
51
|
+
SLATE_ARGUMENT(float, Radius)
|
|
52
|
+
SLATE_ARGUMENT(float, Deadzone)
|
|
53
|
+
SLATE_ARGUMENT(FLinearColor, Accent)
|
|
54
|
+
SLATE_END_ARGS()
|
|
55
|
+
void Construct(const FArguments& Args) {
|
|
56
|
+
Radius=Args._Radius; Deadzone=Args._Deadzone; Accent=Args._Accent;
|
|
57
|
+
SetCanTick(false);
|
|
58
|
+
}
|
|
59
|
+
FVector2D Axis = FVector2D::ZeroVector;
|
|
60
|
+
void Reset() { Owner=INDEX_NONE; OwnerUser=INDEX_NONE; Axis=Visual=FVector2D::ZeroVector; Invalidate(EInvalidateWidgetReason::Paint); }
|
|
61
|
+
static FVector2D Radial(FVector2D Value, float Zone) {
|
|
62
|
+
const double M=Value.Size();
|
|
63
|
+
return M<=Zone || !FMath::IsFinite(M) ? FVector2D::ZeroVector : Value/M*FMath::Min(1.0,(M-Zone)/(1.0-Zone));
|
|
64
|
+
}
|
|
65
|
+
virtual FVector2D ComputeDesiredSize(float) const override { return FVector2D(Radius*2); }
|
|
66
|
+
virtual int32 OnPaint(const FPaintArgs&, const FGeometry& G, const FSlateRect&, FSlateWindowElementList& Out, int32 Layer, const FWidgetStyle&, bool) const override {
|
|
67
|
+
const float R=FMath::Min(G.GetLocalSize().X,G.GetLocalSize().Y)*.5f;
|
|
68
|
+
const FVector2D C=G.GetLocalSize()*.5;
|
|
69
|
+
FSlateDrawElement::MakeBox(Out,Layer,G.ToPaintGeometry(FVector2D(R*2),FSlateLayoutTransform(C-FVector2D(R))),&Circle,ESlateDrawEffect::None,FLinearColor(.06f,.08f,.11f,.72f));
|
|
70
|
+
const float TR=R*.28f;
|
|
71
|
+
FSlateDrawElement::MakeBox(Out,Layer+1,G.ToPaintGeometry(FVector2D(TR*2),FSlateLayoutTransform(C+Visual*R*.65f-FVector2D(TR))),&Circle,ESlateDrawEffect::None,Accent);
|
|
72
|
+
return Layer+1;
|
|
73
|
+
}
|
|
74
|
+
virtual FReply OnMouseButtonDown(const FGeometry& G,const FPointerEvent& E) override { return E.GetEffectingButton()==EKeys::LeftMouseButton ? Begin(G,E) : FReply::Unhandled(); }
|
|
75
|
+
virtual FReply OnMouseMove(const FGeometry& G,const FPointerEvent& E) override { return Move(G,E); }
|
|
76
|
+
virtual FReply OnMouseButtonUp(const FGeometry&,const FPointerEvent& E) override { return E.GetEffectingButton()==EKeys::LeftMouseButton ? End(E) : FReply::Unhandled(); }
|
|
77
|
+
virtual FReply OnTouchStarted(const FGeometry& G,const FPointerEvent& E) override { return Begin(G,E); }
|
|
78
|
+
virtual FReply OnTouchMoved(const FGeometry& G,const FPointerEvent& E) override { return Move(G,E); }
|
|
79
|
+
virtual FReply OnTouchEnded(const FGeometry&,const FPointerEvent& E) override { return End(E); }
|
|
80
|
+
virtual void OnMouseCaptureLost(const FCaptureLostEvent&) override { Reset(); }
|
|
81
|
+
private:
|
|
82
|
+
int32 Owner=INDEX_NONE, OwnerUser=INDEX_NONE;
|
|
83
|
+
float Radius=72, Deadzone=.15f;
|
|
84
|
+
FVector2D Visual=FVector2D::ZeroVector;
|
|
85
|
+
FLinearColor Accent;
|
|
86
|
+
const FSlateRoundedBoxBrush Circle{FLinearColor::White,1000.f};
|
|
87
|
+
bool Owns(const FPointerEvent& E) const { return Owner==(int32)E.GetPointerIndex() && OwnerUser==(int32)E.GetUserIndex(); }
|
|
88
|
+
void Update(const FGeometry& G,const FPointerEvent& E) {
|
|
89
|
+
const double Travel=FMath::Min(G.GetLocalSize().X,G.GetLocalSize().Y)*.325;
|
|
90
|
+
if(Travel<=0){Reset();return;}
|
|
91
|
+
const FVector2D Raw=(G.AbsoluteToLocal(E.GetScreenSpacePosition())-G.GetLocalSize()*.5)/Travel;
|
|
92
|
+
Visual=Raw/FMath::Max(1.0,Raw.Size());
|
|
93
|
+
Axis=Radial(FVector2D(Raw.X,-Raw.Y),Deadzone);
|
|
94
|
+
Invalidate(EInvalidateWidgetReason::Paint);
|
|
95
|
+
}
|
|
96
|
+
FReply Begin(const FGeometry& G,const FPointerEvent& E) {
|
|
97
|
+
if(Owner!=INDEX_NONE)return FReply::Handled();
|
|
98
|
+
Owner=E.GetPointerIndex(); OwnerUser=E.GetUserIndex(); Update(G,E);
|
|
99
|
+
return FReply::Handled().CaptureMouse(SharedThis(this));
|
|
100
|
+
}
|
|
101
|
+
FReply Move(const FGeometry& G,const FPointerEvent& E) { if(!Owns(E))return FReply::Unhandled(); Update(G,E); return FReply::Handled(); }
|
|
102
|
+
FReply End(const FPointerEvent& E) { if(!Owns(E))return FReply::Unhandled(); Reset(); return FReply::Handled().ReleaseMouseCapture(); }
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
TSharedRef<SWidget> UGripForgeJoystickWidget::RebuildWidget() {
|
|
106
|
+
auto Overlay=SNew(SOverlay).Visibility(EVisibility::SelfHitTestInvisible);
|
|
107
|
+
Overlay->AddSlot().HAlign(HAlign_Left).VAlign(VAlign_Bottom).Padding(24)[
|
|
108
|
+
SAssignNew(MoveStick,SGripForgeStick).Radius(Radius).Deadzone(Deadzone).Accent(Accent)];
|
|
109
|
+
if(bDual) Overlay->AddSlot().HAlign(HAlign_Right).VAlign(VAlign_Bottom).Padding(24)[
|
|
110
|
+
SAssignNew(LookStick,SGripForgeStick).Radius(Radius).Deadzone(Deadzone).Accent(Accent)];
|
|
111
|
+
return SNew(SSafeZone).Visibility(EVisibility::SelfHitTestInvisible)[Overlay];
|
|
112
|
+
}
|
|
113
|
+
FVector2D UGripForgeJoystickWidget::GetMove() const { return MoveStick.IsValid()?MoveStick->Axis:FVector2D::ZeroVector; }
|
|
114
|
+
FVector2D UGripForgeJoystickWidget::GetLook() const { return LookStick.IsValid()?LookStick->Axis:FVector2D::ZeroVector; }
|
|
115
|
+
void UGripForgeJoystickWidget::ResetSticks(){if(MoveStick)MoveStick->Reset();if(LookStick)LookStick->Reset();}
|
|
116
|
+
void UGripForgeJoystickWidget::ReleaseSlateResources(bool bReleaseChildren){ResetSticks();Super::ReleaseSlateResources(bReleaseChildren);MoveStick.Reset();LookStick.Reset();}
|
|
117
|
+
` }, { path: src + 'Public/GripForgeJoystickComponent.h', content: `#pragma once
|
|
118
|
+
#include "CoreMinimal.h"
|
|
119
|
+
#include "Components/ActorComponent.h"
|
|
120
|
+
#include "GripForgeJoystickComponent.generated.h"
|
|
121
|
+
class UGripForgeJoystickWidget;
|
|
122
|
+
class APlayerController;
|
|
123
|
+
|
|
124
|
+
UCLASS(ClassGroup=(Input), meta=(BlueprintSpawnableComponent))
|
|
125
|
+
class GRIPFORGEJOYSTICK_API UGripForgeJoystickComponent : public UActorComponent {
|
|
126
|
+
GENERATED_BODY()
|
|
127
|
+
public:
|
|
128
|
+
UGripForgeJoystickComponent();
|
|
129
|
+
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Joystick") bool bDual = ${c.layout === 'dual' ? 'true' : 'false'};
|
|
130
|
+
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Joystick",meta=(ClampMin="32",ClampMax="160")) float Radius = ${c.radius.toFixed(4)}f;
|
|
131
|
+
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Joystick",meta=(ClampMin="0",ClampMax="0.5")) float Deadzone = ${c.deadzone.toFixed(4)}f;
|
|
132
|
+
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Joystick") FLinearColor Accent = FLinearColor(FColor::FromHex(TEXT("${c.accent.slice(1)}")));
|
|
133
|
+
// Turn these off if your existing motor reads GetMove/GetLook itself.
|
|
134
|
+
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Joystick") bool bDriveMovement = true;
|
|
135
|
+
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Joystick") bool bDriveLook = true;
|
|
136
|
+
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Joystick") float LookDegreesPerSecond = 120.f;
|
|
137
|
+
UFUNCTION(BlueprintPure, Category="Joystick") FVector2D GetMove() const;
|
|
138
|
+
UFUNCTION(BlueprintPure, Category="Joystick") FVector2D GetLook() const;
|
|
139
|
+
UFUNCTION(BlueprintCallable, Category="Joystick") void ResetInput();
|
|
140
|
+
virtual void Deactivate() override;
|
|
141
|
+
virtual void TickComponent(float Delta, ELevelTick TickType, FActorComponentTickFunction* ThisTick) override;
|
|
142
|
+
protected:
|
|
143
|
+
virtual void BeginPlay() override;
|
|
144
|
+
virtual void EndPlay(const EEndPlayReason::Type Reason) override;
|
|
145
|
+
private:
|
|
146
|
+
UPROPERTY(Transient) TObjectPtr<UGripForgeJoystickWidget> Widget;
|
|
147
|
+
TWeakObjectPtr<APlayerController> Controller;
|
|
148
|
+
FDelegateHandle FocusHandle, BackgroundHandle;
|
|
149
|
+
void RemoveWidget();
|
|
150
|
+
};
|
|
151
|
+
` }, { path: src + 'Private/GripForgeJoystickComponent.cpp', content: `#include "GripForgeJoystickComponent.h"
|
|
152
|
+
#include "GripForgeJoystickWidget.h"
|
|
153
|
+
#include "GameFramework/Pawn.h"
|
|
154
|
+
#include "GameFramework/PlayerController.h"
|
|
155
|
+
#include "GameFramework/PawnMovementComponent.h"
|
|
156
|
+
#include "Misc/CoreDelegates.h"
|
|
157
|
+
#include "Math/RotationMatrix.h"
|
|
158
|
+
|
|
159
|
+
UGripForgeJoystickComponent::UGripForgeJoystickComponent(){bAutoActivate=true;PrimaryComponentTick.bCanEverTick=true;PrimaryComponentTick.TickGroup=TG_PrePhysics;}
|
|
160
|
+
void UGripForgeJoystickComponent::BeginPlay(){
|
|
161
|
+
Super::BeginPlay();
|
|
162
|
+
FocusHandle=FCoreDelegates::ApplicationWillDeactivateDelegate.AddUObject(this,&UGripForgeJoystickComponent::ResetInput);
|
|
163
|
+
BackgroundHandle=FCoreDelegates::ApplicationWillEnterBackgroundDelegate.AddUObject(this,&UGripForgeJoystickComponent::ResetInput);
|
|
164
|
+
if(auto* Pawn=Cast<APawn>(GetOwner()))if(auto* Motor=Pawn->GetMovementComponent())Motor->AddTickPrerequisiteComponent(this);
|
|
165
|
+
}
|
|
166
|
+
void UGripForgeJoystickComponent::TickComponent(float Delta,ELevelTick TickType,FActorComponentTickFunction* ThisTick){
|
|
167
|
+
Super::TickComponent(Delta,TickType,ThisTick);
|
|
168
|
+
auto* Pawn=Cast<APawn>(GetOwner());
|
|
169
|
+
auto* PC=Pawn?Cast<APlayerController>(Pawn->GetController()):nullptr;
|
|
170
|
+
if(!PC || !Pawn->IsLocallyControlled() || !PC->IsLocalController()){RemoveWidget();return;}
|
|
171
|
+
// Possession may occur AFTER BeginPlay. Recreate on local controller changes.
|
|
172
|
+
if(Controller.Get()!=PC){RemoveWidget();Controller=PC;}
|
|
173
|
+
if(!Widget){
|
|
174
|
+
Widget=CreateWidget<UGripForgeJoystickWidget>(PC);
|
|
175
|
+
if(!Widget)return;
|
|
176
|
+
Widget->bDual=bDual;Widget->Radius=FMath::Clamp(Radius,32.f,160.f);Widget->Deadzone=FMath::Clamp(Deadzone,0.f,.5f);Widget->Accent=Accent;
|
|
177
|
+
Widget->SetVisibility(ESlateVisibility::SelfHitTestInvisible);
|
|
178
|
+
Widget->AddToPlayerScreen(20);
|
|
179
|
+
}
|
|
180
|
+
if(PC->IsPaused()){ResetInput();return;}
|
|
181
|
+
const FVector2D Move=GetMove(),Look=GetLook();
|
|
182
|
+
if(bDriveMovement && !PC->IsMoveInputIgnored()){
|
|
183
|
+
const FRotationMatrix Basis(FRotator(0,PC->GetControlRotation().Yaw,0));
|
|
184
|
+
Pawn->AddMovementInput(Basis.GetUnitAxis(EAxis::X),Move.Y);
|
|
185
|
+
Pawn->AddMovementInput(Basis.GetUnitAxis(EAxis::Y),Move.X);
|
|
186
|
+
}
|
|
187
|
+
if(bDriveLook && !PC->IsLookInputIgnored()){
|
|
188
|
+
// InputYaw/PitchScale belong to the game's controller, just as for other axes.
|
|
189
|
+
Pawn->AddControllerYawInput(Look.X*LookDegreesPerSecond*Delta);
|
|
190
|
+
Pawn->AddControllerPitchInput(-Look.Y*LookDegreesPerSecond*Delta);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
FVector2D UGripForgeJoystickComponent::GetMove() const {return IsActive()&&Widget?Widget->GetMove():FVector2D::ZeroVector;}
|
|
194
|
+
FVector2D UGripForgeJoystickComponent::GetLook() const {return IsActive()&&Widget?Widget->GetLook():FVector2D::ZeroVector;}
|
|
195
|
+
void UGripForgeJoystickComponent::ResetInput(){if(Widget)Widget->ResetSticks();}
|
|
196
|
+
void UGripForgeJoystickComponent::Deactivate(){RemoveWidget();Super::Deactivate();}
|
|
197
|
+
void UGripForgeJoystickComponent::RemoveWidget(){if(Widget){Widget->ResetSticks();Widget->RemoveFromParent();Widget=nullptr;}Controller.Reset();}
|
|
198
|
+
void UGripForgeJoystickComponent::EndPlay(const EEndPlayReason::Type Reason){
|
|
199
|
+
RemoveWidget();FCoreDelegates::ApplicationWillDeactivateDelegate.Remove(FocusHandle);FCoreDelegates::ApplicationWillEnterBackgroundDelegate.Remove(BackgroundHandle);
|
|
200
|
+
if(auto* Pawn=Cast<APawn>(GetOwner()))if(auto* Motor=Pawn->GetMovementComponent())Motor->RemoveTickPrerequisiteComponent(this);
|
|
201
|
+
Super::EndPlay(Reason);
|
|
202
|
+
}
|
|
203
|
+
` }, { path: 'README.md', content: `# GripForge joystick · Unreal Engine 5
|
|
204
|
+
|
|
205
|
+
1. Copy Plugins/GripForgeJoystick into the project (not Engine). Compile the
|
|
206
|
+
runtime plugin with your UE version/toolchain, then reopen the editor. A
|
|
207
|
+
Blueprint-only project can add a C++ class first to enable project builds.
|
|
208
|
+
2. Enable GripForge Joystick in Plugins if needed. On the actual possessed Pawn
|
|
209
|
+
Blueprint, Add Component → GripForge Joystick. It waits for local possession.
|
|
210
|
+
3. Press Play. Left stick feeds your Pawn's existing AddMovementInput; right
|
|
211
|
+
stick feeds controller yaw/pitch. CharacterMovement still owns collision,
|
|
212
|
+
gravity, acceleration and network prediction. No GameMode replacement.
|
|
213
|
+
|
|
214
|
+
Mouse and touch work directly through Slate; Use Mouse for Touch is not required.
|
|
215
|
+
For a desktop mouse test, use Game and UI input mode and show the cursor in your
|
|
216
|
+
PlayerController. Keep the game viewport focused. The plugin deliberately does
|
|
217
|
+
not change your game's input mode/cursor. Native touch works in Game Only mode.
|
|
218
|
+
Remove/disable the previous virtual joystick to avoid overlapping controls.
|
|
219
|
+
|
|
220
|
+
For a custom motor disable Drive Movement/Drive Look and read Get Move/Get Look
|
|
221
|
+
from the component every tick. Axes: X right, Y forward/up; magnitude <= 1. Apply
|
|
222
|
+
look deltaTime once. Existing gamepad/keyboard actions are not overwritten.
|
|
223
|
+
Deadzone, radius, color and dual-stick layout are set before the UI is created.
|
|
224
|
+
The overlay respects Slate DPI/safe zones. Release, capture loss, application
|
|
225
|
+
focus loss, losing possession and EndPlay reset the axes. Use Reset Input when
|
|
226
|
+
opening your own modal menus. One component per locally controlled pawn.
|
|
227
|
+
|
|
228
|
+
This is a source plugin, not a precompiled binary for every UE release. Compile
|
|
229
|
+
for the target UE/platform and test in your real project before packaging.
|
|
230
|
+
` }];
|
|
231
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Identical deterministic delivery in the hosted MCP and npm client. */
|
|
2
|
+
import { z } from 'zod/v4';
|
|
3
|
+
import { createJoystickKit } from './joystick-kit/index.js';
|
|
4
|
+
export const JOYSTICK_TOOL_NAMES = ['gripforge_joystick_kit'];
|
|
5
|
+
export function registerJoystickTools(register, schema = z) {
|
|
6
|
+
const shape = {
|
|
7
|
+
target: schema.enum(['threejs', 'godot', 'unity', 'unreal', 'all']).describe('Engine delivery. all returns each of the four source kits.'),
|
|
8
|
+
layout: schema.enum(['move', 'dual']).default('dual').describe('One movement stick, or independent movement + camera sticks.'),
|
|
9
|
+
deadzone: schema.number().min(0).max(.5).default(.15).describe('Radial deadzone, continuously rescaled to a unit disc.'),
|
|
10
|
+
radius: schema.number().min(32).max(160).default(72).describe('Stick radius in CSS/UI logical pixels, before engine DPI scaling.'),
|
|
11
|
+
accent: schema.string().regex(/^#[\da-f]{6}$/i).default('#ff681f').describe('Thumb color, six-digit hexadecimal RGB.'),
|
|
12
|
+
};
|
|
13
|
+
register('gripforge_joystick_kit', {
|
|
14
|
+
title: 'Ready-to-use joystick · Unreal / Godot / Unity / Three.js',
|
|
15
|
+
description: 'Generate input.joystick source files and installation instructions: touch + mouse, independent dual sticks, radial deadzone, continuous holding and release/focus reset. Unreal runtime Pawn component, Godot CanvasLayer scene, Unity UI component, Three.js DOM controls + GripForge input.actions bridge. Feeds the existing movement controller rather than replacing physics, collision or networking. Returns files with relative path/content; the agent writes them into the target project and compiles/tests there. Does not install automatically, modify project settings or claim runtime validation. 0 credits.',
|
|
16
|
+
inputSchema: shape,
|
|
17
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
18
|
+
}, async (args) => {
|
|
19
|
+
const parsed = schema.object(shape).safeParse(args);
|
|
20
|
+
if (!parsed.success)
|
|
21
|
+
return { isError: true, content: [{ type: 'text', text: parsed.error.message }] };
|
|
22
|
+
const { target, ...config } = parsed.data;
|
|
23
|
+
const result = createJoystickKit(target, config);
|
|
24
|
+
return { content: [{ type: 'text', text: JSON.stringify(result) }], structuredContent: result };
|
|
25
|
+
});
|
|
26
|
+
}
|
package/dist/look.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Le rendu (« look ») des outils MCP : un paramètre à part du style de jeu. Même vocabulaire que
|
|
3
|
+
* `LOOKS` de `@gripforge/core` (look.ts) — `qa/look-check.mjs` vérifie que les deux listes concordent.
|
|
4
|
+
*/
|
|
5
|
+
export const LOOK_VALUES = ['stylized', 'painted', 'toon', 'anime', 'realistic', 'lowpoly', 'pixel'];
|
|
6
|
+
export const LOOK_DESCRIPTION = 'Rendering look, separate from the game style: stylized (neutral) | painted | toon | anime | realistic | lowpoly | pixel. Old values (handpainted, cartoon, low_poly…) are read as aliases. Omit for the style default.';
|