mapa
This commit is contained in:
@@ -93,6 +93,16 @@ class BuildingSystem {
|
||||
const success = terrain.placeStructure(gridX, gridY, `struct_${this.selectedBuilding}`);
|
||||
if (success) {
|
||||
this.showFloatingText(`Built ${building.name}!`, gridX, gridY, '#00FF00');
|
||||
|
||||
// Build Sound
|
||||
if (this.scene.soundManager) {
|
||||
this.scene.soundManager.playBuild();
|
||||
}
|
||||
|
||||
// Quest Tracking
|
||||
if (this.scene.questSystem) {
|
||||
this.scene.questSystem.trackAction(`build_${this.selectedBuilding}`);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -67,12 +67,25 @@ class FarmingSystem {
|
||||
maxTime: 10 // Seconds per stage?
|
||||
};
|
||||
terrain.addCrop(x, y, cropData);
|
||||
|
||||
// Plant Sound
|
||||
if (this.scene.soundManager) {
|
||||
this.scene.soundManager.playPlant();
|
||||
}
|
||||
|
||||
// Quest Tracking
|
||||
if (this.scene.questSystem) this.scene.questSystem.trackAction('plant');
|
||||
}
|
||||
|
||||
harvest(x, y) {
|
||||
const terrain = this.scene.terrainSystem;
|
||||
console.log('🌾 Harvesting!');
|
||||
|
||||
// Harvest Sound
|
||||
if (this.scene.soundManager) {
|
||||
this.scene.soundManager.playHarvest();
|
||||
}
|
||||
|
||||
// Spawn loot
|
||||
if (this.scene.interactionSystem) {
|
||||
this.scene.interactionSystem.spawnLoot(x, y, 'wheat');
|
||||
|
||||
@@ -150,6 +150,11 @@ class InteractionSystem {
|
||||
decor.hp -= damage;
|
||||
this.showFloatingText(`${-damage}`, gridX, gridY, '#ffaaaa');
|
||||
|
||||
// Chop Sound
|
||||
if (this.scene.soundManager) {
|
||||
this.scene.soundManager.playChop();
|
||||
}
|
||||
|
||||
if (decor.hp <= 0) {
|
||||
const type = this.scene.terrainSystem.removeDecoration(gridX, gridY);
|
||||
// Loot logic via LootSystem
|
||||
|
||||
@@ -68,9 +68,25 @@ class InventorySystem {
|
||||
}
|
||||
}
|
||||
this.updateUI();
|
||||
this.updateUI();
|
||||
return false; // Not enough items
|
||||
}
|
||||
|
||||
getItemCount(type) {
|
||||
let total = 0;
|
||||
for (const slot of this.slots) {
|
||||
if (slot && slot.type === type) {
|
||||
total += slot.count;
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
addGold(amount) {
|
||||
this.gold += amount;
|
||||
this.updateUI();
|
||||
}
|
||||
|
||||
updateUI() {
|
||||
const uiScene = this.scene.scene.get('UIScene');
|
||||
if (uiScene) {
|
||||
|
||||
@@ -84,10 +84,15 @@ class LootSystem {
|
||||
const leftover = this.scene.inventorySystem.addItem(drop.type, drop.count);
|
||||
|
||||
if (leftover === 0) {
|
||||
// Success
|
||||
this.scene.sound.play('pickup_sound')
|
||||
// (Assuming sound exists, if not it will just warn silently or fail)
|
||||
// Actually, let's skip sound call if not sure to avoid error spam
|
||||
// Success - Play Sound
|
||||
if (this.scene.soundManager) {
|
||||
this.scene.soundManager.playPickup();
|
||||
}
|
||||
|
||||
// Sparkle Effect
|
||||
if (this.scene.particleEffects) {
|
||||
this.scene.particleEffects.sparkle(drop.x, drop.y);
|
||||
}
|
||||
|
||||
// Float text effect
|
||||
this.showFloatingText(`+${drop.count} ${drop.type}`, drop.x, drop.y);
|
||||
|
||||
129
src/systems/MultiplayerSystem.js
Normal file
129
src/systems/MultiplayerSystem.js
Normal file
@@ -0,0 +1,129 @@
|
||||
class MultiplayerSystem {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
this.socket = null;
|
||||
this.otherPlayers = {}; // Map socketId -> Sprite
|
||||
this.isConnected = false;
|
||||
|
||||
// Try to connect
|
||||
this.connect();
|
||||
}
|
||||
|
||||
connect() {
|
||||
if (typeof io === 'undefined') {
|
||||
console.warn('⚠️ Socket.IO not found. Multiplayer disabled.');
|
||||
console.warn('Please run: npm install socket.io-client OR include CDN.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('🌐 Connecting to Multiplayer Server...');
|
||||
// Connect to localhost:3000
|
||||
this.socket = io('http://localhost:3000');
|
||||
|
||||
this.socket.on('connect', () => {
|
||||
console.log('✅ Connected to Server! ID:', this.socket.id);
|
||||
this.isConnected = true;
|
||||
|
||||
// Send initial pos
|
||||
if (this.scene.player) {
|
||||
const pos = this.scene.player.getPosition();
|
||||
this.socket.emit('playerMovement', {
|
||||
x: pos.x,
|
||||
y: pos.y,
|
||||
anim: 'idle',
|
||||
flipX: false
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.socket.on('currentPlayers', (players) => {
|
||||
Object.keys(players).forEach((id) => {
|
||||
if (id === this.socket.id) return;
|
||||
this.addOtherPlayer(players[id]);
|
||||
});
|
||||
});
|
||||
|
||||
this.socket.on('newPlayer', (playerInfo) => {
|
||||
this.addOtherPlayer(playerInfo);
|
||||
});
|
||||
|
||||
this.socket.on('playerDisconnected', (playerId) => {
|
||||
this.removeOtherPlayer(playerId);
|
||||
});
|
||||
|
||||
this.socket.on('playerMoved', (playerInfo) => {
|
||||
if (this.otherPlayers[playerInfo.id]) {
|
||||
const sprite = this.otherPlayers[playerInfo.id];
|
||||
// Update target pos for interpolation (TODO: logic)
|
||||
// For now direct teleport
|
||||
|
||||
// Convert grid to screen
|
||||
const iso = new IsometricUtils(48, 24);
|
||||
const screen = iso.toScreen(playerInfo.x, playerInfo.y);
|
||||
|
||||
sprite.setPosition(screen.x + this.scene.terrainOffsetX, screen.y + this.scene.terrainOffsetY);
|
||||
sprite.setDepth(sprite.y);
|
||||
|
||||
// Anim/Flip logic could go here
|
||||
if (playerInfo.flipX !== undefined) sprite.setFlipX(playerInfo.flipX);
|
||||
}
|
||||
});
|
||||
|
||||
this.socket.on('worldAction', (action) => {
|
||||
// Handle world syncing
|
||||
if (action.type === 'build' && this.scene.buildingSystem) {
|
||||
// Hacky: place building remotely
|
||||
// this.scene.terrainSystem.placeStructure(...)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addOtherPlayer(playerInfo) {
|
||||
if (this.otherPlayers[playerInfo.id]) return;
|
||||
|
||||
console.log('👤 New Player Joined:', playerInfo.id);
|
||||
|
||||
// Use player sprite
|
||||
const iso = new IsometricUtils(48, 24);
|
||||
const screen = iso.toScreen(playerInfo.x, playerInfo.y);
|
||||
|
||||
const sprite = this.scene.add.sprite(
|
||||
screen.x + this.scene.terrainOffsetX,
|
||||
screen.y + this.scene.terrainOffsetY,
|
||||
'player' // or player_idle
|
||||
);
|
||||
sprite.setOrigin(0.5, 1);
|
||||
sprite.setScale(0.3); // Same as local player
|
||||
|
||||
// Add name tag
|
||||
const text = this.scene.add.text(0, -50, 'Player', { fontSize: '12px', fill: '#ffffff' });
|
||||
text.setOrigin(0.5);
|
||||
// Container? For now just sprite, text handling is complex
|
||||
|
||||
this.otherPlayers[playerInfo.id] = sprite;
|
||||
}
|
||||
|
||||
removeOtherPlayer(playerId) {
|
||||
if (this.otherPlayers[playerId]) {
|
||||
console.log('👋 Player Left:', playerId);
|
||||
this.otherPlayers[playerId].destroy();
|
||||
delete this.otherPlayers[playerId];
|
||||
}
|
||||
}
|
||||
|
||||
update(delta) {
|
||||
if (!this.isConnected || !this.socket || !this.scene.player) return;
|
||||
|
||||
// Rate limit: send 10 times second? Or every frame?
|
||||
// Let's send only if moved
|
||||
const player = this.scene.player;
|
||||
if (player.isMoving) {
|
||||
this.socket.emit('playerMovement', {
|
||||
x: player.gridX,
|
||||
y: player.gridY,
|
||||
anim: 'walk',
|
||||
flipX: player.sprite.flipX // Accessing internal sprite
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
105
src/systems/ParticleEffects.js
Normal file
105
src/systems/ParticleEffects.js
Normal file
@@ -0,0 +1,105 @@
|
||||
// ParticleEffects System
|
||||
// Proceduralno generiranje particle efektov
|
||||
class ParticleEffects {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
this.createParticleTextures();
|
||||
}
|
||||
|
||||
createParticleTextures() {
|
||||
// Blood particle
|
||||
if (!this.scene.textures.exists('blood_particle')) {
|
||||
const graphics = this.scene.make.graphics({ x: 0, y: 0, add: false });
|
||||
graphics.fillStyle(0xff0000, 1);
|
||||
graphics.fillCircle(2, 2, 2);
|
||||
graphics.generateTexture('blood_particle', 4, 4);
|
||||
graphics.destroy();
|
||||
}
|
||||
|
||||
// Leaf particle
|
||||
if (!this.scene.textures.exists('leaf_particle')) {
|
||||
const graphics = this.scene.make.graphics({ x: 0, y: 0, add: false });
|
||||
graphics.fillStyle(0x44aa44, 1);
|
||||
graphics.fillRect(0, 0, 4, 6);
|
||||
graphics.generateTexture('leaf_particle', 4, 6);
|
||||
graphics.destroy();
|
||||
}
|
||||
|
||||
// Sparkle particle
|
||||
if (!this.scene.textures.exists('sparkle_particle')) {
|
||||
const graphics = this.scene.make.graphics({ x: 0, y: 0, add: false });
|
||||
graphics.fillStyle(0xffff00, 1);
|
||||
graphics.fillCircle(2, 2, 2);
|
||||
graphics.generateTexture('sparkle_particle', 4, 4);
|
||||
graphics.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
// Blood Splash on damage
|
||||
bloodSplash(x, y) {
|
||||
const emitter = this.scene.add.particles(x, y, 'blood_particle', {
|
||||
speed: { min: 50, max: 150 },
|
||||
angle: { min: 0, max: 360 },
|
||||
scale: { start: 1, end: 0 },
|
||||
lifespan: 500,
|
||||
quantity: 8,
|
||||
emitting: false
|
||||
});
|
||||
|
||||
emitter.setDepth(10000);
|
||||
emitter.explode();
|
||||
|
||||
this.scene.time.delayedCall(600, () => emitter.destroy());
|
||||
}
|
||||
|
||||
// Falling Leaves ambient effect
|
||||
createFallingLeaves() {
|
||||
if (!this.scene.settings || this.scene.settings.particles === 'NONE') return;
|
||||
|
||||
const width = this.scene.scale.width;
|
||||
const quantity = this.scene.settings.particles === 'LOW' ? 1 : 2;
|
||||
|
||||
const leavesEmitter = this.scene.add.particles(0, -20, 'leaf_particle', {
|
||||
x: { min: 0, max: width },
|
||||
y: -20,
|
||||
speedY: { min: 30, max: 80 },
|
||||
speedX: { min: -20, max: 20 },
|
||||
quantity: quantity,
|
||||
frequency: 2000, // Every 2 seconds
|
||||
lifespan: 8000,
|
||||
scale: { min: 0.5, max: 1.0 },
|
||||
rotation: { min: 0, max: 360 },
|
||||
angle: { min: -10, max: 10 },
|
||||
alpha: { start: 0.8, end: 0 }
|
||||
});
|
||||
|
||||
leavesEmitter.setDepth(-980); // Below UI, above world
|
||||
this.leavesEmitter = leavesEmitter;
|
||||
|
||||
return leavesEmitter;
|
||||
}
|
||||
|
||||
// Sparkles on item pickup
|
||||
sparkle(x, y) {
|
||||
const emitter = this.scene.add.particles(x, y, 'sparkle_particle', {
|
||||
speed: { min: 20, max: 80 },
|
||||
angle: { min: 0, max: 360 },
|
||||
scale: { start: 1.5, end: 0 },
|
||||
lifespan: 800,
|
||||
quantity: 5,
|
||||
emitting: false
|
||||
});
|
||||
|
||||
emitter.setDepth(10000);
|
||||
emitter.explode();
|
||||
|
||||
this.scene.time.delayedCall(900, () => emitter.destroy());
|
||||
}
|
||||
|
||||
// Destroy all effects
|
||||
destroy() {
|
||||
if (this.leavesEmitter) {
|
||||
this.leavesEmitter.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
92
src/systems/PathfindingSystem.js
Normal file
92
src/systems/PathfindingSystem.js
Normal file
@@ -0,0 +1,92 @@
|
||||
class PathfindingSystem {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
this.worker = null;
|
||||
this.callbacks = new Map();
|
||||
this.requestId = 0;
|
||||
this.initialized = false;
|
||||
|
||||
try {
|
||||
// Ustvarimo workerja
|
||||
this.worker = new Worker('src/workers/pathfinding.worker.js');
|
||||
this.worker.onmessage = this.handleMessage.bind(this);
|
||||
console.log('✅ PathfindingWorker initialized.');
|
||||
this.initialized = true;
|
||||
} catch (err) {
|
||||
console.error('❌ Failed to init PathfindingWorker:', err);
|
||||
}
|
||||
}
|
||||
|
||||
updateGrid() {
|
||||
if (!this.initialized || !this.scene.terrainSystem) return;
|
||||
|
||||
const ts = this.scene.terrainSystem;
|
||||
const width = ts.width;
|
||||
const height = ts.height;
|
||||
|
||||
// Ustvarimo flat array (0 = prehodno, 1 = ovira)
|
||||
// Uporabimo Uint8Array za učinkovitost prenosa
|
||||
const grid = new Uint8Array(width * height);
|
||||
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
let blocked = 0;
|
||||
const tile = ts.tiles[y][x];
|
||||
|
||||
// 1. Voda in void
|
||||
if (!tile || tile.type === 'water' || tile.type === 'void') {
|
||||
blocked = 1;
|
||||
} else {
|
||||
// 2. Dekoracije (Ovire)
|
||||
// Uporabimo že obstoječo logiko v TerrainSystemu (če obstaja) ali preverimo dekoracije
|
||||
const key = `${x},${y}`;
|
||||
const decor = ts.decorationsMap.get(key);
|
||||
if (decor) {
|
||||
const solidTypes = [
|
||||
'tree', 'tree_green', 'tree_blue', 'tree_dead',
|
||||
'tree_green_new', 'tree_blue_new', 'tree_dead_new',
|
||||
'rock', 'rock_asset', 'rock_new', 'rock_small', 'rock_1', 'rock_2',
|
||||
'wall', 'fence', 'house', 'gravestone'
|
||||
];
|
||||
// Preverimo substring za tipe (npr. 'tree' ujame 'tree_blue')
|
||||
const isSolid = solidTypes.some(t => decor.type.includes(t));
|
||||
if (isSolid) blocked = 1;
|
||||
}
|
||||
}
|
||||
|
||||
grid[y * width + x] = blocked;
|
||||
}
|
||||
}
|
||||
|
||||
this.worker.postMessage({
|
||||
type: 'UPDATE_GRID',
|
||||
payload: { grid, width, height }
|
||||
});
|
||||
|
||||
// console.log('🗺️ Pathfinding Grid updated sent to worker.');
|
||||
}
|
||||
|
||||
findPath(startX, startY, endX, endY, callback) {
|
||||
if (!this.initialized) return;
|
||||
|
||||
const id = this.requestId++;
|
||||
this.callbacks.set(id, callback);
|
||||
|
||||
this.worker.postMessage({
|
||||
type: 'FIND_PATH',
|
||||
id: id,
|
||||
payload: { startX, startY, endX, endY }
|
||||
});
|
||||
}
|
||||
|
||||
handleMessage(e) {
|
||||
const { type, id, path } = e.data;
|
||||
if (type === 'PATH_FOUND') {
|
||||
const callback = this.callbacks.get(id);
|
||||
if (callback) {
|
||||
callback(path);
|
||||
this.callbacks.delete(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
197
src/systems/QuestSystem.js
Normal file
197
src/systems/QuestSystem.js
Normal file
@@ -0,0 +1,197 @@
|
||||
class QuestSystem {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
|
||||
// Quest Definitions
|
||||
this.questDB = {
|
||||
'q1_start': {
|
||||
id: 'q1_start',
|
||||
title: 'Survival Basics',
|
||||
description: 'Collect Wood and Stone to build your first defense.',
|
||||
objectives: [
|
||||
{ type: 'collect', item: 'wood', amount: 5, current: 0, done: false },
|
||||
{ type: 'collect', item: 'stone', amount: 3, current: 0, done: false }
|
||||
],
|
||||
reward: { gold: 10, xp: 50 },
|
||||
nextQuest: 'q2_farm',
|
||||
giver: 'villager'
|
||||
},
|
||||
'q2_farm': {
|
||||
id: 'q2_farm',
|
||||
title: 'The Farmer',
|
||||
description: 'Plant some seeds to grow food. You will need it.',
|
||||
objectives: [
|
||||
{ type: 'action', action: 'plant', amount: 3, current: 0, done: false }
|
||||
],
|
||||
reward: { gold: 20, item: 'wood', amount: 10 },
|
||||
nextQuest: 'q3_defense',
|
||||
giver: 'villager'
|
||||
},
|
||||
'q3_defense': {
|
||||
id: 'q3_defense',
|
||||
title: 'Fortification',
|
||||
description: 'Build a Fence to keep zombies out.',
|
||||
objectives: [
|
||||
{ type: 'action', action: 'build_fence', amount: 2, current: 0, done: false }
|
||||
],
|
||||
reward: { gold: 50, item: 'sword', amount: 1 },
|
||||
nextQuest: 'q4_slayer',
|
||||
giver: 'merchant'
|
||||
},
|
||||
'q4_slayer': {
|
||||
id: 'q4_slayer',
|
||||
title: 'Zombie Slayer',
|
||||
description: 'Kill 3 Zombies using your new sword.',
|
||||
objectives: [
|
||||
{ type: 'kill', target: 'zombie', amount: 3, current: 0, done: false }
|
||||
],
|
||||
reward: { gold: 100, item: 'gold', amount: 50 },
|
||||
nextQuest: null,
|
||||
giver: 'villager'
|
||||
}
|
||||
};
|
||||
|
||||
this.activeQuest = null;
|
||||
this.completedQuests = [];
|
||||
}
|
||||
|
||||
getAvailableQuest(npcType) {
|
||||
const chain = ['q1_start', 'q2_farm', 'q3_defense', 'q4_slayer'];
|
||||
let targetId = null;
|
||||
for (const id of chain) {
|
||||
if (!this.completedQuests.includes(id)) {
|
||||
targetId = id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetId) return null;
|
||||
if (this.activeQuest && this.activeQuest.id === targetId) return null;
|
||||
|
||||
const q = this.questDB[targetId];
|
||||
if (q.giver === npcType) return q;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
startQuest(id) {
|
||||
if (this.completedQuests.includes(id)) return;
|
||||
|
||||
const template = this.questDB[id];
|
||||
if (!template) return;
|
||||
|
||||
this.activeQuest = JSON.parse(JSON.stringify(template));
|
||||
console.log(`📜 Quest Started: ${this.activeQuest.title}`);
|
||||
|
||||
this.updateUI();
|
||||
|
||||
// Notification
|
||||
this.scene.events.emit('show-floating-text', {
|
||||
x: this.scene.player.x,
|
||||
y: this.scene.player.y - 50,
|
||||
text: "Quest Accepted!",
|
||||
color: '#FFFF00'
|
||||
});
|
||||
}
|
||||
|
||||
update(delta) {
|
||||
if (!this.activeQuest) return;
|
||||
|
||||
let changed = false;
|
||||
let allDone = true;
|
||||
|
||||
if (this.scene.inventorySystem) {
|
||||
const inv = this.scene.inventorySystem;
|
||||
|
||||
for (const obj of this.activeQuest.objectives) {
|
||||
if (obj.done) continue;
|
||||
|
||||
if (obj.type === 'collect') {
|
||||
const count = inv.getItemCount(obj.item);
|
||||
if (count !== obj.current) {
|
||||
obj.current = count;
|
||||
changed = true;
|
||||
}
|
||||
if (obj.current >= obj.amount) {
|
||||
obj.done = true;
|
||||
this.scene.events.emit('show-floating-text', { x: this.scene.player.x, y: this.scene.player.y, text: "Objective Complete!" });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const obj of this.activeQuest.objectives) {
|
||||
if (!obj.done) allDone = false;
|
||||
}
|
||||
|
||||
if (changed) this.updateUI();
|
||||
|
||||
if (allDone) {
|
||||
this.completeQuest();
|
||||
}
|
||||
}
|
||||
|
||||
trackAction(actionType, amount = 1) {
|
||||
if (!this.activeQuest) return;
|
||||
|
||||
let changed = false;
|
||||
for (const obj of this.activeQuest.objectives) {
|
||||
if (obj.done) continue;
|
||||
|
||||
if (obj.type === 'action' && obj.action === actionType) {
|
||||
obj.current += amount;
|
||||
changed = true;
|
||||
if (obj.current >= obj.amount) {
|
||||
obj.done = true;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (obj.type === 'kill' && obj.target === actionType) {
|
||||
obj.current += amount;
|
||||
changed = true;
|
||||
if (obj.current >= obj.amount) {
|
||||
obj.done = true;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (changed) this.updateUI();
|
||||
}
|
||||
|
||||
completeQuest() {
|
||||
console.log(`🏆 Quest Complete: ${this.activeQuest.title}`);
|
||||
|
||||
if (this.activeQuest.reward) {
|
||||
const r = this.activeQuest.reward;
|
||||
if (r.gold && this.scene.inventorySystem) {
|
||||
this.scene.inventorySystem.addGold(r.gold);
|
||||
}
|
||||
if (r.item && this.scene.inventorySystem) {
|
||||
this.scene.inventorySystem.addItem(r.item, r.amount || 1);
|
||||
}
|
||||
}
|
||||
|
||||
this.scene.events.emit('show-floating-text', {
|
||||
x: this.scene.player.x,
|
||||
y: this.scene.player.y - 50,
|
||||
text: "Quest Complete!",
|
||||
color: '#00FF00'
|
||||
});
|
||||
|
||||
this.completedQuests.push(this.activeQuest.id);
|
||||
const next = this.activeQuest.nextQuest;
|
||||
this.activeQuest = null;
|
||||
this.updateUI();
|
||||
|
||||
if (next) {
|
||||
console.log('Next quest available at NPC.');
|
||||
}
|
||||
}
|
||||
|
||||
updateUI() {
|
||||
const ui = this.scene.scene.get('UIScene');
|
||||
if (ui && ui.updateQuestTracker) {
|
||||
ui.updateQuestTracker(this.activeQuest);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ class SaveSystem {
|
||||
};
|
||||
|
||||
const saveData = {
|
||||
version: 1.1,
|
||||
version: 2.4, // Nazaj na pixel art
|
||||
timestamp: Date.now(),
|
||||
player: { x: playerPos.x, y: playerPos.y },
|
||||
terrain: {
|
||||
@@ -62,11 +62,18 @@ class SaveSystem {
|
||||
|
||||
try {
|
||||
const jsonString = JSON.stringify(saveData);
|
||||
localStorage.setItem(this.storageKey, jsonString);
|
||||
// Compress data to save space
|
||||
try {
|
||||
const compressed = Compression.compress(jsonString);
|
||||
localStorage.setItem(this.storageKey, 'LZW:' + compressed);
|
||||
console.log(`✅ Game saved! Size: ${jsonString.length} -> ${compressed.length} chars`);
|
||||
} catch (compErr) {
|
||||
console.warn("Compression failed, saving raw JSON:", compErr);
|
||||
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');
|
||||
@@ -76,17 +83,32 @@ class SaveSystem {
|
||||
loadGame() {
|
||||
console.log('📂 Loading game...');
|
||||
|
||||
const jsonString = localStorage.getItem(this.storageKey);
|
||||
if (!jsonString) {
|
||||
let rawData = localStorage.getItem(this.storageKey);
|
||||
if (!rawData) {
|
||||
console.log('⚠️ No save file found.');
|
||||
this.showNotification('NO SAVE FOUND');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
let jsonString = rawData;
|
||||
// Check for compression
|
||||
if (rawData.startsWith('LZW:')) {
|
||||
const compressed = rawData.substring(4); // Remove prefix
|
||||
jsonString = Compression.decompress(compressed);
|
||||
}
|
||||
|
||||
const saveData = JSON.parse(jsonString);
|
||||
console.log('Loading save data:', saveData);
|
||||
|
||||
// Preveri verzijo - če je stara, izbriši save
|
||||
if (!saveData.version || saveData.version < 2.4) {
|
||||
console.log('⚠️ Stara verzija save file-a detected, clearing...');
|
||||
localStorage.removeItem(this.storageKey);
|
||||
this.showNotification('OLD SAVE CLEARED - NEW GAME');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 1. Load Player
|
||||
if (this.scene.player) {
|
||||
// Zahteva metodo setPosition(gridX, gridY) v Player.js
|
||||
@@ -131,8 +153,7 @@ class SaveSystem {
|
||||
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();
|
||||
// Sproščanje objektov se samodejno dogaja preko release() v clearanju zgoraj
|
||||
|
||||
// B) Restore Crops
|
||||
if (saveData.terrain.crops) {
|
||||
|
||||
@@ -117,6 +117,111 @@ class SoundManager {
|
||||
osc.stop(ctx.currentTime + 0.2);
|
||||
}
|
||||
|
||||
beepAttack() {
|
||||
if (!this.scene.sound.context) return;
|
||||
const ctx = this.scene.sound.context;
|
||||
const osc = ctx.createOscillator();
|
||||
const gain = ctx.createGain();
|
||||
osc.connect(gain);
|
||||
gain.connect(ctx.destination);
|
||||
osc.frequency.setValueAtTime(400, ctx.currentTime);
|
||||
osc.frequency.exponentialRampToValueAtTime(100, ctx.currentTime + 0.08);
|
||||
osc.type = 'sawtooth';
|
||||
gain.gain.setValueAtTime(0.2, ctx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.08);
|
||||
osc.start();
|
||||
osc.stop(ctx.currentTime + 0.08);
|
||||
}
|
||||
|
||||
beepHit() {
|
||||
if (!this.scene.sound.context) return;
|
||||
const ctx = this.scene.sound.context;
|
||||
const osc = ctx.createOscillator();
|
||||
const gain = ctx.createGain();
|
||||
osc.connect(gain);
|
||||
gain.connect(ctx.destination);
|
||||
osc.frequency.value = 80;
|
||||
osc.type = 'square';
|
||||
gain.gain.setValueAtTime(0.25, ctx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.05);
|
||||
osc.start();
|
||||
osc.stop(ctx.currentTime + 0.05);
|
||||
}
|
||||
|
||||
beepFootstep() {
|
||||
if (!this.scene.sound.context) return;
|
||||
const ctx = this.scene.sound.context;
|
||||
const osc = ctx.createOscillator();
|
||||
const gain = ctx.createGain();
|
||||
osc.connect(gain);
|
||||
gain.connect(ctx.destination);
|
||||
osc.frequency.value = 120 + Math.random() * 20;
|
||||
osc.type = 'sine';
|
||||
gain.gain.setValueAtTime(0.05, ctx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.03);
|
||||
osc.start();
|
||||
osc.stop(ctx.currentTime + 0.03);
|
||||
}
|
||||
|
||||
beepDeath() {
|
||||
if (!this.scene.sound.context) return;
|
||||
const ctx = this.scene.sound.context;
|
||||
const osc = ctx.createOscillator();
|
||||
const gain = ctx.createGain();
|
||||
osc.connect(gain);
|
||||
gain.connect(ctx.destination);
|
||||
osc.frequency.setValueAtTime(300, ctx.currentTime);
|
||||
osc.frequency.exponentialRampToValueAtTime(50, ctx.currentTime + 0.5);
|
||||
osc.type = 'sawtooth';
|
||||
gain.gain.setValueAtTime(0.2, ctx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.5);
|
||||
osc.start();
|
||||
osc.stop(ctx.currentTime + 0.5);
|
||||
}
|
||||
|
||||
startRainNoise() {
|
||||
if (!this.scene.sound.context || this.rainNode) return;
|
||||
const ctx = this.scene.sound.context;
|
||||
|
||||
// Create noise buffer
|
||||
const bufferSize = 2 * ctx.sampleRate;
|
||||
const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
|
||||
const output = buffer.getChannelData(0);
|
||||
for (let i = 0; i < bufferSize; i++) {
|
||||
output[i] = Math.random() * 2 - 1;
|
||||
}
|
||||
|
||||
this.rainNode = ctx.createBufferSource();
|
||||
this.rainNode.buffer = buffer;
|
||||
this.rainNode.loop = true;
|
||||
|
||||
// Lowpass filter for rain sound
|
||||
const filter = ctx.createBiquadFilter();
|
||||
filter.type = 'lowpass';
|
||||
filter.frequency.value = 800;
|
||||
|
||||
this.rainGain = ctx.createGain();
|
||||
this.rainGain.gain.value = 0.05 * this.sfxVolume;
|
||||
|
||||
this.rainNode.connect(filter);
|
||||
filter.connect(this.rainGain);
|
||||
this.rainGain.connect(ctx.destination);
|
||||
|
||||
this.rainNode.start();
|
||||
}
|
||||
|
||||
stopRainNoise() {
|
||||
if (this.rainNode) {
|
||||
this.rainNode.stop();
|
||||
this.rainNode.disconnect();
|
||||
this.rainNode = null;
|
||||
}
|
||||
if (this.rainGain) {
|
||||
this.rainGain.disconnect();
|
||||
this.rainGain = null;
|
||||
}
|
||||
}
|
||||
|
||||
playAmbient(key, loop = true) {
|
||||
if (this.isMuted) return;
|
||||
if (this.currentAmbient) this.currentAmbient.stop();
|
||||
@@ -132,6 +237,55 @@ class SoundManager {
|
||||
}
|
||||
}
|
||||
|
||||
startMusic() {
|
||||
if (!this.scene.sound.context || this.musicInterval) return;
|
||||
|
||||
console.log('🎵 Starting Ambient Music...');
|
||||
// Simple C Minor Pentatonic: C3, Eb3, F3, G3, Bb3
|
||||
const scale = [130.81, 155.56, 174.61, 196.00, 233.08, 261.63];
|
||||
|
||||
// Loop every 3-5 seconds play a note
|
||||
this.musicInterval = setInterval(() => {
|
||||
if (this.isMuted) return;
|
||||
// 40% chance to play a note
|
||||
if (Math.random() > 0.6) {
|
||||
const freq = scale[Math.floor(Math.random() * scale.length)];
|
||||
this.playProceduralNote(freq);
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
stopMusic() {
|
||||
if (this.musicInterval) {
|
||||
clearInterval(this.musicInterval);
|
||||
this.musicInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
playProceduralNote(freq) {
|
||||
if (!this.scene.sound.context) return;
|
||||
const ctx = this.scene.sound.context;
|
||||
|
||||
const osc = ctx.createOscillator();
|
||||
const gain = ctx.createGain();
|
||||
|
||||
osc.connect(gain);
|
||||
gain.connect(ctx.destination);
|
||||
|
||||
osc.frequency.value = freq;
|
||||
osc.type = 'sine'; // Soft tone
|
||||
|
||||
const now = ctx.currentTime;
|
||||
const duration = 2.0; // Long decay like reverb/pad
|
||||
|
||||
gain.gain.setValueAtTime(0, now);
|
||||
gain.gain.linearRampToValueAtTime(0.05 * this.musicVolume, now + 0.5); // Slow attack
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + duration); // Long release
|
||||
|
||||
osc.start(now);
|
||||
osc.stop(now + duration);
|
||||
}
|
||||
|
||||
toggleMute() {
|
||||
this.isMuted = !this.isMuted;
|
||||
this.scene.sound.mute = this.isMuted;
|
||||
@@ -143,6 +297,10 @@ class SoundManager {
|
||||
playHarvest() { this.playSFX('harvest'); }
|
||||
playBuild() { this.playSFX('build'); }
|
||||
playPickup() { this.playSFX('pickup'); }
|
||||
playRainSound() { this.playAmbient('rain_loop'); }
|
||||
stopRainSound() { this.stopAmbient(); }
|
||||
playAttack() { this.beepAttack(); }
|
||||
playHit() { this.beepHit(); }
|
||||
playFootstep() { this.beepFootstep(); }
|
||||
playDeath() { this.beepDeath(); }
|
||||
playRainSound() { this.startRainNoise(); }
|
||||
stopRainSound() { this.stopRainNoise(); }
|
||||
}
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
// ========================================================
|
||||
// NOVE GLOBALNE KONSTANTE ZA LOKACIJE
|
||||
// ========================================================
|
||||
const FARM_SIZE = 8;
|
||||
const FARM_CENTER_X = 20; // Lokacija farme na X osi
|
||||
const FARM_CENTER_Y = 20; // Lokacija farme na Y osi
|
||||
|
||||
const CITY_SIZE = 15;
|
||||
const CITY_START_X = 65; // Desni del mape (npr. med 65 in 80)
|
||||
const CITY_START_Y = 65;
|
||||
|
||||
// Terrain Generator System
|
||||
// Generira proceduralni isometrični teren in skrbi za optimizacijo (Tilemap + Culling)
|
||||
class TerrainSystem {
|
||||
constructor(scene, width = 100, height = 100) {
|
||||
this.scene = scene;
|
||||
@@ -14,10 +24,29 @@ class TerrainSystem {
|
||||
this.decorationsMap = new Map();
|
||||
this.cropsMap = new Map();
|
||||
|
||||
this.visibleTiles = new Map();
|
||||
this.visibleDecorations = new Map();
|
||||
this.visibleCrops = new Map();
|
||||
|
||||
// Pool for Decorations (Trees, Rocks, etc.)
|
||||
this.tilePool = {
|
||||
active: [],
|
||||
inactive: [],
|
||||
get: () => {
|
||||
if (this.tilePool.inactive.length > 0) {
|
||||
const s = this.tilePool.inactive.pop();
|
||||
s.setVisible(true);
|
||||
return s;
|
||||
}
|
||||
const s = this.scene.add.sprite(0, 0, 'dirt');
|
||||
s.setOrigin(0.5, 0.5);
|
||||
return s;
|
||||
},
|
||||
release: (sprite) => {
|
||||
sprite.setVisible(false);
|
||||
this.tilePool.inactive.push(sprite);
|
||||
}
|
||||
};
|
||||
|
||||
this.decorationPool = {
|
||||
active: [],
|
||||
inactive: [],
|
||||
@@ -36,7 +65,6 @@ class TerrainSystem {
|
||||
}
|
||||
};
|
||||
|
||||
// Pool for Crops
|
||||
this.cropPool = {
|
||||
active: [],
|
||||
inactive: [],
|
||||
@@ -55,14 +83,16 @@ class TerrainSystem {
|
||||
};
|
||||
|
||||
this.terrainTypes = {
|
||||
WATER: { name: 'water', height: 0, color: 0x4444ff, index: 0 },
|
||||
SAND: { name: 'sand', height: 0.2, color: 0xdddd44, index: 1 },
|
||||
GRASS_FULL: { name: 'grass_full', height: 0.35, color: 0x44aa44, index: 2 },
|
||||
GRASS_TOP: { name: 'grass_top', height: 0.45, color: 0x66cc66, index: 3 },
|
||||
DIRT: { name: 'dirt', height: 0.5, color: 0x8b4513, index: 4 },
|
||||
STONE: { name: 'stone', height: 0.7, color: 0x888888, index: 5 },
|
||||
PATH: { name: 'path', height: -1, color: 0xc2b280, index: 6 },
|
||||
FARMLAND: { name: 'farmland', height: -1, color: 0x5c4033, index: 7 }
|
||||
WATER: { name: 'water', height: 0, color: 0x4444ff },
|
||||
SAND: { name: 'sand', height: 0.2, color: 0xdddd44 },
|
||||
GRASS_FULL: { name: 'grass_full', height: 0.35, color: 0x44aa44 },
|
||||
GRASS_TOP: { name: 'grass_top', height: 0.45, color: 0x66cc66 },
|
||||
DIRT: { name: 'dirt', height: 0.5, color: 0x8b4513 },
|
||||
STONE: { name: 'stone', height: 0.7, color: 0x888888 },
|
||||
PAVEMENT: { name: 'pavement', height: 0.6, color: 0x777777 },
|
||||
RUINS: { name: 'ruins', height: 0.6, color: 0x555555 },
|
||||
PATH: { name: 'path', height: -1, color: 0xc2b280 },
|
||||
FARMLAND: { name: 'farmland', height: -1, color: 0x5c4033 }
|
||||
};
|
||||
|
||||
this.offsetX = 0;
|
||||
@@ -70,53 +100,21 @@ class TerrainSystem {
|
||||
}
|
||||
|
||||
createTileTextures() {
|
||||
// Create a single spritesheet for tiles (Tilemap Optimization)
|
||||
const graphics = this.scene.make.graphics({ x: 0, y: 0, add: false });
|
||||
|
||||
const tileWidth = 48;
|
||||
const tileHeight = 32; // 24 for iso + 8 depth
|
||||
const tileHeight = 60;
|
||||
const types = Object.values(this.terrainTypes);
|
||||
|
||||
// Draw all tiles horizontally
|
||||
types.forEach((type, index) => {
|
||||
// Update index just in case
|
||||
type.index = index;
|
||||
types.forEach((type) => {
|
||||
if (this.scene.textures.exists(type.name)) return;
|
||||
|
||||
const x = index * tileWidth;
|
||||
graphics.fillStyle(type.color);
|
||||
|
||||
// Draw Isometic Tile (Diamond + Thickness)
|
||||
const top = 0;
|
||||
const midX = x + 24;
|
||||
const graphics = this.scene.make.graphics({ x: 0, y: 0, add: false });
|
||||
const x = 0;
|
||||
const midX = 24;
|
||||
const midY = 12;
|
||||
const bottomY = 24;
|
||||
const depth = 8;
|
||||
const depth = 20;
|
||||
|
||||
// Top Face
|
||||
graphics.beginPath();
|
||||
graphics.moveTo(midX, top);
|
||||
graphics.lineTo(x + 48, midY);
|
||||
graphics.lineTo(midX, bottomY);
|
||||
graphics.lineTo(x, midY);
|
||||
graphics.closePath();
|
||||
graphics.fill();
|
||||
|
||||
// Add stroke to prevent seams/gaps (Robust Fix)
|
||||
graphics.lineStyle(2, type.color);
|
||||
graphics.strokePath();
|
||||
|
||||
// Thickness (Right)
|
||||
graphics.fillStyle(Phaser.Display.Color.IntegerToColor(type.color).darken(20).color);
|
||||
graphics.beginPath();
|
||||
graphics.moveTo(x + 48, midY);
|
||||
graphics.lineTo(x + 48, midY + depth);
|
||||
graphics.lineTo(midX, bottomY + depth);
|
||||
graphics.lineTo(midX, bottomY);
|
||||
graphics.closePath();
|
||||
graphics.fill();
|
||||
|
||||
// Thickness (Left)
|
||||
graphics.fillStyle(Phaser.Display.Color.IntegerToColor(type.color).darken(40).color);
|
||||
graphics.fillStyle(0x8B4513);
|
||||
graphics.beginPath();
|
||||
graphics.moveTo(midX, bottomY);
|
||||
graphics.lineTo(midX, bottomY + depth);
|
||||
@@ -125,28 +123,59 @@ class TerrainSystem {
|
||||
graphics.closePath();
|
||||
graphics.fill();
|
||||
|
||||
// Detail (Grass)
|
||||
if (type.name.includes('grass')) {
|
||||
graphics.fillStyle(0x339933); // Darker green blades
|
||||
for (let i = 0; i < 15; i++) {
|
||||
const rx = x + 8 + Math.random() * 32;
|
||||
const ry = 4 + Math.random() * 16;
|
||||
graphics.fillRect(rx, ry, 2, 2);
|
||||
}
|
||||
}
|
||||
// Detail (Dirt)
|
||||
if (type.name.includes('dirt')) {
|
||||
graphics.fillStyle(0x5c4033);
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const rx = x + 8 + Math.random() * 32;
|
||||
const ry = 4 + Math.random() * 16;
|
||||
graphics.fillRect(rx, ry, 2, 2);
|
||||
}
|
||||
}
|
||||
});
|
||||
graphics.fillStyle(0x6B3410);
|
||||
graphics.beginPath();
|
||||
graphics.moveTo(x + 48, midY);
|
||||
graphics.lineTo(x + 48, midY + depth);
|
||||
graphics.lineTo(midX, bottomY + depth);
|
||||
graphics.lineTo(midX, bottomY);
|
||||
graphics.closePath();
|
||||
graphics.fill();
|
||||
|
||||
graphics.generateTexture('terrain_tileset', tileWidth * types.length, tileHeight);
|
||||
graphics.destroy();
|
||||
graphics.fillStyle(type.color);
|
||||
graphics.beginPath();
|
||||
graphics.moveTo(midX, 0);
|
||||
graphics.lineTo(x + 48, midY);
|
||||
graphics.lineTo(midX, bottomY);
|
||||
graphics.lineTo(x, midY);
|
||||
graphics.closePath();
|
||||
graphics.fill();
|
||||
|
||||
graphics.lineStyle(1, 0xffffff, 0.15);
|
||||
graphics.beginPath();
|
||||
graphics.moveTo(x, midY);
|
||||
graphics.lineTo(midX, 0);
|
||||
graphics.lineTo(x + 48, midY);
|
||||
graphics.strokePath();
|
||||
|
||||
if (type.name.includes('grass')) {
|
||||
graphics.fillStyle(Phaser.Display.Color.IntegerToColor(type.color).lighten(10).color);
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const rx = x + 10 + Math.random() * 28;
|
||||
const ry = 4 + Math.random() * 16;
|
||||
graphics.fillRect(rx, ry, 2, 2);
|
||||
}
|
||||
}
|
||||
if (type.name.includes('stone') || type.name.includes('ruins')) {
|
||||
graphics.fillStyle(0x444444);
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const rx = x + 8 + Math.random() * 30;
|
||||
const ry = 4 + Math.random() * 16;
|
||||
graphics.fillRect(rx, ry, 3, 3);
|
||||
}
|
||||
}
|
||||
|
||||
if (type.name.includes('pavement')) {
|
||||
graphics.lineStyle(1, 0x555555, 0.5);
|
||||
graphics.beginPath();
|
||||
graphics.moveTo(x + 12, midY + 6);
|
||||
graphics.lineTo(x + 36, midY - 6);
|
||||
graphics.strokePath();
|
||||
}
|
||||
|
||||
graphics.generateTexture(type.name, tileWidth, tileHeight);
|
||||
graphics.destroy();
|
||||
});
|
||||
}
|
||||
|
||||
generate() {
|
||||
@@ -159,11 +188,29 @@ class TerrainSystem {
|
||||
const ny = y * 0.1;
|
||||
const elevation = this.noise.noise(nx, ny);
|
||||
|
||||
let terrainType = this.terrainTypes.WATER;
|
||||
if (elevation > this.terrainTypes.SAND.height) terrainType = this.terrainTypes.SAND;
|
||||
if (elevation > this.terrainTypes.GRASS_FULL.height) terrainType = this.terrainTypes.GRASS_FULL;
|
||||
if (elevation > this.terrainTypes.DIRT.height) terrainType = this.terrainTypes.DIRT;
|
||||
if (elevation > this.terrainTypes.STONE.height) terrainType = this.terrainTypes.STONE;
|
||||
let terrainType = this.terrainTypes.GRASS_FULL;
|
||||
|
||||
if (x < 3 || x >= this.width - 3 || y < 3 || y >= this.height - 3) {
|
||||
terrainType = this.terrainTypes.GRASS_FULL;
|
||||
} else {
|
||||
if (elevation < -0.6) terrainType = this.terrainTypes.WATER;
|
||||
else if (elevation > 0.1) terrainType = this.terrainTypes.SAND;
|
||||
else if (elevation > 0.2) terrainType = this.terrainTypes.GRASS_FULL;
|
||||
else if (elevation > 0.3) terrainType = this.terrainTypes.GRASS_TOP;
|
||||
else if (elevation > 0.7) terrainType = this.terrainTypes.DIRT;
|
||||
else if (elevation > 0.85) terrainType = this.terrainTypes.STONE;
|
||||
}
|
||||
|
||||
if (Math.abs(x - FARM_CENTER_X) <= FARM_SIZE / 2 && Math.abs(y - FARM_CENTER_Y) <= FARM_SIZE / 2) {
|
||||
terrainType = this.terrainTypes.DIRT;
|
||||
}
|
||||
if (x >= CITY_START_X && x < CITY_START_X + CITY_SIZE &&
|
||||
y >= CITY_START_Y && y < CITY_START_Y + CITY_SIZE) {
|
||||
terrainType = this.terrainTypes.PAVEMENT;
|
||||
if (Math.random() < 0.2) {
|
||||
terrainType = this.terrainTypes.RUINS;
|
||||
}
|
||||
}
|
||||
|
||||
this.tiles[y][x] = {
|
||||
type: terrainType.name,
|
||||
@@ -171,86 +218,85 @@ class TerrainSystem {
|
||||
hasDecoration: false,
|
||||
hasCrop: false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Vegetation logic (Rich World)
|
||||
if (x > 5 && x < this.width - 5 && y > 5 && y < this.height - 5) {
|
||||
let decorType = null;
|
||||
let maxHp = 1;
|
||||
let scale = 1.0;
|
||||
let treeCount = 0;
|
||||
let rockCount = 0;
|
||||
let flowerCount = 0;
|
||||
|
||||
if (terrainType.name.includes('grass')) {
|
||||
const rand = Math.random();
|
||||
if (elevation > 0.6 && rand < 0.1) {
|
||||
decorType = 'bush';
|
||||
maxHp = 5;
|
||||
} else if (rand < 0.15) { // Common trees
|
||||
decorType = 'tree';
|
||||
maxHp = 5;
|
||||
const sizeRand = Math.random();
|
||||
if (sizeRand < 0.2) scale = 0.8;
|
||||
else if (sizeRand < 0.8) scale = 1.0 + Math.random() * 0.3;
|
||||
else scale = 1.3;
|
||||
} else if (rand < 0.18) { // Rocks
|
||||
decorType = 'rock';
|
||||
maxHp = 8;
|
||||
scale = 1.2 + Math.random() * 0.5;
|
||||
} else if (rand < 0.19) {
|
||||
decorType = 'gravestone';
|
||||
maxHp = 10;
|
||||
} else if (rand < 0.30) {
|
||||
decorType = 'flower';
|
||||
maxHp = 1;
|
||||
}
|
||||
} else if (terrainType.name === 'dirt' && Math.random() < 0.05) {
|
||||
decorType = 'bush';
|
||||
maxHp = 3;
|
||||
}
|
||||
const validPositions = [];
|
||||
const isFarm = (x, y) => Math.abs(x - FARM_CENTER_X) <= (FARM_SIZE / 2 + 2) && Math.abs(y - FARM_CENTER_Y) <= (FARM_SIZE / 2 + 2);
|
||||
const isCity = (x, y) => x >= CITY_START_X - 2 && x < CITY_START_X + CITY_SIZE + 2 && y >= CITY_START_Y - 2 && y < CITY_START_Y + CITY_SIZE + 2;
|
||||
|
||||
if (decorType) {
|
||||
const key = `${x},${y}`;
|
||||
const decorData = {
|
||||
gridX: x,
|
||||
gridY: y,
|
||||
type: decorType,
|
||||
id: key,
|
||||
maxHp: maxHp,
|
||||
hp: maxHp,
|
||||
scale: scale
|
||||
};
|
||||
this.decorations.push(decorData);
|
||||
this.decorationsMap.set(key, decorData);
|
||||
this.tiles[y][x].hasDecoration = true;
|
||||
}
|
||||
for (let y = 5; y < this.height - 5; y++) {
|
||||
for (let x = 5; x < this.width - 5; x++) {
|
||||
if (isFarm(x, y) || isCity(x, y)) continue;
|
||||
|
||||
const tile = this.tiles[y][x];
|
||||
if (tile.type !== 'water' && tile.type !== 'sand' && tile.type !== 'stone') {
|
||||
validPositions.push({ x, y });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('✅ Terrain and decorations generated!');
|
||||
for (let i = validPositions.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[validPositions[i], validPositions[j]] = [validPositions[j], validPositions[i]];
|
||||
}
|
||||
|
||||
// --- TILEMAP IMPLEMENTATION (Performance) ---
|
||||
if (this.map) this.map.destroy();
|
||||
this.map = this.scene.make.tilemap({
|
||||
tileWidth: this.iso.tileWidth, // 48
|
||||
tileHeight: this.iso.tileHeight, // 24
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
orientation: Phaser.Tilemaps.Orientation.ISOMETRIC
|
||||
});
|
||||
for (let i = 0; i < Math.min(25, validPositions.length); i++) {
|
||||
const pos = validPositions[i];
|
||||
let treeType = 'tree_green_new';
|
||||
const rand = Math.random();
|
||||
if (rand < 0.15) treeType = 'tree_blue_new';
|
||||
else if (rand < 0.25) treeType = 'tree_dead_new';
|
||||
|
||||
// 48x32 tileset
|
||||
const tileset = this.map.addTilesetImage('terrain_tileset', 'terrain_tileset', 48, 32);
|
||||
this.layer = this.map.createBlankLayer('Ground', tileset, this.offsetX, this.offsetY);
|
||||
this.addDecoration(pos.x, pos.y, treeType);
|
||||
treeCount++;
|
||||
}
|
||||
for (let i = 25; i < Math.min(50, validPositions.length); i++) {
|
||||
const pos = validPositions[i];
|
||||
// Uporabi uporabnikove kamne
|
||||
const rockType = Math.random() > 0.5 ? 'rock_1' : 'rock_2';
|
||||
this.addDecoration(pos.x, pos.y, rockType);
|
||||
rockCount++;
|
||||
}
|
||||
|
||||
for (let y = 0; y < this.height; y++) {
|
||||
for (let x = 0; x < this.width; x++) {
|
||||
const t = this.tiles[y][x];
|
||||
const typeDef = Object.values(this.terrainTypes).find(tt => tt.name === t.type);
|
||||
if (typeDef) {
|
||||
this.layer.putTileAt(typeDef.index, x, y);
|
||||
const flowerNoise = new PerlinNoise(Date.now() + 3000);
|
||||
for (let y = 5; y < this.height - 5; y++) {
|
||||
for (let x = 5; x < this.width - 5; x++) {
|
||||
if (isFarm(x, y) || isCity(x, y)) continue;
|
||||
|
||||
const tile = this.tiles[y][x];
|
||||
const val = flowerNoise.noise(x * 0.12, y * 0.12);
|
||||
if (val > 0.85 && tile.type.includes('grass')) {
|
||||
this.addDecoration(x, y, 'flowers_new');
|
||||
flowerCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.layer.setDepth(0); // Ground level
|
||||
|
||||
|
||||
const roomSize = 5;
|
||||
const roomsAcross = Math.floor(CITY_SIZE / roomSize);
|
||||
|
||||
for (let ry = 0; ry < roomsAcross; ry++) {
|
||||
for (let rx = 0; rx < roomsAcross; rx++) {
|
||||
if (Math.random() < 0.75) {
|
||||
const gx = CITY_START_X + rx * roomSize;
|
||||
const gy = CITY_START_Y + ry * roomSize;
|
||||
this.placeStructure(gx, gy, 'ruin_room');
|
||||
} else {
|
||||
const gx = CITY_START_X + rx * roomSize + 2;
|
||||
const gy = CITY_START_Y + ry * roomSize + 2;
|
||||
const rockType = Math.random() > 0.5 ? 'rock_1' : 'rock_2';
|
||||
this.addDecoration(gx, gy, rockType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✅ Teren generiran: ${treeCount} dreves, ${rockCount} kamnov.`);
|
||||
}
|
||||
|
||||
damageDecoration(x, y, amount) {
|
||||
@@ -299,33 +345,115 @@ class TerrainSystem {
|
||||
return decor.type;
|
||||
}
|
||||
|
||||
placeStructure(x, y, structureType) {
|
||||
if (this.decorationsMap.has(`${x},${y}`)) return false;
|
||||
init(offsetX, offsetY) {
|
||||
this.offsetX = offsetX;
|
||||
this.offsetY = offsetY;
|
||||
}
|
||||
|
||||
setTile(x, y, type) {
|
||||
if (x >= 0 && x < this.width && y >= 0 && y < this.height) {
|
||||
this.tiles[y][x].type = type;
|
||||
}
|
||||
}
|
||||
|
||||
placeStructure(gridX, gridY, type) {
|
||||
if (type === 'ruin') {
|
||||
for (let y = 0; y < 6; y++) {
|
||||
for (let x = 0; x < 6; x++) {
|
||||
if (Math.random() > 0.6) this.addDecoration(gridX + x, gridY + y, 'fence');
|
||||
this.setTile(gridX + x, gridY + y, 'stone');
|
||||
}
|
||||
}
|
||||
}
|
||||
if (type === 'arena') {
|
||||
const size = 12;
|
||||
for (let y = 0; y < size; y++) {
|
||||
for (let x = 0; x < size; x++) {
|
||||
const tx = gridX + x;
|
||||
const ty = gridY + y;
|
||||
|
||||
this.setTile(tx, ty, 'stone');
|
||||
if (x === 0 || x === size - 1 || y === 0 || y === size - 1) {
|
||||
if (!(x === Math.floor(size / 2) && y === size - 1)) {
|
||||
this.addDecoration(tx, ty, 'fence');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.addDecoration(gridX + 6, gridY + 6, 'gravestone');
|
||||
}
|
||||
if (type === 'ruin_room') {
|
||||
for (let y = 0; y < 5; y++) {
|
||||
for (let x = 0; x < 5; x++) {
|
||||
const tx = gridX + x;
|
||||
const ty = gridY + y;
|
||||
if (x > 0 && x < 4 && y > 0 && y < 4) {
|
||||
this.setTile(tx, ty, 'ruins');
|
||||
}
|
||||
if (x === 0 || x === 4 || y === 0 || y === 4) {
|
||||
const isCenter = (x === 2 || y === 2);
|
||||
if (isCenter && Math.random() > 0.5) continue;
|
||||
if (Math.random() > 0.3) {
|
||||
this.addDecoration(tx, ty, 'fence');
|
||||
} else {
|
||||
// User rocks in ruins
|
||||
if (Math.random() > 0.5) {
|
||||
const rType = Math.random() > 0.5 ? 'rock_1' : 'rock_2';
|
||||
this.addDecoration(tx, ty, rType);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addDecoration(gridX, gridY, type) {
|
||||
if (gridX < 0 || gridX >= this.width || gridY < 0 || gridY >= this.height) return;
|
||||
|
||||
const key = `${gridX},${gridY}`;
|
||||
if (this.decorationsMap.has(key)) return;
|
||||
|
||||
let scale = 1.0;
|
||||
|
||||
if (type === 'rock_1' || type === 'rock_2') scale = 1.5; // Povečano (bilo 0.5)
|
||||
else if (type === 'tree_green_new' || type === 'tree_blue_new' || type === 'tree_dead_new') scale = 0.04;
|
||||
else if (type === 'flowers_new') scale = 0.02;
|
||||
else if (type === 'fence') scale = 0.025;
|
||||
else if (type === 'gravestone') scale = 0.03;
|
||||
else if (type === 'hill_sprite') scale = 0.025;
|
||||
else {
|
||||
// Old Assets (Low Res)
|
||||
if (type.includes('tree')) scale = 1.2 + Math.random() * 0.4;
|
||||
else if (type.includes('rock')) scale = 0.8;
|
||||
else scale = 1.0;
|
||||
}
|
||||
|
||||
const decorData = {
|
||||
gridX: x,
|
||||
gridY: y,
|
||||
type: structureType,
|
||||
id: `${x},${y}`,
|
||||
maxHp: 5,
|
||||
hp: 5
|
||||
gridX: gridX,
|
||||
gridY: gridY,
|
||||
type: type,
|
||||
id: key,
|
||||
maxHp: 10,
|
||||
hp: 10,
|
||||
scale: scale
|
||||
};
|
||||
this.decorations.push(decorData);
|
||||
this.decorationsMap.set(decorData.id, decorData);
|
||||
const tile = this.getTile(x, y);
|
||||
if (tile) tile.hasDecoration = true;
|
||||
this.lastCullX = -9999;
|
||||
return true;
|
||||
this.decorationsMap.set(key, decorData);
|
||||
|
||||
if (this.tiles[gridY] && this.tiles[gridY][gridX]) {
|
||||
this.tiles[gridY][gridX].hasDecoration = true;
|
||||
}
|
||||
}
|
||||
|
||||
setTileType(x, y, typeName) {
|
||||
if (!this.tiles[y] || !this.tiles[y][x]) return;
|
||||
const typeDef = Object.values(this.terrainTypes).find(t => t.name === typeName);
|
||||
if (!typeDef) return;
|
||||
|
||||
this.tiles[y][x].type = typeName;
|
||||
// Tilemap update
|
||||
if (this.layer) {
|
||||
this.layer.putTileAt(typeDef.index, x, y);
|
||||
|
||||
const key = `${x},${y}`;
|
||||
if (this.visibleTiles.has(key)) {
|
||||
const sprite = this.visibleTiles.get(key);
|
||||
sprite.setTexture(typeName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,7 +461,6 @@ class TerrainSystem {
|
||||
const key = `${x},${y}`;
|
||||
this.cropsMap.set(key, cropData);
|
||||
this.tiles[y][x].hasCrop = true;
|
||||
this.lastCullX = -9999;
|
||||
}
|
||||
|
||||
removeCrop(x, y) {
|
||||
@@ -358,11 +485,6 @@ class TerrainSystem {
|
||||
}
|
||||
}
|
||||
|
||||
init(offsetX, offsetY) {
|
||||
this.offsetX = offsetX;
|
||||
this.offsetY = offsetY;
|
||||
}
|
||||
|
||||
getTile(x, y) {
|
||||
if (this.tiles[y] && this.tiles[y][x]) {
|
||||
return this.tiles[y][x];
|
||||
@@ -371,10 +493,10 @@ class TerrainSystem {
|
||||
}
|
||||
|
||||
updateCulling(camera) {
|
||||
// Culling for Decorations & Crops (Tiles controlled by Tilemap)
|
||||
const view = camera.worldView;
|
||||
let buffer = 200;
|
||||
if (this.scene.settings && this.scene.settings.viewDistance === 'LOW') buffer = 50;
|
||||
|
||||
const left = view.x - buffer - this.offsetX;
|
||||
const top = view.y - buffer - this.offsetY;
|
||||
const right = view.x + view.width + buffer - this.offsetX;
|
||||
@@ -395,14 +517,29 @@ class TerrainSystem {
|
||||
const startY = Math.max(0, minGridY);
|
||||
const endY = Math.min(this.height, maxGridY);
|
||||
|
||||
const neededTileKeys = new Set();
|
||||
const neededDecorKeys = new Set();
|
||||
const neededCropKeys = new Set();
|
||||
|
||||
const voxelOffset = 12;
|
||||
|
||||
for (let y = startY; y < endY; y++) {
|
||||
for (let x = startX; x < endX; x++) {
|
||||
const key = `${x},${y}`;
|
||||
const tile = this.tiles[y][x];
|
||||
|
||||
if (tile) {
|
||||
neededTileKeys.add(key);
|
||||
if (!this.visibleTiles.has(key)) {
|
||||
const sprite = this.tilePool.get();
|
||||
sprite.setTexture(tile.type);
|
||||
const screenPos = this.iso.toScreen(x, y);
|
||||
sprite.setPosition(screenPos.x + this.offsetX, screenPos.y + this.offsetY);
|
||||
sprite.setDepth(this.iso.getDepth(x, y));
|
||||
this.visibleTiles.set(key, sprite);
|
||||
}
|
||||
}
|
||||
|
||||
// DECORATIONS
|
||||
const decor = this.decorationsMap.get(key);
|
||||
if (decor) {
|
||||
neededDecorKeys.add(key);
|
||||
@@ -410,37 +547,35 @@ class TerrainSystem {
|
||||
const sprite = this.decorationPool.get();
|
||||
const screenPos = this.iso.toScreen(x, y);
|
||||
|
||||
sprite.setPosition(screenPos.x + this.offsetX, screenPos.y + this.offsetY);
|
||||
sprite.setPosition(screenPos.x + this.offsetX, screenPos.y + this.offsetY - voxelOffset);
|
||||
|
||||
// Origin adjusted for volumetric sprites
|
||||
// Trees/Rocks usually look best with origin (0.5, 0.9) to sit on the ground
|
||||
if (decor.type.includes('house') || decor.type.includes('market') || decor.type.includes('structure')) {
|
||||
sprite.setOrigin(0.5, 0.8);
|
||||
} else {
|
||||
sprite.setOrigin(0.5, 0.9);
|
||||
sprite.setOrigin(0.5, 1.0);
|
||||
}
|
||||
|
||||
// Texture & Scale
|
||||
sprite.setTexture(decor.type);
|
||||
sprite.setScale(decor.scale || 1.0);
|
||||
|
||||
if (decor.alpha !== undefined) {
|
||||
sprite.setAlpha(decor.alpha);
|
||||
}
|
||||
|
||||
sprite.setDepth(this.iso.getDepth(x, y) + 1);
|
||||
this.visibleDecorations.set(key, sprite);
|
||||
}
|
||||
}
|
||||
|
||||
// CROPS
|
||||
const crop = this.cropsMap.get(key);
|
||||
if (crop) {
|
||||
neededCropKeys.add(key);
|
||||
if (!this.visibleCrops.has(key)) {
|
||||
const sprite = this.cropPool.get();
|
||||
const screenPos = this.iso.toScreen(x, y);
|
||||
sprite.setPosition(screenPos.x + this.offsetX, screenPos.y + this.offsetY);
|
||||
sprite.setPosition(screenPos.x + this.offsetX, screenPos.y + this.offsetY - voxelOffset);
|
||||
sprite.setTexture(`crop_stage_${crop.stage}`);
|
||||
// Crop origin
|
||||
sprite.setOrigin(0.5, 1);
|
||||
// Crop depth
|
||||
sprite.setDepth(this.iso.getDepth(x, y) + 0.5);
|
||||
this.visibleCrops.set(key, sprite);
|
||||
}
|
||||
@@ -448,7 +583,13 @@ class TerrainSystem {
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
for (const [key, sprite] of this.visibleTiles) {
|
||||
if (!neededTileKeys.has(key)) {
|
||||
sprite.setVisible(false);
|
||||
this.tilePool.release(sprite);
|
||||
this.visibleTiles.delete(key);
|
||||
}
|
||||
}
|
||||
for (const [key, sprite] of this.visibleDecorations) {
|
||||
if (!neededDecorKeys.has(key)) {
|
||||
sprite.setVisible(false);
|
||||
|
||||
@@ -233,6 +233,11 @@ class WeatherSystem {
|
||||
|
||||
// Depth just above overlay (-1000)
|
||||
this.rainEmitter.setDepth(-990);
|
||||
|
||||
// Play Sound
|
||||
if (this.scene.soundManager) {
|
||||
this.scene.soundManager.playRainSound();
|
||||
}
|
||||
}
|
||||
|
||||
clearWeather() {
|
||||
@@ -240,6 +245,11 @@ class WeatherSystem {
|
||||
this.rainEmitter.destroy();
|
||||
this.rainEmitter = null;
|
||||
}
|
||||
|
||||
// Stop Sound
|
||||
if (this.scene.soundManager) {
|
||||
this.scene.soundManager.stopRainSound();
|
||||
}
|
||||
}
|
||||
|
||||
// --- Getters for Other Systems ---
|
||||
@@ -260,4 +270,9 @@ class WeatherSystem {
|
||||
const hour = this.gameTime;
|
||||
return hour >= 7 && hour < 18;
|
||||
}
|
||||
|
||||
isHordeNight() {
|
||||
// Every 3rd night is a Horde Night
|
||||
return this.dayCount > 0 && this.dayCount % 3 === 0;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user