Session 12: Save Slot System (3 slots, autosave toggle, cross-slot import/export) + fix goldenBlooms field missing from reset()/switchSlot empty-state
This commit is contained in:
+199
-6
@@ -20,6 +20,10 @@ const Game = {
|
||||
totalPrestigeEssence: 0,
|
||||
prestigeUpgrades: [],
|
||||
|
||||
// Save Slot System
|
||||
activeSlot: 0, // index of the currently selected save slot (0..SLOTS-1)
|
||||
autoSave: true, // whether the periodic autosave loop is running
|
||||
|
||||
// Golden Starbloom random event state
|
||||
goldenBlooms: {
|
||||
active: false, // is a bloom currently visible on screen?
|
||||
@@ -34,6 +38,10 @@ const Game = {
|
||||
tickRate: 100, // ms per tick (10 ticks/sec)
|
||||
autoSaveInterval: 30000, // 30 seconds
|
||||
costScaling: 1.15, // Cost multiplier per building level
|
||||
|
||||
// Save Slot System - number of independent save slots available
|
||||
SLOTS: 3,
|
||||
|
||||
goldenBloomMinMs: 45000, // min ms between bloom spawns (45s)
|
||||
goldenBloomMaxMs: 90000, // max ms between bloom spawns (90s)
|
||||
goldenBloomDuration: 12, // seconds a visible bloom stays before vanishing
|
||||
@@ -59,6 +67,9 @@ const Game = {
|
||||
* Setup event listeners
|
||||
*/
|
||||
setupEventListeners() {
|
||||
// Track which slot the user has selected for importing.
|
||||
this.importSlot = 0;
|
||||
|
||||
// Harvest button - support both click and touch
|
||||
const harvestBtn = document.getElementById('harvest-btn');
|
||||
|
||||
@@ -100,9 +111,12 @@ const Game = {
|
||||
document.getElementById('confirm-import').addEventListener('click', () => {
|
||||
const data = document.getElementById('import-data').value;
|
||||
if (data) {
|
||||
this.loadFromData(data);
|
||||
const ok = this.importSlotFromData(data, this.importSlot);
|
||||
UI.hideImportModal();
|
||||
UI.showNotification('Save imported!', 'success');
|
||||
if (ok) {
|
||||
// If the import landed in the active slot, switchSlot already re-rendered.
|
||||
UI.update();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -116,6 +130,37 @@ const Game = {
|
||||
}
|
||||
});
|
||||
|
||||
// Save Slot System: slot selection buttons.
|
||||
document.querySelectorAll('.slot-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
if (this.state.activeSlot === parseInt(btn.dataset.slot, 10)) {
|
||||
UI.showNotification(`Already on Slot ${parseInt(btn.dataset.slot, 10) + 1}`, 'info');
|
||||
return;
|
||||
}
|
||||
if (confirm(`Switch to Slot ${parseInt(btn.dataset.slot, 10) + 1}?\nUnsaved changes will be saved first.`)) {
|
||||
this.save(); // Persist current slot before switching.
|
||||
this.switchSlot(parseInt(btn.dataset.slot, 10));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Autosave toggle button.
|
||||
document.getElementById('autosave-btn').addEventListener('click', () => {
|
||||
this.state.autoSave = !this.state.autoSave;
|
||||
const btn = document.getElementById('autosave-btn');
|
||||
if (this.state.autoSave) {
|
||||
btn.classList.add('on');
|
||||
btn.textContent = 'ON';
|
||||
UI.showNotification('Autosave enabled', 'success');
|
||||
} else {
|
||||
btn.classList.remove('on');
|
||||
btn.textContent = 'OFF';
|
||||
UI.showNotification('Autosave disabled', 'info');
|
||||
}
|
||||
// Persist the toggle preference immediately.
|
||||
localStorage.setItem('cosmicGardenAutoSave', String(this.state.autoSave));
|
||||
});
|
||||
|
||||
// Tab switching
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
@@ -126,6 +171,14 @@ const Game = {
|
||||
document.getElementById(`${tab}-tab`).classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
// Import slot selector buttons.
|
||||
document.querySelectorAll('[data-import-slot]').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
this.importSlot = parseInt(btn.dataset.importSlot, 10);
|
||||
UI.showNotification(`Importing into Slot ${this.importSlot + 1}`, 'info');
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -480,6 +533,8 @@ const Game = {
|
||||
*/
|
||||
startAutoSave() {
|
||||
setInterval(() => {
|
||||
// Only autosave when the player has enabled it (default on).
|
||||
if (!this.state.autoSave) return;
|
||||
this.save();
|
||||
document.getElementById('last-saved').textContent =
|
||||
`Last saved: ${new Date().toLocaleTimeString()}`;
|
||||
@@ -487,22 +542,46 @@ const Game = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Save game state
|
||||
* localStorage key for a given slot's save data.
|
||||
*/
|
||||
getSaveKey(slot) {
|
||||
return `cosmicGardenSave-${slot}`;
|
||||
},
|
||||
|
||||
/**
|
||||
* The key holding the selected slot index across page loads.
|
||||
*/
|
||||
getActiveSlotKey() {
|
||||
return 'cosmicGardenActiveSlot';
|
||||
},
|
||||
|
||||
/**
|
||||
* Save game state to the currently active slot.
|
||||
*/
|
||||
save() {
|
||||
this.state.lastSaveTime = Date.now();
|
||||
localStorage.setItem('cosmicGardenSave', JSON.stringify(this.state));
|
||||
localStorage.setItem(this.getSaveKey(this.state.activeSlot), JSON.stringify(this.state));
|
||||
},
|
||||
|
||||
/**
|
||||
* Load game state
|
||||
*/
|
||||
loadState() {
|
||||
const saved = localStorage.getItem('cosmicGardenSave');
|
||||
// Restore the persisted selected-slot index before reading its data.
|
||||
const persistedSlot = localStorage.getItem(this.getActiveSlotKey());
|
||||
if (persistedSlot !== null) {
|
||||
this.state.activeSlot = parseInt(persistedSlot, 10) || 0;
|
||||
}
|
||||
|
||||
const saved = localStorage.getItem(this.getSaveKey(this.state.activeSlot));
|
||||
if (saved) {
|
||||
try {
|
||||
const parsed = JSON.parse(saved);
|
||||
this.state = { ...this.state, ...parsed };
|
||||
// The explicitly-persisted slot selection always wins.
|
||||
if (persistedSlot !== null) {
|
||||
this.state.activeSlot = parseInt(persistedSlot, 10) || 0;
|
||||
}
|
||||
|
||||
// Calculate offline progress
|
||||
const offlineTime = (Date.now() - this.state.lastSaveTime) / 1000;
|
||||
@@ -543,6 +622,117 @@ const Game = {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Load the data stored in a specific slot index (without changing it).
|
||||
* Returns the parsed state, or null if that slot is empty.
|
||||
*/
|
||||
loadSlot(slot) {
|
||||
const raw = localStorage.getItem(this.getSaveKey(slot));
|
||||
if (!raw) return null;
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch (e) {
|
||||
console.error(`Failed to read slot ${slot}:`, e);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Switch the active save slot. Persists the selection and reloads that
|
||||
* slot's data from localStorage into the live game state, preserving
|
||||
* prestige-related counters that should carry across slots only when
|
||||
* present in the target slot.
|
||||
*/
|
||||
switchSlot(slot) {
|
||||
slot = ((parseInt(slot, 10) || 0) + this.config.SLOTS) % this.config.SLOTS;
|
||||
|
||||
// Persist the selection first so loadState() reads it back.
|
||||
localStorage.setItem(this.getActiveSlotKey(), String(slot));
|
||||
this.state.activeSlot = slot;
|
||||
|
||||
const data = this.loadSlot(slot);
|
||||
if (data) {
|
||||
try {
|
||||
const parsed = JSON.parse(JSON.stringify(data));
|
||||
this.state = { ...this.state, ...parsed };
|
||||
// Ensure the slot index stays consistent.
|
||||
this.state.activeSlot = slot;
|
||||
|
||||
UI.showNotification(
|
||||
`Saved to Slot ${slot + 1}!`, 'success'
|
||||
);
|
||||
} catch (e) {
|
||||
console.error('Failed to load selected slot:', e);
|
||||
UI.showNotification('Could not load that slot!', 'warning');
|
||||
}
|
||||
} else {
|
||||
// Target slot is empty: reset to a fresh start but keep essence.
|
||||
this.state = {
|
||||
stardust: 0,
|
||||
totalStardust: 0,
|
||||
totalClicks: 0,
|
||||
clickPower: 1,
|
||||
startTime: Date.now(),
|
||||
lastSaveTime: Date.now(),
|
||||
buildings: {},
|
||||
upgrades: [],
|
||||
achievements: [],
|
||||
stellarEssence: this.state.stellarEssence || 0,
|
||||
prestigeCount: this.state.prestigeCount || 0,
|
||||
totalPrestigeEssence: this.state.totalPrestigeEssence || 0,
|
||||
prestigeUpgrades: this.state.prestigeUpgrades || [],
|
||||
goldenBlooms: { active: false, timer: 0, lastSpawn: Date.now(), totalClaimed: 0 },
|
||||
activeSlot: slot,
|
||||
autoSave: this.state.autoSave
|
||||
};
|
||||
UI.showNotification(
|
||||
`Empty Slot ${slot + 1} - new game started!`, 'success'
|
||||
);
|
||||
}
|
||||
|
||||
// Re-render dynamic UI and refresh the golden-bloom badge.
|
||||
UI.init();
|
||||
UI.update();
|
||||
},
|
||||
|
||||
/**
|
||||
* Export a single slot's save as base64 (for backup or transfer).
|
||||
*/
|
||||
exportSlotToBase64(slot) {
|
||||
const data = this.loadSlot(slot);
|
||||
if (!data) {
|
||||
UI.showNotification(`Slot ${slot + 1} is empty!`, 'warning');
|
||||
return null;
|
||||
}
|
||||
const encoded = btoa(JSON.stringify(data));
|
||||
navigator.clipboard.writeText(encoded).then(() => {
|
||||
UI.showNotification(`Slot ${slot + 1} copied to clipboard!`, 'success');
|
||||
});
|
||||
return encoded;
|
||||
},
|
||||
|
||||
/**
|
||||
* Import base64 save data into a chosen slot.
|
||||
*/
|
||||
importSlotFromData(data, slot) {
|
||||
try {
|
||||
const parsed = JSON.parse(atob(data));
|
||||
localStorage.setItem(this.getSaveKey(slot), JSON.stringify(parsed));
|
||||
UI.showNotification(`Imported into Slot ${slot + 1}!`, 'success');
|
||||
|
||||
// If we imported into the active slot, reload it.
|
||||
if (parseInt(slot, 10) === this.state.activeSlot) {
|
||||
this.switchSlot(slot);
|
||||
} else {
|
||||
UI.update();
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
UI.showNotification('Invalid save data!', 'warning');
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Export save as base64
|
||||
*/
|
||||
@@ -572,7 +762,10 @@ const Game = {
|
||||
stellarEssence: 0,
|
||||
prestigeCount: 0,
|
||||
totalPrestigeEssence: 0,
|
||||
prestigeUpgrades: []
|
||||
prestigeUpgrades: [],
|
||||
goldenBlooms: { active: false, timer: 0, lastSpawn: Date.now(), totalClaimed: 0 },
|
||||
activeSlot: this.state.activeSlot,
|
||||
autoSave: this.state.autoSave
|
||||
};
|
||||
UI.update();
|
||||
UI.showNotification('Game reset!', 'warning');
|
||||
|
||||
+24
@@ -75,6 +75,16 @@ const Stats = {
|
||||
<div class="stat-bar-fill" id="stat-progress-bar" style="width: 0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Save Slot System -->
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">Active Save Slot</div>
|
||||
<div class="stat-value" id="stat-active-slot">Slot 1 / ${Game.config.SLOTS}</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">Autosave</div>
|
||||
<div class="stat-value" id="stat-autosave">ON</div>
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
|
||||
@@ -132,6 +142,20 @@ const Stats = {
|
||||
achievementsEl.textContent = `${Achievements.getCount()}/${Achievements.getTotal()}`;
|
||||
}
|
||||
|
||||
// Active save slot display
|
||||
const activeSlotEl = document.getElementById('stat-active-slot');
|
||||
if (activeSlotEl) {
|
||||
const idx = Game.state.activeSlot || 0;
|
||||
activeSlotEl.textContent = `Slot ${idx + 1} / ${Game.config.SLOTS}`;
|
||||
}
|
||||
|
||||
// Autosave status display
|
||||
const autosaveEl = document.getElementById('stat-autosave');
|
||||
if (autosaveEl) {
|
||||
autosaveEl.textContent = Game.state.autoSave ? 'ON' : 'OFF';
|
||||
autosaveEl.style.color = Game.state.autoSave ? 'var(--accent-green)' : 'var(--text-muted)';
|
||||
}
|
||||
|
||||
// Total Golden Blooms claimed
|
||||
const goldenBloomsEl = document.getElementById('stat-golden-blooms');
|
||||
if (goldenBloomsEl) {
|
||||
|
||||
@@ -11,6 +11,7 @@ const UI = {
|
||||
this.renderBuildings();
|
||||
this.renderUpgrades();
|
||||
this.renderAchievements();
|
||||
this.updateSlotUI();
|
||||
this.update();
|
||||
},
|
||||
|
||||
@@ -41,6 +42,9 @@ const UI = {
|
||||
|
||||
// Golden Starbloom countdown badge
|
||||
this.updateGoldenBloomBadge();
|
||||
|
||||
// Save Slot System highlight / toggle state
|
||||
this.updateSlotUI();
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -297,6 +301,30 @@ const UI = {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Reflect the active save slot highlight and autosave toggle state.
|
||||
*/
|
||||
updateSlotUI() {
|
||||
// Highlight the selected slot button.
|
||||
document.querySelectorAll('.slot-btn').forEach(btn => {
|
||||
const idx = parseInt(btn.dataset.slot, 10);
|
||||
btn.classList.toggle('active', idx === Game.state.activeSlot);
|
||||
btn.disabled = idx === Game.state.activeSlot;
|
||||
});
|
||||
|
||||
// Reflect the autosave toggle state.
|
||||
const autosaveBtn = document.getElementById('autosave-btn');
|
||||
if (autosaveBtn) {
|
||||
if (Game.state.autoSave) {
|
||||
autosaveBtn.classList.add('on');
|
||||
autosaveBtn.textContent = 'ON';
|
||||
} else {
|
||||
autosaveBtn.classList.remove('on');
|
||||
autosaveBtn.textContent = 'OFF';
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Show floating text animation
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user