FAZA 17: 2.5D Minecraft-Style Terrain + Y-Layer Stacking + Custom Sprites
COMPLETED FEATURES: Custom Sprite Integration: - Player, Zombie, Merchant sprites (0.2 scale) - 11 custom sprites + 5 asset packs loaded - Auto-transparency processing (white/brown removal) - Gravestone system with atlas extraction 2.5D Minecraft-Style Terrain: - Volumetric blocks with 25px thickness - Strong left/right side shading (30%/50% darker) - Minecraft-style texture patterns (grass, dirt, stone) - Crisp black outlines for definition Y-Layer Stacking System: - GRASS_FULL: All green (elevation > 0.7) - GRASS_TOP: Green top + brown sides (elevation 0.4-0.7) - DIRT: All brown (elevation < 0.4) - Dynamic terrain depth based on height Floating Island World Edge: - Stone cliff walls at map borders - 2-tile transition zone - Elevation flattening for cliff drop-off effect - 100x100 world with defined boundaries Performance & Polish: - Canvas renderer for pixel-perfect sharpness - CSS image-rendering: crisp-edges - willReadFrequently optimization - No Canvas2D warnings Technical: - 3D volumetric trees and rocks - Hybrid rendering (2.5D terrain + 2D characters) - Procedural texture generation - Y-layer aware terrain type selection
This commit is contained in:
246
src/systems/SaveSystem.js
Normal file
246
src/systems/SaveSystem.js
Normal file
@@ -0,0 +1,246 @@
|
||||
class SaveSystem {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
this.storageKey = 'novafarma_savefile';
|
||||
}
|
||||
|
||||
saveGame() {
|
||||
console.log('💾 Saving game...');
|
||||
|
||||
if (!this.scene.player || !this.scene.terrainSystem) {
|
||||
console.error('Cannot save: Player or TerrainSystem missing.');
|
||||
return;
|
||||
}
|
||||
|
||||
const playerPos = this.scene.player.getPosition();
|
||||
|
||||
// Zberi podatke o NPCjih
|
||||
const npcsData = this.scene.npcs.map(npc => ({
|
||||
type: npc.type,
|
||||
x: npc.gridX,
|
||||
y: npc.gridY
|
||||
}));
|
||||
|
||||
// Zberi podatke o terenu
|
||||
const terrainSeed = this.scene.terrainSystem.noise.seed;
|
||||
|
||||
// Zberi dinamične podatke terena (Crops & Modified Decor/Buildings)
|
||||
const cropsData = Array.from(this.scene.terrainSystem.cropsMap.entries());
|
||||
// We only save Decorations that are NOT default?
|
||||
// For simplicity, let's save ALL current decorations, and on Load clear everything and rebuild.
|
||||
// Actually, decorationsMap contains objects with gridX, gridY, type.
|
||||
const decorData = Array.from(this.scene.terrainSystem.decorationsMap.values());
|
||||
|
||||
// Inventory
|
||||
const inventoryData = {
|
||||
slots: this.scene.inventorySystem.slots,
|
||||
gold: this.scene.inventorySystem.gold || 0
|
||||
};
|
||||
|
||||
const saveData = {
|
||||
version: 1.1,
|
||||
timestamp: Date.now(),
|
||||
player: { x: playerPos.x, y: playerPos.y },
|
||||
terrain: {
|
||||
seed: terrainSeed,
|
||||
crops: cropsData, // array of [key, value]
|
||||
decorations: decorData // array of objects
|
||||
},
|
||||
npcs: npcsData,
|
||||
inventory: inventoryData,
|
||||
time: {
|
||||
gameTime: this.scene.timeSystem ? this.scene.timeSystem.gameTime : 8,
|
||||
dayCount: this.scene.timeSystem ? this.scene.timeSystem.dayCount : 1
|
||||
},
|
||||
stats: this.scene.statsSystem ? {
|
||||
health: this.scene.statsSystem.health,
|
||||
hunger: this.scene.statsSystem.hunger,
|
||||
thirst: this.scene.statsSystem.thirst
|
||||
} : null,
|
||||
camera: { zoom: this.scene.cameras.main.zoom }
|
||||
};
|
||||
|
||||
try {
|
||||
const jsonString = JSON.stringify(saveData);
|
||||
localStorage.setItem(this.storageKey, jsonString);
|
||||
|
||||
// Pokaži obvestilo (preko UIScene če obstaja)
|
||||
this.showNotification('GAME SAVED');
|
||||
console.log('✅ Game saved successfully!', saveData);
|
||||
} catch (e) {
|
||||
console.error('❌ Failed to save game:', e);
|
||||
this.showNotification('SAVE FAILED');
|
||||
}
|
||||
}
|
||||
|
||||
loadGame() {
|
||||
console.log('📂 Loading game...');
|
||||
|
||||
const jsonString = localStorage.getItem(this.storageKey);
|
||||
if (!jsonString) {
|
||||
console.log('⚠️ No save file found.');
|
||||
this.showNotification('NO SAVE FOUND');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const saveData = JSON.parse(jsonString);
|
||||
console.log('Loading save data:', saveData);
|
||||
|
||||
// 1. Load Player
|
||||
if (this.scene.player) {
|
||||
// Zahteva metodo setPosition(gridX, gridY) v Player.js
|
||||
// Trenutno imamo moveToGrid ampak za instant load rabimo direkten set.
|
||||
// Uporabimo updatePosition logic iz NPC, ali pa kar moveToGrid s hitrostjo 0?
|
||||
// Bolje dodati setGridPosition v Player.js.
|
||||
// Za zdaj workaround:
|
||||
this.scene.player.gridX = saveData.player.x;
|
||||
this.scene.player.gridY = saveData.player.y;
|
||||
// Force update screen pos
|
||||
const screenPos = this.scene.player.iso.toScreen(saveData.player.x, saveData.player.y);
|
||||
this.scene.player.sprite.setPosition(
|
||||
screenPos.x + this.scene.player.offsetX,
|
||||
screenPos.y + this.scene.player.offsetY
|
||||
);
|
||||
this.scene.player.updateDepth();
|
||||
}
|
||||
|
||||
// 2. Load Terrain (Regenerate + Restore dynamic)
|
||||
if (this.scene.terrainSystem && saveData.terrain) {
|
||||
// A) Seed / Base Terrain
|
||||
if (saveData.terrain.seed && this.scene.terrainSystem.noise.seed !== saveData.terrain.seed) {
|
||||
// Regenerate world if seed mismatch
|
||||
// (Actually we might want to ALWAYS regenerate to clear default decors then overwrite?)
|
||||
// Current logic: generate() spawns default decorations.
|
||||
// To handle persistence properly:
|
||||
// 1. Clear current decorations
|
||||
// 2. Load saved decorations
|
||||
|
||||
this.scene.terrainSystem.noise = new PerlinNoise(saveData.terrain.seed);
|
||||
// this.scene.terrainSystem.generate(); // This re-adds random flowers
|
||||
// Instead of full generate, we might just re-calc tiles if seed changed?
|
||||
// For now assume seed is constant for "New Game", but let's re-run generate to be safe
|
||||
}
|
||||
|
||||
// Clear EVERYTHING first
|
||||
this.scene.terrainSystem.decorationsMap.clear();
|
||||
this.scene.terrainSystem.decorations = [];
|
||||
this.scene.terrainSystem.cropsMap.clear();
|
||||
// We should also hide active sprites?
|
||||
this.scene.terrainSystem.visibleDecorations.forEach(s => s.setVisible(false));
|
||||
this.scene.terrainSystem.visibleDecorations.clear();
|
||||
this.scene.terrainSystem.visibleCrops.forEach(s => s.setVisible(false));
|
||||
this.scene.terrainSystem.visibleCrops.clear();
|
||||
this.scene.terrainSystem.decorationPool.releaseAll();
|
||||
this.scene.terrainSystem.cropPool.releaseAll();
|
||||
|
||||
// B) Restore Crops
|
||||
if (saveData.terrain.crops) {
|
||||
// Map was saved as array of entries
|
||||
saveData.terrain.crops.forEach(entry => {
|
||||
const [key, cropData] = entry;
|
||||
this.scene.terrainSystem.cropsMap.set(key, cropData);
|
||||
|
||||
// Set flag on tile
|
||||
const [gx, gy] = key.split(',').map(Number);
|
||||
const tile = this.scene.terrainSystem.getTile(gx, gy);
|
||||
if (tile) tile.hasCrop = true;
|
||||
});
|
||||
}
|
||||
|
||||
// C) Restore Decorations (Flowers, Houses, Walls, Fences...)
|
||||
if (saveData.terrain.decorations) {
|
||||
saveData.terrain.decorations.forEach(d => {
|
||||
this.scene.terrainSystem.decorations.push(d);
|
||||
this.scene.terrainSystem.decorationsMap.set(d.id, d);
|
||||
|
||||
const tile = this.scene.terrainSystem.getTile(d.gridX, d.gridY);
|
||||
if (tile) tile.hasDecoration = true;
|
||||
});
|
||||
}
|
||||
|
||||
// Force Update Visuals
|
||||
this.scene.terrainSystem.updateCulling(this.scene.cameras.main);
|
||||
}
|
||||
|
||||
// 3. Load Inventory
|
||||
if (this.scene.inventorySystem && saveData.inventory) {
|
||||
this.scene.inventorySystem.slots = saveData.inventory.slots;
|
||||
this.scene.inventorySystem.gold = saveData.inventory.gold;
|
||||
this.scene.inventorySystem.updateUI();
|
||||
}
|
||||
|
||||
// 4. Load Time & Stats
|
||||
if (this.scene.timeSystem && saveData.time) {
|
||||
this.scene.timeSystem.gameTime = saveData.time.gameTime;
|
||||
this.scene.timeSystem.dayCount = saveData.time.dayCount || 1;
|
||||
}
|
||||
if (this.scene.statsSystem && saveData.stats) {
|
||||
this.scene.statsSystem.health = saveData.stats.health;
|
||||
this.scene.statsSystem.hunger = saveData.stats.hunger;
|
||||
this.scene.statsSystem.thirst = saveData.stats.thirst;
|
||||
}
|
||||
|
||||
// 3. Load NPCs
|
||||
// Pobriši trenutne
|
||||
this.scene.npcs.forEach(npc => npc.destroy());
|
||||
this.scene.npcs = [];
|
||||
|
||||
// Ustvari shranjene
|
||||
if (saveData.npcs) {
|
||||
saveData.npcs.forEach(npcData => {
|
||||
const npc = new NPC(
|
||||
this.scene,
|
||||
npcData.x,
|
||||
npcData.y,
|
||||
this.scene.terrainOffsetX,
|
||||
this.scene.terrainOffsetY,
|
||||
npcData.type
|
||||
);
|
||||
this.scene.npcs.push(npc);
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Camera
|
||||
if (saveData.camera) {
|
||||
this.scene.cameras.main.setZoom(saveData.camera.zoom);
|
||||
}
|
||||
|
||||
this.showNotification('GAME LOADED');
|
||||
return true;
|
||||
|
||||
} catch (e) {
|
||||
console.error('❌ Failed to load game:', e);
|
||||
this.showNotification('LOAD FAILED');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
showNotification(text) {
|
||||
const uiScene = this.scene.scene.get('UIScene');
|
||||
if (uiScene) {
|
||||
const width = uiScene.cameras.main.width;
|
||||
const height = uiScene.cameras.main.height;
|
||||
|
||||
const msg = uiScene.add.text(width / 2, height / 2, text, {
|
||||
fontFamily: 'Courier New',
|
||||
fontSize: '32px',
|
||||
fill: '#ffffff',
|
||||
backgroundColor: '#000000',
|
||||
padding: { x: 10, y: 5 }
|
||||
});
|
||||
msg.setOrigin(0.5);
|
||||
msg.setScrollFactor(0);
|
||||
|
||||
uiScene.tweens.add({
|
||||
targets: msg,
|
||||
alpha: 0,
|
||||
duration: 2000,
|
||||
delay: 500,
|
||||
onComplete: () => {
|
||||
msg.destroy();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user