openvibers/openvibe.quest Commits

Each badge has its own picture: a glyph for what it was earned for (#174)

OP OpenVibers committed 11 days ago · c07d0187a8 · 3 files, +33 −8

Changes

ddiff --git a/server/http/api.js b/server/http/api.js
iindex 20e78ac..97089a4 100644
---- a/server/http/api.js
++++ b/server/http/api.js
@@@ -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 };
ddiff --git a/server/quests/catalog.js b/server/quests/catalog.js
iindex a7d367a..b218a23 100644
---- a/server/quests/catalog.js
++++ b/server/quests/catalog.js
@@@ -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 };
ddiff --git a/test/quests-api.test.js b/test/quests-api.test.js
iindex b4c8508..3993615 100644
---- a/test/quests-api.test.js
++++ b/test/quests-api.test.js
@@@ -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');