2026-03-15 21:57:31 +00:00
|
|
|
//
|
|
|
|
|
// 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: "Cube",
|
|
|
|
|
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) {
|
|
|
|
|
var results = Entities.findEntitiesByType("Shape", 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));
|
|
|
|
|
|
|
|
|
|
Entities.addEntity({
|
|
|
|
|
type: "Shape",
|
|
|
|
|
shape: SHAPE_TYPE[_shapeId] || "Sphere",
|
|
|
|
|
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)");
|
2026-03-16 19:46:35 +00:00
|
|
|
// _busy stays true until trigger release (stopNearTrigger/stopFarTrigger/clickUpOnEntity)
|
2026-03-15 21:57:31 +00:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── lifecycle ────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
this.preload = function (entityID) {
|
|
|
|
|
_entityID = entityID;
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-16 19:46:35 +00:00
|
|
|
// Mouse
|
|
|
|
|
this.clickDownOnEntity = function () { onClickDown(); };
|
|
|
|
|
this.clickUpOnEntity = 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; };
|
2026-03-15 21:57:31 +00:00
|
|
|
|
|
|
|
|
this.unload = function () {
|
|
|
|
|
_token = null;
|
|
|
|
|
_username = null;
|
|
|
|
|
};
|
|
|
|
|
});
|