Session 13: Save Slot Preview (footer shows each slot's stardust, buildings, achievements, last-saved; hover to preview)
This commit is contained in:
@@ -565,6 +565,31 @@ h2, h3 {
|
|||||||
opacity: 0.5;
|
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 */
|
/* Autosave toggle button */
|
||||||
.toggle-btn {
|
.toggle-btn {
|
||||||
background: var(--bg-card);
|
background: var(--bg-card);
|
||||||
|
|||||||
@@ -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.
|
- Consider adding "Buy Max" to upgrades too.
|
||||||
- Potential new building tiers or prestige milestones.
|
- Potential new building tiers or prestige milestones.
|
||||||
- Sound variations or background music.
|
- 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.
|
||||||
|
|||||||
@@ -108,6 +108,8 @@
|
|||||||
<button class="slot-btn" data-slot="0">1</button>
|
<button class="slot-btn" data-slot="0">1</button>
|
||||||
<button class="slot-btn" data-slot="1">2</button>
|
<button class="slot-btn" data-slot="1">2</button>
|
||||||
<button class="slot-btn" data-slot="2">3</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>
|
||||||
<div class="footer-section">
|
<div class="footer-section">
|
||||||
<span class="footer-label">Auto:</span>
|
<span class="footer-label">Auto:</span>
|
||||||
|
|||||||
+76
@@ -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
|
* Switch the active save slot. Persists the selection and reloads that
|
||||||
* slot's data from localStorage into the live game state, preserving
|
* slot's data from localStorage into the live game state, preserving
|
||||||
|
|||||||
@@ -45,6 +45,9 @@ const UI = {
|
|||||||
|
|
||||||
// Save Slot System highlight / toggle state
|
// Save Slot System highlight / toggle state
|
||||||
this.updateSlotUI();
|
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
|
* Show floating text animation
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user