Public Access
feat(points): unlock items, progress bars and skins
Locked items show a progress bar until the goal is reached. Four CSS-only skins (Pastel, Retro CRT, Cyberpunk, Steam) in skins.css, light and dark. A skin overrides accent and corners; badges, borders, frames, effects and game art stay on top. New Platinum shine and Equalizer effects.
This commit is contained in:
1 parent
8598024bce
commit
b6f3d123fb
22 files changed
+622
-38
No files matched your search
@@ -39,9 +39,12 @@ export const readCachedAppearance = () => {
|
||||
|
||||
export const cacheAppearance = (appearance) => storage.set(STORAGE_KEY, JSON.stringify(appearance));
|
||||
|
||||
// accentOverride: a premium accent worn from the shop (Core owns it), which wins over the saved one.
|
||||
export const applyAppearance = (appearance, accentOverride = null, root = document.documentElement) => {
|
||||
root.dataset.accent = accentOverride ?? appearance.accent;
|
||||
// accent: a premium accent worn from the shop (Core owns it), which wins over the saved one. skin: the skin worn;
|
||||
// skins.css lets it win over accent and corners.
|
||||
export const applyAppearance = (appearance, {accent = null, skin = null} = {}, root = document.documentElement) => {
|
||||
root.dataset.accent = accent ?? appearance.accent;
|
||||
if (skin) root.dataset.skin = skin;
|
||||
else delete root.dataset.skin;
|
||||
root.dataset.corners = appearance.corners;
|
||||
root.toggleAttribute('data-reduce-motion', appearance.reduceMotion === true);
|
||||
};
|
||||
|
||||
@@ -45,7 +45,7 @@ describe('applyAppearance', () => {
|
||||
it('sets the presets as data attributes for the CSS', () => {
|
||||
const root = document.createElement('html');
|
||||
|
||||
applyAppearance({accent: 'orange', corners: 'round', wideLayout: true, backgroundAppid: 620}, null, root);
|
||||
applyAppearance({accent: 'orange', corners: 'round', wideLayout: true, backgroundAppid: 620}, {}, root);
|
||||
|
||||
expect(root.dataset.accent).toBe('orange');
|
||||
expect(root.dataset.corners).toBe('round');
|
||||
@@ -54,10 +54,21 @@ describe('applyAppearance', () => {
|
||||
it('lets a worn premium accent win over the saved one', () => {
|
||||
const root = document.createElement('html');
|
||||
|
||||
applyAppearance({accent: 'orange', corners: 'round'}, 'gold', root);
|
||||
applyAppearance({accent: 'orange', corners: 'round'}, {accent: 'gold'}, root);
|
||||
|
||||
expect(root.dataset.accent).toBe('gold');
|
||||
});
|
||||
|
||||
it('sets the worn skin and removes it again', () => {
|
||||
const root = document.createElement('html');
|
||||
|
||||
applyAppearance({accent: 'orange', corners: 'round'}, {accent: 'gold', skin: 'cyberpunk'}, root);
|
||||
expect(root.dataset.skin).toBe('cyberpunk');
|
||||
expect(root.dataset.accent).toBe('gold');
|
||||
|
||||
applyAppearance({accent: 'orange', corners: 'round'}, {}, root);
|
||||
expect(root.dataset.skin).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('reduce motion', () => {
|
||||
@@ -71,10 +82,10 @@ describe('reduce motion', () => {
|
||||
it('is set on the root by applyAppearance and removed again', () => {
|
||||
const root = document.createElement('html');
|
||||
|
||||
applyAppearance({accent: 'blue', corners: 'default', reduceMotion: true}, null, root);
|
||||
applyAppearance({accent: 'blue', corners: 'default', reduceMotion: true}, {}, root);
|
||||
expect(root.hasAttribute('data-reduce-motion')).toBe(true);
|
||||
|
||||
applyAppearance({accent: 'blue', corners: 'default', reduceMotion: false}, null, root);
|
||||
applyAppearance({accent: 'blue', corners: 'default', reduceMotion: false}, {}, root);
|
||||
expect(root.hasAttribute('data-reduce-motion')).toBe(false);
|
||||
});
|
||||
|
||||
|
||||
+22
-2
@@ -1,11 +1,11 @@
|
||||
import {Swords, Trophy} from 'lucide-react';
|
||||
import {Flame, Flag, Gem, Swords, TrendingUp, Trophy} from 'lucide-react';
|
||||
import {apiFetch, request} from './api.js';
|
||||
import {storage} from './storage.js';
|
||||
|
||||
const REWARDS_URL = `${import.meta.env.VITE_CORE_API_URL}/api/rewards`;
|
||||
const JSON_HEADERS = {'Content-Type': 'application/json'};
|
||||
|
||||
export const SLOTS = ['AVATAR_BORDER', 'BADGE', 'ACCENT', 'BANNER_FRAME', 'COMPLETED_EFFECT', 'PAGE_BACKGROUND', 'BANNER_BACKGROUND'];
|
||||
export const SLOTS = ['AVATAR_BORDER', 'BADGE', 'ACCENT', 'BANNER_FRAME', 'COMPLETED_EFFECT', 'PAGE_BACKGROUND', 'BANNER_BACKGROUND', 'SKIN'];
|
||||
|
||||
// The shop catalog of Core (ShopItem) as looks: only these classes are ever applied, never a string from the server.
|
||||
// An unknown code is skipped.
|
||||
@@ -40,11 +40,31 @@ export const LOOKS = {
|
||||
BN_GRID: {effect: 'grid'},
|
||||
BN_WAVES: {effect: 'waves'},
|
||||
BN_BOKEH: {effect: 'bokeh'},
|
||||
// Unlock-only items (Core: earned by progress, never sold).
|
||||
BADGE_FINISH_10: {Icon: Flag, className: 'bg-emerald-500/15 text-emerald-500'},
|
||||
BORDER_FINISH_25: {className: 'ring-4 ring-slate-300 ring-offset-2 ring-offset-background'},
|
||||
FRAME_FINISH_50: {className: 'ring-2 ring-emerald-400 ring-offset-2 ring-offset-background shadow-[0_0_14px] shadow-emerald-400/50'},
|
||||
BADGE_PERFECT_5: {Icon: Gem, className: 'bg-cyan-500/15 text-cyan-500'},
|
||||
FX_PLATINUM: {cover: 'fx fx-cover-platinum outline outline-2 -outline-offset-2 outline-slate-200', detail: 'fx-detail-platinum'},
|
||||
BADGE_STREAK_4: {Icon: Flame, className: 'bg-orange-500/15 text-orange-500'},
|
||||
BORDER_STREAK_12: {className: 'ring-4 ring-orange-500 shadow-[0_0_14px] shadow-orange-500/60'},
|
||||
FRAME_PILE_DOWN_3: {className: 'ring-2 ring-teal-400 ring-offset-2 ring-offset-background'},
|
||||
BADGE_PLAY_MORE_1: {Icon: TrendingUp, className: 'bg-sky-500/15 text-sky-500'},
|
||||
BORDER_PLAY_MORE_3: {className: 'ring-4 ring-sky-400 ring-offset-2 ring-offset-background'},
|
||||
BN_EQUALIZER: {effect: 'equalizer'},
|
||||
// Skins (skins.css): full themes, set as data-skin.
|
||||
SKIN_PASTEL: {skin: 'pastel'},
|
||||
SKIN_RETRO_CRT: {skin: 'retro-crt'},
|
||||
SKIN_CYBERPUNK: {skin: 'cyberpunk'},
|
||||
SKIN_STEAM: {skin: 'steam'},
|
||||
};
|
||||
|
||||
/** The premium accent worn, or null (then the appearance accent applies). */
|
||||
export const equippedAccent = (equipped) => LOOKS[equipped?.ACCENT]?.accent ?? null;
|
||||
|
||||
/** The skin worn (a data-skin value of skins.css), or null. */
|
||||
export const equippedSkin = (equipped) => LOOKS[equipped?.SKIN]?.skin ?? null;
|
||||
|
||||
/** The classes of the item worn in a slot, or undefined. */
|
||||
export const lookClass = (equipped, slot) => LOOKS[equipped?.[slot]]?.className;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {describe, expect, it} from 'vitest';
|
||||
import {backgroundEffect, bannerEffect, coverEffect, detailEffect, LOOKS} from './rewards.js';
|
||||
import {backgroundEffect, bannerEffect, coverEffect, detailEffect, equippedSkin, LOOKS} from './rewards.js';
|
||||
|
||||
describe('effects worn', () => {
|
||||
const equipped = {COMPLETED_EFFECT: 'FX_GOLD_SHINE', PAGE_BACKGROUND: 'BG_STARFIELD', BANNER_BACKGROUND: 'BN_WAVES'};
|
||||
@@ -24,3 +24,12 @@ describe('effects worn', () => {
|
||||
expect(LOOKS.BG_GRID.effect).toBe(LOOKS.BN_GRID.effect);
|
||||
});
|
||||
});
|
||||
|
||||
describe('equippedSkin', () => {
|
||||
it('gives the data-skin value of a known skin and null otherwise', () => {
|
||||
expect(equippedSkin({SKIN: 'SKIN_RETRO_CRT'})).toBe('retro-crt');
|
||||
expect(equippedSkin({SKIN: 'SKIN_FUTURE'})).toBeNull();
|
||||
expect(equippedSkin({})).toBeNull();
|
||||
expect(equippedSkin(undefined)).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user