Game — Achievement / Trophy Unlock Toast
Console-style achievement unlock toast system: a slide-in notification with glowing trophy badge, rarity tier, achievement name, and gamerscore reward, finished with a shine sweep, sound-pulse ring, and auto-dismiss timer bar. Toasts queue and play one at a time across five rarities from common to platinum, while an achievements list flips locked rows to unlocked with animated progress bars and a count-up gamerscore counter. Vanilla JS with a WebAudio chime and reduced-motion support.
MCP
Code
:root {
--bg: #0a0b10;
--bg-2: #12131c;
--panel: #171926;
--panel-2: #1f2233;
--text: #e7e9f3;
--muted: #9aa0bf;
--line: rgba(231, 233, 243, 0.10);
--line-2: rgba(231, 233, 243, 0.18);
--accent: #00e5ff;
--accent-2: #7c4dff;
--accent-3: #ff3d71;
--success: #36e27a;
--warn: #ffc857;
--danger: #ff4d4d;
--glow: 0 0 18px rgba(0, 229, 255, 0.45);
--r-sm: 6px;
--r-md: 10px;
--r-lg: 16px;
/* rarity tiers */
--t-common: #9aa6c4;
--t-rare: #34c6ff;
--t-epic: #b475ff;
--t-legendary: #ffb627;
--t-platinum: #6ff3ff;
}
* { box-sizing: border-box; }
html, body { margin: 0; }
body {
min-height: 100vh;
font-family: "Inter", system-ui, sans-serif;
line-height: 1.5;
color: var(--text);
background:
radial-gradient(900px 480px at 12% -8%, rgba(124, 77, 255, 0.18), transparent 60%),
radial-gradient(820px 520px at 100% 0%, rgba(0, 229, 255, 0.14), transparent 58%),
var(--bg);
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
padding: clamp(16px, 4vw, 40px);
}
.scanlines {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 1;
background-image: repeating-linear-gradient(
to bottom,
rgba(255, 255, 255, 0.018) 0,
rgba(255, 255, 255, 0.018) 1px,
transparent 1px,
transparent 3px
);
mix-blend-mode: overlay;
}
.shell {
position: relative;
z-index: 2;
max-width: 820px;
margin: 0 auto;
display: grid;
gap: 18px;
}
/* ---------------- header ---------------- */
.hud-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
}
.hud-head__brand { display: flex; align-items: center; gap: 14px; }
.logo {
display: grid;
place-items: center;
width: 46px;
height: 46px;
color: var(--bg);
background: linear-gradient(135deg, var(--accent), var(--accent-2));
clip-path: polygon(18% 0, 100% 0, 100% 82%, 82% 100%, 0 100%, 0 18%);
box-shadow: var(--glow);
}
.logo svg { fill: currentColor; }
.logo-kicker {
margin: 0;
font-family: "Orbitron", sans-serif;
font-size: 10px;
letter-spacing: 0.32em;
color: var(--accent);
}
.logo-title {
margin: 2px 0 0;
font-family: "Orbitron", sans-serif;
font-weight: 900;
font-size: clamp(18px, 4.5vw, 26px);
letter-spacing: 0.06em;
}
.score-board {
display: flex;
align-items: baseline;
gap: 8px;
padding: 10px 18px;
background: linear-gradient(180deg, var(--panel-2), var(--panel));
border: 1px solid var(--line-2);
border-radius: var(--r-md);
clip-path: polygon(10px 0, 100% 0, 100% calc(100% - 10px), calc(100% - 10px) 100%, 0 100%, 0 10px);
}
.score-board__label {
font-family: "Orbitron", sans-serif;
font-size: 9px;
letter-spacing: 0.26em;
color: var(--muted);
}
.score-board__value {
font-family: "Orbitron", sans-serif;
font-weight: 900;
font-size: clamp(22px, 5vw, 30px);
color: var(--text);
text-shadow: 0 0 14px rgba(0, 229, 255, 0.4);
font-variant-numeric: tabular-nums;
}
.score-board__g {
font-family: "Orbitron", sans-serif;
font-weight: 700;
color: var(--accent);
font-size: 14px;
}
/* ---------------- panels ---------------- */
.panel {
position: relative;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.03), transparent 40%),
var(--panel);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 18px;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04), 0 18px 40px -28px rgba(0, 0, 0, 0.9);
}
.panel::before {
content: "";
position: absolute;
left: 18px;
top: 0;
width: 56px;
height: 2px;
background: linear-gradient(90deg, var(--accent), transparent);
}
.panel__bar {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
margin-bottom: 14px;
flex-wrap: wrap;
}
.panel__title {
margin: 0;
font-family: "Orbitron", sans-serif;
font-weight: 700;
font-size: 15px;
letter-spacing: 0.08em;
}
.panel__hint { font-size: 12px; color: var(--muted); }
/* ---------------- trigger grid ---------------- */
.trigger-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 10px;
}
.rarity-btn {
--tier: var(--t-common);
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 14px 8px;
color: var(--text);
background: linear-gradient(180deg, var(--panel-2), var(--panel));
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
clip-path: polygon(8px 0, 100% 0, 100% calc(100% - 8px), calc(100% - 8px) 100%, 0 100%, 0 8px);
transition: transform 0.14s ease, border-color 0.14s ease, box-shadow 0.14s ease, background 0.14s ease;
}
.rarity-btn[data-rarity="common"] { --tier: var(--t-common); }
.rarity-btn[data-rarity="rare"] { --tier: var(--t-rare); }
.rarity-btn[data-rarity="epic"] { --tier: var(--t-epic); }
.rarity-btn[data-rarity="legendary"] { --tier: var(--t-legendary); }
.rarity-btn[data-rarity="platinum"] { --tier: var(--t-platinum); }
.rarity-btn:hover {
transform: translateY(-3px);
border-color: var(--tier);
box-shadow: 0 0 0 1px var(--tier), 0 10px 26px -16px var(--tier);
background: linear-gradient(180deg, color-mix(in srgb, var(--tier) 16%, var(--panel-2)), var(--panel));
}
.rarity-btn:active { transform: translateY(-1px) scale(0.98); }
.rarity-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.rarity-btn__tier {
font-family: "Orbitron", sans-serif;
font-weight: 700;
font-size: 11px;
letter-spacing: 0.08em;
color: var(--tier);
text-align: center;
}
.rarity-btn__pts {
font-family: "Orbitron", sans-serif;
font-size: 12px;
font-weight: 900;
color: var(--muted);
}
.rarity-btn:hover .rarity-btn__pts { color: var(--text); }
.trigger-actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
margin-top: 14px;
flex-wrap: wrap;
}
.ghost-btn {
cursor: pointer;
font-family: "Orbitron", sans-serif;
font-weight: 700;
font-size: 12px;
letter-spacing: 0.1em;
color: var(--accent);
background: transparent;
border: 1px solid var(--accent);
border-radius: var(--r-sm);
padding: 9px 18px;
transition: background 0.14s ease, box-shadow 0.14s ease, transform 0.1s ease;
}
.ghost-btn:hover { background: rgba(0, 229, 255, 0.12); box-shadow: var(--glow); }
.ghost-btn:active { transform: scale(0.97); }
.ghost-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.mute-toggle {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: var(--muted);
cursor: pointer;
}
.mute-toggle input { accent-color: var(--accent-2); width: 16px; height: 16px; }
/* ---------------- achievement list ---------------- */
.ach-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
.ach-row {
--tier: var(--t-common);
display: grid;
grid-template-columns: 48px 1fr auto;
align-items: center;
gap: 14px;
padding: 12px 14px;
background: var(--bg-2);
border: 1px solid var(--line);
border-left: 3px solid var(--line-2);
border-radius: var(--r-md);
transition: border-color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease;
}
.ach-row.is-unlocked {
border-left-color: var(--tier);
background: linear-gradient(90deg, color-mix(in srgb, var(--tier) 10%, var(--bg-2)), var(--bg-2) 70%);
}
.ach-row.just-unlocked { box-shadow: 0 0 0 1px var(--tier), 0 0 26px -6px var(--tier); }
.ach-badge {
position: relative;
display: grid;
place-items: center;
width: 48px;
height: 48px;
border-radius: 50%;
color: var(--muted);
background: var(--panel-2);
border: 1px solid var(--line-2);
transition: color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease;
}
.ach-row.is-unlocked .ach-badge {
color: var(--tier);
background: radial-gradient(circle at 50% 35%, color-mix(in srgb, var(--tier) 30%, var(--panel-2)), var(--panel-2));
box-shadow: 0 0 16px -2px var(--tier);
}
.ach-badge svg { width: 24px; height: 24px; fill: currentColor; }
.ach-main { min-width: 0; }
.ach-name {
margin: 0;
font-weight: 700;
font-size: 14px;
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.ach-tier-chip {
font-family: "Orbitron", sans-serif;
font-size: 9px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--tier);
border: 1px solid color-mix(in srgb, var(--tier) 50%, transparent);
border-radius: 999px;
padding: 2px 7px;
}
.ach-desc { margin: 3px 0 0; font-size: 12px; color: var(--muted); }
.ach-progress {
margin-top: 8px;
height: 6px;
border-radius: 999px;
background: rgba(231, 233, 243, 0.08);
overflow: hidden;
}
.ach-progress__fill {
height: 100%;
width: 0%;
border-radius: 999px;
background: linear-gradient(90deg, var(--tier), color-mix(in srgb, var(--tier) 40%, #fff));
transition: width 0.7s cubic-bezier(0.22, 1, 0.36, 1);
}
.ach-row.is-unlocked .ach-progress { display: none; }
.ach-meta { text-align: right; display: grid; gap: 3px; }
.ach-points {
font-family: "Orbitron", sans-serif;
font-weight: 900;
font-size: 15px;
color: var(--muted);
}
.ach-row.is-unlocked .ach-points { color: var(--tier); text-shadow: 0 0 10px color-mix(in srgb, var(--tier) 60%, transparent); }
.ach-state { font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted); }
.ach-row.is-unlocked .ach-state { color: var(--success); }
/* ---------------- toast ---------------- */
.toast-stack {
position: fixed;
z-index: 50;
right: 22px;
bottom: 22px;
display: grid;
gap: 12px;
width: min(360px, calc(100vw - 32px));
}
.toast {
--tier: var(--t-common);
position: relative;
overflow: hidden;
display: grid;
grid-template-columns: 56px 1fr auto;
align-items: center;
gap: 14px;
padding: 14px 16px;
color: var(--text);
background: linear-gradient(180deg, var(--panel-2), var(--panel));
border: 1px solid var(--tier);
border-radius: var(--r-md);
clip-path: polygon(0 0, 100% 0, 100% calc(100% - 12px), calc(100% - 12px) 100%, 0 100%);
box-shadow: 0 0 0 1px color-mix(in srgb, var(--tier) 30%, transparent), 0 22px 50px -22px var(--tier);
transform: translateX(120%);
opacity: 0;
transition: transform 0.42s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.3s ease;
}
.toast.is-in { transform: translateX(0); opacity: 1; }
.toast.is-out { transform: translateX(120%); opacity: 0; }
/* shine sweep */
.toast::after {
content: "";
position: absolute;
top: 0;
left: -60%;
width: 50%;
height: 100%;
background: linear-gradient(100deg, transparent, rgba(255, 255, 255, 0.35), transparent);
transform: skewX(-18deg);
pointer-events: none;
}
.toast.is-in::after { animation: shine 1.1s ease 0.25s 1 forwards; }
@keyframes shine {
from { left: -60%; }
to { left: 130%; }
}
.toast__icon {
position: relative;
display: grid;
place-items: center;
width: 56px;
height: 56px;
border-radius: 50%;
color: var(--tier);
background: radial-gradient(circle at 50% 30%, color-mix(in srgb, var(--tier) 32%, var(--panel-2)), var(--panel-2));
box-shadow: 0 0 22px -4px var(--tier), inset 0 0 0 1px color-mix(in srgb, var(--tier) 50%, transparent);
}
.toast__icon svg { width: 28px; height: 28px; fill: currentColor; }
.toast.is-in .toast__icon { animation: badge-pop 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) both; }
@keyframes badge-pop {
0% { transform: scale(0.4) rotate(-12deg); }
70% { transform: scale(1.12) rotate(4deg); }
100% { transform: scale(1) rotate(0); }
}
/* sound pulse ring */
.toast__icon::before {
content: "";
position: absolute;
inset: -4px;
border-radius: 50%;
border: 2px solid var(--tier);
opacity: 0;
}
.toast.is-in:not(.is-muted) .toast__icon::before { animation: pulse-ring 1s ease-out 0.2s 2; }
@keyframes pulse-ring {
0% { transform: scale(0.9); opacity: 0.7; }
100% { transform: scale(1.5); opacity: 0; }
}
.toast__body { min-width: 0; }
.toast__kicker {
margin: 0;
font-family: "Orbitron", sans-serif;
font-size: 9px;
letter-spacing: 0.26em;
color: var(--tier);
}
.toast__name {
margin: 3px 0 0;
font-weight: 800;
font-size: 15px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.toast__sub { margin: 2px 0 0; font-size: 11px; color: var(--muted); }
.toast__pts {
font-family: "Orbitron", sans-serif;
font-weight: 900;
font-size: 17px;
color: var(--tier);
text-shadow: 0 0 12px color-mix(in srgb, var(--tier) 60%, transparent);
white-space: nowrap;
}
.toast__timer {
position: absolute;
left: 0;
bottom: 0;
height: 3px;
width: 100%;
background: var(--tier);
transform-origin: left;
}
.toast.is-in .toast__timer { animation: timer 3.6s linear forwards; }
@keyframes timer { from { transform: scaleX(1); } to { transform: scaleX(0); } }
/* ---------------- responsive ---------------- */
@media (max-width: 640px) {
.trigger-grid { grid-template-columns: repeat(2, 1fr); }
.rarity-btn[data-rarity="platinum"] { grid-column: span 2; flex-direction: row; justify-content: center; gap: 12px; }
}
@media (max-width: 520px) {
body { padding: 14px; }
.hud-head { gap: 12px; }
.score-board { padding: 8px 14px; }
.ach-row { grid-template-columns: 40px 1fr; }
.ach-meta { grid-column: 2; text-align: left; flex-direction: row; display: flex; gap: 12px; align-items: center; }
.ach-badge { width: 40px; height: 40px; }
.toast-stack { right: 12px; bottom: 12px; left: 12px; width: auto; }
.toast__name { white-space: normal; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.08s !important; }
.toast { transform: none; opacity: 1; }
}(function () {
"use strict";
/* ---------- SVG icon set (per rarity) ---------- */
var ICONS = {
trophy: '<svg viewBox="0 0 24 24"><path d="M6 3h12v2h3v3a4 4 0 0 1-4 4 6 6 0 0 1-4 3.9V19h3v2H8v-2h3v-3.1A6 6 0 0 1 7 12a4 4 0 0 1-4-4V5h3V3zm0 4H5v1a2 2 0 0 0 1 1.7V7zm12 0v2.7A2 2 0 0 0 19 8V7h-1z"/></svg>',
star: '<svg viewBox="0 0 24 24"><path d="M12 2l2.9 6.3L22 9.3l-5 4.7L18.2 21 12 17.5 5.8 21 7 14 2 9.3l7.1-1z"/></svg>',
medal: '<svg viewBox="0 0 24 24"><path d="M8 2h8l-2.2 6.4a5 5 0 1 1-3.6 0L8 2zm4 8a3 3 0 1 0 0 6 3 3 0 0 0 0-6z"/></svg>',
skull: '<svg viewBox="0 0 24 24"><path d="M12 2a8 8 0 0 0-5 14.3V20a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2v-3.7A8 8 0 0 0 12 2zM9 12a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm6 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm-4 6h2v2h-2z"/></svg>',
crown: '<svg viewBox="0 0 24 24"><path d="M3 7l4 4 5-7 5 7 4-4 -2 12H5L3 7zm2 14h14v2H5z"/></svg>'
};
/* ---------- rarity definitions ---------- */
var RARITY = {
common: { label: "Common", points: 15, icon: "medal", cssVar: "--t-common", kicker: "ACHIEVEMENT UNLOCKED" },
rare: { label: "Rare", points: 40, icon: "star", cssVar: "--t-rare", kicker: "ACHIEVEMENT UNLOCKED" },
epic: { label: "Epic", points: 90, icon: "trophy", cssVar: "--t-epic", kicker: "EPIC UNLOCK" },
legendary: { label: "Legendary", points: 150, icon: "skull", cssVar: "--t-legendary", kicker: "LEGENDARY UNLOCK" },
platinum: { label: "Platinum", points: 300, icon: "crown", cssVar: "--t-platinum", kicker: "PLATINUM TROPHY" }
};
var ORDER = ["common", "rare", "epic", "legendary", "platinum"];
/* ---------- catalog of achievements ---------- */
var CATALOG = [
{ id: "first-blood", rarity: "common", name: "First Light", desc: "Survive the prologue at Dawnhold.", progress: 100 },
{ id: "drift-king", rarity: "rare", name: "Neon Drift", desc: "Win a race without braking through Sector 7.", progress: 64 },
{ id: "no-shield", rarity: "rare", name: "Bare Steel", desc: "Defeat 25 raiders with no shield equipped.", progress: 40 },
{ id: "hollow-reign", rarity: "epic", name: "Hollow Reign", desc: "Clear the Sunless Citadel on Veteran.", progress: 30 },
{ id: "ghost-run", rarity: "epic", name: "Ghost Protocol", desc: "Finish a heist fully undetected.", progress: 12 },
{ id: "vanguard", rarity: "legendary", name: "Ashen Vanguard", desc: "Reach Prestige X in Conquest.", progress: 8 },
{ id: "platinum", rarity: "platinum", name: "The Completionist", desc: "Unlock every achievement in Ashen Vanguard.", progress: 0 }
];
/* ---------- elements ---------- */
var listEl = document.getElementById("achList");
var stackEl = document.getElementById("toastStack");
var scoreEl = document.getElementById("totalScore");
var unlockedCountEl = document.getElementById("unlockedCount");
var totalCountEl = document.getElementById("totalCount");
var muteToggle = document.getElementById("muteToggle");
var rollBtn = document.getElementById("rollBtn");
var unlocked = {}; // id -> true
var queue = [];
var playing = false;
var displayedScore = 0;
var prefersReduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
/* ---------- render list ---------- */
function tierVar(rarity) { return RARITY[rarity].cssVar; }
function renderList() {
listEl.innerHTML = "";
CATALOG.forEach(function (a) {
var r = RARITY[a.rarity];
var li = document.createElement("li");
li.className = "ach-row" + (unlocked[a.id] ? " is-unlocked" : "");
li.style.setProperty("--tier", "var(" + r.cssVar + ")");
li.dataset.id = a.id;
li.innerHTML =
'<span class="ach-badge">' + ICONS[r.icon] + '</span>' +
'<div class="ach-main">' +
'<p class="ach-name">' + a.name +
'<span class="ach-tier-chip">' + r.label + '</span>' +
'</p>' +
'<p class="ach-desc">' + a.desc + '</p>' +
'<div class="ach-progress"><span class="ach-progress__fill"></span></div>' +
'</div>' +
'<div class="ach-meta">' +
'<span class="ach-points">' + r.points + 'G</span>' +
'<span class="ach-state">' + (unlocked[a.id] ? "Unlocked" : "Locked") + '</span>' +
'</div>';
listEl.appendChild(li);
// animate progress fill for locked rows
if (!unlocked[a.id]) {
var fill = li.querySelector(".ach-progress__fill");
requestAnimationFrame(function () { fill.style.width = a.progress + "%"; });
}
});
totalCountEl.textContent = CATALOG.length;
refreshCounts();
}
function refreshCounts() {
var n = Object.keys(unlocked).length;
unlockedCountEl.textContent = n;
}
/* ---------- count-up for score ---------- */
function animateScore(target) {
if (prefersReduced) { displayedScore = target; scoreEl.textContent = target.toLocaleString(); return; }
var start = displayedScore;
var diff = target - start;
var dur = 700;
var t0 = null;
function step(ts) {
if (t0 === null) t0 = ts;
var p = Math.min(1, (ts - t0) / dur);
var eased = 1 - Math.pow(1 - p, 3);
var val = Math.round(start + diff * eased);
scoreEl.textContent = val.toLocaleString();
if (p < 1) requestAnimationFrame(step);
else displayedScore = target;
}
requestAnimationFrame(step);
}
/* ---------- WebAudio unlock chime ---------- */
var audioCtx = null;
function chime(rarity) {
if (muteToggle.checked) return;
try {
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
if (audioCtx.state === "suspended") audioCtx.resume();
// higher-tier = brighter, longer arpeggio
var tier = ORDER.indexOf(rarity);
var base = 392 + tier * 60;
var notes = [base, base * 1.25, base * 1.5];
if (tier >= 3) notes.push(base * 2);
var now = audioCtx.currentTime;
notes.forEach(function (f, i) {
var osc = audioCtx.createOscillator();
var g = audioCtx.createGain();
osc.type = tier >= 3 ? "sawtooth" : "triangle";
osc.frequency.value = f;
var t = now + i * 0.07;
g.gain.setValueAtTime(0.0001, t);
g.gain.exponentialRampToValueAtTime(0.12, t + 0.02);
g.gain.exponentialRampToValueAtTime(0.0001, t + 0.32);
osc.connect(g).connect(audioCtx.destination);
osc.start(t);
osc.stop(t + 0.34);
});
} catch (e) { /* audio unavailable — ignore */ }
}
/* ---------- toast queue ---------- */
function enqueue(achievement) {
queue.push(achievement);
if (!playing) playNext();
}
function playNext() {
if (!queue.length) { playing = false; return; }
playing = true;
var a = queue.shift();
var r = RARITY[a.rarity];
var toast = document.createElement("div");
toast.className = "toast" + (muteToggle.checked ? " is-muted" : "");
toast.style.setProperty("--tier", "var(" + r.cssVar + ")");
toast.setAttribute("role", "alert");
toast.innerHTML =
'<span class="toast__icon">' + ICONS[r.icon] + '</span>' +
'<div class="toast__body">' +
'<p class="toast__kicker">' + r.kicker + '</p>' +
'<p class="toast__name">' + a.name + '</p>' +
'<p class="toast__sub">' + r.label + ' · Ashen Vanguard</p>' +
'</div>' +
'<span class="toast__pts">+' + r.points + 'G</span>' +
'<span class="toast__timer"></span>';
stackEl.appendChild(toast);
chime(a.rarity);
requestAnimationFrame(function () { toast.classList.add("is-in"); });
var hold = prefersReduced ? 1600 : 3600;
setTimeout(function () {
toast.classList.remove("is-in");
toast.classList.add("is-out");
setTimeout(function () {
toast.remove();
playNext();
}, prefersReduced ? 60 : 420);
}, hold);
}
/* ---------- unlock logic ---------- */
function flashRow(id) {
var row = listEl.querySelector('.ach-row[data-id="' + id + '"]');
if (!row) return;
row.classList.add("is-unlocked", "just-unlocked");
row.querySelector(".ach-state").textContent = "Unlocked";
setTimeout(function () { row.classList.remove("just-unlocked"); }, 1200);
}
function unlockById(id) {
if (unlocked[id]) return false;
var a = CATALOG.find(function (x) { return x.id === id; });
if (!a) return false;
unlocked[id] = true;
flashRow(id);
refreshCounts();
animateScore(displayedScore + RARITY[a.rarity].points);
enqueue(a);
return true;
}
// pick a not-yet-unlocked achievement of a given rarity; fall back to any of that rarity
function unlockByRarity(rarity) {
var pool = CATALOG.filter(function (a) { return a.rarity === rarity && !unlocked[a.id]; });
if (pool.length) { unlockById(pool[0].id); return; }
// none left of that rarity — synthesize a repeat unlock toast (no list change, no score double-count)
var template = CATALOG.filter(function (a) { return a.rarity === rarity; })[0];
if (template) {
enqueue({ id: "_repeat", rarity: rarity, name: template.name });
}
}
/* ---------- wire buttons ---------- */
document.querySelectorAll(".rarity-btn").forEach(function (btn) {
btn.addEventListener("click", function () {
unlockByRarity(btn.dataset.rarity);
});
});
rollBtn.addEventListener("click", function () {
// weighted toward common, but can roll anything still locked
var locked = CATALOG.filter(function (a) { return !unlocked[a.id]; });
if (!locked.length) {
var any = CATALOG[Math.floor(Math.random() * CATALOG.length)];
enqueue({ id: "_repeat", rarity: any.rarity, name: any.name });
return;
}
var pick = locked[Math.floor(Math.random() * locked.length)];
unlockById(pick.id);
});
/* ---------- init ---------- */
renderList();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Achievement Unlock Toast — Ashen Vanguard</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@500;700;900&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="scanlines" aria-hidden="true"></div>
<main class="shell">
<header class="hud-head">
<div class="hud-head__brand">
<span class="logo" aria-hidden="true">
<svg viewBox="0 0 24 24" width="22" height="22"><path d="M12 2l2.4 6.1L21 9l-5 4.3L17.7 20 12 16.4 6.3 20 8 13.3 3 9l6.6-.9z"/></svg>
</span>
<div>
<p class="logo-kicker">NULLFORGE NETWORK</p>
<h1 class="logo-title">ASHEN VANGUARD</h1>
</div>
</div>
<div class="score-board" role="status" aria-live="polite">
<span class="score-board__label">GAMERSCORE</span>
<span class="score-board__value" id="totalScore" data-points="0">0</span>
<span class="score-board__g" aria-hidden="true">G</span>
</div>
</header>
<section class="panel trigger-panel" aria-labelledby="triggerHeading">
<div class="panel__bar">
<h2 id="triggerHeading" class="panel__title">Trigger an unlock</h2>
<span class="panel__hint">Toasts queue and play one at a time</span>
</div>
<div class="trigger-grid">
<button class="rarity-btn" data-rarity="common" type="button">
<span class="rarity-btn__tier">Common</span>
<span class="rarity-btn__pts">+15G</span>
</button>
<button class="rarity-btn" data-rarity="rare" type="button">
<span class="rarity-btn__tier">Rare</span>
<span class="rarity-btn__pts">+40G</span>
</button>
<button class="rarity-btn" data-rarity="epic" type="button">
<span class="rarity-btn__tier">Epic</span>
<span class="rarity-btn__pts">+90G</span>
</button>
<button class="rarity-btn" data-rarity="legendary" type="button">
<span class="rarity-btn__tier">Legendary</span>
<span class="rarity-btn__pts">+150G</span>
</button>
<button class="rarity-btn" data-rarity="platinum" type="button">
<span class="rarity-btn__tier">Platinum</span>
<span class="rarity-btn__pts">+300G</span>
</button>
</div>
<div class="trigger-actions">
<button class="ghost-btn" id="rollBtn" type="button">Surprise me</button>
<label class="mute-toggle">
<input type="checkbox" id="muteToggle" />
<span>Mute unlock chime</span>
</label>
</div>
</section>
<section class="panel list-panel" aria-labelledby="listHeading">
<div class="panel__bar">
<h2 id="listHeading" class="panel__title">Achievements</h2>
<span class="panel__hint"><span id="unlockedCount">0</span> / <span id="totalCount">0</span> unlocked</span>
</div>
<ul class="ach-list" id="achList"></ul>
</section>
</main>
<div class="toast-stack" id="toastStack" aria-live="assertive" aria-atomic="true"></div>
<script src="script.js"></script>
</body>
</html>Achievement / Trophy Unlock Toast
A dark, neon-trimmed trophy room for the fictional Nullforge title Ashen Vanguard. The header pairs a clipped-corner studio badge with a live gamerscore board, and below it a trigger panel offers five rarity buttons — Common through Platinum — each tinted with its own tier color and point value. Pressing one fires an Xbox/PSN-style unlock toast: it slides in from the right with a popping badge icon, rarity kicker, achievement name, +G reward, a shine sweep across the card, a pulsing sound ring, and a draining timer bar before it slides back out.
Unlocks are queued, so mashing several buttons plays the toasts one at a time instead of stacking chaos. Each unlock also flips the matching row in the achievements list from locked to unlocked — swapping its muted badge for a glowing tier-colored one — and the total gamerscore counts up with an eased animation. Locked rows show animated completion progress bars, a “Surprise me” button rolls a random remaining unlock, and a mute toggle silences the WebAudio chime, which plays a brighter, longer arpeggio for higher tiers.
Everything is vanilla HTML, CSS, and JS: rarity theming is driven by a single --tier custom property per row and toast, the queue is a tiny array with a playNext() loop, and prefers-reduced-motion shortens holds and disables the slide and sweep animations.
Illustrative UI only — fictional games, studios, characters, and data. Not engine integrations.