Compare commits

...

3 Commits

Author SHA1 Message Date
Alexander Whitestone
7c3510a680 beacon: add progression toast notifications for phase/building/project unlocks
Animated toasts slide in from the right when:
- A new phase is reached (gold border)
- A building unlocks for the first time (blue border)
- A research project completes (green border, gold title for milestones)

Toasts auto-dismiss after 4s with fade-out. Max 4 visible at once.
seenBuildings state persisted in save data so toasts don't re-fire on reload.

This makes progression events impossible to miss — previously they only
showed in the scrolling system log.
2026-04-10 08:32:29 -04:00
6081844387 [auto-merge] Welcome Back popup
Auto-merged by PR review bot: Welcome Back popup
2026-04-10 11:48:34 +00:00
Timmy-Sprint
09b8c02307 beacon: add Welcome Back popup for offline gains + fix missing resource tracking
- Added modal popup showing detailed offline resource gains when player returns
- Fixed offline tracking to include ops, trust, and creativity (were silently missing)
- Popup shows all resources with color-coded labels and 50% efficiency note
- Log message now shows time in human-readable format (minutes/seconds)
2026-04-10 07:35:52 -04:00
2 changed files with 104 additions and 1 deletions

82
game.js
View File

@@ -118,6 +118,9 @@ const G = {
// Bulk buy multiplier (1, 10, or -1 for max)
buyAmount: 1,
// Track which buildings have had their unlock toasted
seenBuildings: {},
// Time tracking
playTime: 0,
startTime: 0
@@ -1072,6 +1075,21 @@ function tick() {
}
}
// === TOAST NOTIFICATIONS ===
function showToast(title, msg, type) {
const container = document.getElementById('toast-container');
if (!container) return;
const el = document.createElement('div');
el.className = `toast toast-${type}`;
el.innerHTML = `<div class="toast-title">${title}</div><div class="toast-msg">${msg}</div>`;
container.appendChild(el);
// Auto-dismiss after 4s
setTimeout(() => { el.classList.add('fade-out'); }, 4000);
setTimeout(() => { el.remove(); }, 4600);
// Cap visible toasts at 4
while (container.children.length > 4) container.removeChild(container.firstChild);
}
function checkMilestones() {
for (const m of MILESTONES) {
if (!G.milestones.includes(m.flag)) {
@@ -1089,6 +1107,7 @@ function checkMilestones() {
if (G.totalCode >= phase.threshold && parseInt(phaseNum) > G.phase) {
G.phase = parseInt(phaseNum);
log(`PHASE ${G.phase}: ${phase.name}`, true);
showToast(`PHASE ${G.phase}: ${phase.name}`, phase.desc, 'phase');
}
}
}
@@ -1161,6 +1180,8 @@ function buyProject(id) {
G.activeProjects = G.activeProjects.filter(aid => aid !== pDef.id);
}
showToast(pDef.milestone ? '★ ' + pDef.name : pDef.name, pDef.desc, 'project');
updateRates();
render();
}
@@ -1608,6 +1629,12 @@ function renderBuildings() {
visibleCount++;
const count = G.buildings[def.id] || 0;
// Toast on first unlock
if (isUnlocked && !G.seenBuildings[def.id]) {
G.seenBuildings[def.id] = true;
showToast('NEW: ' + def.name, def.desc, 'building');
}
// Locked preview: show dimmed with unlock hint
if (!isUnlocked) {
html += `<div class="build-btn" style="opacity:0.25;cursor:default" title="${def.edu || ''}">`;
@@ -1925,6 +1952,35 @@ function renderAlignment() {
}
}
// === OFFLINE GAINS POPUP ===
function showOfflinePopup(timeLabel, gains, offSec) {
const el = document.getElementById('offline-popup');
if (!el) return;
const timeEl = document.getElementById('offline-time-label');
if (timeEl) timeEl.textContent = `You were away for ${timeLabel}.`;
const listEl = document.getElementById('offline-gains-list');
if (listEl) {
let html = '';
for (const g of gains) {
html += `<div style="display:flex;justify-content:space-between;padding:2px 0;border-bottom:1px solid #111">`;
html += `<span style="color:${g.color}">${g.label}</span>`;
html += `<span style="color:#4caf50;font-weight:600">+${fmt(g.value)}</span>`;
html += `</div>`;
}
// Show offline efficiency note
html += `<div style="color:#555;font-size:9px;margin-top:8px;font-style:italic">Offline efficiency: 50%</div>`;
listEl.innerHTML = html;
}
el.style.display = 'flex';
}
function dismissOfflinePopup() {
const el = document.getElementById('offline-popup');
if (el) el.style.display = 'none';
}
// === SAVE / LOAD ===
function showSaveToast() {
const el = document.getElementById('save-toast');
@@ -1966,6 +2022,7 @@ function saveGame() {
activeDebuffIds: debuffIds,
totalEventsResolved: G.totalEventsResolved || 0,
buyAmount: G.buyAmount || 1,
seenBuildings: G.seenBuildings || {},
savedAt: Date.now()
};
@@ -2024,13 +2081,36 @@ function loadGame() {
G.totalUsers += uc; G.totalImpact += ic;
G.totalRescues += rc;
// Show welcome-back popup with all gains
const gains = [];
if (gc > 0) gains.push({ label: 'Code', value: gc, color: '#4a9eff' });
if (cc > 0) gains.push({ label: 'Compute', value: cc, color: '#4a9eff' });
if (kc > 0) gains.push({ label: 'Knowledge', value: kc, color: '#4a9eff' });
if (uc > 0) gains.push({ label: 'Users', value: uc, color: '#4a9eff' });
if (ic > 0) gains.push({ label: 'Impact', value: ic, color: '#4a9eff' });
if (rc > 0) gains.push({ label: 'Rescues', value: rc, color: '#4caf50' });
if (oc > 0) gains.push({ label: 'Ops', value: oc, color: '#b388ff' });
if (tc > 0) gains.push({ label: 'Trust', value: tc, color: '#4caf50' });
if (crc > 0) gains.push({ label: 'Creativity', value: crc, color: '#ffd700' });
const awayMin = Math.floor(offSec / 60);
const awaySec = Math.floor(offSec % 60);
const timeLabel = awayMin >= 1 ? `${awayMin} minute${awayMin !== 1 ? 's' : ''}` : `${awaySec} seconds`;
if (gains.length > 0) {
showOfflinePopup(timeLabel, gains, offSec);
}
// Log summary
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`);
log(`Welcome back! While away (${Math.floor(offSec / 60)}m): ${parts.join(', ')}`);
if (oc > 0) parts.push(`${fmt(oc)} ops`);
if (tc > 0) parts.push(`${fmt(tc)} trust`);
log(`Welcome back! While away (${timeLabel}): ${parts.join(', ')}`);
}
}

View File

@@ -84,6 +84,18 @@ 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}
#toast-container{position:fixed;top:60px;right:16px;z-index:80;display:flex;flex-direction:column;gap:8px;pointer-events:none}
.toast{background:#0e1420;border:1px solid #2a3a4a;border-radius:6px;padding:10px 16px;max-width:320px;animation:toast-in 0.3s ease-out;opacity:1;transition:opacity 0.5s ease-out;pointer-events:auto}
.toast.fade-out{opacity:0}
.toast-phase{border-color:var(--gold);border-width:2px}
.toast-phase .toast-title{color:var(--gold);text-shadow:0 0 12px rgba(255,215,0,0.3)}
.toast-building{border-color:var(--accent)}
.toast-building .toast-title{color:var(--accent)}
.toast-project{border-color:var(--green)}
.toast-project .toast-title{color:var(--green)}
.toast-title{font-size:12px;font-weight:700;letter-spacing:1px;margin-bottom:2px}
.toast-msg{font-size:10px;color:#888;line-height:1.4}
@keyframes toast-in{from{transform:translateX(100%);opacity:0}to{transform:translateX(0);opacity:1}}
</style>
</head>
<body>
@@ -160,6 +172,7 @@ Events Resolved: <span id="st-resolved">0</span>
<h2>SYSTEM LOG</h2>
<div id="log-entries"></div>
</div>
<div id="toast-container"></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="drift-ending">
<h2>THE DRIFT</h2>
@@ -174,5 +187,15 @@ The light is on. The room is empty."
<button onclick="if(confirm('Start over? The old save will be lost.')){localStorage.removeItem('the-beacon-v2');location.reload()}">START OVER</button>
</div>
<script src="game.js"></script>
<div id="offline-popup" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(8,8,16,0.92);z-index:90;justify-content:center;align-items:center;flex-direction:column;text-align:center;padding:40px">
<div style="background:#0e0e1a;border:1px solid #1a3a5a;border-radius:8px;padding:24px 32px;max-width:400px;width:100%">
<h3 style="color:#4a9eff;font-size:14px;letter-spacing:2px;margin-bottom:16px">WELCOME BACK</h3>
<p style="color:#888;font-size:10px;margin-bottom:12px" id="offline-time-label">You were away for 0 minutes.</p>
<div id="offline-gains-list" style="text-align:left;font-size:11px;line-height:1.8;margin-bottom:16px"></div>
<button onclick="dismissOfflinePopup()" style="background:#1a2a3a;border:1px solid #4a9eff;color:#4a9eff;padding:8px 20px;border-radius:4px;cursor:pointer;font-family:inherit;font-size:11px">Continue</button>
</div>
</div>
</body>
</html>