// // shelfShape.js — Manifold Shop Entity Script // // Attach to each shelf shape entity. Set userData to: // {"shapeId": "tetrahedron"} // // Clones are domain-owned. The domain script (manifoldDomain.js) handles // all cleanup via WebSocket presence events and recall messages. // (function () { var API_BASE = "https://wizards.cyou/api"; var SPAWN_OFFSET = { x: 0, y: 0.3, z: 0.5 }; var SHAPE_TYPE = { tetrahedron: "Tetrahedron", hexahedron: "Box", octahedron: "Octahedron", dodecahedron: "Dodecahedron", icosahedron: "Icosahedron", }; var SHAPE_COLOR = { tetrahedron: { red: 255, green: 180, blue: 60 }, hexahedron: { red: 180, green: 220, blue: 255 }, octahedron: { red: 140, green: 255, blue: 180 }, dodecahedron: { red: 255, green: 120, blue: 180 }, icosahedron: { red: 200, green: 140, blue: 255 }, }; var _entityID = null; var _shapeId = null; var _token = null; var _username = null; var _busy = false; // ── helpers ────────────────────────────────────────────────── function getShapeId() { try { var props = Entities.getEntityProperties(_entityID, ["userData"]); return JSON.parse(props.userData || "{}").shapeId || null; } catch (e) { return null; } } function notify(msg) { Window.displayAnnouncement(msg); } // ── session ────────────────────────────────────────────────── function ensureSession(cb) { if (_token) { cb(null, _token); return; } _username = MyAvatar.displayName; if (!_username) { cb("Not logged in to Overte"); return; } var xhr = new XMLHttpRequest(); xhr.open("POST", API_BASE + "/session", true); xhr.setRequestHeader("Content-Type", "application/json"); xhr.onreadystatechange = function () { if (xhr.readyState !== 4) return; if (xhr.status === 200) { try { _token = JSON.parse(xhr.responseText).token; Script.setTimeout(function () { _token = null; }, 5 * 60 * 1000); cb(null, _token); } catch (e) { cb("Session parse error"); } } else if (xhr.status === 403) { cb("Must be in-world to use the shop"); } else { cb("Session error (" + xhr.status + ")"); } }; xhr.send(JSON.stringify({ username: _username })); } // ── inventory ──────────────────────────────────────────────── function getInventory(cb) { var xhr = new XMLHttpRequest(); xhr.open("GET", API_BASE + "/inventory/" + _username, true); xhr.onreadystatechange = function () { if (xhr.readyState !== 4) return; if (xhr.status === 200) { try { cb(null, JSON.parse(xhr.responseText).inventory || {}); } catch (e) { cb("Inventory parse error"); } } else { cb("Inventory fetch failed (" + xhr.status + ")"); } }; xhr.send(); } function countLiveClones(username, shapeId) { // Scan both Shape and Box — hexahedron/cube uses the Box entity type var results = Entities.findEntitiesByType("Shape", MyAvatar.position, 100) .concat(Entities.findEntitiesByType("Box", MyAvatar.position, 100)); var count = 0; results.forEach(function (id) { try { var ud = JSON.parse(Entities.getEntityProperties(id, ["userData"]).userData || "{}"); if (ud.manifoldClone && ud.owner === username && ud.shapeId === shapeId) count++; } catch (e) {} }); return count; } // ── spawn ──────────────────────────────────────────────────── function spawnClone() { var shelfProps = Entities.getEntityProperties(_entityID, ["position", "rotation"]); var spawnPos = Vec3.sum(shelfProps.position, Vec3.multiplyQbyV(shelfProps.rotation, SPAWN_OFFSET)); var shapeType = SHAPE_TYPE[_shapeId] || "Sphere"; var entityType = (shapeType === "Box") ? "Box" : "Shape"; Entities.addEntity({ type: entityType, shape: shapeType, name: "manifold_" + _shapeId + "_" + _username, position: spawnPos, dimensions: { x: 0.25, y: 0.25, z: 0.25 }, color: SHAPE_COLOR[_shapeId] || { red: 200, green: 200, blue: 200 }, userData: JSON.stringify({ manifoldClone: true, owner: _username, shapeId: _shapeId }), dynamic: true, grabbable: true, restitution: 0.6, friction: 0.4, density: 800, emissive: true, lifetime: 3600, // hard ceiling fallback }, "domain"); } // ── click handler ──────────────────────────────────────────── function onClickDown() { if (_busy) return; _busy = true; _shapeId = getShapeId(); if (!_shapeId) { notify("Shop item not configured (missing shapeId in userData)"); _busy = false; return; } ensureSession(function (err) { if (err) { notify(err); _busy = false; return; } getInventory(function (err, inventory) { if (err) { notify(err); _busy = false; return; } var owned = inventory[_shapeId] || 0; if (owned === 0) { notify("You don't own any " + _shapeId + "s. Buy one in the Manifold tablet."); _busy = false; return; } var live = countLiveClones(_username, _shapeId); if (live >= owned) { notify("You have " + live + "/" + owned + " " + _shapeId + "(s) active. Recall them or buy more."); _busy = false; return; } spawnClone(); notify("Spawned " + _shapeId + " (" + (live + 1) + "/" + owned + " active)"); // _busy stays true until trigger release (stopNearTrigger/stopFarTrigger/clickUpOnEntity) }); }); } // ── lifecycle ──────────────────────────────────────────────── this.preload = function (entityID) { _entityID = entityID; }; // Mouse this.clickDownOnEntity = function () { onClickDown(); }; this.clickReleaseOnEntity = function () { _busy = false; }; // Controller — near (<0.3m) and far (>0.3m) // Requires "Triggerable" checkbox enabled on the entity. this.startNearTrigger = function () { onClickDown(); }; this.stopNearTrigger = function () { _busy = false; }; this.startFarTrigger = function () { onClickDown(); }; this.stopFarTrigger = function () { _busy = false; }; this.unload = function () { _token = null; _username = null; }; });