Files
novafarma/src/scenes/UIScene.js
NovaFarma Dev 9eb57ed117 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
2025-12-07 01:44:16 +01:00

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);
}
}