Session 13: Save Slot Preview (footer shows each slot's stardust, buildings, achievements, last-saved; hover to preview)

This commit is contained in:
2026-08-23 17:10:56 +00:00
parent 1b9932391c
commit 40b85f5707
5 changed files with 176 additions and 0 deletions
+25
View File
@@ -565,6 +565,31 @@ h2, h3 {
opacity: 0.5;
}
/* Live one-line preview of the active/hovered save slot in the footer */
.slot-preview {
color: var(--text-secondary);
font-size: 0.78rem;
font-family: monospace;
white-space: nowrap;
max-width: 320px;
overflow: hidden;
text-overflow: ellipsis;
opacity: 0.85;
transition: opacity 0.15s ease, color 0.15s ease;
}
.slot-preview.empty {
color: var(--text-muted);
font-style: italic;
opacity: 0.6;
}
#game-footer:hover .slot-preview,
.slot-preview:not(.empty):hover {
opacity: 1;
color: var(--accent-purple);
}
/* Autosave toggle button */
.toggle-btn {
background: var(--bg-card);
+27
View File
@@ -359,3 +359,30 @@ Headless test at /opt/data/idle-game-test/golden-bloom-test.mjs — **22/22 pass
- Consider adding "Buy Max" to upgrades too.
- Potential new building tiers or prestige milestones.
- Sound variations or background music.
---
### Session 13 - 2026-08-23 (Morning)
**Status:** ✅ Save Slot Preview Added
#### What was done:
- Followed up on Session 12's stated next goal by adding a **save-slot preview** so players can tell their save slots apart at a glance.
- Added `Game.getSlotPreview(slot)` — a read-only helper that reads a given slot's data from localStorage WITHOUT mutating the live game state. It returns: `present` (has any saved data), `stardust`, `totalStardust`, `buildings` (sum of all building counts), `achievements` [count/total], `prestigeCount`, and a human-readable `lastSaved` "time ago" string.
- Added `Game.formatTimeAgo(secs)` — formats a second-count into compact relative labels ("just now", "1m ago", "3h ago", "2d ago", "7mo ago", etc.).
- Wired the preview into the footer: a monospace `.slot-preview` span now shows the active slot's live summary every tick (`Slot N: 1.2K ✨ · 8 plants · 2/33 · just now`). Hovering any slot button swaps the preview to that slot's contents; leaving falls back to the active slot.
- Added CSS for `.slot-preview` (monospace, ellipsis-truncated, dims until hover) and a `.empty` style for unreadable slots ("Slot N: empty").
- All display logic is read-only — `getSlotPreview()` calls the existing `loadSlot()` and never touches `Game.state`, verified by a dedicated test.
#### Files modified:
- `js/game.js` - added `getSlotPreview(slot)` + `formatTimeAgo(secs)` methods (inserted before `switchSlot()`)
- `index.html` - added `<span id="slot-preview">` inside the footer Slot section
- `js/ui.js` - added `updateSlotPreview()`; called it from `UI.update()` each tick and wired per-slot hover/leave listeners (re-cloning slot buttons each render to avoid duplicate listeners)
- `css/game.css` - `.slot-preview`, `.slot-preview.empty`, and hover-brightening rules
#### Verification:
Wrote a headless Node.js test (`/opt/data/idle-game-test/slot-preview-test.mjs`) evaluating the real game.js in-process with localStorage + UI stubs. **22/22 checks pass**, covering: empty-slot handling (present:false, zeroed), populated-slot field reads (stardust, totalStardust, summed buildings, achievement count/total, prestigeCount), formatTimeAgo buckets (just now / m / h / d), relative lastSaved strings, read-only guarantee (live state unchanged after previews), and graceful handling of malformed/missing fields (minimal object → present:true zeroed; non-numeric building count coerced to 0 with no crash). Existing golden-bloom test re-ran clean (22/22, no regression). All modified JS files pass `node --check`.
#### Next goals:
- Consider adding a "Buy Max" button to upgrades too (one-time purchases — less critical).
- Potential new building tiers or prestige milestones.
- Sound variations or background music.
+2
View File
@@ -108,6 +108,8 @@
<button class="slot-btn" data-slot="0">1</button>
<button class="slot-btn" data-slot="1">2</button>
<button class="slot-btn" data-slot="2">3</button>
<!-- Live one-line preview of the active slot; hover a slot button to preview it -->
<span id="slot-preview" class="slot-preview" title="Hover a slot button to preview its contents"></span>
</div>
<div class="footer-section">
<span class="footer-label">Auto:</span>
+76
View File
@@ -637,6 +637,82 @@ const Game = {
}
},
/**
* Build a lightweight preview of a given save slot WITHOUT mutating the
* live game state. Returns an object describing what that slot holds:
* - present: whether the slot has any saved data
* - stardust: current (live) stardust production/sec for the slot
* - totalStardust: lifetime stardust earned in the slot
* - buildings: total number of buildings owned in the slot
* - achievements: count/total of achievements unlocked in the slot
* - prestigeCount: how many times the slot has been prestiged
* - lastSaved: human-readable "time ago" string for the slot's save time
* An empty/unreadable slot returns present:false with zeroed fields.
*/
getSlotPreview(slot) {
const data = this.loadSlot(slot);
if (!data) {
return {
present: false,
stardust: 0,
totalStardust: 0,
buildings: 0,
achievements: [0, 33],
prestigeCount: 0,
lastSaved: 'never'
};
}
const buildingCount = Object.values(data.buildings || {})
.reduce((sum, c) => sum + (Number(c) || 0), 0);
let timeAgo = 'never';
if (typeof data.lastSaveTime === 'number') {
const secs = Math.max(0, Math.floor((Date.now() - data.lastSaveTime) / 1000));
timeAgo = this.formatTimeAgo(secs);
}
return {
present: true,
stardust: Number(data.stardust) || 0,
totalStardust: Number(data.totalStardust) || 0,
buildings: buildingCount,
achievements: [
Array.isArray(data.achievements) ? data.achievements.length : 0,
33
],
prestigeCount: Number(data.prestigeCount) || 0,
lastSaved: timeAgo
};
},
/**
* Format a number of seconds as a compact "time ago" string
* (e.g. "just now", "1m ago", "3h ago", "2d ago"). Used by slot previews.
*/
formatTimeAgo(secs) {
if (secs < 5) return 'just now';
// Largest units first; each entry is [thresholdSeconds, label].
const buckets = [
[365 * 86400, 'y'],
[30 * 86400, 'mo'],
[86400, 'd'],
[3600, 'h'],
[60, 'm']
];
for (const [threshold, label] of buckets) {
if (secs >= threshold) {
const value = Math.floor(secs / threshold);
return `${value}${label} ago`;
}
}
// Below one minute.
return `${Math.max(1, secs)}s ago`;
},
/**
* Switch the active save slot. Persists the selection and reloads that
* slot's data from localStorage into the live game state, preserving
+46
View File
@@ -45,6 +45,9 @@ const UI = {
// Save Slot System highlight / toggle state
this.updateSlotUI();
// Save Slot System live preview in the footer
this.updateSlotPreview();
},
/**
@@ -325,6 +328,49 @@ const UI = {
}
},
/**
* Update the one-line live preview of the active slot in the footer.
* Also wires hover handlers onto each slot button so hovering previews a
* different slot's contents (stardust, buildings, achievements, last saved).
* This is a read-only display helper and never mutates game state.
*/
updateSlotPreview() {
const el = document.getElementById('slot-preview');
if (!el) return;
// Hovering a slot button previews that slot's contents instead of the
// active one. Track which slot is currently being previewed on hover.
let hovering = null;
const render = (slot) => {
const preview = Game.getSlotPreview(slot);
if (!preview.present) {
el.textContent = `Slot ${slot + 1}: empty`;
el.classList.add('empty');
return;
}
el.classList.remove('empty');
el.textContent =
`Slot ${slot + 1}: ${Game.formatNumber(preview.stardust)} ✨ · ` +
`${preview.buildings} plants · ${preview.achievements[0]}/${preview.achievements[1]} · ${preview.lastSaved}`;
};
// Default: preview the active slot live (updates every tick).
render(Game.state.activeSlot);
// Remove any previously-attached listeners to avoid duplicates on re-render.
document.querySelectorAll('.slot-btn').forEach(btn => {
btn.replaceWith(btn.cloneNode(true));
});
// On hover, preview the hovered slot; on leave, fall back to active.
document.querySelectorAll('.slot-btn').forEach(btn => {
const idx = parseInt(btn.dataset.slot, 10);
btn.addEventListener('mouseenter', () => { hovering = idx; render(idx); });
btn.addEventListener('mouseleave', () => { hovering = null; render(Game.state.activeSlot); });
});
},
/**
* Show floating text animation
*/