overte-www/scripts/shelfShape.js
2026-03-15 21:57:31 +00:00

186 lines
6.9 KiB
JavaScript

//
// 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)");
_busy = false;
});
});
}
// ── lifecycle ────────────────────────────────────────────────
this.preload = function (entityID) {
_entityID = entityID;
};
this.clickDownOnEntity = function () {
onClickDown();
};
this.unload = function () {
_token = null;
_username = null;
};
});