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:
2025-12-07 01:44:16 +01:00
parent 34a2d07538
commit 9eb57ed117
60 changed files with 5082 additions and 195 deletions

246
src/systems/SaveSystem.js Normal file
View 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();
}
});
}
}
}