openvibers/openvibe.quest Commits
Each badge has its own picture: a glyph for what it was earned for (#174)
Changes
@@@ -146,15 +146,15 @@ const completionWire = (c) => ({});/**-- * A badge as anyone may see it: the id, its catalog name, and the site whose quest awards it (a profile shows that site's-- * icon on the badge), and nothing about the person.++ * A badge as anyone may see it: the id, its catalog name, the site whose quest awards it, and its picture (a glyph name++ * from openvibe-shared/glyphs: what it was earned for), and nothing about the person.*/let appBadgeName = () => null;/** server/app.js points this at the app quests' badge names. */const setAppBadgeNames = (fn) => { appBadgeName = typeof fn === 'function' ? fn : () => null; };const badgeWire = (badgeId) => {const badge = catalog.badgeById(badgeId);-- return { id: badgeId, name: badge ? badge.name : (appBadgeName(badgeId) || badgeId), ...(badge && badge.site ? { site: badge.site } : {}) };++ return { id: badgeId, name: badge ? badge.name : (appBadgeName(badgeId) || badgeId), ...(badge && badge.site ? { site: badge.site } : {}), ...(badge && badge.icon ? { icon: badge.icon } : {}) };};module.exports = { createApi, questWire, completionWire, badgeWire, setAppBadgeNames, SUBJECT_RE };@@@ -516,10 +516,28 @@ const seriesById = (id) => {const x = Object.prototype.hasOwnProperty.call(SERIES, String(id)) ? SERIES[String(id)] : null;return x ? { ...x, quests: QUESTS.filter((q) => q.series === x.id) } : null;};++/**++ * Each badge's picture: what it was earned for, as a glyph from openvibe-shared/glyphs (a profile draws it on the badge;++ * 21 badges with the same medal read as one blur, 2026-11-02). Every catalog badge has one (test/quests-api.test.js).++ */++const BADGE_ICONS = Object.freeze({++ 'first-broadcast': 'live', 'regular-broadcaster': 'calendar', 'first-follow': 'heart', 'thread-starter': 'chat',++ 'first-reply': 'reply', commenter: 'quote', 'said-hello': 'hand', 'wiki-editor': 'book-open', 'player-one': 'game',++ toolsmith: 'wrench', 'space-thread-starter': 'rocket', 'first-post': 'pencil', 'deal-spotter': 'tag', 'deal-judge': 'thumbs-up',++ 'job-runner': 'terminal', 'dressed-up': 'shirt', 'robot-pilot': 'robot', 'world-builder': 'globe', premiere: 'clapper',++ snapshot: 'camera', 'settled-in': 'home',++ 'live-setup-slot-holder': 'layers', 'live-setup-method-picked': 'sliders', 'live-setup-restreamer': 'share',++ 'live-setup-profile-ready': 'user-check', 'live-setup-offline-screen': 'image', 'live-setup-emote-maker': 'smile',++ 'live-setup-sound-board': 'volume', 'live-setup-goal-setter': 'target', 'live-setup-tip-jar': 'coin',++ 'live-setup-panel-writer': 'columns', 'live-setup-followed': 'members', 'live-setup-mod-squad': 'shield',++ 'live-setup-request-line': 'music', 'live-setup-rule-maker': 'gavel', 'live-setup-robot-wrangler': 'nodes',++ 'live-setup-bot-host': 'server', 'live-setup-paste-author': 'file', 'live-setup-complete': 'star',++});++/** The badge for a quest, by badge id (the public badge endpoint stores only the id). */const badgeById = (badgeId) => {-- for (const q of QUESTS) if (q.reward.badge.id === badgeId) return { ...q.reward.badge, quest_id: q.id, site: q.site };++ for (const q of QUESTS) if (q.reward.badge.id === badgeId) return { ...q.reward.badge, quest_id: q.id, site: q.site, icon: BADGE_ICONS[badgeId] || null };return null;};--module.exports = { QUESTS, SITES, SERIES, EVENTS, EVENT_TYPES, SITE_ORDER, all, byId, withEvent, forEvent, stepCounts, grouped, seriesById, badgeById };++module.exports = { QUESTS, SITES, SERIES, EVENTS, EVENT_TYPES, SITE_ORDER, all, byId, withEvent, forEvent, stepCounts, grouped, seriesById, badgeById, BADGE_ICONS };@@@ -99,9 +99,16 @@ const { render } = require('../server/render/html');assert.strictEqual(anon.status, 200, 'no token needed');const body = anon.json();assert.strictEqual(body.subject, kim.subject);-- assert.deepStrictEqual(body.badges, [{ id: 'first-broadcast', name: 'First Broadcast', site: 'openvibe.live', awarded_at: '2026-10-01T10:00:00.000Z' }]);-- // The site is the badge's (the site whose quest awards it), not the person's.-- for (const b of body.badges) assert.deepStrictEqual(Object.keys(b).sort(), ['awarded_at', 'id', 'name', 'site'], 'nothing else about the person');++ assert.deepStrictEqual(body.badges, [{ id: 'first-broadcast', name: 'First Broadcast', site: 'openvibe.live', icon: 'live', awarded_at: '2026-10-01T10:00:00.000Z' }]);++ // The site and the icon are the badge's (the site whose quest awards it, what it is for), not the person's.++ for (const b of body.badges) assert.deepStrictEqual(Object.keys(b).sort(), ['awarded_at', 'icon', 'id', 'name', 'site'], 'nothing else about the person');++ // Every catalog badge has a picture, and every picture is a glyph Shared can draw.++ const { NAMES } = require('openvibe-shared/glyphs');++ const catalog = require('../server/quests/catalog');++ for (const q of catalog.QUESTS) {++ const icon = catalog.BADGE_ICONS[q.reward.badge.id];++ assert.ok(icon && NAMES.includes(icon), `${q.reward.badge.id} has a glyph (${icon})`);++ }assert.ok(!anon.text.includes('qcp_'), 'no completion');assert.ok(!anon.text.includes(kim.username), 'no username');assert.ok(!anon.text.includes('coins'), 'no coins');
@@ -146,15 +146,15 @@ const completionWire = (c) => ({
@@ -146,15 +146,15 @@ const completionWire = (c) => ({
});
});
/**
/**
- * A badge as anyone may see it: the id, its catalog name, and the site whose quest awards it (a profile shows that site's
- * icon on the badge), and nothing about the person.
+ * A badge as anyone may see it: the id, its catalog name, the site whose quest awards it, and its picture (a glyph name
+ * from openvibe-shared/glyphs: what it was earned for), and nothing about the person.
*/
*/
let appBadgeName = () => null;
let appBadgeName = () => null;
/** server/app.js points this at the app quests' badge names. */
/** server/app.js points this at the app quests' badge names. */
const setAppBadgeNames = (fn) => { appBadgeName = typeof fn === 'function' ? fn : () => null; };
const setAppBadgeNames = (fn) => { appBadgeName = typeof fn === 'function' ? fn : () => null; };
const badgeWire = (badgeId) => {
const badgeWire = (badgeId) => {
const badge = catalog.badgeById(badgeId);
const badge = catalog.badgeById(badgeId);
- return { id: badgeId, name: badge ? badge.name : (appBadgeName(badgeId) || badgeId), ...(badge && badge.site ? { site: badge.site } : {}) };
+ return { id: badgeId, name: badge ? badge.name : (appBadgeName(badgeId) || badgeId), ...(badge && badge.site ? { site: badge.site } : {}), ...(badge && badge.icon ? { icon: badge.icon } : {}) };
};
};
module.exports = { createApi, questWire, completionWire, badgeWire, setAppBadgeNames, SUBJECT_RE };
module.exports = { createApi, questWire, completionWire, badgeWire, setAppBadgeNames, SUBJECT_RE };
@@ -516,10 +516,28 @@ const seriesById = (id) => {
@@ -516,10 +516,28 @@ const seriesById = (id) => {
const x = Object.prototype.hasOwnProperty.call(SERIES, String(id)) ? SERIES[String(id)] : null;
const x = Object.prototype.hasOwnProperty.call(SERIES, String(id)) ? SERIES[String(id)] : null;
return x ? { ...x, quests: QUESTS.filter((q) => q.series === x.id) } : null;
return x ? { ...x, quests: QUESTS.filter((q) => q.series === x.id) } : null;
};
};
+/**
+ * Each badge's picture: what it was earned for, as a glyph from openvibe-shared/glyphs (a profile draws it on the badge;
+ * 21 badges with the same medal read as one blur, 2026-11-02). Every catalog badge has one (test/quests-api.test.js).
+ */
+const BADGE_ICONS = Object.freeze({
+ 'first-broadcast': 'live', 'regular-broadcaster': 'calendar', 'first-follow': 'heart', 'thread-starter': 'chat',
+ 'first-reply': 'reply', commenter: 'quote', 'said-hello': 'hand', 'wiki-editor': 'book-open', 'player-one': 'game',
+ toolsmith: 'wrench', 'space-thread-starter': 'rocket', 'first-post': 'pencil', 'deal-spotter': 'tag', 'deal-judge': 'thumbs-up',
+ 'job-runner': 'terminal', 'dressed-up': 'shirt', 'robot-pilot': 'robot', 'world-builder': 'globe', premiere: 'clapper',
+ snapshot: 'camera', 'settled-in': 'home',
+ 'live-setup-slot-holder': 'layers', 'live-setup-method-picked': 'sliders', 'live-setup-restreamer': 'share',
+ 'live-setup-profile-ready': 'user-check', 'live-setup-offline-screen': 'image', 'live-setup-emote-maker': 'smile',
+ 'live-setup-sound-board': 'volume', 'live-setup-goal-setter': 'target', 'live-setup-tip-jar': 'coin',
+ 'live-setup-panel-writer': 'columns', 'live-setup-followed': 'members', 'live-setup-mod-squad': 'shield',
+ 'live-setup-request-line': 'music', 'live-setup-rule-maker': 'gavel', 'live-setup-robot-wrangler': 'nodes',
+ 'live-setup-bot-host': 'server', 'live-setup-paste-author': 'file', 'live-setup-complete': 'star',
+});
+
/** The badge for a quest, by badge id (the public badge endpoint stores only the id). */
/** The badge for a quest, by badge id (the public badge endpoint stores only the id). */
const badgeById = (badgeId) => {
const badgeById = (badgeId) => {
- for (const q of QUESTS) if (q.reward.badge.id === badgeId) return { ...q.reward.badge, quest_id: q.id, site: q.site };
+ for (const q of QUESTS) if (q.reward.badge.id === badgeId) return { ...q.reward.badge, quest_id: q.id, site: q.site, icon: BADGE_ICONS[badgeId] || null };
return null;
return null;
};
};
-module.exports = { QUESTS, SITES, SERIES, EVENTS, EVENT_TYPES, SITE_ORDER, all, byId, withEvent, forEvent, stepCounts, grouped, seriesById, badgeById };
+module.exports = { QUESTS, SITES, SERIES, EVENTS, EVENT_TYPES, SITE_ORDER, all, byId, withEvent, forEvent, stepCounts, grouped, seriesById, badgeById, BADGE_ICONS };
@@ -99,9 +99,16 @@ const { render } = require('../server/render/html');
@@ -99,9 +99,16 @@ const { render } = require('../server/render/html');
assert.strictEqual(anon.status, 200, 'no token needed');
assert.strictEqual(anon.status, 200, 'no token needed');
const body = anon.json();
const body = anon.json();
assert.strictEqual(body.subject, kim.subject);
assert.strictEqual(body.subject, kim.subject);
- assert.deepStrictEqual(body.badges, [{ id: 'first-broadcast', name: 'First Broadcast', site: 'openvibe.live', awarded_at: '2026-10-01T10:00:00.000Z' }]);
- // The site is the badge's (the site whose quest awards it), not the person's.
- for (const b of body.badges) assert.deepStrictEqual(Object.keys(b).sort(), ['awarded_at', 'id', 'name', 'site'], 'nothing else about the person');
+ assert.deepStrictEqual(body.badges, [{ id: 'first-broadcast', name: 'First Broadcast', site: 'openvibe.live', icon: 'live', awarded_at: '2026-10-01T10:00:00.000Z' }]);
+ // The site and the icon are the badge's (the site whose quest awards it, what it is for), not the person's.
+ for (const b of body.badges) assert.deepStrictEqual(Object.keys(b).sort(), ['awarded_at', 'icon', 'id', 'name', 'site'], 'nothing else about the person');
+ // Every catalog badge has a picture, and every picture is a glyph Shared can draw.
+ const { NAMES } = require('openvibe-shared/glyphs');
+ const catalog = require('../server/quests/catalog');
+ for (const q of catalog.QUESTS) {
+ const icon = catalog.BADGE_ICONS[q.reward.badge.id];
+ assert.ok(icon && NAMES.includes(icon), `${q.reward.badge.id} has a glyph (${icon})`);
+ }
assert.ok(!anon.text.includes('qcp_'), 'no completion');
assert.ok(!anon.text.includes('qcp_'), 'no completion');
assert.ok(!anon.text.includes(kim.username), 'no username');
assert.ok(!anon.text.includes(kim.username), 'no username');
assert.ok(!anon.text.includes('coins'), 'no coins');
assert.ok(!anon.text.includes('coins'), 'no coins');