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:
BIN
assets/DEMO_FAZA1/.DS_Store
vendored
BIN
assets/DEMO_FAZA1/.DS_Store
vendored
Binary file not shown.
@@ -1,3 +1,5 @@
|
|||||||
|
import BuildingSystem from '../systems/BuildingSystem.js';
|
||||||
|
|
||||||
export default class GrassSceneClean extends Phaser.Scene {
|
export default class GrassSceneClean extends Phaser.Scene {
|
||||||
constructor() {
|
constructor() {
|
||||||
super({ key: 'GrassSceneClean' });
|
super({ key: 'GrassSceneClean' });
|
||||||
@@ -117,6 +119,7 @@ export default class GrassSceneClean extends Phaser.Scene {
|
|||||||
frameHeight: 256
|
frameHeight: 256
|
||||||
});
|
});
|
||||||
this.load.image('rain_catcher', 'DEMO_FAZA1/Structures/rain_catcher.png');
|
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
|
// 8. Weather System
|
||||||
this.load.image('rain_drops', 'DEMO_FAZA1/Environment/rain_drops.png');
|
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);
|
if (this.riverCollider) this.physics.add.collider(this.kai, this.riverCollider);
|
||||||
// this.physics.add.collider(this.kai, this.obstaclesGroup);
|
// 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 ---
|
// --- ANIMATIONS ---
|
||||||
// 0-3: Down, 4-7: Left, 8-11: Right, 12-15: Up
|
// 0-3: Down, 4-7: Left, 8-11: Right, 12-15: Up
|
||||||
// This is a duplicate animation creation block, removing it.
|
// 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!');
|
console.log('✅ Growth system ready - Island will evolve over time!');
|
||||||
|
|
||||||
// === RAIN WEATHER SYSTEM ===
|
// === RAIN WEATHER SYSTEM (Procedural — natural looking) ===
|
||||||
// Create rain particles that fall from sky
|
// We draw rain as short diagonal lines each frame on a Graphics object.
|
||||||
this.rainParticles = this.add.particles(0, -50, 'rain_drops', {
|
// This avoids the "vertical column" pattern of sprite-based emitters.
|
||||||
x: { min: 0, max: this.scale.width },
|
this.rainGraphics = this.add.graphics()
|
||||||
y: -50,
|
.setScrollFactor(0) // Fixed to screen
|
||||||
speedY: { min: 300, max: 500 },
|
.setDepth(5000);
|
||||||
speedX: { min: -20, max: 20 },
|
|
||||||
lifespan: 3000,
|
// Rain drop pool — randomised positions, speeds, lengths, and alpha
|
||||||
scale: { min: 0.3, max: 0.6 },
|
const DROP_COUNT = 160; // Sparse enough to see island behind it
|
||||||
alpha: { start: 0.7, end: 0.3 },
|
const SCREEN_W = this.cameras.main.width;
|
||||||
frequency: 50,
|
const SCREEN_H = this.cameras.main.height;
|
||||||
rotate: { min: -10, max: 10 },
|
|
||||||
blendMode: 'ADD',
|
this.rainDrops = [];
|
||||||
emitting: true
|
for (let i = 0; i < DROP_COUNT; i++) {
|
||||||
});
|
this.rainDrops.push({
|
||||||
this.rainParticles.setDepth(5000); // Above everything
|
x: Math.random() * SCREEN_W,
|
||||||
this.rainParticles.setScrollFactor(0); // Fixed to camera
|
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 ===
|
// === ULTRA CLEAN: Only Kai depth sorting ===
|
||||||
this.kai.setDepth(this.kai.y);
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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