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
452 lines
15 KiB
JavaScript
452 lines
15 KiB
JavaScript
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);
|
|
}
|
|
}
|