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');
|
||||
|
||||
Reference in New Issue
Block a user