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:
@@ -0,0 +1,188 @@
|
||||
/**
|
||||
* Cosmic Garden - Achievement Definitions & Checker
|
||||
*/
|
||||
|
||||
const ACHIEVEMENTS = {
|
||||
// Click achievements
|
||||
firstClick: {
|
||||
id: 'firstClick',
|
||||
name: 'First Contact',
|
||||
description: 'Click for the first time',
|
||||
icon: '👆',
|
||||
condition: (state) => state.totalClicks >= 1
|
||||
},
|
||||
|
||||
clicker: {
|
||||
id: 'clicker',
|
||||
name: 'Clicker',
|
||||
description: 'Click 100 times',
|
||||
icon: '🖱️',
|
||||
condition: (state) => state.totalClicks >= 100
|
||||
},
|
||||
|
||||
spammer: {
|
||||
id: 'spammer',
|
||||
name: 'Spam Finger',
|
||||
description: 'Click 1,000 times',
|
||||
icon: '🔥',
|
||||
condition: (state) => state.totalClicks >= 1000
|
||||
},
|
||||
|
||||
clickMaster: {
|
||||
id: 'clickMaster',
|
||||
name: 'Click Master',
|
||||
description: 'Click 10,000 times',
|
||||
icon: '⚡',
|
||||
condition: (state) => state.totalClicks >= 10000
|
||||
},
|
||||
|
||||
// Stardust achievements
|
||||
dustCollector: {
|
||||
id: 'dustCollector',
|
||||
name: 'Dust Collector',
|
||||
description: 'Earn 1,000 total stardust',
|
||||
icon: '✨',
|
||||
condition: (state) => state.totalStardust >= 1000
|
||||
},
|
||||
|
||||
stardustMagnate: {
|
||||
id: 'stardustMagnate',
|
||||
name: 'Stardust Magnate',
|
||||
description: 'Earn 100,000 total stardust',
|
||||
icon: '💫',
|
||||
condition: (state) => state.totalStardust >= 100000
|
||||
},
|
||||
|
||||
cosmicTycoon: {
|
||||
id: 'cosmicTycoon',
|
||||
name: 'Cosmic Tycoon',
|
||||
description: 'Earn 10,000,000 total stardust',
|
||||
icon: '🌟',
|
||||
condition: (state) => state.totalStardust >= 10000000
|
||||
},
|
||||
|
||||
galaxyBaron: {
|
||||
id: 'galaxyBaron',
|
||||
name: 'Galaxy Baron',
|
||||
description: 'Earn 1,000,000,000 total stardust',
|
||||
icon: '👑',
|
||||
condition: (state) => state.totalStardust >= 1000000000
|
||||
},
|
||||
|
||||
// Building achievements
|
||||
firstPlant: {
|
||||
id: 'firstPlant',
|
||||
name: 'First Plant',
|
||||
description: 'Buy your first building',
|
||||
icon: '🌱',
|
||||
condition: (state) => Object.values(state.buildings).some(c => c >= 1)
|
||||
},
|
||||
|
||||
gardenStart: {
|
||||
id: 'gardenStart',
|
||||
name: 'Garden Starter',
|
||||
description: 'Own 10 total buildings',
|
||||
icon: '🌿',
|
||||
condition: (state) => Object.values(state.buildings).reduce((a, b) => a + b, 0) >= 10
|
||||
},
|
||||
|
||||
gardenCenter: {
|
||||
id: 'gardenCenter',
|
||||
name: 'Garden Center',
|
||||
description: 'Own 50 total buildings',
|
||||
icon: '🏡',
|
||||
condition: (state) => Object.values(state.buildings).reduce((a, b) => a + b, 0) >= 50
|
||||
},
|
||||
|
||||
cosmicGarden: {
|
||||
id: 'cosmicGarden',
|
||||
name: 'Cosmic Garden',
|
||||
description: 'Own 100 total buildings',
|
||||
icon: '🌌',
|
||||
condition: (state) => Object.values(state.buildings).reduce((a, b) => a + b, 0) >= 100
|
||||
},
|
||||
|
||||
// Specific building achievements
|
||||
sproutFarmer: {
|
||||
id: 'sproutFarmer',
|
||||
name: 'Sprout Farmer',
|
||||
description: 'Own 25 Star Sprouts',
|
||||
icon: '🌱',
|
||||
condition: (state) => (state.buildings.starSprout || 0) >= 25
|
||||
},
|
||||
|
||||
bloomGarden: {
|
||||
id: 'bloomGarden',
|
||||
name: 'Bloom Garden',
|
||||
description: 'Own 25 Moon Blooms',
|
||||
icon: '🌸',
|
||||
condition: (state) => (state.buildings.moonBloom || 0) >= 25
|
||||
},
|
||||
|
||||
solarPowered: {
|
||||
id: 'solarPowered',
|
||||
name: 'Solar Powered',
|
||||
description: 'Own 25 Solar Leaves',
|
||||
icon: '☀️',
|
||||
condition: (state) => (state.buildings.solarLeaf || 0) >= 25
|
||||
},
|
||||
|
||||
// Production achievements
|
||||
steadyStream: {
|
||||
id: 'steadyStream',
|
||||
name: 'Steady Stream',
|
||||
description: 'Reach 100 stardust/sec',
|
||||
icon: '💧',
|
||||
condition: (state) => Game.getProductionPerSecond() >= 100
|
||||
},
|
||||
|
||||
rushingRiver: {
|
||||
id: 'rushingRiver',
|
||||
name: 'Rushing River',
|
||||
description: 'Reach 1,000 stardust/sec',
|
||||
icon: '🌊',
|
||||
condition: (state) => Game.getProductionPerSecond() >= 1000
|
||||
},
|
||||
|
||||
cosmicFlood: {
|
||||
id: 'cosmicFlood',
|
||||
name: 'Cosmic Flood',
|
||||
description: 'Reach 100,000 stardust/sec',
|
||||
icon: '🌊',
|
||||
condition: (state) => Game.getProductionPerSecond() >= 100000
|
||||
}
|
||||
};
|
||||
|
||||
const Achievements = {
|
||||
/**
|
||||
* Check all achievements and unlock new ones
|
||||
*/
|
||||
check() {
|
||||
for (const [id, achievement] of Object.entries(ACHIEVEMENTS)) {
|
||||
if (Game.state.achievements.includes(id)) continue;
|
||||
|
||||
if (achievement.condition(Game.state)) {
|
||||
Game.state.achievements.push(id);
|
||||
UI.showNotification(`Achievement: ${achievement.name}!`, 'achievement');
|
||||
console.log(`🏆 Achievement unlocked: ${achievement.name}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get achievement count
|
||||
*/
|
||||
getCount() {
|
||||
return Game.state.achievements.length;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get total achievements
|
||||
*/
|
||||
getTotal() {
|
||||
return Object.keys(ACHIEVEMENTS).length;
|
||||
}
|
||||
};
|
||||
|
||||
window.ACHIEVEMENTS = ACHIEVEMENTS;
|
||||
window.Achievements = Achievements;
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* Cosmic Garden - Building Definitions
|
||||
* All purchasable buildings that generate stardust
|
||||
*/
|
||||
|
||||
const BUILDINGS = {
|
||||
// Tier 1 - Basic plants
|
||||
starSprout: {
|
||||
id: 'starSprout',
|
||||
name: 'Star Sprout',
|
||||
icon: '🌱',
|
||||
description: 'A tiny seed that glows softly',
|
||||
baseCost: 15,
|
||||
baseProduction: 0.1,
|
||||
unlockAt: 0
|
||||
},
|
||||
|
||||
moonBloom: {
|
||||
id: 'moonBloom',
|
||||
name: 'Moon Bloom',
|
||||
icon: '🌸',
|
||||
description: 'Flowers that only open under moonlight',
|
||||
baseCost: 100,
|
||||
baseProduction: 0.5,
|
||||
unlockAt: 50
|
||||
},
|
||||
|
||||
solarLeaf: {
|
||||
id: 'solarLeaf',
|
||||
name: 'Solar Leaf',
|
||||
icon: '☀️',
|
||||
description: 'Absorbs sunlight to create stardust',
|
||||
baseCost: 500,
|
||||
baseProduction: 2,
|
||||
unlockAt: 200
|
||||
},
|
||||
|
||||
// Tier 2 - Advanced plants
|
||||
nebulaVine: {
|
||||
id: 'nebulaVine',
|
||||
name: 'Nebula Vine',
|
||||
icon: '🌿',
|
||||
description: 'Wraps around cosmic dust clouds',
|
||||
baseCost: 2000,
|
||||
baseProduction: 10,
|
||||
unlockAt: 1000
|
||||
},
|
||||
|
||||
cometRose: {
|
||||
id: 'cometRose',
|
||||
name: 'Comet Rose',
|
||||
icon: '🌹',
|
||||
description: 'Blooms for just a second as it flies',
|
||||
baseCost: 10000,
|
||||
baseProduction: 50,
|
||||
unlockAt: 5000
|
||||
},
|
||||
|
||||
asteroidCactus: {
|
||||
id: 'asteroidCactus',
|
||||
name: 'Asteroid Cactus',
|
||||
icon: '🌵',
|
||||
description: 'Survives in the harshest space conditions',
|
||||
baseCost: 50000,
|
||||
baseProduction: 200,
|
||||
unlockAt: 25000
|
||||
},
|
||||
|
||||
// Tier 3 - Rare plants
|
||||
pulsarPalm: {
|
||||
id: 'pulsarPalm',
|
||||
name: 'Pulsar Palm',
|
||||
icon: '🌴',
|
||||
description: 'Rhythmic pulses of pure energy',
|
||||
baseCost: 250000,
|
||||
baseProduction: 1000,
|
||||
unlockAt: 100000
|
||||
},
|
||||
|
||||
blackholeOrchid: {
|
||||
id: 'blackholeOrchid',
|
||||
name: 'Black Hole Orchid',
|
||||
icon: '🪻',
|
||||
description: 'Bends light itself to grow',
|
||||
baseCost: 1000000,
|
||||
baseProduction: 5000,
|
||||
unlockAt: 500000
|
||||
},
|
||||
|
||||
galaxyTree: {
|
||||
id: 'galaxyTree',
|
||||
name: 'Galaxy Tree',
|
||||
icon: '🌳',
|
||||
description: 'Contains an entire galaxy within its branches',
|
||||
baseCost: 5000000,
|
||||
baseProduction: 25000,
|
||||
unlockAt: 2000000
|
||||
},
|
||||
|
||||
// Tier 4 - Legendary plants
|
||||
cosmicLotus: {
|
||||
id: 'cosmicLotus',
|
||||
name: 'Cosmic Lotus',
|
||||
icon: '🪷',
|
||||
description: 'Blooms at the center of creation',
|
||||
baseCost: 25000000,
|
||||
baseProduction: 100000,
|
||||
unlockAt: 10000000
|
||||
},
|
||||
|
||||
supernovaTulip: {
|
||||
id: 'supernovaTulip',
|
||||
name: 'Supernova Tulip',
|
||||
icon: '🌺',
|
||||
description: 'Explodes with stardust every cycle',
|
||||
baseCost: 100000000,
|
||||
baseProduction: 500000,
|
||||
unlockAt: 50000000
|
||||
},
|
||||
|
||||
voidFern: {
|
||||
id: 'voidFern',
|
||||
name: 'Void Fern',
|
||||
icon: '🍃',
|
||||
description: 'Grows in the spaces between dimensions',
|
||||
baseCost: 500000000,
|
||||
baseProduction: 2500000,
|
||||
unlockAt: 200000000
|
||||
}
|
||||
};
|
||||
+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;
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Cosmic Garden - Main Entry Point
|
||||
*/
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
Game.init();
|
||||
|
||||
// Show last save time
|
||||
const lastSave = SaveSystem.getLastSaveTime();
|
||||
if (lastSave) {
|
||||
document.getElementById('last-saved').textContent =
|
||||
`Last saved: ${lastSave.toLocaleTimeString()}`;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Cosmic Garden - Save System
|
||||
* Handles localStorage persistence and export/import
|
||||
*/
|
||||
|
||||
const SaveSystem = {
|
||||
SAVE_KEY: 'cosmicGardenSave',
|
||||
|
||||
/**
|
||||
* Save game to localStorage
|
||||
*/
|
||||
save() {
|
||||
const data = JSON.stringify(Game.state);
|
||||
localStorage.setItem(this.SAVE_KEY, data);
|
||||
console.log('💾 Game saved');
|
||||
},
|
||||
|
||||
/**
|
||||
* Load game from localStorage
|
||||
*/
|
||||
load() {
|
||||
const data = localStorage.getItem(this.SAVE_KEY);
|
||||
if (data) {
|
||||
try {
|
||||
return JSON.parse(data);
|
||||
} catch (e) {
|
||||
console.error('Failed to parse save:', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Export save as base64 string
|
||||
*/
|
||||
exportSave() {
|
||||
const data = btoa(JSON.stringify(Game.state));
|
||||
navigator.clipboard.writeText(data).then(() => {
|
||||
UI.showNotification('Save copied to clipboard!', 'success');
|
||||
}).catch(() => {
|
||||
// Fallback for older browsers
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = data;
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textarea);
|
||||
UI.showNotification('Save copied to clipboard!', 'success');
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Import save from base64 string
|
||||
*/
|
||||
importSave(data) {
|
||||
try {
|
||||
const parsed = JSON.parse(atob(data));
|
||||
|
||||
// Validate save data
|
||||
if (!parsed.stardust || !parsed.buildings) {
|
||||
throw new Error('Invalid save format');
|
||||
}
|
||||
|
||||
// Merge with existing state
|
||||
Game.state = { ...Game.state, ...parsed };
|
||||
Game.state.lastSaveTime = Date.now();
|
||||
|
||||
UI.update();
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error('Failed to import save:', e);
|
||||
UI.showNotification('Invalid save data!', 'warning');
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear save data
|
||||
*/
|
||||
clear() {
|
||||
localStorage.removeItem(this.SAVE_KEY);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get last save time
|
||||
*/
|
||||
getLastSaveTime() {
|
||||
const data = this.load();
|
||||
if (data && data.lastSaveTime) {
|
||||
return new Date(data.lastSaveTime);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if auto-save is available
|
||||
*/
|
||||
hasAutoSave() {
|
||||
return !!localStorage.getItem(this.SAVE_KEY);
|
||||
}
|
||||
};
|
||||
|
||||
window.SaveSystem = SaveSystem;
|
||||
@@ -0,0 +1,253 @@
|
||||
/**
|
||||
* Cosmic Garden - UI Manager
|
||||
* Handles all rendering and user interface updates
|
||||
*/
|
||||
|
||||
const UI = {
|
||||
/**
|
||||
* Initialize the UI
|
||||
*/
|
||||
init() {
|
||||
this.renderBuildings();
|
||||
this.renderUpgrades();
|
||||
this.renderAchievements();
|
||||
this.update();
|
||||
},
|
||||
|
||||
/**
|
||||
* Update all UI elements
|
||||
*/
|
||||
update() {
|
||||
// Header
|
||||
document.getElementById('stardust-count').textContent = Game.formatNumber(Game.state.stardust);
|
||||
document.getElementById('stardust-ps').textContent = Game.formatNumber(Game.getProductionPerSecond());
|
||||
document.getElementById('click-power').textContent = Game.formatNumber(Game.getClickPower());
|
||||
document.getElementById('total-click-count').textContent = Game.formatNumber(Game.state.totalClicks);
|
||||
|
||||
// Buildings
|
||||
this.updateBuildings();
|
||||
|
||||
// Upgrades
|
||||
this.updateUpgrades();
|
||||
|
||||
// Achievements
|
||||
this.updateAchievements();
|
||||
},
|
||||
|
||||
/**
|
||||
* Render building cards
|
||||
*/
|
||||
renderBuildings() {
|
||||
const container = document.getElementById('buildings-list');
|
||||
container.innerHTML = '';
|
||||
|
||||
for (const [id, building] of Object.entries(BUILDINGS)) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'building-card';
|
||||
card.id = `building-${id}`;
|
||||
|
||||
const count = Game.state.buildings[id] || 0;
|
||||
const cost = Game.getBuildingCost(id);
|
||||
const canAfford = Game.state.stardust >= cost;
|
||||
const locked = Game.state.totalStardust < building.unlockAt && count === 0;
|
||||
|
||||
card.classList.toggle('locked', locked);
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="building-info">
|
||||
<h4>${building.icon} ${building.name}</h4>
|
||||
<div class="building-desc">${building.description}</div>
|
||||
<div class="building-production">+${Game.formatNumber(building.baseProduction)}/s each</div>
|
||||
</div>
|
||||
<div class="building-stats">
|
||||
<div class="building-cost">${Game.formatNumber(cost)} ✨</div>
|
||||
<div class="building-count">${count}</div>
|
||||
<button class="buy-btn" ${!canAfford || locked ? 'disabled' : ''}>Buy</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (!locked) {
|
||||
card.querySelector('.buy-btn').addEventListener('click', () => {
|
||||
Game.buyBuilding(id);
|
||||
});
|
||||
}
|
||||
|
||||
container.appendChild(card);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update building states
|
||||
*/
|
||||
updateBuildings() {
|
||||
for (const [id, building] of Object.entries(BUILDINGS)) {
|
||||
const card = document.getElementById(`building-${id}`);
|
||||
if (!card) continue;
|
||||
|
||||
const count = Game.state.buildings[id] || 0;
|
||||
const cost = Game.getBuildingCost(id);
|
||||
const canAfford = Game.state.stardust >= cost;
|
||||
const locked = Game.state.totalStardust < building.unlockAt && count === 0;
|
||||
|
||||
card.querySelector('.building-cost').textContent = `${Game.formatNumber(cost)} ✨`;
|
||||
card.querySelector('.building-count').textContent = count;
|
||||
|
||||
const btn = card.querySelector('.buy-btn');
|
||||
if (btn) {
|
||||
btn.disabled = !canAfford || locked;
|
||||
}
|
||||
|
||||
card.classList.toggle('locked', locked);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Render upgrade cards
|
||||
*/
|
||||
renderUpgrades() {
|
||||
const container = document.getElementById('upgrades-list');
|
||||
container.innerHTML = '';
|
||||
|
||||
for (const [id, upgrade] of Object.entries(UPGRADES)) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'upgrade-card';
|
||||
card.id = `upgrade-${id}`;
|
||||
|
||||
const purchased = Game.state.upgrades.includes(id);
|
||||
const canAfford = Game.state.stardust >= upgrade.cost;
|
||||
|
||||
// Check requirements
|
||||
let requirementsMet = true;
|
||||
let reqText = '';
|
||||
if (upgrade.requirements) {
|
||||
const reqParts = [];
|
||||
for (const [reqId, reqCount] of Object.entries(upgrade.requirements)) {
|
||||
const currentCount = Game.state.buildings[reqId] || 0;
|
||||
const building = BUILDINGS[reqId];
|
||||
if (currentCount < reqCount) requirementsMet = false;
|
||||
reqParts.push(`${building?.name || reqId}: ${currentCount}/${reqCount}`);
|
||||
}
|
||||
reqText = `<div class="upgrade-desc">Requires: ${reqParts.join(', ')}</div>`;
|
||||
}
|
||||
|
||||
card.classList.toggle('purchased', purchased);
|
||||
card.classList.toggle('locked', !requirementsMet && !purchased);
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="upgrade-name">${upgrade.icon} ${upgrade.name}</div>
|
||||
<div class="upgrade-desc">${upgrade.description}</div>
|
||||
${reqText}
|
||||
<div class="upgrade-cost">${purchased ? '✓ Owned' : `${Game.formatNumber(upgrade.cost)} ✨`}</div>
|
||||
`;
|
||||
|
||||
if (!purchased && requirementsMet) {
|
||||
card.addEventListener('click', () => {
|
||||
Game.buyUpgrade(id);
|
||||
});
|
||||
}
|
||||
|
||||
container.appendChild(card);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update upgrade states
|
||||
*/
|
||||
updateUpgrades() {
|
||||
for (const [id, upgrade] of Object.entries(UPGRADES)) {
|
||||
const card = document.getElementById(`upgrade-${id}`);
|
||||
if (!card) continue;
|
||||
|
||||
const purchased = Game.state.upgrades.includes(id);
|
||||
const costEl = card.querySelector('.upgrade-cost');
|
||||
|
||||
if (costEl && !purchased) {
|
||||
const canAfford = Game.state.stardust >= upgrade.cost;
|
||||
costEl.textContent = canAfford ? `${Game.formatNumber(upgrade.cost)} ✨` : `❌ ${Game.formatNumber(upgrade.cost)} ✨`;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Render achievements
|
||||
*/
|
||||
renderAchievements() {
|
||||
const container = document.getElementById('achievements-list');
|
||||
container.innerHTML = '';
|
||||
|
||||
for (const [id, achievement] of Object.entries(ACHIEVEMENTS)) {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'achievement';
|
||||
|
||||
const unlocked = Game.state.achievements.includes(id);
|
||||
el.classList.toggle('unlocked', unlocked);
|
||||
el.classList.toggle('locked', !unlocked);
|
||||
|
||||
el.innerHTML = `
|
||||
<div class="achievement-icon">${unlocked ? achievement.icon : '🔒'}</div>
|
||||
<div class="achievement-name">${unlocked ? achievement.name : '???'}</div>
|
||||
`;
|
||||
|
||||
if (unlocked) {
|
||||
el.title = `${achievement.name}: ${achievement.description}`;
|
||||
}
|
||||
|
||||
container.appendChild(el);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update achievements display
|
||||
*/
|
||||
updateAchievements() {
|
||||
// Only re-render when needed (on new unlocks)
|
||||
const countEl = document.querySelector('#achievements-tab h3');
|
||||
if (countEl) {
|
||||
countEl.textContent = `Achievements (${Achievements.getCount()}/${Achievements.getTotal()})`;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Show floating text animation
|
||||
*/
|
||||
showFloatingText(x, y, text) {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'float-text';
|
||||
el.textContent = text;
|
||||
el.style.left = `${x}px`;
|
||||
el.style.top = `${y - 20}px`;
|
||||
document.body.appendChild(el);
|
||||
|
||||
setTimeout(() => el.remove(), 1000);
|
||||
},
|
||||
|
||||
/**
|
||||
* Show notification
|
||||
*/
|
||||
showNotification(message, type = 'info') {
|
||||
const container = document.getElementById('notifications');
|
||||
const el = document.createElement('div');
|
||||
el.className = `notification ${type}`;
|
||||
el.textContent = message;
|
||||
container.appendChild(el);
|
||||
|
||||
setTimeout(() => el.remove(), 3000);
|
||||
},
|
||||
|
||||
/**
|
||||
* Show import modal
|
||||
*/
|
||||
showImportModal() {
|
||||
document.getElementById('import-modal').classList.remove('hidden');
|
||||
},
|
||||
|
||||
/**
|
||||
* Hide import modal
|
||||
*/
|
||||
hideImportModal() {
|
||||
document.getElementById('import-modal').classList.add('hidden');
|
||||
document.getElementById('import-data').value = '';
|
||||
}
|
||||
};
|
||||
|
||||
window.UI = UI;
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* Cosmic Garden - Upgrade Definitions
|
||||
* One-time purchases that boost production
|
||||
*/
|
||||
|
||||
const UPGRADES = {
|
||||
// Click upgrades
|
||||
ironFingers: {
|
||||
id: 'ironFingers',
|
||||
name: 'Iron Fingers',
|
||||
description: 'Click power x2',
|
||||
icon: '👆',
|
||||
cost: 100,
|
||||
clickPowerBonus: 2,
|
||||
requirements: {}
|
||||
},
|
||||
|
||||
cosmicTouch: {
|
||||
id: 'cosmicTouch',
|
||||
name: 'Cosmic Touch',
|
||||
description: 'Click power x3',
|
||||
icon: '✨',
|
||||
cost: 1000,
|
||||
clickPowerBonus: 3,
|
||||
requirements: { starSprout: 10 }
|
||||
},
|
||||
|
||||
stellarHands: {
|
||||
id: 'stellarHands',
|
||||
name: 'Stellar Hands',
|
||||
description: 'Click power x5',
|
||||
icon: '🌟',
|
||||
cost: 10000,
|
||||
clickPowerBonus: 5,
|
||||
requirements: { moonBloom: 10 }
|
||||
},
|
||||
|
||||
// Building upgrades - Star Sprout
|
||||
sproutBoost1: {
|
||||
id: 'sproutBoost1',
|
||||
name: 'Sprout Nutrition',
|
||||
description: 'Star Sprouts produce x2',
|
||||
icon: '🌱',
|
||||
cost: 500,
|
||||
buildingBonus: { starSprout: 2 },
|
||||
requirements: { starSprout: 1 }
|
||||
},
|
||||
|
||||
sproutBoost2: {
|
||||
id: 'sproutBoost2',
|
||||
name: 'Star Fertilizer',
|
||||
description: 'Star Sprouts produce x3',
|
||||
icon: '⭐',
|
||||
cost: 5000,
|
||||
buildingBonus: { starSprout: 3 },
|
||||
requirements: { starSprout: 25 }
|
||||
},
|
||||
|
||||
// Building upgrades - Moon Bloom
|
||||
bloomBoost1: {
|
||||
id: 'bloomBoost1',
|
||||
name: 'Lunar Energy',
|
||||
description: 'Moon Blooms produce x2',
|
||||
icon: '🌙',
|
||||
cost: 2000,
|
||||
buildingBonus: { moonBloom: 2 },
|
||||
requirements: { moonBloom: 1 }
|
||||
},
|
||||
|
||||
bloomBoost2: {
|
||||
id: 'bloomBoost2',
|
||||
name: 'Full Moon Ritual',
|
||||
description: 'Moon Blooms produce x3',
|
||||
icon: '🌕',
|
||||
cost: 20000,
|
||||
buildingBonus: { moonBloom: 3 },
|
||||
requirements: { moonBloom: 25 }
|
||||
},
|
||||
|
||||
// Building upgrades - Solar Leaf
|
||||
leafBoost1: {
|
||||
id: 'leafBoost1',
|
||||
name: 'Photosynthesis Plus',
|
||||
description: 'Solar Leaves produce x2',
|
||||
icon: '☀️',
|
||||
cost: 10000,
|
||||
buildingBonus: { solarLeaf: 2 },
|
||||
requirements: { solarLeaf: 1 }
|
||||
},
|
||||
|
||||
leafBoost2: {
|
||||
id: 'leafBoost2',
|
||||
name: 'Solar Flare',
|
||||
description: 'Solar Leaves produce x3',
|
||||
icon: '💥',
|
||||
cost: 100000,
|
||||
buildingBonus: { solarLeaf: 3 },
|
||||
requirements: { solarLeaf: 25 }
|
||||
},
|
||||
|
||||
// Building upgrades - Nebula Vine
|
||||
vineBoost1: {
|
||||
id: 'vineBoost1',
|
||||
name: 'Cosmic Compost',
|
||||
description: 'Nebula Vines produce x2',
|
||||
icon: '🌿',
|
||||
cost: 50000,
|
||||
buildingBonus: { nebulaVine: 2 },
|
||||
requirements: { nebulaVine: 1 }
|
||||
},
|
||||
|
||||
vineBoost2: {
|
||||
id: 'vineBoost2',
|
||||
name: 'Dust Storm',
|
||||
description: 'Nebula Vines produce x3',
|
||||
icon: '🌪️',
|
||||
cost: 500000,
|
||||
buildingBonus: { nebulaVine: 3 },
|
||||
requirements: { nebulaVine: 25 }
|
||||
},
|
||||
|
||||
// Building upgrades - Comet Rose
|
||||
roseBoost1: {
|
||||
id: 'roseBoost1',
|
||||
name: 'Comet Trail',
|
||||
description: 'Comet Roses produce x2',
|
||||
icon: '🌹',
|
||||
cost: 250000,
|
||||
buildingBonus: { cometRose: 2 },
|
||||
requirements: { cometRose: 1 }
|
||||
},
|
||||
|
||||
roseBoost2: {
|
||||
id: 'roseBoost2',
|
||||
name: 'Perihelion Bloom',
|
||||
description: 'Comet Roses produce x3',
|
||||
icon: '☄️',
|
||||
cost: 2500000,
|
||||
buildingBonus: { cometRose: 3 },
|
||||
requirements: { cometRose: 25 }
|
||||
},
|
||||
|
||||
// Global upgrades
|
||||
cosmicGarden1: {
|
||||
id: 'cosmicGarden1',
|
||||
name: 'Cosmic Garden',
|
||||
description: 'All production x1.5',
|
||||
icon: '🌌',
|
||||
cost: 1000000,
|
||||
globalBonus: 1.5,
|
||||
requirements: { starSprout: 50, moonBloom: 25, solarLeaf: 10 }
|
||||
},
|
||||
|
||||
stellarNursery: {
|
||||
id: 'stellarNursery',
|
||||
name: 'Stellar Nursery',
|
||||
description: 'All production x2',
|
||||
icon: '🌟',
|
||||
cost: 10000000,
|
||||
globalBonus: 2,
|
||||
requirements: { nebulaVine: 25, cometRose: 10 }
|
||||
},
|
||||
|
||||
galacticBotanist: {
|
||||
id: 'galacticBotanist',
|
||||
name: 'Galactic Botanist',
|
||||
description: 'All production x3',
|
||||
icon: '👨🌾',
|
||||
cost: 100000000,
|
||||
globalBonus: 3,
|
||||
requirements: { pulsarPalm: 10, blackholeOrchid: 5 }
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user