openvibers/openvibe.inventory Commits
Inventory: Games may grant six of Live's items as achievement rewards; their pages say which achievement (#36)
Changes
@@@ -1,11 +1,67 @@{-- "_about": "Live's items that other services may grant (ADR-054 §3 grantors), by Live's item id. OpenVibe.Quest gives these as quest rewards: Sparkle for saying hello in chat, Hearts for a first follow, the Basic Cap for going live, Fire Name for streaming three times, Ice Name for starting a thread, Rainbow Name for improving a wiki page.",++ "_about": "Live's items that other services may grant (ADR-054 §3 grantors), by Live's item id. OpenVibe.Quest gives these as quest rewards: Sparkle for saying hello in chat, Hearts for a first follow, the Basic Cap for going live, Fire Name for streaming three times, Ice Name for starting a thread, Rainbow Name for improving a wiki page. OpenVibe.Games gives six for achievements (`earn` names which): a person signed in to OpenVibe gets each once.","grantors": {-- "px_sparkle": ["service:quest"],-- "px_hearts": ["service:quest"],-- "hat_basic_cap": ["service:quest"],-- "fx_fire": ["service:quest"],-- "fx_ice": ["service:quest"],-- "fx_rainbow": ["service:quest"]++ "px_sparkle": [++ "service:quest"++ ],++ "px_hearts": [++ "service:quest"++ ],++ "hat_basic_cap": [++ "service:quest"++ ],++ "fx_fire": [++ "service:quest"++ ],++ "fx_ice": [++ "service:quest"++ ],++ "fx_rainbow": [++ "service:quest"++ ],++ "fx_toxic": [++ "service:games"++ ],++ "fx_golden": [++ "service:games"++ ],++ "hat_cowboy": [++ "service:games"++ ],++ "fx_neon": [++ "service:games"++ ],++ "px_flames": [++ "service:games"++ ],++ "px_stars": [++ "service:games"++ ]++ },++ "earn": {++ "fx_toxic": {++ "service": "service:games",++ "how": "First Haul in Scraplandia (extract valuables to Scrap City)"++ },++ "fx_golden": {++ "service": "service:games",++ "how": "Big Haul in Scraplandia (500 haul points in one extraction)"++ },++ "hat_cowboy": {++ "service": "service:games",++ "how": "Haul Week in Scraplandia (2,000 haul points in a week)"++ },++ "fx_neon": {++ "service": "service:games",++ "how": "First Haul in Salvage Rush (score in a round)"++ },++ "px_flames": {++ "service": "service:games",++ "how": "Big Haul in Salvage Rush (five points in one round)"++ },++ "px_stars": {++ "service": "service:games",++ "how": "Arena Winner in Salvage Rush"++ }}}@@@ -33,6 +33,14 @@ const KIND_ICON = { 'live.name_effect': 'ov:text', 'live.particle': 'ov:theme',const surfaceLabel = (s) => SURFACE_LABEL[s] || (s.startsWith('game:') ? `the game ${s.slice(5)}` : s);const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;++// What earns an item on another service (server/data/live-grantors.json `earn`, by Live's alias): the achievement a++// game names, shown on the item's page.++const EARN = (() => { try { return require('../data/live-grantors.json').earn || {}; } catch { return {}; } })();++function earnHow(d) {++ for (const a of (d.aliases || [])) { const alias = typeof a === 'string' ? a : a && (a.alias || a.value); if (alias && EARN[alias]) return EARN[alias].how; }++ return null;++}++/** The item's picture: its emoji, or a Media image, in a square tile tinted by rarity. */function art(d, { size = 'md' } = {}) {const a = d.art || {};@@@ -152,10 +160,12 @@ ${shown.map((k) => html`<section class="inv-kind" aria-labelledby="k-${k.id.repl</ul>${(d.grantors || []).includes('service:quest') && d.status === 'published' ? html`<h2>How to get it</h2><p>Earn it on <a href="https://openvibe.quest/">OpenVibe.Quest</a>: it is the reward for one of the quests there, given once to everyone who completes it.</p>` : ''}++${(d.grantors || []).includes('service:games') && d.status === 'published' ? html`<h2>How to get it</h2>++<p>Earn it on <a href="https://openvibe.games/">OpenVibe.Games</a>${earnHow(d) ? html`: ${earnHow(d)}` : ''}. Every player signed in with their OpenVibe account gets it once, the first time.</p>` : ''}<h2>Where it shows</h2><p>${k.surfaces.map((s) => surfaceLabel(s.surface)).join(', ').replace(/^./, (c) => c.toUpperCase())}.</p><h2>Who gives it</h2>--<p>${issuer}. Only its issuer can give this item; it is never sold here.</p>++<p>${issuer}${(d.grantors || []).length ? ', and the services it names (see How to get it)' : ''}. ${(d.grantors || []).length ? 'Nobody else' : 'Only its issuer'} can give this item; it is never sold here.</p><p class="muted small">Item <code>${d.id}</code>${(d.aliases || []).length ? html` · also known as <code>${d.aliases.join(', ')}</code>` : ''} · <a href="/api/v1/definitions/${d.id}">JSON</a></p>`,});});@@@ -30,6 +30,7 @@ function describe(row) {const rarity = RARITY_LABEL[row.rarity] || row.rarity;const kind = String(row.kind_name || row.kind);const fromQuest = listOf(row.grantors).includes('service:quest');++ const fromGames = listOf(row.grantors).includes('service:games');const wearable = listOf(row.kind_slots).length > 0;return {listed: row.status === 'published' || row.status === 'retired',@@@ -37,7 +38,8 @@ function describe(row) {title: `${row.name} · ${rarity} ${kind.toLowerCase()}`,summary: row.description || `${row.name}, a ${rarity.toLowerCase()} ${kind.toLowerCase()} on OpenVibe.`,body: [row.name, `${rarity} ${kind}`, row.description, wearable ? 'Wearable' : 'Collectible', `Given by ${issuerName(row)}`,-- fromQuest ? 'Earn it on OpenVibe.Quest: the reward for one of the quests there.' : ''].filter(Boolean).join('\n'),++ fromQuest ? 'Earn it on OpenVibe.Quest: the reward for one of the quests there.' : '',++ fromGames ? 'Earn it on OpenVibe.Games: an achievement there gives it.' : ''].filter(Boolean).join('\n'),facets: { kind: row.kind, rarity: row.rarity, issuer: row.issuer, wearable },authorship: { mode: 'human' },publishedAt: row.published_at || row.created_at,@@@ -17,17 +17,23 @@ const { boot, check, done } = require('./helpers/boot');const A = 'usr_01JZ0000000000000000000AAA';const inv = t.ctx.inv;try {-- await check('the boot seed names Quest on the six reward items, and only those', async () => {++ await check('the boot seed names Quest on its six reward items and Games on its six, and only those', async () => {const live = (await inv.listDefinitions({ issuer: 'service:live', limit: 500 }));-- const named = live.filter((d) => (d.grantors || []).length).map((d) => d.aliases[0]).sort();-- assert.deepStrictEqual(named, ['fx_fire', 'fx_ice', 'fx_rainbow', 'hat_basic_cap', 'px_hearts', 'px_sparkle']);-- assert.ok(live.filter((d) => d.grantors).every((d) => JSON.stringify(d.grantors) === '["service:quest"]'));++ const by = (who) => live.filter((d) => JSON.stringify(d.grantors || []) === JSON.stringify([who])).map((d) => d.aliases[0]).sort();++ assert.deepStrictEqual(by('service:quest'), ['fx_fire', 'fx_ice', 'fx_rainbow', 'hat_basic_cap', 'px_hearts', 'px_sparkle']);++ assert.deepStrictEqual(by('service:games'), ['fx_golden', 'fx_neon', 'fx_toxic', 'hat_cowboy', 'px_flames', 'px_stars']);++ assert.strictEqual(live.filter((d) => (d.grantors || []).length).length, 12, 'no other item names a grantor');++ const named = by('service:quest');const r = await api(`/definitions/${named.length && (await inv.byAlias('service:live', 'px_sparkle')).id}`);assert.deepStrictEqual(r.json().definition.grantors, ['service:quest'], 'the public definition says who may grant it');const page = await t.get(`/items/${(await inv.byAlias('service:live', 'px_sparkle')).id}`);assert.ok(page.text.includes('How to get it') && page.text.includes('https://openvibe.quest/'), 'its page says where to earn it');const golden = await t.get(`/items/${(await inv.byAlias('service:live', 'fx_golden')).id}`);-- assert.ok(!golden.text.includes('How to get it'));++ assert.ok(golden.text.includes('How to get it') && golden.text.includes('https://openvibe.games/'), 'a Games reward says where');++ assert.ok(golden.text.includes('Big Haul in Scraplandia'), 'and which achievement');++ const cap = await t.get(`/items/${(await inv.byAlias('service:live', 'hat_crown')).id}`);++ assert.ok(!cap.text.includes('How to get it'), 'an item nobody else gives has no such section');++ assert.ok(cap.text.includes('Only its issuer can give this item'));const again = await require('../server/inventory/seed').seed(inv, { log: { log() {} } });assert.strictEqual(again.grantors, 0, 'a second boot changes nothing');});
@@ -1,11 +1,67 @@
@@ -1,11 +1,67 @@
{
{
- "_about": "Live's items that other services may grant (ADR-054 §3 grantors), by Live's item id. OpenVibe.Quest gives these as quest rewards: Sparkle for saying hello in chat, Hearts for a first follow, the Basic Cap for going live, Fire Name for streaming three times, Ice Name for starting a thread, Rainbow Name for improving a wiki page.",
+ "_about": "Live's items that other services may grant (ADR-054 §3 grantors), by Live's item id. OpenVibe.Quest gives these as quest rewards: Sparkle for saying hello in chat, Hearts for a first follow, the Basic Cap for going live, Fire Name for streaming three times, Ice Name for starting a thread, Rainbow Name for improving a wiki page. OpenVibe.Games gives six for achievements (`earn` names which): a person signed in to OpenVibe gets each once.",
"grantors": {
"grantors": {
- "px_sparkle": ["service:quest"],
- "px_hearts": ["service:quest"],
- "hat_basic_cap": ["service:quest"],
- "fx_fire": ["service:quest"],
- "fx_ice": ["service:quest"],
- "fx_rainbow": ["service:quest"]
+ "px_sparkle": [
+ "service:quest"
+ ],
+ "px_hearts": [
+ "service:quest"
+ ],
+ "hat_basic_cap": [
+ "service:quest"
+ ],
+ "fx_fire": [
+ "service:quest"
+ ],
+ "fx_ice": [
+ "service:quest"
+ ],
+ "fx_rainbow": [
+ "service:quest"
+ ],
+ "fx_toxic": [
+ "service:games"
+ ],
+ "fx_golden": [
+ "service:games"
+ ],
+ "hat_cowboy": [
+ "service:games"
+ ],
+ "fx_neon": [
+ "service:games"
+ ],
+ "px_flames": [
+ "service:games"
+ ],
+ "px_stars": [
+ "service:games"
+ ]
+ },
+ "earn": {
+ "fx_toxic": {
+ "service": "service:games",
+ "how": "First Haul in Scraplandia (extract valuables to Scrap City)"
+ },
+ "fx_golden": {
+ "service": "service:games",
+ "how": "Big Haul in Scraplandia (500 haul points in one extraction)"
+ },
+ "hat_cowboy": {
+ "service": "service:games",
+ "how": "Haul Week in Scraplandia (2,000 haul points in a week)"
+ },
+ "fx_neon": {
+ "service": "service:games",
+ "how": "First Haul in Salvage Rush (score in a round)"
+ },
+ "px_flames": {
+ "service": "service:games",
+ "how": "Big Haul in Salvage Rush (five points in one round)"
+ },
+ "px_stars": {
+ "service": "service:games",
+ "how": "Arena Winner in Salvage Rush"
+ }
}
}
}
}
@@ -33,6 +33,14 @@ const KIND_ICON = { 'live.name_effect': 'ov:text', 'live.particle': 'ov:theme',
@@ -33,6 +33,14 @@ const KIND_ICON = { 'live.name_effect': 'ov:text', 'live.particle': 'ov:theme',
const surfaceLabel = (s) => SURFACE_LABEL[s] || (s.startsWith('game:') ? `the game ${s.slice(5)}` : s);
const surfaceLabel = (s) => SURFACE_LABEL[s] || (s.startsWith('game:') ? `the game ${s.slice(5)}` : s);
const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
+// What earns an item on another service (server/data/live-grantors.json `earn`, by Live's alias): the achievement a
+// game names, shown on the item's page.
+const EARN = (() => { try { return require('../data/live-grantors.json').earn || {}; } catch { return {}; } })();
+function earnHow(d) {
+ for (const a of (d.aliases || [])) { const alias = typeof a === 'string' ? a : a && (a.alias || a.value); if (alias && EARN[alias]) return EARN[alias].how; }
+ return null;
+}
+
/** The item's picture: its emoji, or a Media image, in a square tile tinted by rarity. */
/** The item's picture: its emoji, or a Media image, in a square tile tinted by rarity. */
function art(d, { size = 'md' } = {}) {
function art(d, { size = 'md' } = {}) {
const a = d.art || {};
const a = d.art || {};
@@ -152,10 +160,12 @@ ${shown.map((k) => html`<section class="inv-kind" aria-labelledby="k-${k.id.repl
@@ -152,10 +160,12 @@ ${shown.map((k) => html`<section class="inv-kind" aria-labelledby="k-${k.id.repl
</ul>
</ul>
${(d.grantors || []).includes('service:quest') && d.status === 'published' ? html`<h2>How to get it</h2>
${(d.grantors || []).includes('service:quest') && d.status === 'published' ? html`<h2>How to get it</h2>
<p>Earn it on <a href="https://openvibe.quest/">OpenVibe.Quest</a>: it is the reward for one of the quests there, given once to everyone who completes it.</p>` : ''}
<p>Earn it on <a href="https://openvibe.quest/">OpenVibe.Quest</a>: it is the reward for one of the quests there, given once to everyone who completes it.</p>` : ''}
+${(d.grantors || []).includes('service:games') && d.status === 'published' ? html`<h2>How to get it</h2>
+<p>Earn it on <a href="https://openvibe.games/">OpenVibe.Games</a>${earnHow(d) ? html`: ${earnHow(d)}` : ''}. Every player signed in with their OpenVibe account gets it once, the first time.</p>` : ''}
<h2>Where it shows</h2>
<h2>Where it shows</h2>
<p>${k.surfaces.map((s) => surfaceLabel(s.surface)).join(', ').replace(/^./, (c) => c.toUpperCase())}.</p>
<p>${k.surfaces.map((s) => surfaceLabel(s.surface)).join(', ').replace(/^./, (c) => c.toUpperCase())}.</p>
<h2>Who gives it</h2>
<h2>Who gives it</h2>
-<p>${issuer}. Only its issuer can give this item; it is never sold here.</p>
+<p>${issuer}${(d.grantors || []).length ? ', and the services it names (see How to get it)' : ''}. ${(d.grantors || []).length ? 'Nobody else' : 'Only its issuer'} can give this item; it is never sold here.</p>
<p class="muted small">Item <code>${d.id}</code>${(d.aliases || []).length ? html` · also known as <code>${d.aliases.join(', ')}</code>` : ''} · <a href="/api/v1/definitions/${d.id}">JSON</a></p>`,
<p class="muted small">Item <code>${d.id}</code>${(d.aliases || []).length ? html` · also known as <code>${d.aliases.join(', ')}</code>` : ''} · <a href="/api/v1/definitions/${d.id}">JSON</a></p>`,
});
});
});
});
@@ -30,6 +30,7 @@ function describe(row) {
@@ -30,6 +30,7 @@ function describe(row) {
const rarity = RARITY_LABEL[row.rarity] || row.rarity;
const rarity = RARITY_LABEL[row.rarity] || row.rarity;
const kind = String(row.kind_name || row.kind);
const kind = String(row.kind_name || row.kind);
const fromQuest = listOf(row.grantors).includes('service:quest');
const fromQuest = listOf(row.grantors).includes('service:quest');
+ const fromGames = listOf(row.grantors).includes('service:games');
const wearable = listOf(row.kind_slots).length > 0;
const wearable = listOf(row.kind_slots).length > 0;
return {
return {
listed: row.status === 'published' || row.status === 'retired',
listed: row.status === 'published' || row.status === 'retired',
@@ -37,7 +38,8 @@ function describe(row) {
@@ -37,7 +38,8 @@ function describe(row) {
title: `${row.name} · ${rarity} ${kind.toLowerCase()}`,
title: `${row.name} · ${rarity} ${kind.toLowerCase()}`,
summary: row.description || `${row.name}, a ${rarity.toLowerCase()} ${kind.toLowerCase()} on OpenVibe.`,
summary: row.description || `${row.name}, a ${rarity.toLowerCase()} ${kind.toLowerCase()} on OpenVibe.`,
body: [row.name, `${rarity} ${kind}`, row.description, wearable ? 'Wearable' : 'Collectible', `Given by ${issuerName(row)}`,
body: [row.name, `${rarity} ${kind}`, row.description, wearable ? 'Wearable' : 'Collectible', `Given by ${issuerName(row)}`,
- fromQuest ? 'Earn it on OpenVibe.Quest: the reward for one of the quests there.' : ''].filter(Boolean).join('\n'),
+ fromQuest ? 'Earn it on OpenVibe.Quest: the reward for one of the quests there.' : '',
+ fromGames ? 'Earn it on OpenVibe.Games: an achievement there gives it.' : ''].filter(Boolean).join('\n'),
facets: { kind: row.kind, rarity: row.rarity, issuer: row.issuer, wearable },
facets: { kind: row.kind, rarity: row.rarity, issuer: row.issuer, wearable },
authorship: { mode: 'human' },
authorship: { mode: 'human' },
publishedAt: row.published_at || row.created_at,
publishedAt: row.published_at || row.created_at,
@@ -17,17 +17,23 @@ const { boot, check, done } = require('./helpers/boot');
@@ -17,17 +17,23 @@ const { boot, check, done } = require('./helpers/boot');
const A = 'usr_01JZ0000000000000000000AAA';
const A = 'usr_01JZ0000000000000000000AAA';
const inv = t.ctx.inv;
const inv = t.ctx.inv;
try {
try {
- await check('the boot seed names Quest on the six reward items, and only those', async () => {
+ await check('the boot seed names Quest on its six reward items and Games on its six, and only those', async () => {
const live = (await inv.listDefinitions({ issuer: 'service:live', limit: 500 }));
const live = (await inv.listDefinitions({ issuer: 'service:live', limit: 500 }));
- const named = live.filter((d) => (d.grantors || []).length).map((d) => d.aliases[0]).sort();
- assert.deepStrictEqual(named, ['fx_fire', 'fx_ice', 'fx_rainbow', 'hat_basic_cap', 'px_hearts', 'px_sparkle']);
- assert.ok(live.filter((d) => d.grantors).every((d) => JSON.stringify(d.grantors) === '["service:quest"]'));
+ const by = (who) => live.filter((d) => JSON.stringify(d.grantors || []) === JSON.stringify([who])).map((d) => d.aliases[0]).sort();
+ assert.deepStrictEqual(by('service:quest'), ['fx_fire', 'fx_ice', 'fx_rainbow', 'hat_basic_cap', 'px_hearts', 'px_sparkle']);
+ assert.deepStrictEqual(by('service:games'), ['fx_golden', 'fx_neon', 'fx_toxic', 'hat_cowboy', 'px_flames', 'px_stars']);
+ assert.strictEqual(live.filter((d) => (d.grantors || []).length).length, 12, 'no other item names a grantor');
+ const named = by('service:quest');
const r = await api(`/definitions/${named.length && (await inv.byAlias('service:live', 'px_sparkle')).id}`);
const r = await api(`/definitions/${named.length && (await inv.byAlias('service:live', 'px_sparkle')).id}`);
assert.deepStrictEqual(r.json().definition.grantors, ['service:quest'], 'the public definition says who may grant it');
assert.deepStrictEqual(r.json().definition.grantors, ['service:quest'], 'the public definition says who may grant it');
const page = await t.get(`/items/${(await inv.byAlias('service:live', 'px_sparkle')).id}`);
const page = await t.get(`/items/${(await inv.byAlias('service:live', 'px_sparkle')).id}`);
assert.ok(page.text.includes('How to get it') && page.text.includes('https://openvibe.quest/'), 'its page says where to earn it');
assert.ok(page.text.includes('How to get it') && page.text.includes('https://openvibe.quest/'), 'its page says where to earn it');
const golden = await t.get(`/items/${(await inv.byAlias('service:live', 'fx_golden')).id}`);
const golden = await t.get(`/items/${(await inv.byAlias('service:live', 'fx_golden')).id}`);
- assert.ok(!golden.text.includes('How to get it'));
+ assert.ok(golden.text.includes('How to get it') && golden.text.includes('https://openvibe.games/'), 'a Games reward says where');
+ assert.ok(golden.text.includes('Big Haul in Scraplandia'), 'and which achievement');
+ const cap = await t.get(`/items/${(await inv.byAlias('service:live', 'hat_crown')).id}`);
+ assert.ok(!cap.text.includes('How to get it'), 'an item nobody else gives has no such section');
+ assert.ok(cap.text.includes('Only its issuer can give this item'));
const again = await require('../server/inventory/seed').seed(inv, { log: { log() {} } });
const again = await require('../server/inventory/seed').seed(inv, { log: { log() {} } });
assert.strictEqual(again.grantors, 0, 'a second boot changes nothing');
assert.strictEqual(again.grantors, 0, 'a second boot changes nothing');
});
});