portals-mcp 1.3.3 → 1.3.4

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 (38) hide show
  1. package/dist/marketplace-client.d.ts +9 -0
  2. package/dist/marketplace-client.d.ts.map +1 -1
  3. package/dist/marketplace-client.js +120 -20
  4. package/dist/resources/index/effects.json +88 -1
  5. package/dist/resources/index/items.json +59 -8
  6. package/dist/resources/index/knowledge-map.json +6 -0
  7. package/dist/resources/index/triggers.json +28 -0
  8. package/dist/resources/logic/js-function-reference.md +16 -6
  9. package/dist/resources/logic/js-use-effector.md +60 -7
  10. package/dist/resources/python/README.md +2 -0
  11. package/dist/resources/python/lib/portals_core.py +159 -8
  12. package/dist/resources/python/lib/portals_effects.py +180 -7
  13. package/dist/resources/python/lib/portals_ops.py +148 -16
  14. package/dist/resources/python/lib/portals_utils.py +1171 -1170
  15. package/dist/resources/python/tools/build_reactgui_feature_test_room.py +386 -0
  16. package/dist/resources/python/tools/validate_room.py +430 -40
  17. package/dist/resources/python/tools/verify_reactgui_feature_room.py +199 -0
  18. package/dist/resources/ref/items/camera.json +52 -47
  19. package/dist/resources/ref/items/cube.json +51 -5
  20. package/dist/resources/ref/items/effects-vfx.json +171 -115
  21. package/dist/resources/ref/items/glb.json +97 -64
  22. package/dist/resources/ref/items/text.json +71 -69
  23. package/dist/resources/ref/items/trigger-zone.json +4 -0
  24. package/dist/resources/ref/settings.json +198 -191
  25. package/dist/resources/ref/systems/function-effector-js.json +9 -6
  26. package/dist/resources/ref/systems/use-effector.json +74 -73
  27. package/dist/resources/reference/api-cheatsheet.md +16 -3
  28. package/dist/resources/reference/cameras.md +569 -565
  29. package/dist/resources/reference/gotchas.md +13 -3
  30. package/dist/resources/reference/interactions.md +6 -1
  31. package/dist/resources/reference/items/building.md +52 -0
  32. package/dist/resources/reference/items/effects.md +110 -170
  33. package/dist/resources/reference/items/gameplay.md +7 -5
  34. package/dist/resources/reference/items/models.md +184 -168
  35. package/dist/resources/reference/reactgui-feature-test-room.md +97 -0
  36. package/dist/tools.d.ts.map +1 -1
  37. package/dist/tools.js +149 -19
  38. package/package.json +1 -1
@@ -24,6 +24,12 @@ export interface MarketplaceItem {
24
24
  claimCount: number;
25
25
  acquireCount: number;
26
26
  }
27
+ export interface MarketplaceGlbResolution {
28
+ glb: string | null;
29
+ source: "catalog_direct" | "user_inventory" | "room_build_items" | "unresolved";
30
+ attachedToRoomInventory: boolean;
31
+ reason?: string | undefined;
32
+ }
27
33
  export interface MarketplaceFilter {
28
34
  query?: string | undefined;
29
35
  type?: string | undefined;
@@ -44,6 +50,8 @@ export interface ClaimResult {
44
50
  itemId: string;
45
51
  message?: string | undefined;
46
52
  }
53
+ export declare function isMarketplaceAssetRoute(value: string | null | undefined): boolean;
54
+ export declare function isRuntimeGlbUrl(value: string | null | undefined): boolean;
47
55
  export declare function clearMarketplaceCache(): void;
48
56
  export declare function searchMarketplace(filter?: MarketplaceFilter): Promise<MarketplaceItem[]>;
49
57
  export declare function getPackItems(packId: string): Promise<MarketplaceItem[]>;
@@ -116,4 +124,5 @@ export interface OwnershipCheck {
116
124
  export declare function verifyOwnership(cids: string[]): Promise<OwnershipCheck[]>;
117
125
  export declare function attachItemsToRoom(roomId: string, itemIds: string[]): Promise<AttachToRoomResult>;
118
126
  export declare function claimMarketplaceItem(itemId: string): Promise<ClaimResult>;
127
+ export declare function resolveMarketplaceGlbUrl(item: MarketplaceItem, roomId?: string): Promise<MarketplaceGlbResolution>;
119
128
  //# sourceMappingURL=marketplace-client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"marketplace-client.d.ts","sourceRoot":"","sources":["../src/marketplace-client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"marketplace-client.d.ts","sourceRoot":"","sources":["../src/marketplace-client.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,54 @@
1
1
  import fetch from "node-fetch";
2
+ import { getAccessKey, isAuthenticated } from "./api.js";
2
3
  const API_BASE = process.env.API_BASE || "https://portals.to";
3
4
  const CACHE_TTL_MS = 60_000;
4
5
  let catalogueCache = null;
6
+ function getRuntimeAccessKey() {
7
+ if (process.env.PORTALS_ACCESS_KEY)
8
+ return process.env.PORTALS_ACCESS_KEY;
9
+ if (!isAuthenticated())
10
+ return null;
11
+ try {
12
+ return getAccessKey();
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ }
18
+ function authHeaders() {
19
+ const headers = {};
20
+ const token = process.env.PORTALS_USER_TOKEN;
21
+ const accessKey = getRuntimeAccessKey();
22
+ if (token)
23
+ headers["Authorization"] = `Bearer ${token}`;
24
+ if (accessKey)
25
+ headers["x-access-key"] = accessKey;
26
+ return headers;
27
+ }
28
+ function jsonAuthHeaders() {
29
+ return { "Content-Type": "application/json", ...authHeaders() };
30
+ }
31
+ export function isMarketplaceAssetRoute(value) {
32
+ if (!value)
33
+ return false;
34
+ try {
35
+ return new URL(value, API_BASE).pathname.startsWith("/api/v2/marketplace/asset/");
36
+ }
37
+ catch {
38
+ return value.startsWith("/api/v2/marketplace/asset/");
39
+ }
40
+ }
41
+ export function isRuntimeGlbUrl(value) {
42
+ if (!value)
43
+ return false;
44
+ if (isMarketplaceAssetRoute(value))
45
+ return false;
46
+ const trimmed = value.trim();
47
+ if (trimmed.startsWith("http://") || trimmed.startsWith("https://") || trimmed.startsWith("~") || trimmed.startsWith("#")) {
48
+ return /\.glb(?:[?#]|$)/i.test(trimmed);
49
+ }
50
+ return false;
51
+ }
5
52
  async function fetchCatalogue() {
6
53
  if (catalogueCache && Date.now() - catalogueCache.ts < CACHE_TTL_MS) {
7
54
  return catalogueCache.items;
@@ -156,7 +203,7 @@ export async function getUserInventory() {
156
203
  if (!token)
157
204
  return [];
158
205
  const res = await fetch(`${API_BASE}/api/v2/marketplace/my-items`, {
159
- headers: { Authorization: `Bearer ${token}` },
206
+ headers: authHeaders(),
160
207
  });
161
208
  if (!res.ok)
162
209
  return [];
@@ -171,17 +218,10 @@ export async function getUserInventory() {
171
218
  * scene with AI so we know what assets the user actually has.
172
219
  */
173
220
  export async function getRoomBuildItems(roomId) {
174
- const token = process.env.PORTALS_USER_TOKEN;
175
- const accessKey = process.env.PORTALS_ACCESS_KEY;
176
221
  if (!roomId)
177
222
  return [];
178
- const headers = {};
179
- if (token)
180
- headers["Authorization"] = `Bearer ${token}`;
181
- if (accessKey)
182
- headers["x-access-key"] = accessKey;
183
223
  try {
184
- const res = await fetch(`${API_BASE}/api/v2/room/get-build-items?roomId=${encodeURIComponent(roomId)}`, { headers });
224
+ const res = await fetch(`${API_BASE}/api/v2/room/get-build-items?roomId=${encodeURIComponent(roomId)}`, { headers: authHeaders() });
185
225
  if (!res.ok)
186
226
  return [];
187
227
  const json = (await res.json());
@@ -199,7 +239,7 @@ export async function getRoomBuildItems(roomId) {
199
239
  */
200
240
  export async function verifyOwnership(cids) {
201
241
  const token = process.env.PORTALS_USER_TOKEN;
202
- const accessKey = process.env.PORTALS_ACCESS_KEY;
242
+ const accessKey = getRuntimeAccessKey();
203
243
  if (!token && !accessKey) {
204
244
  // No auth — return unknowns (the caller decides how to proceed).
205
245
  return cids.map((cid) => ({
@@ -210,9 +250,7 @@ export async function verifyOwnership(cids) {
210
250
  }));
211
251
  }
212
252
  // Pull catalogue + user inventory in parallel.
213
- const headers = {};
214
- if (token)
215
- headers["Authorization"] = `Bearer ${token}`;
253
+ const headers = authHeaders();
216
254
  const [inventoryRes, catalogue] = await Promise.all([
217
255
  token ? fetch(`${API_BASE}/api/v2/marketplace/my-items`, { headers }) : Promise.resolve(null),
218
256
  fetchCatalogue(),
@@ -248,18 +286,14 @@ export async function verifyOwnership(cids) {
248
286
  }
249
287
  export async function attachItemsToRoom(roomId, itemIds) {
250
288
  const token = process.env.PORTALS_USER_TOKEN;
251
- const accessKey = process.env.PORTALS_ACCESS_KEY;
289
+ const accessKey = getRuntimeAccessKey();
252
290
  if (!token && !accessKey) {
253
- return { ok: false, code: "AUTH_REQUIRED", message: "No Firebase token or access key in env.", attachedItemIds: [] };
291
+ return { ok: false, code: "AUTH_REQUIRED", message: "No Firebase token, access key env, or authenticated MCP access key available.", attachedItemIds: [] };
254
292
  }
255
293
  if (!roomId) {
256
294
  return { ok: false, code: "ROOM_ID_REQUIRED", message: "roomId is required.", attachedItemIds: [] };
257
295
  }
258
- const headers = { "Content-Type": "application/json" };
259
- if (token)
260
- headers["Authorization"] = `Bearer ${token}`;
261
- if (accessKey)
262
- headers["x-access-key"] = accessKey;
296
+ const headers = jsonAuthHeaders();
263
297
  let existing = [];
264
298
  try {
265
299
  const settingsRes = await fetch(`${API_BASE}/api/v2/room/get-settings?roomID=${encodeURIComponent(roomId)}`, { headers });
@@ -324,3 +358,69 @@ export async function claimMarketplaceItem(itemId) {
324
358
  message: body.message,
325
359
  };
326
360
  }
361
+ function matchBuildItem(items, itemId) {
362
+ return items.find((item) => item.id === itemId) ?? null;
363
+ }
364
+ function matchInventoryItem(items, itemId) {
365
+ return items.find((item) => item.itemId === itemId || item.id === itemId) ?? null;
366
+ }
367
+ export async function resolveMarketplaceGlbUrl(item, roomId) {
368
+ if (isRuntimeGlbUrl(item.glb)) {
369
+ return {
370
+ glb: item.glb,
371
+ source: "catalog_direct",
372
+ attachedToRoomInventory: false,
373
+ };
374
+ }
375
+ const inventoryItem = matchInventoryItem(await getUserInventory(), item.id);
376
+ if (inventoryItem?.glb && isRuntimeGlbUrl(inventoryItem.glb)) {
377
+ return {
378
+ glb: inventoryItem.glb,
379
+ source: "user_inventory",
380
+ attachedToRoomInventory: false,
381
+ };
382
+ }
383
+ if (!roomId) {
384
+ return {
385
+ glb: null,
386
+ source: "unresolved",
387
+ attachedToRoomInventory: false,
388
+ reason: isMarketplaceAssetRoute(item.glb)
389
+ ? "Catalog glb is a protected marketplace asset route; pass roomId so MCP can resolve the room build-palette CDN URL."
390
+ : "Catalog item did not include a runtime GLB URL.",
391
+ };
392
+ }
393
+ let buildItems = await getRoomBuildItems(roomId);
394
+ let buildItem = matchBuildItem(buildItems, item.id);
395
+ if (buildItem?.glb && isRuntimeGlbUrl(buildItem.glb)) {
396
+ return {
397
+ glb: buildItem.glb,
398
+ source: "room_build_items",
399
+ attachedToRoomInventory: false,
400
+ };
401
+ }
402
+ const attachResult = await attachItemsToRoom(roomId, [item.id]);
403
+ if (!attachResult.ok) {
404
+ return {
405
+ glb: null,
406
+ source: "unresolved",
407
+ attachedToRoomInventory: false,
408
+ reason: `Could not attach marketplace item to room inventory for URL resolution (${attachResult.code}${attachResult.message ? `: ${attachResult.message}` : ""}).`,
409
+ };
410
+ }
411
+ buildItems = await getRoomBuildItems(roomId);
412
+ buildItem = matchBuildItem(buildItems, item.id);
413
+ if (buildItem?.glb && isRuntimeGlbUrl(buildItem.glb)) {
414
+ return {
415
+ glb: buildItem.glb,
416
+ source: "room_build_items",
417
+ attachedToRoomInventory: true,
418
+ };
419
+ }
420
+ return {
421
+ glb: null,
422
+ source: "unresolved",
423
+ attachedToRoomInventory: true,
424
+ reason: "Item was attached to the room inventory, but get_room_build_items did not return a runtime .glb URL for it.",
425
+ };
426
+ }
@@ -39,6 +39,16 @@
39
39
  "Scale in relative mode is ADDITIVE (scale [1,1,1] adds 1 to each axis per trigger)"
40
40
  ]
41
41
  },
42
+ {
43
+ "$type": "SetTransform",
44
+ "category": "movement",
45
+ "parameters": {
46
+ "position": "array - optional [x,y,z] world position",
47
+ "rotation": "array - optional [x,y,z] Euler degrees",
48
+ "scale": "array - optional [x,y,z] scale"
49
+ },
50
+ "gotchas": ["Immediate local-player transform write. Include at least one of position, rotation, or scale."]
51
+ },
42
52
  {
43
53
  "$type": "PortalsAnimation",
44
54
  "category": "movement",
@@ -76,6 +86,82 @@
76
86
  "parameters": {},
77
87
  "gotchas": []
78
88
  },
89
+ {
90
+ "$type": "AddForceToCubeEffector",
91
+ "category": "physics",
92
+ "parameters": {
93
+ "fx": "float - impulse X",
94
+ "fy": "float - impulse Y",
95
+ "fz": "float - impulse Z",
96
+ "rel": "bool - true=force vector is local to item rotation"
97
+ },
98
+ "gotchas": ["Direct task effector only; not in ReactGUI UseEffector/PrepareEffector map. Works on ResizableCube and GLB-family physics items."]
99
+ },
100
+ {
101
+ "$type": "AddTorqueToCubeEffector",
102
+ "category": "physics",
103
+ "parameters": {
104
+ "tx": "float - rotational impulse X",
105
+ "ty": "float - rotational impulse Y",
106
+ "tz": "float - rotational impulse Z",
107
+ "rel": "bool - true=torque vector is local to item rotation"
108
+ },
109
+ "gotchas": ["Direct task effector only; not in ReactGUI UseEffector/PrepareEffector map. Works on ResizableCube and GLB-family physics items."]
110
+ },
111
+ {
112
+ "$type": "ExplosionForceCubeEffector",
113
+ "category": "physics",
114
+ "parameters": {
115
+ "f": "float - explosion impulse force",
116
+ "r": "float - explosion radius",
117
+ "u": "float - upward modifier",
118
+ "ox/oy/oz": "floats - origin offset from item position",
119
+ "aff": "bool - true=also affect nearby physics cubes"
120
+ },
121
+ "gotchas": ["Direct task effector only; not in ReactGUI UseEffector/PrepareEffector map."]
122
+ },
123
+ {
124
+ "$type": "SetCubeVelocityEffector",
125
+ "category": "physics",
126
+ "parameters": {
127
+ "vx/vy/vz": "floats - linear velocity",
128
+ "ax/ay/az": "floats - angular velocity",
129
+ "sl": "bool - set linear velocity",
130
+ "sa": "bool - set angular velocity",
131
+ "rel": "bool - true=vectors are local to item rotation"
132
+ },
133
+ "gotchas": ["Direct task effector only. Velocity assignment is most reliable when this client already owns the physics item."]
134
+ },
135
+ {
136
+ "$type": "ResetCubeVelocityEffector",
137
+ "category": "physics",
138
+ "parameters": {},
139
+ "gotchas": ["Direct task effector only. Stops linear and angular velocity on the owned physics item."]
140
+ },
141
+ {
142
+ "$type": "ToggleCubeGravityEffector",
143
+ "category": "physics",
144
+ "parameters": {
145
+ "g": "bool - true=use gravity, false=float"
146
+ },
147
+ "gotchas": ["Direct task effector only. Ensures physics is attached before changing gravity."]
148
+ },
149
+ {
150
+ "$type": "AttachToMouseCubeEffector",
151
+ "category": "physics",
152
+ "parameters": {
153
+ "snap": "bool - true=snap item bottom to placeable surfaces"
154
+ },
155
+ "gotchas": ["JS UseEffector name is AttachToMouse, while direct room-data $type is AttachToMouseCubeEffector."]
156
+ },
157
+ {
158
+ "$type": "DetachFromMouseCubeEffector",
159
+ "category": "physics",
160
+ "parameters": {
161
+ "save": "bool - true=persist final drop position when server permissions and item opt-in allow it"
162
+ },
163
+ "gotchas": ["JS UseEffector name is DetachFromMouse, while direct room-data $type is DetachFromMouseCubeEffector."]
164
+ },
79
165
  {
80
166
  "$type": "AddVelocityToPlayer",
81
167
  "category": "player",
@@ -235,7 +321,8 @@
235
321
  "category": "camera",
236
322
  "parameters": {
237
323
  "camState": "string — camera state name (matches CameraObject sn)",
238
- "transitionSpeed": "floattransition speed"
324
+ "smoothTransition": "boolblend to the new state, default true",
325
+ "transitionSpeed": "float — transition speed, default 6.0"
239
326
  },
240
327
  "gotchas": []
241
328
  },
@@ -15,9 +15,52 @@
15
15
  "s": "bool — shadows (false=off)",
16
16
  "nav": "bool — AI nav mesh surface",
17
17
  "t": "float — texture tiling",
18
+ "sm": "float — material smoothness 0-1",
19
+ "m": "float — material metallic 0-1",
20
+ "p": "bool — has physics",
21
+ "sp": "bool — save settled physics position",
22
+ "pm": "bool — pushable by player",
23
+ "mass": "float — physics mass 0.1-50",
24
+ "lbl": "string — physics label for trigger-zone filtering",
18
25
  "title": "string — internal label"
19
26
  },
20
- "valid_triggers": ["OnClickEvent", "OnCollideEvent", "OnCollisionStoppedEvent", "OnHoverStartEvent", "OnHoverEndEvent", "OnPlayerLoggedIn", "OnKeyPressedEvent", "OnKeyReleasedEvent", "OnPlayerDied", "OnPlayerRevived", "OnPlayerMove", "OnPlayerStoppedMoving", "OnMicrophoneUnmuted", "OnTimerStopped", "OnCountdownTimerFinished", "ScoreTrigger", "OnAnimationStoppedEvent", "OnItemClickEvent", "PlayerLeave", "SwapVolume", "OnDestroyedEvent"],
27
+ "valid_triggers": ["OnClickEvent", "OnDragStartEvent", "OnCollideEvent", "OnCollisionStoppedEvent", "OnHoverStartEvent", "OnHoverEndEvent", "OnPlayerLoggedIn", "OnKeyPressedEvent", "OnKeyReleasedEvent", "OnPlayerDied", "OnPlayerRevived", "OnPlayerMove", "OnPlayerStoppedMoving", "OnMicrophoneUnmuted", "OnTimerStopped", "OnCountdownTimerFinished", "ScoreTrigger", "OnAnimationStoppedEvent", "OnItemClickEvent", "PlayerLeave", "SwapVolume", "OnDestroyedEvent"],
28
+ "spec_uri": "docs://ref/items/cube"
29
+ },
30
+ {
31
+ "id": "resizable-shapes",
32
+ "name": "Resizable Shape Variants",
33
+ "prefabNames": ["ResizableSphere", "ResizableCylinder", "ResizableCone", "ResizablePyramid", "ResizableWedge", "ResizableTriangularPrism", "ResizableCapsule", "ResizableTorus"],
34
+ "category": "building",
35
+ "aliases": {
36
+ "Sphere": "ResizableSphere",
37
+ "Cylinder": "ResizableCylinder",
38
+ "Cone": "ResizableCone",
39
+ "Pyramid": "ResizablePyramid",
40
+ "Ramp": "ResizableWedge",
41
+ "Wedge": "ResizableWedge",
42
+ "Prism": "ResizableTriangularPrism",
43
+ "TriangularPrism": "ResizableTriangularPrism",
44
+ "Capsule": "ResizableCapsule",
45
+ "Ring": "ResizableTorus",
46
+ "Torus": "ResizableTorus"
47
+ },
48
+ "key_fields": {
49
+ "col": "string (6-char hex, no #) — color",
50
+ "contentString": "string — optional texture URL",
51
+ "e": "float — emission/glow 0.0-1.0+",
52
+ "o": "float — opacity 0.0-1.0",
53
+ "sm": "float — material smoothness 0-1",
54
+ "m": "float — material metallic 0-1",
55
+ "c": "bool — collider (false=pass-through)",
56
+ "nav": "bool — AI nav mesh surface",
57
+ "p": "bool — has physics",
58
+ "pm": "bool — pushable by player",
59
+ "mass": "float — physics mass 0.1-50",
60
+ "lbl": "string — physics label for trigger-zone filtering"
61
+ },
62
+ "notes": "ReactGUI Shapes panel variants use the same extraData schema as ResizableCube. cf is GLB-only and is invalid on these shapes.",
63
+ "valid_triggers": ["OnClickEvent", "OnDragStartEvent", "OnCollideEvent", "OnCollisionStoppedEvent", "OnHoverStartEvent", "OnHoverEndEvent", "OnPlayerLoggedIn", "OnKeyPressedEvent", "OnKeyReleasedEvent", "OnPlayerDied", "OnPlayerRevived", "OnPlayerMove", "OnPlayerStoppedMoving", "OnMicrophoneUnmuted", "OnTimerStopped", "OnCountdownTimerFinished", "ScoreTrigger", "OnAnimationStoppedEvent", "OnItemClickEvent", "PlayerLeave", "SwapVolume", "OnDestroyedEvent"],
21
64
  "spec_uri": "docs://ref/items/cube"
22
65
  },
23
66
  {
@@ -27,9 +70,10 @@
27
70
  "category": "building",
28
71
  "key_fields": {
29
72
  "text": "string — content with Unity rich text tags",
30
- "lookAtCamera": "bool — billboard mode (default true)"
73
+ "lookAtCamera": "bool — billboard mode (default true)",
74
+ "font": "string — optional ReactGUI font identifier/name"
31
75
  },
32
- "valid_triggers": ["OnClickEvent", "OnCollideEvent", "OnCollisionStoppedEvent", "OnHoverStartEvent", "OnHoverEndEvent", "OnPlayerLoggedIn", "OnKeyPressedEvent", "OnKeyReleasedEvent", "ScoreTrigger", "OnAnimationStoppedEvent", "OnPlayerDied", "OnPlayerRevived", "PlayerLeave"],
76
+ "valid_triggers": ["OnClickEvent", "OnDragStartEvent", "OnCollideEvent", "OnCollisionStoppedEvent", "OnHoverStartEvent", "OnHoverEndEvent", "OnPlayerLoggedIn", "OnKeyPressedEvent", "OnKeyReleasedEvent", "ScoreTrigger", "OnAnimationStoppedEvent", "OnPlayerDied", "OnPlayerRevived", "PlayerLeave"],
33
77
  "spec_uri": "docs://ref/items/text"
34
78
  },
35
79
  {
@@ -68,7 +112,12 @@
68
112
  "c": "bool — collider",
69
113
  "l": "bool — local animation (true=per-client)",
70
114
  "f": "bool — camera fade",
71
- "r": "bool — remove first frame"
115
+ "r": "bool — remove first frame",
116
+ "p": "bool — has physics",
117
+ "pm": "bool — pushable by player",
118
+ "mass": "float — physics mass 0.1-50",
119
+ "lbl": "string — physics label for trigger-zone filtering",
120
+ "cf": "int — physics collider shape 0-3"
72
121
  },
73
122
  "valid_triggers": ["OnClickEvent", "OnCollideEvent", "OnCollisionStoppedEvent", "OnHoverStartEvent", "OnHoverEndEvent", "OnPlayerLoggedIn", "OnKeyPressedEvent", "OnKeyReleasedEvent", "ScoreTrigger", "OnAnimationStoppedEvent", "OnPlayerDied", "OnPlayerRevived", "PlayerLeave"],
74
123
  "spec_uri": "docs://ref/items/glb"
@@ -118,8 +167,8 @@
118
167
  "cm": "string — custom press message",
119
168
  "opacity": "float — editor opacity"
120
169
  },
121
- "valid_triggers": ["OnEnterEvent", "OnExitEvent", "OnKeyPressedEvent", "OnKeyReleasedEvent", "OnPlayerLoggedIn", "ScoreTrigger", "OnTimerStopped", "OnCountdownTimerFinished", "OnAnimationStoppedEvent", "OnPlayerDied", "OnPlayerRevived", "PlayerLeave"],
122
- "notes": "Invisible during play. NEVER use OnClickEvent/OnHoverStart/OnHoverEnd on triggers. pressBtn:true gates OnEnterEvent until keyCode is pressed while inside; it is not global OnKeyPressedEvent.",
170
+ "valid_triggers": ["OnEnterEvent", "OnExitEvent", "OnPhysicsItemEnterEvent", "OnPhysicsItemExitEvent", "OnKeyPressedEvent", "OnKeyReleasedEvent", "OnPlayerLoggedIn", "ScoreTrigger", "OnTimerStopped", "OnCountdownTimerFinished", "OnAnimationStoppedEvent", "OnPlayerDied", "OnPlayerRevived", "PlayerLeave"],
171
+ "notes": "Invisible during play. NEVER use OnClickEvent/OnHoverStart/OnHoverEnd on triggers. pressBtn:true gates OnEnterEvent until keyCode is pressed while inside; it is not global OnKeyPressedEvent. Physics item enter/exit only detects physics items with p:true/CubePhysicsSync, optionally filtered by lbl/label.",
123
172
  "spec_uri": "docs://ref/items/trigger-zone"
124
173
  },
125
174
  {
@@ -173,7 +222,9 @@
173
222
  "category": "gameplay",
174
223
  "key_fields": {
175
224
  "fov": "float — field of view degrees",
176
- "sn": "string — state name (must match customCameraStates)"
225
+ "sn": "string — unique CameraObject state name for ChangeCamState",
226
+ "aP": "bool — Look At Player",
227
+ "renderRateFollow": "bool — active camera copies this CameraObject every render frame"
177
228
  },
178
229
  "valid_triggers": [],
179
230
  "notes": "Place at desired camera viewpoint. Use ChangeCamState effect to activate.",
@@ -385,7 +436,7 @@
385
436
  "prefabName": "Addressable",
386
437
  "category": "effects",
387
438
  "key_fields": {},
388
- "notes": "contentString=FurnitureAddressables/{EffectName}. No special extraData. Toggle with Show/HideObjectEvent.",
439
+ "notes": "contentString=FurnitureAddressables/{EffectName}. Set c:false to avoid blocking movement. ReactGUI catalog includes fire, explosions, lightning, sparks, smoke, weather, water, trails, and hit impacts. Toggle with Show/HideObjectEvent.",
389
440
  "valid_triggers": ["OnPlayerLoggedIn", "OnClickEvent", "OnCollideEvent"],
390
441
  "spec_uri": "docs://ref/items/effects-vfx"
391
442
  }
@@ -224,6 +224,12 @@
224
224
  "description": "Quick reference for MCP tool calls and room JSON structure",
225
225
  "uri": "docs://reference/api-cheatsheet"
226
226
  },
227
+ {
228
+ "id": "logic.guides.reactgui-feature-test-room",
229
+ "label": "ReactGUI Feature Test Room",
230
+ "description": "Checklist, generator, and validation notes for ReactGUI/Core feature coverage in room data",
231
+ "uri": "docs://reference/reactgui-feature-test-room"
232
+ },
227
233
  {
228
234
  "id": "logic.guides.sandbox-environment",
229
235
  "label": "JS Sandbox Environment",
@@ -8,6 +8,16 @@
8
8
  "applicable_items": "any",
9
9
  "gotchas": ["Requires VISIBLE item — NEVER use on Trigger cubes (invisible)"]
10
10
  },
11
+ {
12
+ "$type": "OnDragStartEvent",
13
+ "description": "Build/edit drag starts on this item",
14
+ "applicable_items": "any",
15
+ "params": {
16
+ "adminOnly": "optional bool; true restricts to admins/builders",
17
+ "dis": "optional distance in meters; 0 or omitted means unlimited"
18
+ },
19
+ "gotchas": ["This is a creator/build interaction trigger, not a runtime physics collision trigger."]
20
+ },
11
21
  {
12
22
  "$type": "OnEnterEvent",
13
23
  "description": "Player enters trigger zone. If the Trigger item has pressBtn:true, this fires only after the player enters and presses the Trigger keyCode.",
@@ -20,6 +30,24 @@
20
30
  "applicable_items": ["Trigger"],
21
31
  "gotchas": ["ONLY works on Trigger cubes, not ResizableCubes or GLBs"]
22
32
  },
33
+ {
34
+ "$type": "OnPhysicsItemEnterEvent",
35
+ "description": "Physics-enabled item enters a trigger zone, optionally filtered by physics label",
36
+ "applicable_items": ["Trigger"],
37
+ "params": {
38
+ "label": "optional string; empty or omitted matches any physics item"
39
+ },
40
+ "gotchas": ["ONLY works on Trigger cubes", "The entering item needs physics p:true and a CubePhysicsSync body", "Held/build-mode items are ignored by Core's physics trigger detector"]
41
+ },
42
+ {
43
+ "$type": "OnPhysicsItemExitEvent",
44
+ "description": "Physics-enabled item exits a trigger zone, optionally filtered by physics label",
45
+ "applicable_items": ["Trigger"],
46
+ "params": {
47
+ "label": "optional string; empty or omitted matches any physics item"
48
+ },
49
+ "gotchas": ["ONLY works on Trigger cubes", "The exiting item needs physics p:true and a CubePhysicsSync body", "Held/build-mode items are ignored by Core's physics trigger detector"]
50
+ },
23
51
  {
24
52
  "$type": "OnCollideEvent",
25
53
  "description": "Player collides with item",
@@ -38,18 +38,23 @@ FunctionEffectorJS runs JavaScript code inside the Portals engine. It is more po
38
38
  | `S` | bool | Conditional | **Activate on Start.** REQUIRED when trigger is `OnPlayerLoggedIn`. Without it, the JS silently never fires on login. |
39
39
  | `R` | bool | Optional | **Trigger on Tasks Change.** Auto-re-fires the JS whenever any referenced variable or task changes. Use for reactive watchers. |
40
40
 
41
- ## Reading Variables
41
+ ## Reading Values
42
42
 
43
- Use `$N{}` to read **all** variable types (numeric and string):
43
+ Use `$N{}` to read variables, player data, and timer-like values:
44
44
 
45
45
  ```javascript
46
46
  var score = $N{score};
47
47
  var team = $N{teamname};
48
48
  ```
49
49
 
50
- **`$N{}` is the only way to read variables in JS.** There is no `$S{}` syntax.
50
+ `FunctionEffectorJS` in the current ReactGUI branch also rewrites task-state macros before evaluation:
51
51
 
52
- **`$T{}` and `$TN{}` for quest state were NOT verified working in JS** (B002/B003). They are listed in some older docs but live testing produced no usable value. To read quest state from JS, write a mirror numeric/string variable from the same task or effector that writes the quest state, and read the mirror with `$N{...}`. `SetTask(...)` to write quest state IS verified (B003 — use the bare task suffix, not the `0_…` numbered name).
52
+ ```javascript
53
+ var stateName = $T{intro}; // "NotActive", "Active", "Completed", or "Undefined"
54
+ var stateCode = $TN{intro}; // 0, 1, 2, or -1
55
+ ```
56
+
57
+ `$N{}` is still the preferred read path for ordinary numeric/string variables. There is no `$S{}` syntax.
53
58
 
54
59
  ## Writing Variables
55
60
 
@@ -83,6 +88,10 @@ var alive = CountArray(SelectPlayers({{Players}}, 'alive', 'true'));
83
88
  // Assign sequential numbers
84
89
  AssignNumbersInOrder({{Players}}, 'playernum');
85
90
 
91
+ // Set/read a parameter for the local player
92
+ SetPlayerParameter('ready', 'true');
93
+ var localReady = SelectPlayerParameter('ready');
94
+
86
95
  // Chain operations with +
87
96
  SetPlayersParameters({{Players}}, 'team', 'blue') + SetPlayersParameters(SelectRandomPlayers({{Players}}, 3), 'team', 'red');
88
97
 
@@ -99,12 +108,13 @@ See `docs://logic/multiplayer` for full reference on opTypes, variable scopes, a
99
108
 
100
109
  ## Calling Effects from JS
101
110
 
102
- Use `UseEffector()` to trigger any Portals effect. See `docs://logic/js-use-effector` for the full effect catalog.
111
+ Use `UseEffector()` to trigger any Portals effect exposed by Core's `EffectorsLists`. Use `PrepareEffector()` with the same arguments to create/cache the temporary effector without activating it yet. See `docs://logic/js-use-effector` for the full effect catalog.
103
112
 
104
113
  ```javascript
105
114
  UseEffector({{Object}}, 'HideObject');
106
115
  UseEffector('760', 'ShowObject');
107
116
  UseEffector({{Object}}, 'MoveToSpot', '{"transformState":{"position":[0,5,0],"rotation":[0,0,0,1],"scale":[1,1,1],"duration":2.0},"relative":true}');
117
+ PrepareEffector({{Object}}, 'SetTransform', '{"scale":[2,2,2]}', 'grow');
108
118
  ```
109
119
 
110
120
  **Targets:** `{{Object}}`, `{{Parent}}`, `{{Child}}` are unquoted template tokens. String IDs like `'760'` are quoted.
@@ -153,7 +163,7 @@ In FunctionEffectorJS, use `return;` to exit early. Do not use the NCalc pattern
153
163
  "V": "if (Number($N{done}) == 1.0) { return; } SetVariable('counter', $N{counter} + 1.0, 0.0);"
154
164
  ```
155
165
 
156
- Use a mirror variable like `done` for quest/task state checks in JS. `$T{}` / `$TN{}` task reads were not verified working in B002/B003.
166
+ For quest/task state checks, the current ReactGUI JS bridge rewrites `$T{task}` to a state name and `$TN{task}` to a numeric state code. Use mirror variables only when you need custom derived state.
157
167
 
158
168
  ### 6. Reserved-keyword caveats
159
169
  Some older docs warn that `from` causes a silent parse error. Live testing (B002) showed `var from = 6.0;` worked on the current build, so the warning is unreliable as a strict rule. Still — prefer non-keyword names (`source`, `origin`, `start`) so the code is robust across engine versions.