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:
@@ -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) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
70
src/scenes/StoryScene.js
Normal 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
451
src/scenes/UIScene.js
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user