openvibers/openvibe.live Commits
Live: the inventory says where a locked item is earned (a quest on OpenVibe.Quest) instead of the retired game, and links the whole inventory (#60)
Changes
@@@ -1,8 +1,8 @@/*** OpenVibe.Live — Cosmetics Inventory UI* Opens from the OpenCoins badge in the navbar.-- * Allows equipping/unequipping name effects, particles, hats, and voices.-- * Supports activate (game item → global cosmetic) and deactivate (back to game item).++ * Allows equipping/unequipping name effects, particles, hats, and voices. Items live in OpenVibe.Inventory; a locked++ * item says where it is earned (a quest on OpenVibe.Quest) or that it cannot be earned yet.*//* ── State ────────────────────────────────────────────────────── */@@@ -128,7 +128,9 @@ function renderCosmeticsUI() {`;}} else {-- actionsHtml = `<span class="cosmetic-desc" style="font-style:italic;opacity:0.7">🎮 Unlock in OpenVibeGame, then activate from your game inventory</span>`;++ actionsHtml = item.earn === 'quest'++ ? `<span class="cosmetic-desc" style="opacity:0.85">🏅 Earn it with a quest on <a href="https://openvibe.quest/" target="_blank" rel="noopener">OpenVibe.Quest</a></span>`++ : `<span class="cosmetic-desc" style="font-style:italic;opacity:0.7">Not available to earn yet</span>`;}const badgeHtml = isEquipped@@@ -152,6 +154,17 @@ function renderCosmeticsUI() {container.innerHTML = '';container.appendChild(grid);}++++ // Where the rest lives: the whole inventory on OpenVibe.Inventory, and the quests that give items.++ const body = document.getElementById('cosmetics-body');++ if (body && !document.getElementById('cosmetics-more')) {++ const more = document.createElement('p');++ more.id = 'cosmetics-more';++ more.className = 'cosmetic-desc';++ more.style.cssText = 'text-align:center;margin:12px 0 4px';++ more.innerHTML = '<a href="https://inventory.openvibe.network/me" target="_blank" rel="noopener">Your whole inventory on OpenVibe.Inventory</a> · <a href="https://openvibe.quest/" target="_blank" rel="noopener">Earn items on OpenVibe.Quest</a>';++ body.after(more);++ }}/* ── API Actions ──────────────────────────────────────────────── */@@@ -15,11 +15,13 @@ const cosmetics = require('./cosmetics');const router = express.Router();// ── Get Full Catalog ─────────────────────────────────────────--router.get('/catalog', (req, res) => {++router.get('/catalog', async (req, res) => {++ // `earn: 'quest'` marks the items a quest on OpenVibe.Quest gives (Inventory's grantors for them).++ const earn = await cosmetics.earnable();const catalog = {};for (const [id, c] of Object.entries(cosmetics.COSMETICS)) {if (!catalog[c.category]) catalog[c.category] = [];-- catalog[c.category].push({ itemId: id, ...c });++ catalog[c.category].push({ itemId: id, ...c, ...(earn[id] ? { earn: earn[id] } : {}) });}// Sort each category by tierfor (const arr of Object.values(catalog)) arr.sort((a, b) => a.tier - b.tier);@@@ -170,6 +170,19 @@ async function getCosmeticProfiles(userIds) {return out;}++/**++ * Where each item can be earned: { itemId → 'quest' } for the items Live lets OpenVibe.Quest give as quest rewards++ * (Inventory's definition grantors, ADR-054 §3). Inventory down: {} (the shop just says nothing about earning).++ */++async function earnable() {++ return await quietly(async () => {++ const { byAlias } = await inventory.catalog();++ const out = {};++ for (const [alias, d] of byAlias) if (COSMETICS[alias] && (d.grantors || []).includes('service:quest')) out[alias] = 'quest';++ return out;++ }, {});++}++// ── Check if user owns a cosmetic ────────────────────────────async function ownsCosmetic(userId, itemId) {return (await getUnlocked(userId)).some((u) => u.item_id === itemId);@@@ -264,4 +277,5 @@ module.exports = {equipCosmetic,unequipSlot,getFullInventory,++ earnable,};@@@ -13,7 +13,7 @@ console.warn = () => {};const A = 'usr_01JZ0000000000000000000AAA';const DEFS = [-- { id: 'itd_01JZ00000000000000000001R1', kind: 'live.name_effect', aliases: ['fx_rainbow'] },++ { id: 'itd_01JZ00000000000000000001R1', kind: 'live.name_effect', aliases: ['fx_rainbow'], grantors: ['service:quest'] },{ id: 'itd_01JZ00000000000000000001R2', kind: 'live.hat', aliases: ['hat_crown'] },{ id: 'itd_01JZ00000000000000000001R3', kind: 'live.particle', aliases: ['px_sparkle'] },];@@@ -103,6 +103,9 @@ function stub() {assert.strictEqual((await cosmetics.revokeCosmetic(70, 'hat_crown')).success, true);assert.ok(!(await cosmetics.getUnlocked(70)).some((u) => u.item_id === 'hat_crown'));++ // The shop says where an item is earned: Quest gives the Rainbow (Inventory's grantors).++ assert.deepStrictEqual(await cosmetics.earnable(), { fx_rainbow: 'quest' });++// OpenVibe.Chat's decor lookup: Ana and 250 others in three batched reads, never one per person; an account// without a subject or an unknown id answers an empty profile.await cosmetics.equipCosmetic(70, 'fx_rainbow');@@@ -132,6 +135,7 @@ function stub() {assert.match((await cosmetics.equipCosmetic(70, 'px_sparkle')).error, /unavailable|own/i);inventory.reset();assert.deepStrictEqual(await cosmetics.getCosmeticProfiles([70]), { 70: {} }, 'down: an empty profile, not an error');++ assert.deepStrictEqual(await cosmetics.earnable(), {}, 'down: nothing said about earning');state.down = false;process.stdout.write('cosmetics through OpenVibe.Inventory: all checks passed\n');} finally {
@@ -1,8 +1,8 @@
@@ -1,8 +1,8 @@
/**
/**
* OpenVibe.Live — Cosmetics Inventory UI
* OpenVibe.Live — Cosmetics Inventory UI
* Opens from the OpenCoins badge in the navbar.
* Opens from the OpenCoins badge in the navbar.
- * Allows equipping/unequipping name effects, particles, hats, and voices.
- * Supports activate (game item → global cosmetic) and deactivate (back to game item).
+ * Allows equipping/unequipping name effects, particles, hats, and voices. Items live in OpenVibe.Inventory; a locked
+ * item says where it is earned (a quest on OpenVibe.Quest) or that it cannot be earned yet.
*/
*/
/* ── State ────────────────────────────────────────────────────── */
/* ── State ────────────────────────────────────────────────────── */
@@ -128,7 +128,9 @@ function renderCosmeticsUI() {
@@ -128,7 +128,9 @@ function renderCosmeticsUI() {
`;
`;
}
}
} else {
} else {
- actionsHtml = `<span class="cosmetic-desc" style="font-style:italic;opacity:0.7">🎮 Unlock in OpenVibeGame, then activate from your game inventory</span>`;
+ actionsHtml = item.earn === 'quest'
+ ? `<span class="cosmetic-desc" style="opacity:0.85">🏅 Earn it with a quest on <a href="https://openvibe.quest/" target="_blank" rel="noopener">OpenVibe.Quest</a></span>`
+ : `<span class="cosmetic-desc" style="font-style:italic;opacity:0.7">Not available to earn yet</span>`;
}
}
const badgeHtml = isEquipped
const badgeHtml = isEquipped
@@ -152,6 +154,17 @@ function renderCosmeticsUI() {
@@ -152,6 +154,17 @@ function renderCosmeticsUI() {
container.innerHTML = '';
container.innerHTML = '';
container.appendChild(grid);
container.appendChild(grid);
}
}
+
+ // Where the rest lives: the whole inventory on OpenVibe.Inventory, and the quests that give items.
+ const body = document.getElementById('cosmetics-body');
+ if (body && !document.getElementById('cosmetics-more')) {
+ const more = document.createElement('p');
+ more.id = 'cosmetics-more';
+ more.className = 'cosmetic-desc';
+ more.style.cssText = 'text-align:center;margin:12px 0 4px';
+ more.innerHTML = '<a href="https://inventory.openvibe.network/me" target="_blank" rel="noopener">Your whole inventory on OpenVibe.Inventory</a> · <a href="https://openvibe.quest/" target="_blank" rel="noopener">Earn items on OpenVibe.Quest</a>';
+ body.after(more);
+ }
}
}
/* ── API Actions ──────────────────────────────────────────────── */
/* ── API Actions ──────────────────────────────────────────────── */
@@ -15,11 +15,13 @@ const cosmetics = require('./cosmetics');
@@ -15,11 +15,13 @@ const cosmetics = require('./cosmetics');
const router = express.Router();
const router = express.Router();
// ── Get Full Catalog ─────────────────────────────────────────
// ── Get Full Catalog ─────────────────────────────────────────
-router.get('/catalog', (req, res) => {
+router.get('/catalog', async (req, res) => {
+ // `earn: 'quest'` marks the items a quest on OpenVibe.Quest gives (Inventory's grantors for them).
+ const earn = await cosmetics.earnable();
const catalog = {};
const catalog = {};
for (const [id, c] of Object.entries(cosmetics.COSMETICS)) {
for (const [id, c] of Object.entries(cosmetics.COSMETICS)) {
if (!catalog[c.category]) catalog[c.category] = [];
if (!catalog[c.category]) catalog[c.category] = [];
- catalog[c.category].push({ itemId: id, ...c });
+ catalog[c.category].push({ itemId: id, ...c, ...(earn[id] ? { earn: earn[id] } : {}) });
}
}
// Sort each category by tier
// Sort each category by tier
for (const arr of Object.values(catalog)) arr.sort((a, b) => a.tier - b.tier);
for (const arr of Object.values(catalog)) arr.sort((a, b) => a.tier - b.tier);
@@ -170,6 +170,19 @@ async function getCosmeticProfiles(userIds) {
@@ -170,6 +170,19 @@ async function getCosmeticProfiles(userIds) {
return out;
return out;
}
}
+/**
+ * Where each item can be earned: { itemId → 'quest' } for the items Live lets OpenVibe.Quest give as quest rewards
+ * (Inventory's definition grantors, ADR-054 §3). Inventory down: {} (the shop just says nothing about earning).
+ */
+async function earnable() {
+ return await quietly(async () => {
+ const { byAlias } = await inventory.catalog();
+ const out = {};
+ for (const [alias, d] of byAlias) if (COSMETICS[alias] && (d.grantors || []).includes('service:quest')) out[alias] = 'quest';
+ return out;
+ }, {});
+}
+
// ── Check if user owns a cosmetic ────────────────────────────
// ── Check if user owns a cosmetic ────────────────────────────
async function ownsCosmetic(userId, itemId) {
async function ownsCosmetic(userId, itemId) {
return (await getUnlocked(userId)).some((u) => u.item_id === itemId);
return (await getUnlocked(userId)).some((u) => u.item_id === itemId);
@@ -264,4 +277,5 @@ module.exports = {
@@ -264,4 +277,5 @@ module.exports = {
equipCosmetic,
equipCosmetic,
unequipSlot,
unequipSlot,
getFullInventory,
getFullInventory,
+ earnable,
};
};
@@ -13,7 +13,7 @@ console.warn = () => {};
@@ -13,7 +13,7 @@ console.warn = () => {};
const A = 'usr_01JZ0000000000000000000AAA';
const A = 'usr_01JZ0000000000000000000AAA';
const DEFS = [
const DEFS = [
- { id: 'itd_01JZ00000000000000000001R1', kind: 'live.name_effect', aliases: ['fx_rainbow'] },
+ { id: 'itd_01JZ00000000000000000001R1', kind: 'live.name_effect', aliases: ['fx_rainbow'], grantors: ['service:quest'] },
{ id: 'itd_01JZ00000000000000000001R2', kind: 'live.hat', aliases: ['hat_crown'] },
{ id: 'itd_01JZ00000000000000000001R2', kind: 'live.hat', aliases: ['hat_crown'] },
{ id: 'itd_01JZ00000000000000000001R3', kind: 'live.particle', aliases: ['px_sparkle'] },
{ id: 'itd_01JZ00000000000000000001R3', kind: 'live.particle', aliases: ['px_sparkle'] },
];
];
@@ -103,6 +103,9 @@ function stub() {
@@ -103,6 +103,9 @@ function stub() {
assert.strictEqual((await cosmetics.revokeCosmetic(70, 'hat_crown')).success, true);
assert.strictEqual((await cosmetics.revokeCosmetic(70, 'hat_crown')).success, true);
assert.ok(!(await cosmetics.getUnlocked(70)).some((u) => u.item_id === 'hat_crown'));
assert.ok(!(await cosmetics.getUnlocked(70)).some((u) => u.item_id === 'hat_crown'));
+ // The shop says where an item is earned: Quest gives the Rainbow (Inventory's grantors).
+ assert.deepStrictEqual(await cosmetics.earnable(), { fx_rainbow: 'quest' });
+
// OpenVibe.Chat's decor lookup: Ana and 250 others in three batched reads, never one per person; an account
// OpenVibe.Chat's decor lookup: Ana and 250 others in three batched reads, never one per person; an account
// without a subject or an unknown id answers an empty profile.
// without a subject or an unknown id answers an empty profile.
await cosmetics.equipCosmetic(70, 'fx_rainbow');
await cosmetics.equipCosmetic(70, 'fx_rainbow');
@@ -132,6 +135,7 @@ function stub() {
@@ -132,6 +135,7 @@ function stub() {
assert.match((await cosmetics.equipCosmetic(70, 'px_sparkle')).error, /unavailable|own/i);
assert.match((await cosmetics.equipCosmetic(70, 'px_sparkle')).error, /unavailable|own/i);
inventory.reset();
inventory.reset();
assert.deepStrictEqual(await cosmetics.getCosmeticProfiles([70]), { 70: {} }, 'down: an empty profile, not an error');
assert.deepStrictEqual(await cosmetics.getCosmeticProfiles([70]), { 70: {} }, 'down: an empty profile, not an error');
+ assert.deepStrictEqual(await cosmetics.earnable(), {}, 'down: nothing said about earning');
state.down = false;
state.down = false;
process.stdout.write('cosmetics through OpenVibe.Inventory: all checks passed\n');
process.stdout.write('cosmetics through OpenVibe.Inventory: all checks passed\n');
} finally {
} finally {