Initial commit: Cosmic Garden idle game foundation
- Complete game architecture with modular JS structure - 12 buildings across 4 tiers with scaling costs - 15+ upgrades with requirements and bonuses - 20 achievements to unlock - Auto-save system with offline progress (50% efficiency) - Export/Import save functionality - Responsive dark-themed UI with cosmic aesthetic - Floating text animations and notification system
This commit is contained in:
+345
@@ -0,0 +1,345 @@
|
||||
/**
|
||||
* Cosmic Garden - Main Game Engine
|
||||
* Handles game state, tick loop, and core logic
|
||||
*/
|
||||
|
||||
const Game = {
|
||||
// Core game state
|
||||
state: {
|
||||
stardust: 0,
|
||||
totalStardust: 0,
|
||||
totalClicks: 0,
|
||||
clickPower: 1,
|
||||
startTime: Date.now(),
|
||||
lastSaveTime: Date.now(),
|
||||
buildings: {},
|
||||
upgrades: [],
|
||||
achievements: []
|
||||
},
|
||||
|
||||
// Configuration
|
||||
config: {
|
||||
tickRate: 100, // ms per tick (10 ticks/sec)
|
||||
autoSaveInterval: 30000, // 30 seconds
|
||||
costScaling: 1.15 // Cost multiplier per building level
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize the game
|
||||
*/
|
||||
init() {
|
||||
this.loadState();
|
||||
this.setupEventListeners();
|
||||
this.startGameLoop();
|
||||
this.startAutoSave();
|
||||
UI.init();
|
||||
console.log('🌌 Cosmic Garden initialized!');
|
||||
},
|
||||
|
||||
/**
|
||||
* Setup event listeners
|
||||
*/
|
||||
setupEventListeners() {
|
||||
// Harvest button
|
||||
document.getElementById('harvest-btn').addEventListener('click', (e) => {
|
||||
this.harvest(e);
|
||||
});
|
||||
|
||||
// Save buttons
|
||||
document.getElementById('save-btn').addEventListener('click', () => {
|
||||
this.save();
|
||||
UI.showNotification('Game saved!', 'success');
|
||||
});
|
||||
|
||||
document.getElementById('export-btn').addEventListener('click', () => {
|
||||
SaveSystem.exportSave();
|
||||
});
|
||||
|
||||
document.getElementById('import-btn').addEventListener('click', () => {
|
||||
UI.showImportModal();
|
||||
});
|
||||
|
||||
document.getElementById('confirm-import').addEventListener('click', () => {
|
||||
const data = document.getElementById('import-data').value;
|
||||
if (data) {
|
||||
this.loadFromData(data);
|
||||
UI.hideImportModal();
|
||||
UI.showNotification('Save imported!', 'success');
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('cancel-import').addEventListener('click', () => {
|
||||
UI.hideImportModal();
|
||||
});
|
||||
|
||||
document.getElementById('reset-btn').addEventListener('click', () => {
|
||||
if (confirm('Are you sure you want to reset all progress? This cannot be undone!')) {
|
||||
this.reset();
|
||||
}
|
||||
});
|
||||
|
||||
// Tab switching
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const tab = e.target.dataset.tab;
|
||||
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
||||
e.target.classList.add('active');
|
||||
document.getElementById(`${tab}-tab`).classList.add('active');
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Harvest stardust by clicking
|
||||
*/
|
||||
harvest(event) {
|
||||
const amount = this.getClickPower();
|
||||
this.addStardust(amount);
|
||||
this.state.totalClicks++;
|
||||
|
||||
// Visual feedback
|
||||
UI.showFloatingText(event.clientX, event.clientY, `+${this.formatNumber(amount)}`);
|
||||
UI.update();
|
||||
},
|
||||
|
||||
/**
|
||||
* Add stardust to the game
|
||||
*/
|
||||
addStardust(amount) {
|
||||
this.state.stardust += amount;
|
||||
this.state.totalStardust += amount;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get current click power
|
||||
*/
|
||||
getClickPower() {
|
||||
let power = this.config.baseClickPower || 1;
|
||||
|
||||
// Apply upgrade bonuses
|
||||
for (const upgradeId of this.state.upgrades) {
|
||||
const upgrade = UPGRADES[upgradeId];
|
||||
if (upgrade && upgrade.clickPowerBonus) {
|
||||
power *= upgrade.clickPowerBonus;
|
||||
}
|
||||
}
|
||||
|
||||
return Math.floor(power);
|
||||
},
|
||||
|
||||
/**
|
||||
* Calculate production per second
|
||||
*/
|
||||
getProductionPerSecond() {
|
||||
let total = 0;
|
||||
|
||||
for (const [buildingId, count] of Object.entries(this.state.buildings)) {
|
||||
if (count === 0) continue;
|
||||
|
||||
const building = BUILDINGS[buildingId];
|
||||
if (!building) continue;
|
||||
|
||||
let production = building.baseProduction * count;
|
||||
|
||||
// Apply upgrade bonuses
|
||||
for (const upgradeId of this.state.upgrades) {
|
||||
const upgrade = UPGRADES[upgradeId];
|
||||
if (upgrade && upgrade.buildingBonus && upgrade.buildingBonus[buildingId]) {
|
||||
production *= upgrade.buildingBonus[buildingId];
|
||||
}
|
||||
}
|
||||
|
||||
total += production;
|
||||
}
|
||||
|
||||
return total;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get building cost
|
||||
*/
|
||||
getBuildingCost(buildingId) {
|
||||
const building = BUILDINGS[buildingId];
|
||||
const count = this.state.buildings[buildingId] || 0;
|
||||
return Math.floor(building.baseCost * Math.pow(this.config.costScaling, count));
|
||||
},
|
||||
|
||||
/**
|
||||
* Buy a building
|
||||
*/
|
||||
buyBuilding(buildingId) {
|
||||
const cost = this.getBuildingCost(buildingId);
|
||||
|
||||
if (this.state.stardust >= cost) {
|
||||
this.state.stardust -= cost;
|
||||
this.state.buildings[buildingId] = (this.state.buildings[buildingId] || 0) + 1;
|
||||
|
||||
UI.showNotification(`Bought ${BUILDINGS[buildingId].name}!`, 'success');
|
||||
UI.update();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Buy an upgrade
|
||||
*/
|
||||
buyUpgrade(upgradeId) {
|
||||
const upgrade = UPGRADES[upgradeId];
|
||||
if (!upgrade || this.state.upgrades.includes(upgradeId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.state.stardust >= upgrade.cost) {
|
||||
// Check requirements
|
||||
if (upgrade.requirements) {
|
||||
for (const [reqId, reqCount] of Object.entries(upgrade.requirements)) {
|
||||
const currentCount = this.state.buildings[reqId] || 0;
|
||||
if (currentCount < reqCount) {
|
||||
UI.showNotification('Requirements not met!', 'warning');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.state.stardust -= upgrade.cost;
|
||||
this.state.upgrades.push(upgradeId);
|
||||
|
||||
UI.showNotification(`Unlocked: ${upgrade.name}!`, 'success');
|
||||
UI.update();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Game tick - called every config.tickRate ms
|
||||
*/
|
||||
tick() {
|
||||
const production = this.getProductionPerSecond();
|
||||
const tickSeconds = this.config.tickRate / 1000;
|
||||
|
||||
if (production > 0) {
|
||||
this.addStardust(production * tickSeconds);
|
||||
}
|
||||
|
||||
UI.update();
|
||||
Achievements.check();
|
||||
},
|
||||
|
||||
/**
|
||||
* Start the game loop
|
||||
*/
|
||||
startGameLoop() {
|
||||
setInterval(() => this.tick(), this.config.tickRate);
|
||||
},
|
||||
|
||||
/**
|
||||
* Start auto-save
|
||||
*/
|
||||
startAutoSave() {
|
||||
setInterval(() => {
|
||||
this.save();
|
||||
document.getElementById('last-saved').textContent =
|
||||
`Last saved: ${new Date().toLocaleTimeString()}`;
|
||||
}, this.config.autoSaveInterval);
|
||||
},
|
||||
|
||||
/**
|
||||
* Save game state
|
||||
*/
|
||||
save() {
|
||||
this.state.lastSaveTime = Date.now();
|
||||
localStorage.setItem('cosmicGardenSave', JSON.stringify(this.state));
|
||||
},
|
||||
|
||||
/**
|
||||
* Load game state
|
||||
*/
|
||||
loadState() {
|
||||
const saved = localStorage.getItem('cosmicGardenSave');
|
||||
if (saved) {
|
||||
try {
|
||||
const parsed = JSON.parse(saved);
|
||||
this.state = { ...this.state, ...parsed };
|
||||
|
||||
// Calculate offline progress
|
||||
const offlineTime = (Date.now() - this.state.lastSaveTime) / 1000;
|
||||
if (offlineTime > 60) {
|
||||
const production = this.getProductionPerSecond();
|
||||
const offlineEarnings = production * offlineTime * 0.5; // 50% offline efficiency
|
||||
if (offlineEarnings > 0) {
|
||||
this.addStardust(offlineEarnings);
|
||||
UI.showNotification(
|
||||
`Welcome back! Earned ${this.formatNumber(offlineEarnings)} ✨ while away`,
|
||||
'success'
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load save:', e);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Load from imported data
|
||||
*/
|
||||
loadFromData(data) {
|
||||
try {
|
||||
const parsed = JSON.parse(atob(data));
|
||||
this.state = { ...this.state, ...parsed };
|
||||
UI.update();
|
||||
} catch (e) {
|
||||
UI.showNotification('Invalid save data!', 'warning');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Export save as base64
|
||||
*/
|
||||
exportSave() {
|
||||
this.save();
|
||||
const data = btoa(JSON.stringify(this.state));
|
||||
navigator.clipboard.writeText(data).then(() => {
|
||||
UI.showNotification('Save copied to clipboard!', 'success');
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset game
|
||||
*/
|
||||
reset() {
|
||||
localStorage.removeItem('cosmicGardenSave');
|
||||
this.state = {
|
||||
stardust: 0,
|
||||
totalStardust: 0,
|
||||
totalClicks: 0,
|
||||
clickPower: 1,
|
||||
startTime: Date.now(),
|
||||
lastSaveTime: Date.now(),
|
||||
buildings: {},
|
||||
upgrades: [],
|
||||
achievements: []
|
||||
};
|
||||
UI.update();
|
||||
UI.showNotification('Game reset!', 'warning');
|
||||
},
|
||||
|
||||
/**
|
||||
* Format numbers with commas
|
||||
*/
|
||||
formatNumber(num) {
|
||||
if (num >= 1e12) return (num / 1e12).toFixed(2) + 'T';
|
||||
if (num >= 1e9) return (num / 1e9).toFixed(2) + 'B';
|
||||
if (num >= 1e6) return (num / 1e6).toFixed(2) + 'M';
|
||||
if (num >= 1e3) return (num / 1e3).toFixed(2) + 'K';
|
||||
return Math.floor(num).toLocaleString();
|
||||
}
|
||||
};
|
||||
|
||||
// Make Game globally accessible
|
||||
window.Game = Game;
|
||||
Reference in New Issue
Block a user