/** * ============================================================ * 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)})`); } }