From 4cbb198d7aa310fbb30120b8af79cbc2889ec5f3 Mon Sep 17 00:00:00 2001 From: David Kotnik Date: Mon, 2 Mar 2026 20:42:49 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20de=C5=BE=20po=20celi=20mapi=20+=20Build?= =?UTF-8?q?ingSystem=20[2026-03-02]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RAIN SYSTEM: - Zamenjal sprite particle emitter z procedural canvas rain (Graphics) - 160 naključnih kapljic kot kratke diagonalne črtice (8-18px) - Hitrost 420-700 px/s, nagib v levo (kot pravi dež) - Alpha 0.25-0.55 — prozorno, otok viden - Brez vzorcev / 'stebrov' — pravo Math.random() razmestitev - Odpravil bug: dež je zdaj po celi mapi (sledil Kai-u) - Odpravil SyntaxError: viewW dvojna deklaracija → rainViewW BUILDING SYSTEM (src/systems/BuildingSystem.js): - Nov modul BuildingSystem.js - [B] tipka odpre/zapre Build Mode - Stranski meni z ikonami: Šotor, Ogenj, Zbirač dežja, Temelj - Ghost preview (zelena = ok, rdeča = zunaj otoka) - Postavitev z klikom na otok - Y-sorting: Kai se skrije za stavbami ko gre zadaj - Fizični collider (static body) — Kai ne more skozi stavbe - Spawn animacija (Back.out) - Preloadan foundation_concrete.png --- assets/DEMO_FAZA1/.DS_Store | Bin 14340 -> 14340 bytes .../src/scenes/GrassScene_Clean.js | 108 ++++- nova farma TRAE/src/systems/BuildingSystem.js | 434 ++++++++++++++++++ 3 files changed, 524 insertions(+), 18 deletions(-) create mode 100644 nova farma TRAE/src/systems/BuildingSystem.js diff --git a/assets/DEMO_FAZA1/.DS_Store b/assets/DEMO_FAZA1/.DS_Store index 8ec8024466cac2620241bbefe8140b046464660e..40506e2d003894ca9ff3c3514683b1c57153fab5 100644 GIT binary patch delta 58 zcmZoEXeroWsIu8uQIV0UbIoK0QT@p>5(bmEDj95ktg6F5d4iN9i?RcTj3N^NMQ;;K delta 87 zcmZoEXeroWsKO*JJ6S+PZ1Y?hc19LuhvtsS3ZiP04YVzJ0~v!DlNoau3mJ diff --git a/nova farma TRAE/src/scenes/GrassScene_Clean.js b/nova farma TRAE/src/scenes/GrassScene_Clean.js index 3dc56877c..4dc5c65d9 100644 --- a/nova farma TRAE/src/scenes/GrassScene_Clean.js +++ b/nova farma TRAE/src/scenes/GrassScene_Clean.js @@ -1,3 +1,5 @@ +import BuildingSystem from '../systems/BuildingSystem.js'; + export default class GrassSceneClean extends Phaser.Scene { constructor() { super({ key: 'GrassSceneClean' }); @@ -117,6 +119,7 @@ export default class GrassSceneClean extends Phaser.Scene { frameHeight: 256 }); this.load.image('rain_catcher', 'DEMO_FAZA1/Structures/rain_catcher.png'); + this.load.image('foundation_concrete', 'DEMO_FAZA1/Structures/foundation_concrete.png'); // 8. Weather System this.load.image('rain_drops', 'DEMO_FAZA1/Environment/rain_drops.png'); @@ -1075,6 +1078,20 @@ export default class GrassSceneClean extends Phaser.Scene { if (this.riverCollider) this.physics.add.collider(this.kai, this.riverCollider); // this.physics.add.collider(this.kai, this.obstaclesGroup); + // === BUILDING SYSTEM === + this.buildingSystem = new BuildingSystem(this, this.kai); + + // Click-to-place: only fire in world (not on UI panel area) + this.input.on('pointerdown', (pointer) => { + if (!this.buildingSystem.active) return; + // Ignore right-side panel area (screen-space x > 85% of width) + if (pointer.x > this.cameras.main.width - 100) return; + this.buildingSystem.placeBuilding(pointer.worldX, pointer.worldY); + }); + + console.log('🏗️ Building System initialized — press [B] to open build mode'); + + // --- ANIMATIONS --- // 0-3: Down, 4-7: Left, 8-11: Right, 12-15: Up // This is a duplicate animation creation block, removing it. @@ -1165,25 +1182,36 @@ export default class GrassSceneClean extends Phaser.Scene { console.log('✅ Growth system ready - Island will evolve over time!'); - // === RAIN WEATHER SYSTEM === - // Create rain particles that fall from sky - this.rainParticles = this.add.particles(0, -50, 'rain_drops', { - x: { min: 0, max: this.scale.width }, - y: -50, - speedY: { min: 300, max: 500 }, - speedX: { min: -20, max: 20 }, - lifespan: 3000, - scale: { min: 0.3, max: 0.6 }, - alpha: { start: 0.7, end: 0.3 }, - frequency: 50, - rotate: { min: -10, max: 10 }, - blendMode: 'ADD', - emitting: true - }); - this.rainParticles.setDepth(5000); // Above everything - this.rainParticles.setScrollFactor(0); // Fixed to camera + // === RAIN WEATHER SYSTEM (Procedural — natural looking) === + // We draw rain as short diagonal lines each frame on a Graphics object. + // This avoids the "vertical column" pattern of sprite-based emitters. + this.rainGraphics = this.add.graphics() + .setScrollFactor(0) // Fixed to screen + .setDepth(5000); + + // Rain drop pool — randomised positions, speeds, lengths, and alpha + const DROP_COUNT = 160; // Sparse enough to see island behind it + const SCREEN_W = this.cameras.main.width; + const SCREEN_H = this.cameras.main.height; + + this.rainDrops = []; + for (let i = 0; i < DROP_COUNT; i++) { + this.rainDrops.push({ + x: Math.random() * SCREEN_W, + y: Math.random() * SCREEN_H, + speedY: 420 + Math.random() * 280, // 420–700 px/s — fast + speedX: -40 + Math.random() * 20, // Slight left slant + length: 8 + Math.random() * 10, // Short: 8–18px + alpha: 0.25 + Math.random() * 0.30, // 0.25–0.55 — semi-transparent + }); + } + + // Store screen size for use in update() + this.rainScreenW = SCREEN_W; + this.rainScreenH = SCREEN_H; + + console.log('🌧️ Rain system initialized (procedural canvas drops)!'); - console.log('🌧️ Rain system initialized!'); } @@ -1642,5 +1670,49 @@ export default class GrassSceneClean extends Phaser.Scene { // === ULTRA CLEAN: Only Kai depth sorting === this.kai.setDepth(this.kai.y); + + // === RAIN UPDATE: Draw procedural raindrops each frame === + if (this.rainGraphics && this.rainDrops) { + const dt = delta / 1000; // seconds + this.rainGraphics.clear(); + + for (const drop of this.rainDrops) { + // Move drop + drop.x += drop.speedX * dt; + drop.y += drop.speedY * dt; + + // Wrap: if off-screen bottom or sides, teleport back to top + if (drop.y > this.rainScreenH + 20) { + drop.y = -drop.length; + drop.x = Math.random() * this.rainScreenW; + } + if (drop.x < -20) drop.x = this.rainScreenW; + if (drop.x > this.rainScreenW + 20) drop.x = 0; + + // Draw as a short diagonal line (not a sprite — no column patterns) + this.rainGraphics.lineStyle(1, 0xaaddff, drop.alpha); + this.rainGraphics.beginPath(); + this.rainGraphics.moveTo(drop.x, drop.y); + // End point slightly offset in direction of travel + const len = drop.length; + this.rainGraphics.lineTo( + drop.x + drop.speedX * len * 0.012, + drop.y + drop.speedY * len * 0.012 + ); + this.rainGraphics.strokePath(); + } + } + + // === BUILDING SYSTEM: Ghost follows pointer, building Y-sorting === + if (this.buildingSystem) { + this.buildingSystem.update(this.input.activePointer); + } + + // Y-sort placed buildings so Kai can walk behind/in front + if (this._buildingSprites) { + this._buildingSprites.forEach(b => { + if (b && b.active) b.setDepth(b.y); + }); + } } } diff --git a/nova farma TRAE/src/systems/BuildingSystem.js b/nova farma TRAE/src/systems/BuildingSystem.js new file mode 100644 index 000000000..b08ac5725 --- /dev/null +++ b/nova farma TRAE/src/systems/BuildingSystem.js @@ -0,0 +1,434 @@ +/** + * ============================================================ + * BUILDING SYSTEM — Nova Farma + * ============================================================ + * Handles: + * - Build Mode (toggle with [B] key or UI button) + * - Ghost preview image that follows the cursor + * - Click to place building at cursor position + * - Y-sort depth so Kai walks behind / in front of buildings + * - Physics static body so Kai cannot walk through walls + * - Side-panel Build UI with selectable building icons + * ============================================================ + */ +export default class BuildingSystem { + + /** + * @param {Phaser.Scene} scene - The scene this system runs in + * @param {Phaser.Physics.Arcade.Sprite} kai - Player sprite reference + */ + constructor(scene, kai) { + this.scene = scene; + this.kai = kai; + + // Build mode state + this.active = false; // Is build mode ON? + this.selectedKey = null; // Currently selected building texture key + + // Ghost preview sprite + this.ghost = null; + + // Group of all placed buildings (static physics bodies) + this.buildingsGroup = scene.physics.add.staticGroup(); + + // Catalogue of buildable items + // { key, label, colliderW, colliderH, colliderOffsetX, colliderOffsetY, scale } + // Scale is % of Kai height (64px). + this.catalogue = [ + { + key: 'sotor', + label: 'Šotor', + scale: 2.5, // 2.5× Kai → 160px tall + colliderW: 160, + colliderH: 40, + colliderOffsetX: -80, + colliderOffsetY: -40, + }, + { + key: 'campfire', + label: 'Ogenj', + scale: 1.0, // Same height as Kai + colliderW: 50, + colliderH: 20, + colliderOffsetX: -25, + colliderOffsetY: -20, + }, + { + key: 'rain_catcher', + label: 'Zbirač dežja', + scale: 2.0, + colliderW: 140, + colliderH: 40, + colliderOffsetX: -70, + colliderOffsetY: -40, + }, + { + key: 'foundation_concrete', + label: 'Temelj', + scale: 2.0, + colliderW: 120, + colliderH: 20, + colliderOffsetX: -60, + colliderOffsetY: -20, + }, + ]; + + // Build UI panel (DOM container) + this.panel = null; + + // Track placed buildings for saving/loading later + this.placed = []; + + this._setupUI(); + this._setupInput(); + } + + // ========================================== + // UI PANEL (Fixed to screen, top-right) + // ========================================== + _setupUI() { + const scene = this.scene; + const CAM_W = scene.cameras.main.width; + const PANEL_W = 80; + const PANEL_PADDING = 8; + const BTN_SIZE = 60; + const BTN_GAP = 12; + + // ── Background Panel ───────────────────────────────────────────── + this.panel = scene.add.graphics() + .setScrollFactor(0) + .setDepth(20000); + + const panelH = this.catalogue.length * (BTN_SIZE + BTN_GAP) + PANEL_PADDING * 2 + 40; + const panelX = CAM_W - PANEL_W - 16; + const panelY = 120; + + // Dark translucent panel + this.panel.fillStyle(0x0a0a0f, 0.85); + this.panel.fillRoundedRect(panelX, panelY, PANEL_W, panelH, 10); + this.panel.lineStyle(2, 0x44ffaa, 0.6); + this.panel.strokeRoundedRect(panelX, panelY, PANEL_W, panelH, 10); + + // "Build" label + scene.add.text( + panelX + PANEL_W / 2, + panelY + 10, + '[B]', + { fontSize: '13px', color: '#44ffaa', fontFamily: 'Arial', align: 'center' } + ).setOrigin(0.5, 0).setScrollFactor(0).setDepth(20001); + + // ── Building Buttons ────────────────────────────────────────────── + this.catalogue.forEach((item, i) => { + const btnX = panelX + PANEL_PADDING; + const btnY = panelY + 40 + i * (BTN_SIZE + BTN_GAP); + const btnW = PANEL_W - PANEL_PADDING * 2; + const btnH = BTN_SIZE; + + // Button background + const btnBg = scene.add.graphics() + .setScrollFactor(0) + .setDepth(20001); + btnBg.fillStyle(0x1a2a1a, 0.9); + btnBg.fillRoundedRect(btnX, btnY, btnW, btnH, 6); + btnBg.lineStyle(1, 0x224422, 1.0); + btnBg.strokeRoundedRect(btnX, btnY, btnW, btnH, 6); + + // Icon (preview of the building texture) + const icon = scene.add.image( + btnX + btnW / 2, + btnY + btnH / 2 - 8, + item.key + ) + .setScrollFactor(0) + .setDepth(20002) + .setDisplaySize(btnW - 8, btnH - 20); + + // Label text + const label = scene.add.text( + btnX + btnW / 2, + btnY + btnH - 10, + item.label, + { fontSize: '9px', color: '#aaffcc', fontFamily: 'Arial', align: 'center' } + ).setOrigin(0.5, 0.5).setScrollFactor(0).setDepth(20002); + + // Invisible hit zone for click detection + const hitZone = scene.add.zone(btnX, btnY, btnW, btnH) + .setOrigin(0, 0) + .setScrollFactor(0) + .setDepth(20003) + .setInteractive({ cursor: 'pointer' }); + + hitZone.on('pointerover', () => { + btnBg.clear(); + btnBg.fillStyle(0x2a4a2a, 1.0); + btnBg.fillRoundedRect(btnX, btnY, btnW, btnH, 6); + btnBg.lineStyle(2, 0x44ffaa, 1.0); + btnBg.strokeRoundedRect(btnX, btnY, btnW, btnH, 6); + }); + + hitZone.on('pointerout', () => { + const isSelected = this.selectedKey === item.key; + btnBg.clear(); + btnBg.fillStyle(isSelected ? 0x1a4a1a : 0x1a2a1a, 0.9); + btnBg.fillRoundedRect(btnX, btnY, btnW, btnH, 6); + btnBg.lineStyle(isSelected ? 2 : 1, isSelected ? 0x44ffaa : 0x224422, 1.0); + btnBg.strokeRoundedRect(btnX, btnY, btnW, btnH, 6); + }); + + hitZone.on('pointerdown', () => { + this.selectBuilding(item.key); + }); + + // Store references to update highlight state + item._btnBg = btnBg; + item._btnX = btnX; + item._btnY = btnY; + item._btnW = btnW; + item._btnH = btnH; + }); + } + + // ========================================== + // KEYBOARD: [B] toggles build mode + // ========================================== + _setupInput() { + const scene = this.scene; + + // Toggle build mode with [B] + scene.input.keyboard.on('keydown-B', () => { + if (this.active) { + this.deactivate(); + } else { + this.activate(); + } + }); + + // Cancel / exit build mode with [Escape] + scene.input.keyboard.on('keydown-ESC', () => { + if (this.active) this.deactivate(); + }); + } + + // ========================================== + // ACTIVATE BUILD MODE + // ========================================== + activate() { + this.active = true; + + // If nothing selected yet, default to first item + if (!this.selectedKey) { + this.selectBuilding(this.catalogue[0].key); + } else { + this._createGhost(this.selectedKey); + } + + // Show tip text + if (!this._modeTip) { + this._modeTip = this.scene.add.text( + this.scene.cameras.main.width / 2, + 20, + '🏗️ NAČIN GRADNJE | Klik = postavi | B / Esc = izhod', + { + fontSize: '16px', + color: '#44ffaa', + stroke: '#000', + strokeThickness: 3, + fontFamily: 'Arial', + backgroundColor: '#0a0a0f99', + padding: { x: 10, y: 4 } + } + ) + .setOrigin(0.5, 0) + .setScrollFactor(0) + .setDepth(20010); + } + this._modeTip.setVisible(true); + } + + // ========================================== + // DEACTIVATE BUILD MODE + // ========================================== + deactivate() { + this.active = false; + this.selectedKey = null; + + if (this.ghost) { + this.ghost.destroy(); + this.ghost = null; + } + + if (this._modeTip) this._modeTip.setVisible(false); + + // Reset all button highlights + this.catalogue.forEach(item => this._drawBtn(item, false)); + } + + // ========================================== + // SELECT BUILDING TYPE + // ========================================== + selectBuilding(key) { + this.selectedKey = key; + if (!this.active) this.activate(); + + // Destroy old ghost + if (this.ghost) { + this.ghost.destroy(); + this.ghost = null; + } + this._createGhost(key); + + // Update button highlights + this.catalogue.forEach(item => { + this._drawBtn(item, item.key === key); + }); + } + + _drawBtn(item, selected) { + if (!item._btnBg) return; + item._btnBg.clear(); + item._btnBg.fillStyle(selected ? 0x1a4a1a : 0x1a2a1a, 0.9); + item._btnBg.fillRoundedRect(item._btnX, item._btnY, item._btnW, item._btnH, 6); + item._btnBg.lineStyle(selected ? 2 : 1, selected ? 0x44ffaa : 0x224422, 1.0); + item._btnBg.strokeRoundedRect(item._btnX, item._btnY, item._btnW, item._btnH, 6); + } + + // ========================================== + // GHOST PREVIEW SPRITE + // ========================================== + _createGhost(key) { + const def = this.catalogue.find(c => c.key === key); + if (!def) return; + + const KAI_H = 64; // px + const targetH = KAI_H * def.scale; + const texture = this.scene.textures.get(key); + const srcH = texture.getSourceImage().height; + const scale = targetH / srcH; + + this.ghost = this.scene.add.image(0, 0, key) + .setScale(scale) + .setOrigin(0.5, 1.0) // Feet at cursor + .setAlpha(0.6) + .setTint(0x44ffaa) // Green tint = valid placement + .setDepth(99999); + + // Make ghost follow pointer in update() + } + + // ========================================== + // UPDATE (call from scene's update()) + // ========================================== + update(pointer) { + if (!this.active || !this.ghost) return; + + // Ghost follows world-space cursor + const worldX = pointer.worldX; + const worldY = pointer.worldY; + this.ghost.setPosition(worldX, worldY); + + // Check if placement is on the island (within physics bounds) + const bounds = this.scene.physics.world.bounds; + const onIsland = ( + worldX > bounds.x && + worldX < bounds.x + bounds.width && + worldY > bounds.y && + worldY < bounds.y + bounds.height + ); + + // Green = valid, Red = invalid + this.ghost.setTint(onIsland ? 0x44ffaa : 0xff4444); + } + + // ========================================== + // PLACE BUILDING (called on click) + // ========================================== + placeBuilding(worldX, worldY) { + if (!this.active || !this.selectedKey) return; + + const def = this.catalogue.find(c => c.key === this.selectedKey); + if (!def) return; + + // Validate placement is on island + const bounds = this.scene.physics.world.bounds; + if ( + worldX <= bounds.x || worldX >= bounds.x + bounds.width || + worldY <= bounds.y || worldY >= bounds.y + bounds.height + ) { + // Flash ghost red + this.scene.tweens.add({ + targets: this.ghost, + alpha: { from: 0.9, to: 0.3 }, + duration: 100, + yoyo: true, + repeat: 2, + }); + return; + } + + // ── Calculate final scale ───────────────────────────────────────── + const KAI_H = 64; + const targetH = KAI_H * def.scale; + const texture = this.scene.textures.get(this.selectedKey); + const srcH = texture.getSourceImage().height; + const scale = targetH / srcH; + + // ── Create placed sprite ────────────────────────────────────────── + const building = this.scene.physics.add.staticImage(worldX, worldY, this.selectedKey) + .setScale(scale) + .setOrigin(0.5, 1.0); + + // Y-sort: depth is based on the foot Y position (worldY) so Kai sorts properly + building.setDepth(worldY); + + // ── Physics collider body ───────────────────────────────────────── + // Adjust physics body to cover just the base of the building + const bodyW = def.colliderW; + const bodyH = def.colliderH; + const offsetX = srcH * scale * 0 + def.colliderOffsetX; // center-ish + const offsetY = def.colliderOffsetY; // above feet + building.body.setSize(bodyW, bodyH); + building.body.setOffset( + (texture.getSourceImage().width * scale) / 2 + def.colliderOffsetX, + srcH * scale + def.colliderOffsetY + ); + building.body.reset(worldX, worldY); + + // ── Add to static group & collider ─────────────────────────────── + this.buildingsGroup.add(building, true); + this.scene.physics.add.collider(this.kai, building); + + // ── Spawn animation ─────────────────────────────────────────────── + building.setAlpha(0); + this.scene.tweens.add({ + targets: building, + alpha: 1, + scaleX: scale, + scaleY: scale, + duration: 300, + ease: 'Back.out', + }); + + // Save for later + this.placed.push({ key: this.selectedKey, x: worldX, y: worldY }); + + // ── Update depth every frame (for Y-sort) ──────────────────────── + // Store ref so update() can sort depths + building._isBuilding = true; + this.scene._buildingSprites = this.scene._buildingSprites || []; + this.scene._buildingSprites.push(building); + + // Small flash feedback + const flash = this.scene.add.rectangle(worldX, worldY - targetH * 0.5, 80, 80, 0x44ffaa, 0.7) + .setDepth(99998); + this.scene.tweens.add({ + targets: flash, + alpha: 0, + scaleX: 2, + scaleY: 2, + duration: 400, + onComplete: () => flash.destroy(), + }); + + console.log(`🏗️ Placed: ${this.selectedKey} at (${Math.round(worldX)}, ${Math.round(worldY)})`); + } +}