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
435 lines
16 KiB
JavaScript
435 lines
16 KiB
JavaScript
/**
|
||
* ============================================================
|
||
* 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)})`);
|
||
}
|
||
}
|