Phase 7: World Structure (New Direction)
This commit is contained in:
122
src/entities/LootChest.js
Normal file
122
src/entities/LootChest.js
Normal file
@@ -0,0 +1,122 @@
|
||||
class LootChest {
|
||||
constructor(scene, gridX, gridY, lootTable = 'basic') {
|
||||
this.scene = scene;
|
||||
this.gridX = gridX;
|
||||
this.gridY = gridY;
|
||||
this.lootTable = lootTable;
|
||||
this.isOpened = false;
|
||||
|
||||
this.createSprite();
|
||||
}
|
||||
|
||||
createSprite() {
|
||||
const screenPos = this.scene.iso.toScreen(this.gridX, this.gridY);
|
||||
const x = screenPos.x + this.scene.terrainOffsetX;
|
||||
const y = screenPos.y + this.scene.terrainOffsetY;
|
||||
|
||||
// Create chest sprite (using existing chest texture)
|
||||
this.sprite = this.scene.add.sprite(x, y, 'chest');
|
||||
this.sprite.setOrigin(0.5, 1);
|
||||
this.sprite.setDepth(this.scene.iso.getDepth(this.gridX, this.gridY, this.scene.iso.LAYER_OBJECTS));
|
||||
|
||||
// Golden glow for unopened chests
|
||||
if (!this.isOpened) {
|
||||
this.sprite.setTint(0xFFDD00);
|
||||
|
||||
// Gentle floating animation
|
||||
this.scene.tweens.add({
|
||||
targets: this.sprite,
|
||||
y: y - 5,
|
||||
duration: 1500,
|
||||
yoyo: true,
|
||||
repeat: -1,
|
||||
ease: 'Sine.easeInOut'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getLootTable() {
|
||||
const tables = {
|
||||
'basic': [
|
||||
{ item: 'seeds', count: 10, chance: 1.0 },
|
||||
{ item: 'seeds_wheat', count: 5, chance: 0.8 },
|
||||
{ item: 'wood', count: 10, chance: 0.6 },
|
||||
{ item: 'stone', count: 10, chance: 0.5 }
|
||||
],
|
||||
'farm_starter': [
|
||||
{ item: 'seeds_wheat', count: 15, chance: 1.0 },
|
||||
{ item: 'seeds_corn', count: 10, chance: 1.0 },
|
||||
{ item: 'hoe', count: 1, chance: 1.0 },
|
||||
{ item: 'watering_can', count: 1, chance: 0.8 },
|
||||
{ item: 'wood', count: 20, chance: 0.9 }
|
||||
],
|
||||
'city': [
|
||||
{ item: 'gold', count: 50, chance: 1.0 },
|
||||
{ item: 'stone', count: 30, chance: 0.9 },
|
||||
{ item: 'iron', count: 10, chance: 0.7 },
|
||||
{ item: 'seeds_corn', count: 5, chance: 0.6 },
|
||||
{ item: 'axe', count: 1, chance: 0.3 },
|
||||
{ item: 'pickaxe', count: 1, chance: 0.3 }
|
||||
],
|
||||
'elite': [
|
||||
{ item: 'gold', count: 100, chance: 1.0 },
|
||||
{ item: 'iron', count: 25, chance: 1.0 },
|
||||
{ item: 'diamond', count: 3, chance: 0.5 },
|
||||
{ item: 'seeds_corn', count: 20, chance: 0.8 }
|
||||
]
|
||||
};
|
||||
|
||||
return tables[this.lootTable] || tables['basic'];
|
||||
}
|
||||
|
||||
open(player) {
|
||||
if (this.isOpened) return false;
|
||||
|
||||
this.isOpened = true;
|
||||
this.sprite.clearTint();
|
||||
this.sprite.setTint(0x888888); // Gray for opened
|
||||
|
||||
// Stop animation
|
||||
this.scene.tweens.killTweensOf(this.sprite);
|
||||
|
||||
// Spawn loot
|
||||
const loot = this.getLootTable();
|
||||
for (const entry of loot) {
|
||||
if (Math.random() < entry.chance) {
|
||||
if (entry.item === 'gold') {
|
||||
if (this.scene.inventorySystem) {
|
||||
this.scene.inventorySystem.addGold(entry.count);
|
||||
}
|
||||
} else {
|
||||
if (this.scene.interactionSystem) {
|
||||
this.scene.interactionSystem.spawnLoot(
|
||||
this.gridX,
|
||||
this.gridY,
|
||||
entry.item,
|
||||
entry.count
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Effects
|
||||
this.scene.events.emit('show-floating-text', {
|
||||
x: this.gridX * 48,
|
||||
y: this.gridY * 48,
|
||||
text: '📦 Chest Opened!',
|
||||
color: '#FFD700'
|
||||
});
|
||||
|
||||
if (this.scene.soundManager) {
|
||||
this.scene.soundManager.playHarvest();
|
||||
}
|
||||
|
||||
console.log(`📦 Opened ${this.lootTable} chest at ${this.gridX},${this.gridY}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
interact(player) {
|
||||
return this.open(player);
|
||||
}
|
||||
}
|
||||
104
src/entities/ZombieSpawner.js
Normal file
104
src/entities/ZombieSpawner.js
Normal file
@@ -0,0 +1,104 @@
|
||||
class ZombieSpawner {
|
||||
constructor(scene, gridX, gridY, spawnRadius = 5, maxZombies = 3, respawnTime = 30000) {
|
||||
this.scene = scene;
|
||||
this.gridX = gridX;
|
||||
this.gridY = gridY;
|
||||
this.spawnRadius = spawnRadius;
|
||||
this.maxZombies = maxZombies;
|
||||
this.respawnTime = respawnTime;
|
||||
this.spawnedZombies = [];
|
||||
this.respawnTimer = 0;
|
||||
this.isActive = true;
|
||||
|
||||
this.createVisual();
|
||||
}
|
||||
|
||||
createVisual() {
|
||||
const screenPos = this.scene.iso.toScreen(this.gridX, this.gridY);
|
||||
const x = screenPos.x + this.scene.terrainOffsetX;
|
||||
const y = screenPos.y + this.scene.terrainOffsetY;
|
||||
|
||||
// Spawner sprite (dark portal/grave)
|
||||
this.sprite = this.scene.add.sprite(x, y, 'gravestone');
|
||||
this.sprite.setOrigin(0.5, 1);
|
||||
this.sprite.setDepth(this.scene.iso.getDepth(this.gridX, this.gridY, this.scene.iso.LAYER_OBJECTS));
|
||||
this.sprite.setTint(0x440044); // Purple tint for spawner
|
||||
|
||||
// Pulsing effect
|
||||
this.scene.tweens.add({
|
||||
targets: this.sprite,
|
||||
alpha: 0.6,
|
||||
duration: 1000,
|
||||
yoyo: true,
|
||||
repeat: -1
|
||||
});
|
||||
}
|
||||
|
||||
spawn() {
|
||||
if (this.spawnedZombies.length >= this.maxZombies) return;
|
||||
|
||||
// Random position around spawner
|
||||
const offsetX = Phaser.Math.Between(-this.spawnRadius, this.spawnRadius);
|
||||
const offsetY = Phaser.Math.Between(-this.spawnRadius, this.spawnRadius);
|
||||
const spawnX = this.gridX + offsetX;
|
||||
const spawnY = this.gridY + offsetY;
|
||||
|
||||
// Create zombie
|
||||
const zombie = new NPC(
|
||||
this.scene,
|
||||
spawnX,
|
||||
spawnY,
|
||||
this.scene.terrainOffsetX,
|
||||
this.scene.terrainOffsetY,
|
||||
'zombie'
|
||||
);
|
||||
|
||||
zombie.spawner = this; // Reference back to spawner
|
||||
this.spawnedZombies.push(zombie);
|
||||
this.scene.npcs.push(zombie);
|
||||
|
||||
// Spawn effect
|
||||
this.scene.events.emit('show-floating-text', {
|
||||
x: spawnX * 48,
|
||||
y: spawnY * 48,
|
||||
text: '💀 Spawn!',
|
||||
color: '#FF00FF'
|
||||
});
|
||||
|
||||
if (this.scene.soundManager) {
|
||||
this.scene.soundManager.playHit(); // Re-use hit sound for spawn
|
||||
}
|
||||
|
||||
console.log(`👹 Spawner at ${this.gridX},${this.gridY} spawned zombie`);
|
||||
}
|
||||
|
||||
removeZombie(zombie) {
|
||||
const index = this.spawnedZombies.indexOf(zombie);
|
||||
if (index > -1) {
|
||||
this.spawnedZombies.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
update(delta) {
|
||||
if (!this.isActive) return;
|
||||
|
||||
// Clean up dead zombies
|
||||
this.spawnedZombies = this.spawnedZombies.filter(z =>
|
||||
this.scene.npcs.includes(z) && z.hp > 0
|
||||
);
|
||||
|
||||
// Respawn check
|
||||
if (this.spawnedZombies.length < this.maxZombies) {
|
||||
this.respawnTimer += delta;
|
||||
if (this.respawnTimer >= this.respawnTime) {
|
||||
this.respawnTimer = 0;
|
||||
this.spawn();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.sprite) this.sprite.destroy();
|
||||
if (this.particles) this.particles.destroy();
|
||||
}
|
||||
}
|
||||
@@ -171,6 +171,42 @@ class GameScene extends Phaser.Scene {
|
||||
const scooter = new Scooter(this, 25, 25);
|
||||
this.vehicles.push(scooter);
|
||||
|
||||
// ZOMBIE SPAWNERS (City area)
|
||||
console.log('👹 Creating Zombie Spawners...');
|
||||
this.spawners = [];
|
||||
|
||||
// City spawners (3 spawners around city)
|
||||
this.spawners.push(new ZombieSpawner(this, 55, 55, 5, 2, 25000)); // NW
|
||||
this.spawners.push(new ZombieSpawner(this, 75, 55, 5, 2, 25000)); // NE
|
||||
this.spawners.push(new ZombieSpawner(this, 65, 75, 5, 3, 20000)); // South (more zombies, faster)
|
||||
|
||||
// LOOT CHESTS
|
||||
console.log('📦 Placing Loot Chests...');
|
||||
this.chests = [];
|
||||
|
||||
// Farm Starter Chest (near spawn)
|
||||
this.chests.push(new LootChest(this, 28, 28, 'farm_starter'));
|
||||
|
||||
// City Chests (3 chests in city)
|
||||
this.chests.push(new LootChest(this, 60, 60, 'city'));
|
||||
this.chests.push(new LootChest(this, 70, 60, 'city'));
|
||||
this.chests.push(new LootChest(this, 65, 70, 'elite'));
|
||||
|
||||
// SIGNPOSTS/NAVIGATION
|
||||
console.log('🪧 Adding Signposts...');
|
||||
this.signposts = [];
|
||||
|
||||
// Path markers (using fence sprites as signposts)
|
||||
const pathMarkers = [
|
||||
{ x: 35, y: 35, label: '→ City' },
|
||||
{ x: 50, y: 50, label: '← Farm' },
|
||||
];
|
||||
|
||||
for (const marker of pathMarkers) {
|
||||
this.terrainSystem.addDecoration(marker.x, marker.y, 'fence');
|
||||
this.signposts.push({ gridX: marker.x, gridY: marker.y, label: marker.label });
|
||||
}
|
||||
|
||||
// Kamera sledi igralcu z gladko interpolacijo (lerp 0.1)
|
||||
this.cameras.main.startFollow(this.player.sprite, true, 0.1, 0.1);
|
||||
|
||||
@@ -353,6 +389,13 @@ class GameScene extends Phaser.Scene {
|
||||
}
|
||||
}
|
||||
|
||||
// Spawners Update
|
||||
if (this.spawners) {
|
||||
for (const spawner of this.spawners) {
|
||||
if (spawner.update) spawner.update(delta);
|
||||
}
|
||||
}
|
||||
|
||||
// Parallax
|
||||
if (this.parallaxSystem && this.player) {
|
||||
const playerPos = this.player.getPosition();
|
||||
|
||||
@@ -39,6 +39,17 @@ class InteractionSystem {
|
||||
}
|
||||
}
|
||||
|
||||
// Check for chests
|
||||
if (this.scene.chests) {
|
||||
for (const chest of this.scene.chests) {
|
||||
const d = Phaser.Math.Distance.Between(playerPos.x, playerPos.y, chest.gridX, chest.gridY);
|
||||
if (d < minDist && !chest.isOpened) {
|
||||
minDist = d;
|
||||
nearest = chest;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const npc of candidates) {
|
||||
const d = Phaser.Math.Distance.Between(playerPos.x, playerPos.y, npc.gridX, npc.gridY);
|
||||
if (d < minDist) {
|
||||
@@ -48,10 +59,14 @@ class InteractionSystem {
|
||||
}
|
||||
|
||||
if (nearest) {
|
||||
console.log('E Interacted with:', nearest.type);
|
||||
console.log('E Interacted with:', nearest.type || nearest.lootTable);
|
||||
if (nearest.type === 'scooter') {
|
||||
nearest.interact(this.scene.player);
|
||||
}
|
||||
else if (nearest.lootTable) {
|
||||
// It's a chest!
|
||||
nearest.interact(this.scene.player);
|
||||
}
|
||||
else if (nearest.type === 'zombie') {
|
||||
// Always Tame on E key (Combat is Space/Click)
|
||||
nearest.tame();
|
||||
|
||||
Reference in New Issue
Block a user