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

View File

@@ -18,21 +18,37 @@ class GameScene extends Phaser.Scene {
// Setup kamere
this.cameras.main.setBackgroundColor('#1a1a2e');
// Initialize Isometric Utils
this.iso = new IsometricUtils();
// Inicializiraj terrain sistem - 100x100 mapa
console.log('🌍 Initializing terrain...');
this.terrainSystem = new TerrainSystem(this, 100, 100);
this.terrainSystem.generate();
try {
this.terrainSystem = new TerrainSystem(this, 100, 100);
this.terrainSystem.generate();
// Terrain offset
this.terrainOffsetX = width / 2;
this.terrainOffsetY = 100;
this.terrainContainer = this.terrainSystem.render(this.terrainOffsetX, this.terrainOffsetY);
// Terrain offset
this.terrainOffsetX = width / 2;
this.terrainOffsetY = 100;
// Dodaj igralca - spawn na sredini mape S TERRAIN OFFSETOM
// Initialization for culling
this.terrainSystem.init(this.terrainOffsetX, this.terrainOffsetY);
// Initial force update to render active tiles before first frame
this.terrainSystem.updateCulling(this.cameras.main);
// FAZA 14: Spawn Ruin (Town Project) at fixed location near player
console.log('🏚️ Spawning Ruin...');
this.terrainSystem.placeStructure(55, 55, 'ruin');
} catch (e) {
console.error("Terrain system failed:", e);
}
// Dodaj igralca
console.log('👤 Initializing player...');
this.player = new Player(this, 50, 50, this.terrainOffsetX, this.terrainOffsetY);
// Dodaj 3 NPCje - random pozicije
// Dodaj 3 NPCje
console.log('🧟 Initializing NPCs...');
const npcTypes = ['zombie', 'villager', 'merchant'];
for (let i = 0; i < 3; i++) {
@@ -42,36 +58,59 @@ class GameScene extends Phaser.Scene {
this.npcs.push(npc);
}
// Kamera sledi igralcu
this.cameras.main.startFollow(this.player.sprite, true, 0.1, 0.1);
// Kamera sledi igralcu z izboljšanimi nastavitvami
this.cameras.main.startFollow(this.player.sprite, true, 1.0, 1.0); // Instant follow (was 0.1)
// Nastavi deadzone (100px border)
this.cameras.main.setDeadzone(100, 100);
// Round pixels za crisp pixel art
this.cameras.main.roundPixels = true;
// Parallax oblaki
this.createClouds();
// Kamera kontrole
this.setupCamera();
// UI elementi
this.createUI();
// Initialize Time & Stats
console.log('⏳ Initializing Time & Stats...');
this.timeSystem = new TimeSystem(this);
this.timeSystem.create();
// Debug info
this.debugText = this.add.text(10, 10, '', {
fontFamily: 'Courier New',
fontSize: '12px',
fill: '#ffffff',
backgroundColor: '#000000',
padding: { x: 5, y: 3 }
});
this.debugText.setScrollFactor(0);
this.debugText.setDepth(1000);
this.statsSystem = new StatsSystem(this);
this.inventorySystem = new InventorySystem(this);
this.interactionSystem = new InteractionSystem(this);
this.farmingSystem = new FarmingSystem(this);
this.buildingSystem = new BuildingSystem(this);
// FPS counter
this.fpsText = this.add.text(10, height - 30, 'FPS: 60', {
fontFamily: 'Courier New',
fontSize: '14px',
fill: '#00ff41'
});
this.fpsText.setScrollFactor(0);
this.fpsText.setDepth(1000);
// Initialize Weather System
console.log('🌦️ Initializing Weather System...');
this.weatherSystem = new WeatherSystem(this);
console.log('✅ GameScene ready - FAZA 3!');
// Initialize Day/Night Cycle
console.log('🌅 Initializing Day/Night System...');
this.dayNightSystem = new DayNightSystem(this, this.timeSystem);
// Initialize Sound Manager
console.log('🎵 Initializing Sound Manager...');
this.soundManager = new SoundManager(this);
// Initialize Parallax System
console.log('🌄 Initializing Parallax System...');
this.parallaxSystem = new ParallaxSystem(this);
// Launch UI Scene
console.log('🖥️ Launching UI Scene...');
this.scene.launch('UIScene');
// Initialize Save System
this.saveSystem = new SaveSystem(this);
// Auto-load if available (optional, for now manual)
// this.saveSystem.loadGame();
console.log('✅ GameScene ready - FAZA 17!');
}
setupCamera() {
@@ -88,51 +127,81 @@ class GameScene extends Phaser.Scene {
cam.setZoom(newZoom);
});
// Pan kontrole (Right click + drag) - DISABLED za FAZA 2
// Player movement sedaj uporablja WASD
// Q/E za zoom
this.zoomKeys = this.input.keyboard.addKeys({
zoomIn: Phaser.Input.Keyboard.KeyCodes.Q,
zoomOut: Phaser.Input.Keyboard.KeyCodes.E
});
}
createUI() {
const width = this.cameras.main.width;
// Naslov
const title = this.add.text(width / 2, 20, 'FAZA 3: NPC-ji in Dekoracije', {
fontFamily: 'Courier New',
fontSize: '20px',
fill: '#00ff41',
fontStyle: 'bold'
});
title.setOrigin(0.5, 0);
title.setScrollFactor(0);
title.setDepth(1000);
// Kontrole info
const controlsText = this.add.text(width - 10, 10,
'Kontrole:\n' +
'WASD - Gibanje igralca\n' +
'Q/E - Zoom\n' +
'Mouse Wheel - Zoom',
{
fontFamily: 'Courier New',
fontSize: '11px',
fill: '#888888',
backgroundColor: '#000000',
padding: { x: 5, y: 3 },
align: 'right'
// Save/Load Keys
this.input.keyboard.on('keydown-F8', () => {
// Save
if (this.saveSystem) {
this.saveSystem.saveGame();
console.log('💾 Game Saved! (F8)');
}
);
controlsText.setOrigin(1, 0);
controlsText.setScrollFactor(0);
controlsText.setDepth(1000);
});
this.input.keyboard.on('keydown-F9', () => {
// Load
if (this.saveSystem) {
this.saveSystem.loadGame();
console.log('📂 Game Loaded! (F9)');
}
});
// Build Mode Keys
this.input.keyboard.on('keydown-B', () => {
if (this.buildingSystem) this.buildingSystem.toggleBuildMode();
});
this.input.keyboard.on('keydown-ONE', () => {
if (this.buildingSystem && this.buildingSystem.isBuildMode) this.buildingSystem.selectBuilding('fence');
else if (this.scene.get('UIScene')) this.scene.get('UIScene').selectSlot(0);
});
this.input.keyboard.on('keydown-TWO', () => {
if (this.buildingSystem && this.buildingSystem.isBuildMode) this.buildingSystem.selectBuilding('wall');
else if (this.scene.get('UIScene')) this.scene.get('UIScene').selectSlot(1);
});
this.input.keyboard.on('keydown-THREE', () => {
if (this.buildingSystem && this.buildingSystem.isBuildMode) this.buildingSystem.selectBuilding('house');
else if (this.scene.get('UIScene')) this.scene.get('UIScene').selectSlot(2);
});
// Soft Reset (F4) - Force Reload Page
this.input.keyboard.on('keydown-F4', () => {
console.log('🔄 Soft Reset Initiated (Force Reload)...');
window.location.reload();
});
// Mute Toggle (M key)
this.input.keyboard.on('keydown-M', () => {
if (this.soundManager) {
this.soundManager.toggleMute();
}
});
}
update(time, delta) {
// Update Systems
if (this.timeSystem) this.timeSystem.update(delta);
if (this.statsSystem) this.statsSystem.update(delta);
if (this.interactionSystem) this.interactionSystem.update(delta);
if (this.farmingSystem) this.farmingSystem.update(delta);
if (this.weatherSystem) this.weatherSystem.update(delta);
if (this.dayNightSystem) this.dayNightSystem.update();
// Update Parallax (foreground grass fading)
if (this.parallaxSystem && this.player) {
const playerPos = this.player.getPosition();
const screenPos = this.iso.toScreen(playerPos.x, playerPos.y);
this.parallaxSystem.update(
screenPos.x + this.terrainOffsetX,
screenPos.y + this.terrainOffsetY
);
}
// Update player
if (this.player) {
this.player.update(delta);
@@ -143,32 +212,61 @@ class GameScene extends Phaser.Scene {
npc.update(delta);
}
// Update FPS
if (this.fpsText) {
this.fpsText.setText(`FPS: ${Math.round(this.game.loop.actualFps)}`);
// Update Terrain Culling
if (this.terrainSystem) {
this.terrainSystem.updateCulling(this.cameras.main);
}
// Zoom controls
const cam = this.cameras.main;
if (this.zoomKeys) {
if (this.zoomKeys.zoomIn.isDown) {
cam.setZoom(Phaser.Math.Clamp(cam.zoom + 0.01, 0.3, 2.0));
}
if (this.zoomKeys.zoomOut.isDown) {
cam.setZoom(Phaser.Math.Clamp(cam.zoom - 0.01, 0.3, 2.0));
// Update clouds
if (this.clouds) {
for (const cloud of this.clouds) {
cloud.sprite.x += cloud.speed * (delta / 1000);
if (cloud.sprite.x > this.terrainOffsetX + 2000) { // Reset far right
cloud.sprite.x = this.terrainOffsetX - 2000;
cloud.sprite.y = Phaser.Math.Between(0, 1000);
}
}
}
// Debug info update
if (this.debugText && this.player) {
// Send debug info to UI Scene
if (this.player) {
const playerPos = this.player.getPosition();
const cam = this.cameras.main;
const visibleTiles = this.terrainSystem ? this.terrainSystem.visibleTiles.size : 0;
this.debugText.setText(
`FAZA 3 - NPCs & Decorations\n` +
`Zoom: ${cam.zoom.toFixed(2)}\n` +
`Player: (${playerPos.x}, ${playerPos.y})\n` +
`NPCs: ${this.npcs.length}`
);
const uiScene = this.scene.get('UIScene');
if (uiScene && uiScene.debugText) {
const activeCrops = this.terrainSystem && this.terrainSystem.cropsMap ? this.terrainSystem.cropsMap.size : 0;
const dropsCount = this.interactionSystem && this.interactionSystem.drops ? this.interactionSystem.drops.length : 0;
uiScene.debugText.setText(
`FAZA 11 - Building\n` +
`[F5] Save | [F9] Load | [B] Build Mode\n` +
`Time: ${this.timeSystem ? this.timeSystem.gameTime.toFixed(1) : '?'}h\n` +
`Active Crops: ${activeCrops}\n` +
`Loot Drops: ${dropsCount}\n` +
`Player: (${playerPos.x}, ${playerPos.y})`
);
}
}
}
createClouds() {
if (!this.textures.exists('cloud')) TextureGenerator.createCloudSprite(this, 'cloud');
this.clouds = [];
console.log('☁️ Creating parallax clouds...');
for (let i = 0; i < 8; i++) {
const x = Phaser.Math.Between(-1000, 3000);
const y = Phaser.Math.Between(-500, 1500);
const cloud = this.add.sprite(x, y, 'cloud');
cloud.setAlpha(0.4);
cloud.setScrollFactor(0.2); // Parallax effect
cloud.setDepth(2000); // Nad vsem
cloud.setScale(Phaser.Math.FloatBetween(2, 4)); // Veliki oblaki
this.clouds.push({ sprite: cloud, speed: Phaser.Math.FloatBetween(10, 30) });
}
}
}

View File

@@ -5,32 +5,125 @@ class PreloadScene extends Phaser.Scene {
}
preload() {
console.log('📦 PreloadScene: Loading assets...');
console.log(' PreloadScene: Loading assets...');
// TODO: Tu bomo nalagali sprite-e, tile-e, audio, itd.
// Za fazo 0 pustimo prazno - samo testiramo osnovni setup
// Load ALL custom sprites
this.load.image('player_sprite', 'assets/player_sprite.png');
this.load.image('zombie_sprite', 'assets/zombie_sprite.png');
this.load.image('merchant_sprite', 'assets/merchant_sprite.png');
this.load.image('house_sprite', 'assets/house_sprite.png');
this.load.image('stone_sprite', 'assets/stone_sprite.png');
this.load.image('tree_sprite', 'assets/tree_sprite.png');
this.load.image('grass_sprite', 'assets/grass_sprite.png');
this.load.image('grass_tile', 'assets/grass_tile.png');
this.load.image('leaf_sprite', 'assets/leaf_sprite.png');
this.load.image('wheat_sprite', 'assets/wheat_sprite.png');
this.load.image('stone_texture', 'assets/stone_texture.png');
// New asset packs
this.load.image('objects_pack', 'assets/objects_pack.png');
this.load.image('walls_pack', 'assets/walls_pack.png');
this.load.image('ground_tiles', 'assets/ground_tiles.png');
this.load.image('objects_pack2', 'assets/objects_pack2.png');
this.load.image('trees_vegetation', 'assets/trees_vegetation.png');
// Wait for load completion then process transparency
this.load.once('complete', () => {
this.processAllTransparency();
});
}
processAllTransparency() {
// Process ALL sprites to remove backgrounds
const spritesToProcess = [
'player_sprite',
'zombie_sprite',
'merchant_sprite',
'house_sprite',
'stone_sprite',
'tree_sprite',
'grass_sprite',
'leaf_sprite',
'wheat_sprite',
'stone_texture'
];
spritesToProcess.forEach(spriteKey => {
this.processSpriteTransparency(spriteKey);
});
console.log('✅ All sprites transparency processed!');
}
processSpriteTransparency(spriteKey) {
if (!this.textures.exists(spriteKey)) return;
const texture = this.textures.get(spriteKey);
const source = texture.getSourceImage();
// Create canvas to process image
const canvas = document.createElement('canvas');
canvas.width = source.width;
canvas.height = source.height;
const ctx = canvas.getContext('2d', { willReadFrequently: true });
// Draw original image
ctx.drawImage(source, 0, 0);
// Get image data
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
// Remove backgrounds
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
// Remove white/light gray backgrounds (all sprites)
if (r > 200 && g > 200 && b > 200) {
data[i + 3] = 0;
}
// Special: Remove brown/tan backgrounds (merchant sprite)
if (spriteKey === 'merchant_sprite') {
// Brown detection: R > G > B, warm tones
const isBrown = r > 100 && r > g && g > b && (r - b) > 40;
if (isBrown) {
data[i + 3] = 0;
}
}
}
// Put processed data back
ctx.putImageData(imageData, 0, 0);
// Create new texture from processed canvas
this.textures.remove(spriteKey);
this.textures.addCanvas(spriteKey, canvas);
}
create() {
console.log('✅ PreloadScene: Assets loaded!');
window.gameState.currentScene = 'PreloadScene';
// Prikaz začetnega sporočila
const width = this.cameras.main.width;
const height = this.cameras.main.height;
const title = this.add.text(width / 2, height / 2 - 50, 'NOVAFARMA', {
const title = this.add.text(width / 2, height / 2 - 50, 'KRVAVA ŽETEV', {
fontFamily: 'Courier New',
fontSize: '48px',
fill: '#00ff41',
fontStyle: 'bold'
fill: '#ff0000',
fontStyle: 'bold',
stroke: '#000000',
strokeThickness: 6
});
title.setOrigin(0.5);
const subtitle = this.add.text(width / 2, height / 2 + 10, '2.5D Isometric Survival Game', {
const subtitle = this.add.text(width / 2, height / 2 + 10, 'Zombie Roots', {
fontFamily: 'Courier New',
fontSize: '20px',
fill: '#ffffff'
fontSize: '24px',
fill: '#00ff41'
});
subtitle.setOrigin(0.5);
@@ -41,7 +134,6 @@ class PreloadScene extends Phaser.Scene {
});
instruction.setOrigin(0.5);
// Blinking effect
this.tweens.add({
targets: instruction,
alpha: 0.3,
@@ -50,10 +142,25 @@ class PreloadScene extends Phaser.Scene {
repeat: -1
});
// Pritisk SPACE za začetek igre
this.input.keyboard.once('keydown-SPACE', () => {
console.log('🎮 Starting GameScene...');
this.scene.start('GameScene');
const startGame = () => {
console.log('🎮 Starting StoryScene...');
this.input.keyboard.off('keydown');
this.input.off('pointerdown');
this.scene.start('StoryScene');
};
this.time.delayedCall(3000, () => {
startGame();
});
this.input.keyboard.on('keydown', (event) => {
if (event.code === 'Space' || event.code === 'Enter') {
startGame();
}
});
this.input.on('pointerdown', () => {
startGame();
});
}
}

70
src/scenes/StoryScene.js Normal file
View File

@@ -0,0 +1,70 @@
class StoryScene extends Phaser.Scene {
constructor() {
super({ key: 'StoryScene' });
}
create() {
const width = this.cameras.main.width;
const height = this.cameras.main.height;
// Black background
this.add.rectangle(0, 0, width, height, 0x000000).setOrigin(0);
const storyText =
`Leto 2084.
Svet, kot smo ga poznali, je izginil.
Virus "Zmaj-Volka" je spremenil človeštvo.
Mesta so ruševine. Narava je divja.
Toda ti si drugačen.
Preživel si napad. Okužen, a imun.
Si HIBRID.
Zombiji te ne napadajo... čutijo te.
Zanje si ALFA.
Tvoja naloga:
1. Najdi izgubljeno sestro.
2. Maščuj starše.
3. Obnovi civilizacijo iz pepela.
Dobrodošel v KRVAVI ŽETVI.`;
const textObj = this.add.text(width / 2, height + 100, storyText, {
fontFamily: 'Courier New',
fontSize: '24px',
fill: '#00ff41',
align: 'center',
lineSpacing: 10
});
textObj.setOrigin(0.5, 0);
// Scroll animation
this.tweens.add({
targets: textObj,
y: 50,
duration: 10000, // 10s scroll
ease: 'Linear',
onComplete: () => {
this.time.delayedCall(2000, () => {
this.scene.start('GameScene');
});
}
});
// Skip instructions
const skip = this.add.text(width - 20, height - 20, '[SPACE] Skip', {
fontSize: '16px', fill: '#666'
}).setOrigin(1);
// Input to skip
this.input.keyboard.on('keydown-SPACE', () => {
this.scene.start('GameScene');
});
this.input.on('pointerdown', () => {
this.scene.start('GameScene');
});
}
}

451
src/scenes/UIScene.js Normal file
View File

@@ -0,0 +1,451 @@
class UIScene extends Phaser.Scene {
constructor() {
super({ key: 'UIScene' });
}
create() {
console.log('🖥️ UIScene: Initialized!');
// Pridobi reference na GameScene podatke (ko bodo na voljo)
this.gameScene = this.scene.get('GameScene');
// Setup UI Container
this.width = this.cameras.main.width;
this.height = this.cameras.main.height;
this.createStatusBars();
this.createInventoryBar();
this.createGoldDisplay();
this.createClock();
this.createDebugInfo();
// Listen for events from GameScene if needed
}
createStatusBars() {
const x = 20;
const y = 20;
const width = 200;
const height = 20;
const padding = 10;
// Style
const boxStyle = {
fillStyle: { color: 0x000000, alpha: 0.5 },
lineStyle: { width: 2, color: 0xffffff, alpha: 0.8 }
};
// 1. Health Bar
this.add.text(x, y - 5, 'HP', { fontSize: '12px', fontFamily: 'Courier New', fill: '#ffffff' });
this.healthBar = this.createBar(x + 30, y, width, height, 0xff0000);
this.setBarValue(this.healthBar, 100);
// 2. Hunger Bar
this.add.text(x, y + height + padding - 5, 'HUN', { fontSize: '12px', fontFamily: 'Courier New', fill: '#ffffff' });
this.hungerBar = this.createBar(x + 30, y + height + padding, width, height, 0xff8800);
this.setBarValue(this.hungerBar, 80);
// 3. Thirst Bar
this.add.text(x, y + (height + padding) * 2 - 5, 'H2O', { fontSize: '12px', fontFamily: 'Courier New', fill: '#ffffff' });
this.thirstBar = this.createBar(x + 30, y + (height + padding) * 2, width, height, 0x0088ff);
this.setBarValue(this.thirstBar, 90);
}
createBar(x, y, width, height, color) {
// Background
const bg = this.add.graphics();
bg.fillStyle(0x000000, 0.5);
bg.fillRect(x, y, width, height);
bg.lineStyle(2, 0xffffff, 0.2);
bg.strokeRect(x, y, width, height);
// Fill
const fill = this.add.graphics();
fill.fillStyle(color, 1);
fill.fillRect(x + 2, y + 2, width - 4, height - 4);
return { bg, fill, x, y, width, height, color };
}
setBarValue(bar, percent) {
// Clamp 0-100
percent = Phaser.Math.Clamp(percent, 0, 100);
bar.fill.clear();
bar.fill.fillStyle(bar.color, 1);
const maxWidth = bar.width - 4;
const currentWidth = (maxWidth * percent) / 100;
bar.fill.fillRect(bar.x + 2, bar.y + 2, currentWidth, bar.height - 4);
}
createInventoryBar() {
const slotCount = 9;
const slotSize = 48; // 48x48 sloti
const padding = 5;
const totalWidth = (slotCount * slotSize) + ((slotCount - 1) * padding);
const startX = (this.width - totalWidth) / 2;
const startY = this.height - slotSize - 20;
this.inventorySlots = [];
this.selectedSlot = 0;
for (let i = 0; i < slotCount; i++) {
const x = startX + i * (slotSize + padding);
// Slot Background
const slot = this.add.graphics();
// Draw function to update style based on selection
slot.userData = { x, y: startY, size: slotSize, index: i };
this.drawSlot(slot, false);
// Add number text
this.add.text(x + 2, startY + 2, (i + 1).toString(), {
fontSize: '10px',
fontFamily: 'monospace',
fill: '#ffffff'
});
this.inventorySlots.push(slot);
}
// Select first one initially
this.selectSlot(0);
// Keyboard inputs 1-9
this.input.keyboard.on('keydown', (event) => {
const num = parseInt(event.key);
if (!isNaN(num) && num >= 1 && num <= 9) {
this.selectSlot(num - 1);
}
});
// Mouse scroll for inventory (optional)
this.input.on('wheel', (pointer, gameObjects, deltaX, deltaY, deltaZ) => {
if (deltaY > 0) {
this.selectSlot((this.selectedSlot + 1) % slotCount);
} else if (deltaY < 0) {
this.selectSlot((this.selectedSlot - 1 + slotCount) % slotCount);
}
});
}
drawSlot(graphics, isSelected) {
const { x, y, size } = graphics.userData;
graphics.clear();
// Background
graphics.fillStyle(0x000000, 0.6);
graphics.fillRect(x, y, size, size);
// Border
if (isSelected) {
graphics.lineStyle(3, 0xffff00, 1); // Yellow thick border for selection
} else {
graphics.lineStyle(2, 0x888888, 0.5); // Grey thin border
}
graphics.strokeRect(x, y, size, size);
}
selectSlot(index) {
// Deselect current
if (this.inventorySlots[this.selectedSlot]) {
this.drawSlot(this.inventorySlots[this.selectedSlot], false);
}
this.selectedSlot = index;
// Select new
this.drawSlot(this.inventorySlots[this.selectedSlot], true);
}
updateInventory(slots) {
if (!this.inventorySlots) return;
for (let i = 0; i < this.inventorySlots.length; i++) {
const slotGraphics = this.inventorySlots[i];
// Clear previous item info (we stored it in container? No, just graphics)
// Ideally slots should be containers.
// For now, let's just redraw the slot and add text on top.
// To do this cleanly, let's remove old item text/sprites if we track them.
if (slotGraphics.itemText) slotGraphics.itemText.destroy();
if (slots[i]) {
const { x, y, size } = slotGraphics.userData;
// Simple representation: Text
const text = this.add.text(x + size / 2, y + size / 2,
`${slots[i].type.substring(0, 2)}\n${slots[i].count}`,
{ fontSize: '10px', align: 'center', color: '#ffff00' }
).setOrigin(0.5);
slotGraphics.itemText = text;
}
}
}
createClock() {
// Clock box top right
const x = this.width - 150;
const y = 20;
// Background
const bg = this.add.graphics();
bg.fillStyle(0x000000, 0.5);
bg.fillRect(x, y, 130, 40);
bg.lineStyle(2, 0xffffff, 0.8);
bg.strokeRect(x, y, 130, 40);
this.clockText = this.add.text(x + 65, y + 20, 'Day 1 - 08:00', {
fontSize: '14px',
fontFamily: 'Courier New',
fill: '#ffffff',
fontStyle: 'bold'
});
this.clockText.setOrigin(0.5, 0.5);
}
createGoldDisplay() {
const x = this.width - 150;
const y = 70; // Below clock
// Background
const bg = this.add.graphics();
bg.fillStyle(0xDAA520, 0.2); // Goldish bg
bg.fillRect(x, y, 130, 30);
bg.lineStyle(2, 0xFFD700, 0.8);
bg.strokeRect(x, y, 130, 30);
this.goldText = this.add.text(x + 65, y + 15, 'GOLD: 0', {
fontSize: '16px',
fontFamily: 'Courier New',
fill: '#FFD700', // Gold color
fontStyle: 'bold'
});
this.goldText.setOrigin(0.5, 0.5);
}
updateGold(amount) {
if (this.goldText) {
this.goldText.setText(`GOLD: ${amount}`);
}
}
createDebugInfo() {
this.debugText = this.add.text(10, 100, '', {
fontSize: '12px',
fontFamily: 'monospace',
fill: '#00ff00'
});
}
update() {
// Here we could update bars based on player stats
// if (this.gameScene && this.gameScene.player) { ... }
}
toggleBuildMenu(isVisible) {
if (!this.buildMenuContainer) {
this.createBuildMenuInfo();
}
this.buildMenuContainer.setVisible(isVisible);
}
createBuildMenuInfo() {
this.buildMenuContainer = this.add.container(this.width / 2, 100);
const bg = this.add.graphics();
bg.fillStyle(0x000000, 0.7);
bg.fillRect(-150, 0, 300, 100);
bg.lineStyle(2, 0x00FF00, 1);
bg.strokeRect(-150, 0, 300, 100);
this.buildMenuContainer.add(bg);
const title = this.add.text(0, 10, 'BUILD MODE [B]', { fontSize: '18px', fill: '#00FF00', fontStyle: 'bold' }).setOrigin(0.5, 0);
this.buildMenuContainer.add(title);
const info = this.add.text(0, 40,
'[1] Fence (2 Wood)\n[2] Wall (2 Stone)\n[3] House (20W 20S 50G)',
{ fontSize: '14px', fill: '#ffffff', align: 'center' }
).setOrigin(0.5, 0);
this.buildMenuContainer.add(info);
this.selectedBuildingText = this.add.text(0, 80, 'Selected: Fence', { fontSize: '14px', fill: '#FFFF00' }).setOrigin(0.5, 0);
this.buildMenuContainer.add(this.selectedBuildingText);
this.buildMenuContainer.setVisible(false);
}
updateBuildSelection(name) {
if (this.selectedBuildingText) {
this.selectedBuildingText.setText(`Selected: ${name.toUpperCase()}`);
}
}
showProjectMenu(ruinData, onContribute) {
if (!this.projectMenuContainer) {
this.createProjectMenu();
}
// Update info
const costText = `Req: ${ruinData.reqWood} Wood, ${ruinData.reqStone} Stone`;
this.projectInfoText.setText(`RESTORING RUINS\n${costText}`);
this.onContributeCallback = onContribute;
this.projectMenuContainer.setVisible(true);
this.projectMenuContainer.setDepth(10000);
}
createProjectMenu() {
this.projectMenuContainer = this.add.container(this.width / 2, this.height / 2);
// BG
const bg = this.add.graphics();
bg.fillStyle(0x222222, 0.9);
bg.fillRect(-150, -100, 300, 200);
bg.lineStyle(2, 0x00FFFF, 1);
bg.strokeRect(-150, -100, 300, 200);
this.projectMenuContainer.add(bg);
// Title
const title = this.add.text(0, -80, 'PROJECT: RESTORATION', { fontSize: '20px', fill: '#00FFFF', fontStyle: 'bold' }).setOrigin(0.5);
this.projectMenuContainer.add(title);
// Info
this.projectInfoText = this.add.text(0, -20, 'Req: ???', { fontSize: '16px', fill: '#ffffff', align: 'center' }).setOrigin(0.5);
this.projectMenuContainer.add(this.projectInfoText);
// Button
const btnBg = this.add.rectangle(0, 50, 200, 40, 0x00aa00);
btnBg.setInteractive();
btnBg.on('pointerdown', () => {
if (this.onContributeCallback) this.onContributeCallback();
// Close menu? Or keep open to see result?
// For now close
this.projectMenuContainer.setVisible(false);
});
this.projectMenuContainer.add(btnBg);
const btnText = this.add.text(0, 50, 'CONTRIBUTE', { fontSize: '18px', fill: '#ffffff' }).setOrigin(0.5);
this.projectMenuContainer.add(btnText);
// Close Button
const closeBtn = this.add.text(130, -90, 'X', { fontSize: '20px', fill: '#ff0000' }).setOrigin(0.5);
closeBtn.setInteractive();
closeBtn.on('pointerdown', () => this.projectMenuContainer.setVisible(false));
this.projectMenuContainer.add(closeBtn);
this.projectMenuContainer.setVisible(false);
}
showTradeMenu(inventorySystem) {
if (!this.tradeMenuContainer) {
this.createTradeMenu(inventorySystem);
}
this.updateTradeMenu(inventorySystem);
this.tradeMenuContainer.setVisible(true);
this.tradeMenuContainer.setDepth(10000);
}
createTradeMenu(inventorySystem) {
this.tradeMenuContainer = this.add.container(this.width / 2, this.height / 2);
// BG
const bg = this.add.graphics();
bg.fillStyle(0x222222, 0.95);
bg.fillRect(-200, -150, 400, 300);
bg.lineStyle(2, 0xFFD700, 1); // Gold border
bg.strokeRect(-200, -150, 400, 300);
this.tradeMenuContainer.add(bg);
// Title
const title = this.add.text(0, -130, 'MERCHANT SHOP', { fontSize: '24px', fill: '#FFD700', fontStyle: 'bold' }).setOrigin(0.5);
this.tradeMenuContainer.add(title);
// Close Button
const closeBtn = this.add.text(180, -140, 'X', { fontSize: '20px', fill: '#ff0000', fontStyle: 'bold' }).setOrigin(0.5);
closeBtn.setInteractive({ useHandCursor: true });
closeBtn.on('pointerdown', () => this.tradeMenuContainer.setVisible(false));
this.tradeMenuContainer.add(closeBtn);
// Content Container (for items)
this.tradeItemsContainer = this.add.container(0, 0);
this.tradeMenuContainer.add(this.tradeItemsContainer);
}
updateTradeMenu(inventorySystem) {
this.tradeItemsContainer.removeAll(true);
// Items to Sell (Player has -> Merchant wants)
// Hardcoded prices for now
const prices = {
'wheat': { price: 10, type: 'sell' },
'wood': { price: 2, type: 'sell' },
'seeds': { price: 5, type: 'buy' }
};
const startY = -80;
let index = 0;
// Header
const header = this.add.text(-180, startY, 'ITEM PRICE ACTION', { fontSize: '16px', fill: '#888888' });
this.tradeItemsContainer.add(header);
// 1. Sell Wheat
this.createTradeRow(inventorySystem, 'wheat', prices.wheat.price, 'SELL', index++, startY + 30);
// 2. Sell Wood
this.createTradeRow(inventorySystem, 'wood', prices.wood.price, 'SELL', index++, startY + 30);
// 3. Buy Seeds
this.createTradeRow(inventorySystem, 'seeds', prices.seeds.price, 'BUY', index++, startY + 30);
}
createTradeRow(inv, itemKey, price, action, index, yOffset) {
const y = yOffset + (index * 40);
// Name
const name = this.add.text(-180, y, itemKey.toUpperCase(), { fontSize: '18px', fill: '#ffffff' });
this.tradeItemsContainer.add(name);
// Price
const priceText = this.add.text(-50, y, `${price}g`, { fontSize: '18px', fill: '#FFD700' });
this.tradeItemsContainer.add(priceText);
// Button
const btnX = 100;
const btnBg = this.add.rectangle(btnX, y + 10, 80, 30, action === 'BUY' ? 0x008800 : 0x880000);
btnBg.setInteractive({ useHandCursor: true });
const btnLabel = this.add.text(btnX, y + 10, action, { fontSize: '16px', fill: '#ffffff' }).setOrigin(0.5);
btnBg.on('pointerdown', () => {
if (action === 'SELL') {
if (inv.hasItem(itemKey, 1)) {
inv.removeItem(itemKey, 1);
inv.gold += price;
inv.updateUI();
// Refresh visuals?
} else {
// Fail feedback
btnLabel.setText('NO ITEM');
this.scene.time.delayedCall(500, () => btnLabel.setText(action));
}
} else if (action === 'BUY') {
if (inv.gold >= price) {
inv.gold -= price;
inv.addItem(itemKey, 1);
inv.updateUI();
} else {
// Fail feedback
btnLabel.setText('NO GOLD');
this.scene.time.delayedCall(500, () => btnLabel.setText(action));
}
}
});
this.tradeItemsContainer.add(btnBg);
this.tradeItemsContainer.add(btnLabel);
}
}