Files
novafarma/nova farma TRAE/src/systems/BuildingSystem.js
David Kotnik 4cbb198d7a feat: dež po celi mapi + BuildingSystem [2026-03-02]
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
2026-03-02 20:42:49 +01:00

435 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* ============================================================
* 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)})`);
}
}