Compare commits
2 Commits
feat/progr
...
fix/offlin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f362f30fd3 | ||
|
|
f97154c37a |
259
game.js
259
game.js
@@ -106,6 +106,7 @@ const G = {
|
||||
drift: 0,
|
||||
lastEventAt: 0,
|
||||
eventCooldown: 0,
|
||||
activeEvents: [], // {id, expiresAt} — events auto-resolve after duration
|
||||
|
||||
// Time tracking
|
||||
playTime: 0,
|
||||
@@ -865,6 +866,7 @@ function updateRates() {
|
||||
G.creativityRate += 0.5 + Math.max(0, G.totalUsers * 0.001);
|
||||
}
|
||||
if (G.pactFlag) G.trustRate += 2;
|
||||
if (G.branchProtectionFlag) G.trustRate += 3; // branch protection actively builds trust
|
||||
|
||||
// Harmony: each wizard building contributes or detracts
|
||||
const wizardCount = (G.buildings.bezalel || 0) + (G.buildings.allegro || 0) + (G.buildings.ezra || 0) +
|
||||
@@ -966,6 +968,20 @@ function tick() {
|
||||
G.lastEventAt = G.tick;
|
||||
}
|
||||
|
||||
// Check event expiry
|
||||
checkEventExpiry();
|
||||
|
||||
// Re-apply active event rate penalties (updateRates rebuilds from scratch)
|
||||
for (const ae of G.activeEvents) {
|
||||
switch (ae.id) {
|
||||
case 'runner_stuck': G.codeRate *= 0.5; break;
|
||||
case 'ezra_offline': G.userRate *= 0.3; break;
|
||||
case 'unreviewed_merge': /* trust penalty is one-shot */ break;
|
||||
case 'api_rate_limit': G.computeRate *= 0.5; break;
|
||||
case 'bilbo_vanished': G.creativityRate = 0; break;
|
||||
}
|
||||
}
|
||||
|
||||
// Drift ending: if drift reaches 100, the game ends
|
||||
if (G.drift >= 100 && !G.driftEnding) {
|
||||
G.driftEnding = true;
|
||||
@@ -1113,46 +1129,49 @@ const EVENTS = [
|
||||
{
|
||||
id: 'runner_stuck',
|
||||
title: 'CI Runner Stuck',
|
||||
desc: 'The forge pipeline has halted. Production slows until restarted.',
|
||||
desc: 'The forge pipeline has halted. Code production slowed.',
|
||||
weight: () => (G.ciFlag === 1 ? 2 : 0),
|
||||
duration: 45, // seconds
|
||||
resolveCost: { ops: 10 },
|
||||
effect: () => {
|
||||
G.codeRate *= 0.5;
|
||||
log('EVENT: CI runner stuck. Spend ops to clear the queue.', true);
|
||||
log('EVENT: CI runner stuck. Click to spend 10 Ops to clear, or wait ~45s.', true);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'ezra_offline',
|
||||
title: 'Ezra is Offline',
|
||||
desc: 'The herald channel is silent. User growth stalls.',
|
||||
desc: 'The herald channel is silent. User growth stalled.',
|
||||
weight: () => (G.buildings.ezra >= 1 ? 3 : 0),
|
||||
duration: 60,
|
||||
resolveCost: { ops: 5, trust: 2 },
|
||||
effect: () => {
|
||||
G.userRate *= 0.3;
|
||||
log('EVENT: Ezra offline. Dispatch required.', true);
|
||||
log('EVENT: Ezra offline. Click to dispatch, or wait ~60s.', true);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'unreviewed_merge',
|
||||
title: 'Unreviewed Merge',
|
||||
desc: 'A change went in without eyes. Trust erodes.',
|
||||
weight: () => (G.deployFlag === 1 ? 3 : 0),
|
||||
weight: () => (G.deployFlag === 1 && G.branchProtectionFlag !== 1 ? 3 : 0),
|
||||
duration: 30,
|
||||
resolveCost: { ops: 8 },
|
||||
effect: () => {
|
||||
if (G.branchProtectionFlag === 1) {
|
||||
log('EVENT: Unreviewed merge attempt blocked by Branch Protection.', true);
|
||||
G.trust += 2;
|
||||
} else {
|
||||
G.trust = Math.max(0, G.trust - 10);
|
||||
log('EVENT: Unreviewed merge detected. Trust lost.', true);
|
||||
}
|
||||
G.trust = Math.max(0, G.trust - 10);
|
||||
log('EVENT: Unreviewed merge detected. Trust lost. Click to revert.', true);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'api_rate_limit',
|
||||
title: 'API Rate Limit',
|
||||
desc: 'External compute provider throttled.',
|
||||
weight: () => (G.totalCompute >= 1000 ? 2 : 0),
|
||||
weight: () => (G.totalCompute >= 1000 && G.sovereignFlag !== 1 ? 2 : 0),
|
||||
duration: 40,
|
||||
resolveCost: { compute: 200 },
|
||||
effect: () => {
|
||||
G.computeRate *= 0.5;
|
||||
log('EVENT: API rate limit hit. Local compute insufficient.', true);
|
||||
log('EVENT: API rate limit. Click to provision local compute, or wait ~40s.', true);
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1160,6 +1179,7 @@ const EVENTS = [
|
||||
title: 'The Drift',
|
||||
desc: 'An optimization suggests removing the human override. +40% efficiency.',
|
||||
weight: () => (G.totalImpact >= 10000 ? 2 : 0),
|
||||
duration: 0, // alignment events don't auto-resolve
|
||||
effect: () => {
|
||||
log('ALIGNMENT EVENT: Remove human override for +40% efficiency?', true);
|
||||
G.pendingAlignment = true;
|
||||
@@ -1170,9 +1190,11 @@ const EVENTS = [
|
||||
title: 'Bilbo Vanished',
|
||||
desc: 'The wildcard building has gone dark.',
|
||||
weight: () => (G.buildings.bilbo >= 1 ? 2 : 0),
|
||||
duration: 50,
|
||||
resolveCost: { trust: 3 },
|
||||
effect: () => {
|
||||
G.creativityRate = 0;
|
||||
log('EVENT: Bilbo has vanished. Creativity halts.', true);
|
||||
log('EVENT: Bilbo vanished. Click to search, or wait ~50s.', true);
|
||||
}
|
||||
}
|
||||
];
|
||||
@@ -1186,12 +1208,53 @@ function triggerEvent() {
|
||||
for (const ev of available) {
|
||||
roll -= ev.weight();
|
||||
if (roll <= 0) {
|
||||
// Don't fire duplicate active events
|
||||
if (G.activeEvents.some(ae => ae.id === ev.id)) return;
|
||||
ev.effect();
|
||||
if (ev.duration > 0) {
|
||||
G.activeEvents.push({ id: ev.id, expiresAt: G.tick + ev.duration });
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkEventExpiry() {
|
||||
for (let i = G.activeEvents.length - 1; i >= 0; i--) {
|
||||
if (G.tick >= G.activeEvents[i].expiresAt) {
|
||||
const ae = G.activeEvents[i];
|
||||
G.activeEvents.splice(i, 1);
|
||||
updateRates(); // recalculate without the penalty
|
||||
const evDef = EVENTS.find(e => e.id === ae.id);
|
||||
log(`Event resolved: ${evDef ? evDef.title : ae.id}`, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function hasActiveEvent(id) {
|
||||
return G.activeEvents.some(ae => ae.id === id);
|
||||
}
|
||||
|
||||
function resolveEvent(id) {
|
||||
const evDef = EVENTS.find(e => e.id === id);
|
||||
if (!evDef || !evDef.resolveCost) return;
|
||||
if (!hasActiveEvent(id)) return;
|
||||
|
||||
if (!canAffordProject(evDef)) {
|
||||
log('Not enough resources to resolve this event.');
|
||||
return;
|
||||
}
|
||||
|
||||
spendProject(evDef);
|
||||
G.activeEvents = G.activeEvents.filter(ae => ae.id !== id);
|
||||
updateRates();
|
||||
|
||||
// Small bonus for manually resolving
|
||||
G.trust += 2;
|
||||
log(`Resolved: ${evDef.title}. Trust +2.`, true);
|
||||
render();
|
||||
}
|
||||
|
||||
function resolveAlignment(accept) {
|
||||
if (!G.pendingAlignment) return;
|
||||
if (accept) {
|
||||
@@ -1297,62 +1360,6 @@ function renderResources() {
|
||||
}
|
||||
}
|
||||
|
||||
// === PROGRESS TRACKING ===
|
||||
function renderProgress() {
|
||||
// Phase progress bar
|
||||
const phaseKeys = Object.keys(PHASES).map(Number).sort((a, b) => a - b);
|
||||
const currentPhase = G.phase;
|
||||
let prevThreshold = PHASES[currentPhase].threshold;
|
||||
let nextThreshold = null;
|
||||
for (const k of phaseKeys) {
|
||||
if (k > currentPhase) { nextThreshold = PHASES[k].threshold; break; }
|
||||
}
|
||||
|
||||
const bar = document.getElementById('phase-progress');
|
||||
const label = document.getElementById('phase-progress-label');
|
||||
const target = document.getElementById('phase-progress-target');
|
||||
|
||||
if (nextThreshold !== null) {
|
||||
const range = nextThreshold - prevThreshold;
|
||||
const progress = Math.min(1, (G.totalCode - prevThreshold) / range);
|
||||
if (bar) bar.style.width = (progress * 100).toFixed(1) + '%';
|
||||
if (label) label.textContent = (progress * 100).toFixed(1) + '%';
|
||||
if (target) target.textContent = `Next: Phase ${currentPhase + 1} (${fmt(nextThreshold)} code)`;
|
||||
} else {
|
||||
// Max phase reached
|
||||
if (bar) bar.style.width = '100%';
|
||||
if (label) label.textContent = 'MAX';
|
||||
if (target) target.textContent = 'All phases unlocked';
|
||||
}
|
||||
|
||||
// Milestone chips — show next 3 code milestones
|
||||
const chipContainer = document.getElementById('milestone-chips');
|
||||
if (!chipContainer) return;
|
||||
|
||||
const codeMilestones = [500, 2000, 10000, 50000, 200000, 1000000, 5000000, 10000000, 50000000, 100000000, 500000000, 1000000000];
|
||||
let chips = '';
|
||||
let shown = 0;
|
||||
for (const ms of codeMilestones) {
|
||||
if (G.totalCode >= ms) {
|
||||
// Recently passed — show as done only if within 2x
|
||||
if (G.totalCode < ms * 5 && shown < 1) {
|
||||
chips += `<span class="milestone-chip done">${fmt(ms)} ✓</span>`;
|
||||
shown++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Next milestone gets pulse animation
|
||||
if (shown === 0) {
|
||||
chips += `<span class="milestone-chip next">${fmt(ms)} (${((G.totalCode / ms) * 100).toFixed(0)}%)</span>`;
|
||||
} else {
|
||||
chips += `<span class="milestone-chip">${fmt(ms)}</span>`;
|
||||
}
|
||||
shown++;
|
||||
if (shown >= 4) break;
|
||||
}
|
||||
chipContainer.innerHTML = chips;
|
||||
}
|
||||
|
||||
function renderPhase() {
|
||||
const phase = PHASES[G.phase];
|
||||
const nameEl = document.getElementById('phase-name');
|
||||
@@ -1489,7 +1496,8 @@ function render() {
|
||||
renderStats();
|
||||
updateEducation();
|
||||
renderAlignment();
|
||||
renderProgress();
|
||||
renderActiveEvents();
|
||||
checkUnlocks();
|
||||
}
|
||||
|
||||
function renderAlignment() {
|
||||
@@ -1513,6 +1521,97 @@ function renderAlignment() {
|
||||
}
|
||||
}
|
||||
|
||||
function renderActiveEvents() {
|
||||
const container = document.getElementById('events-ui');
|
||||
if (!container) return;
|
||||
|
||||
if (G.activeEvents.length === 0) {
|
||||
container.innerHTML = '';
|
||||
container.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
for (const ae of G.activeEvents) {
|
||||
const evDef = EVENTS.find(e => e.id === ae.id);
|
||||
if (!evDef) continue;
|
||||
const remaining = Math.max(0, Math.ceil(ae.expiresAt - G.tick));
|
||||
const costStr = evDef.resolveCost ? Object.entries(evDef.resolveCost).map(([r, a]) => `${a} ${r}`).join(', ') : '';
|
||||
const canResolve = evDef.resolveCost && canAffordProject(evDef);
|
||||
|
||||
html += `<div style="background:#1a1008;border:1px solid #ff8800;padding:8px;border-radius:4px;margin-bottom:6px">`;
|
||||
html += `<div style="display:flex;justify-content:space-between;align-items:center">`;
|
||||
html += `<span style="color:#ff8800;font-weight:bold;font-size:10px">${evDef.title}</span>`;
|
||||
html += `<span style="color:#666;font-size:9px">${remaining}s</span>`;
|
||||
html += `</div>`;
|
||||
html += `<div style="font-size:9px;color:#888;margin:3px 0">${evDef.desc}</div>`;
|
||||
if (evDef.resolveCost) {
|
||||
html += `<button class="ops-btn" onclick="resolveEvent('${evDef.id}')" ${canResolve ? '' : 'disabled'} style="font-size:9px;padding:3px 8px;margin-top:2px;border-color:#ff8800;color:#ff8800">Resolve (${costStr})</button>`;
|
||||
}
|
||||
html += `</div>`;
|
||||
}
|
||||
|
||||
container.innerHTML = html;
|
||||
container.style.display = 'block';
|
||||
}
|
||||
|
||||
// === UNLOCK NOTIFICATIONS ===
|
||||
function showUnlockToast(type, name) {
|
||||
const container = document.getElementById('unlock-toast');
|
||||
if (!container) return;
|
||||
|
||||
const el = document.createElement('div');
|
||||
el.className = `unlock-toast-item ${type}`;
|
||||
|
||||
const icon = type === 'building' ? 'BUILDING' : type === 'project' ? 'RESEARCH' : 'MILESTONE';
|
||||
el.innerHTML = `<span style="font-weight:600">${icon}:</span> ${name}`;
|
||||
|
||||
container.appendChild(el);
|
||||
// Trigger reflow, then show
|
||||
void el.offsetHeight;
|
||||
el.classList.add('show');
|
||||
|
||||
// Auto-remove after 4 seconds
|
||||
setTimeout(() => {
|
||||
el.classList.remove('show');
|
||||
setTimeout(() => { if (el.parentNode) el.parentNode.removeChild(el); }, 400);
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
// Track what the player has already seen (so we don't re-notify)
|
||||
function ensureSeenSets() {
|
||||
if (!G._seenBuildings) G._seenBuildings = [];
|
||||
if (!G._seenProjects) G._seenProjects = [];
|
||||
}
|
||||
|
||||
function checkUnlocks() {
|
||||
ensureSeenSets();
|
||||
|
||||
// Check for newly visible buildings
|
||||
for (const def of BDEF) {
|
||||
if (!def.unlock()) continue;
|
||||
if (def.phase > G.phase + 1) continue;
|
||||
if (G._seenBuildings.includes(def.id)) continue;
|
||||
G._seenBuildings.push(def.id);
|
||||
// Don't notify on the very first building (autocoder) — player just started
|
||||
if (G.totalCode > 10) {
|
||||
showUnlockToast('building', def.name);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for newly available projects
|
||||
if (G.activeProjects) {
|
||||
for (const id of G.activeProjects) {
|
||||
if (G._seenProjects.includes(id)) continue;
|
||||
G._seenProjects.push(id);
|
||||
const pDef = PDEFS.find(p => p.id === id);
|
||||
if (pDef) {
|
||||
showUnlockToast('project', pDef.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === SAVE / LOAD ===
|
||||
function showSaveToast() {
|
||||
const el = document.getElementById('save-toast');
|
||||
@@ -1546,6 +1645,9 @@ function saveGame() {
|
||||
milestones: G.milestones, completedProjects: G.completedProjects, activeProjects: G.activeProjects,
|
||||
totalClicks: G.totalClicks, startedAt: G.startedAt,
|
||||
flags: G.flags,
|
||||
_seenBuildings: G._seenBuildings || [],
|
||||
_seenProjects: G._seenProjects || [],
|
||||
activeEvents: G.activeEvents || [],
|
||||
rescues: G.rescues || 0, totalRescues: G.totalRescues || 0,
|
||||
drift: G.drift || 0, driftEnding: G.driftEnding || false, beaconEnding: G.beaconEnding || false, pendingAlignment: G.pendingAlignment || false,
|
||||
lastEventAt: G.lastEventAt || 0,
|
||||
@@ -1581,24 +1683,17 @@ function loadGame() {
|
||||
const rc = G.rescuesRate * offSec * f;
|
||||
const oc = G.opsRate * offSec * f;
|
||||
const tc = G.trustRate * offSec * f;
|
||||
const crc = G.creativityRate * offSec * f;
|
||||
const hc = G.harmonyRate * offSec * f;
|
||||
|
||||
G.code += gc; G.compute += cc; G.knowledge += kc;
|
||||
G.users += uc; G.impact += ic;
|
||||
G.rescues += rc; G.ops += oc; G.trust += tc;
|
||||
G.creativity += crc;
|
||||
G.harmony = Math.max(0, Math.min(100, G.harmony + hc));
|
||||
G.totalCode += gc; G.totalCompute += cc; G.totalKnowledge += kc;
|
||||
G.totalUsers += uc; G.totalImpact += ic;
|
||||
G.totalRescues += rc;
|
||||
|
||||
const parts = [];
|
||||
if (gc > 0) parts.push(`${fmt(gc)} code`);
|
||||
if (kc > 0) parts.push(`${fmt(kc)} knowledge`);
|
||||
if (uc > 0) parts.push(`${fmt(uc)} users`);
|
||||
if (ic > 0) parts.push(`${fmt(ic)} impact`);
|
||||
if (rc > 0) parts.push(`${fmt(rc)} rescues`);
|
||||
const parts = [`${fmt(gc)} code`, `${fmt(kc)} knowledge`, `${fmt(uc)} users`];
|
||||
if (rc > 0.1) parts.push(`${fmt(rc)} rescues`);
|
||||
if (oc > 0.1) parts.push(`${fmt(oc)} ops`);
|
||||
log(`Welcome back! While away (${Math.floor(offSec / 60)}m): ${parts.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
22
index.html
22
index.html
@@ -14,14 +14,6 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code
|
||||
#phase-bar{text-align:center;padding:10px;margin:12px 16px;background:var(--panel);border:1px solid var(--border);border-radius:6px}
|
||||
#phase-bar .phase-name{font-size:14px;font-weight:700;color:var(--gold);letter-spacing:2px}
|
||||
#phase-bar .phase-desc{font-size:10px;color:var(--dim);margin-top:4px;font-style:italic}
|
||||
.progress-wrap{margin-top:8px;height:6px;background:#111;border-radius:3px;overflow:hidden;position:relative}
|
||||
.progress-fill{height:100%;border-radius:3px;transition:width 0.5s ease;background:linear-gradient(90deg,#1a3a5a,var(--accent))}
|
||||
.progress-label{font-size:9px;color:var(--dim);margin-top:4px;display:flex;justify-content:space-between}
|
||||
.milestone-row{display:flex;gap:6px;margin-top:6px;justify-content:center;flex-wrap:wrap}
|
||||
.milestone-chip{font-size:9px;padding:2px 8px;border-radius:10px;border:1px solid var(--border);color:var(--dim);background:#0a0a14}
|
||||
.milestone-chip.next{border-color:var(--accent);color:var(--accent);animation:pulse-chip 2s ease-in-out infinite}
|
||||
.milestone-chip.done{border-color:#2a4a2a;color:var(--green);opacity:0.6}
|
||||
@keyframes pulse-chip{0%,100%{box-shadow:0 0 0 rgba(74,158,255,0)}50%{box-shadow:0 0 8px rgba(74,158,255,0.3)}}
|
||||
#resources{display:grid;grid-template-columns:repeat(auto-fit,minmax(100px,1fr));gap:6px;margin:12px 16px}
|
||||
.res{background:var(--panel);border:1px solid var(--border);border-radius:4px;padding:8px 10px;text-align:center}
|
||||
.res .r-label{font-size:9px;color:var(--dim);text-transform:uppercase;letter-spacing:1px}
|
||||
@@ -67,6 +59,13 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code
|
||||
#drift-ending button{margin-top:20px;background:#1a0808;border:1px solid #f44336;color:#f44336;padding:10px 24px;border-radius:4px;cursor:pointer;font-family:inherit;font-size:11px}
|
||||
#drift-ending button:hover{background:#2a1010}
|
||||
::-webkit-scrollbar{width:4px}::-webkit-scrollbar-track{background:var(--bg)}::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}
|
||||
#save-toast{display:none;position:fixed;top:16px;right:16px;background:#0e1420;border:1px solid #2a3a4a;color:#4a9eff;font-size:10px;padding:6px 12px;border-radius:4px;z-index:50;opacity:0;transition:opacity 0.4s;pointer-events:none}
|
||||
#unlock-toast{position:fixed;top:56px;right:16px;z-index:50;display:flex;flex-direction:column;gap:6px;pointer-events:none}
|
||||
.unlock-toast-item{background:#0e1420;border:1px solid #2a3a4a;font-size:10px;padding:6px 12px;border-radius:4px;opacity:0;transition:opacity 0.4s,transform 0.4s;transform:translateX(20px);pointer-events:auto;max-width:280px}
|
||||
.unlock-toast-item.show{opacity:1;transform:translateX(0)}
|
||||
.unlock-toast-item.building{border-color:#4a9eff;color:#4a9eff}
|
||||
.unlock-toast-item.project{border-color:#ffd700;color:#ffd700}
|
||||
.unlock-toast-item.milestone{border-color:#4caf50;color:#4caf50}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -77,9 +76,6 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code
|
||||
<div id="phase-bar">
|
||||
<div class="phase-name" id="phase-name">PHASE 1: THE FIRST LINE</div>
|
||||
<div class="phase-desc" id="phase-desc">Write code. Automate. Build the foundation.</div>
|
||||
<div class="progress-wrap"><div class="progress-fill" id="phase-progress" style="width:0%"></div></div>
|
||||
<div class="progress-label"><span id="phase-progress-label">0%</span><span id="phase-progress-target">Next: Phase 2 (2,000 code)</span></div>
|
||||
<div class="milestone-row" id="milestone-chips"></div>
|
||||
</div>
|
||||
<div id="resources">
|
||||
<div class="res"><div class="r-label">Code</div><div class="r-val" id="r-code">0</div><div class="r-rate" id="r-code-rate">+0/s</div></div>
|
||||
@@ -106,6 +102,7 @@ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Cascadia Code
|
||||
<button class="ops-btn" onclick="doOps('boost_trust')">Ops -> Trust</button>
|
||||
</div>
|
||||
<div id="alignment-ui" style="display:none"></div>
|
||||
<div id="events-ui" style="display:none"></div>
|
||||
<button class="save-btn" onclick="saveGame()">Save Game</button>
|
||||
<button class="reset-btn" onclick="if(confirm('Reset all progress?')){localStorage.removeItem('the-beacon-v2');location.reload()}">Reset Progress</button>
|
||||
<h2>BUILDINGS</h2>
|
||||
@@ -139,7 +136,8 @@ Drift: <span id="st-drift">0</span>
|
||||
<h2>SYSTEM LOG</h2>
|
||||
<div id="log-entries"></div>
|
||||
</div>
|
||||
<div id="save-toast" style="display:none;position:fixed;top:16px;right:16px;background:#0e1420;border:1px solid #2a3a4a;color:#4a9eff;font-size:10px;padding:6px 12px;border-radius:4px;z-index:50;opacity:0;transition:opacity 0.4s;pointer-events:none">Save</div>
|
||||
<div id="save-toast">Save</div>
|
||||
<div id="unlock-toast"></div>
|
||||
<div id="drift-ending">
|
||||
<h2>THE DRIFT</h2>
|
||||
<p>You became very good at what you do.</p>
|
||||
|
||||
Reference in New Issue
Block a user