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
This commit is contained in:
434
nova farma TRAE/src/systems/BuildingSystem.js
Normal file
434
nova farma TRAE/src/systems/BuildingSystem.js
Normal file
@@ -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)})`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user