feat: Upgrade Editor to v2 (Sidebar, Layers, Ghost), Tiled Setup, Asset Integration

This commit is contained in:
2026-01-30 05:15:16 +01:00
parent c70e651020
commit 13d640e7a6
25 changed files with 870 additions and 180 deletions

View File

@@ -23,13 +23,24 @@ export default class GrassSceneClean extends Phaser.Scene {
this.load.image('trnje', 'DEMO_FAZA1/Obstacles/trnje.png');
// Generated Assets (Slices)
this.load.image('tree_adult_0', 'DEMO_FAZA1/Trees/tree_adult_0.png');
this.load.image('tree_adult_1', 'DEMO_FAZA1/Trees/tree_adult_1.png');
this.load.image('dead_nature_0', 'DEMO_FAZA1/Environment/dead_nature_0.png'); // Stump
this.load.image('fence_sign_0', 'DEMO_FAZA1/Environment/fence_sign_0.png'); // Fence
// this.load.image('path_mud_0', 'DEMO_FAZA1/Ground/path_mud_0.png'); // Old single slice
// Trees
for (let i = 0; i <= 5; i++) this.load.image(`tree_adult_${i}`, `DEMO_FAZA1/Trees/tree_adult_${i}.png`);
// Tileset (Grid Slices)
// Environment (Dead Nature, Fence, Water)
for (let i = 0; i <= 8; i++) this.load.image(`dead_nature_${i}`, `DEMO_FAZA1/Environment/dead_nature_${i}.png`);
for (let i = 0; i <= 2; i++) this.load.image(`fence_sign_${i}`, `DEMO_FAZA1/Environment/fence_sign_${i}.png`);
for (let i = 0; i < 16; i++) this.load.image(`water_tile_${i}`, `DEMO_FAZA1/Environment/water_tile_${i}.png`);
// Misc Env
this.load.image('sotor', 'DEMO_FAZA1/Environment/sotor.png');
this.load.image('campfire', 'DEMO_FAZA1/Environment/taborni_ogenj.png');
this.load.image('sign_danger', 'DEMO_FAZA1/Environment/sign_danger.png');
// Vegetation
const veg = ['bush_hiding_spot', 'drevo_faza_1', 'drevo_faza_2', 'drevo_srednje', 'drevo_veliko', 'grass_cluster_dense', 'grass_cluster_flowery', 'trava_sop'];
veg.forEach(k => this.load.image(k, `DEMO_FAZA1/Vegetation/${k}.png`));
// Ground (Path)
for (let i = 0; i < 16; i++) {
this.load.image(`path_tile_${i}`, `DEMO_FAZA1/Ground/path_tile_${i}.png`);
}
@@ -117,95 +128,176 @@ export default class GrassSceneClean extends Phaser.Scene {
this.selectedTile = 'path_tile_0';
this.editorGroup = this.add.group(); // Saved tiles
// Toggle Key
this.input.keyboard.on('keydown-E', () => {
this.editorEnabled = !this.editorEnabled;
this.paletteContainer.setVisible(this.editorEnabled);
console.log("Editor Mode:", this.editorEnabled);
});
// Initialize Default State
this.selectedTile = 'path_tile_0';
this.editorEnabled = false;
// UI Palette (Hidden by default)
// FIX: Use viewport dimensions for UI
const VIEW_W = this.scale.width;
const VIEW_H = this.scale.height;
this.paletteContainer = this.add.container(0, 0).setScrollFactor(0).setVisible(false).setDepth(1000);
// --- EDITOR UI SETUP (Sidebar) ---
const SIDEBAR_W = 320;
const PALETTE_X = VIEW_W - SIDEBAR_W;
// Background for Palette (TOP OF SCREEN)
// Moved to y=100 so it covers top 0-200px
let bg = this.add.rectangle(VIEW_W / 2, 100, VIEW_W, 200, 0x000000, 0.7);
this.paletteContainer.add(bg);
// UI Container Group (for toggling visibility)
this.editorUI = this.add.group();
// Generate Eraser Texture
let g = this.make.graphics().fillStyle(0xFF0000).fillRect(0, 0, 64, 64);
g.generateTexture('eraser_icon', 64, 64);
g.destroy();
// 1. Sidebar Background
let sidebarBg = this.add.rectangle(PALETTE_X + SIDEBAR_W / 2, VIEW_H / 2, SIDEBAR_W, VIEW_H, 0x000000, 0.9)
.setScrollFactor(0).setDepth(2000);
this.editorUI.add(sidebarBg);
// Populate Palette
// 16 Path Tiles + Fence + Stump + Eraser
// 2. Layer Switcher (Top of Sidebar)
this.currentLayer = 'ground';
const layerBtns = [];
const createLayerBtn = (label, mode, y) => {
let txt = this.add.text(PALETTE_X + 20, y, label, { fontSize: '18px', color: '#888', fontStyle: 'bold' })
.setScrollFactor(0).setDepth(2002).setInteractive({ useHandCursor: true });
txt.on('pointerdown', () => {
this.currentLayer = mode;
layerBtns.forEach(b => b.setColor('#888'));
txt.setColor('#00ff00');
console.log("Layer:", mode);
});
this.editorUI.add(txt);
return txt;
};
layerBtns.push(createLayerBtn("[1] Ground", 'ground', 30));
layerBtns.push(createLayerBtn("[2] Deco", 'deco', 60));
layerBtns.push(createLayerBtn("[3] Build", 'building', 90));
layerBtns[0].setColor('#00ff00'); // Default
// 3. Palette Content (Scrollable)
const contentY = 140; // Below buttons
const itemContainer = this.add.container(PALETTE_X, contentY).setScrollFactor(0).setDepth(2001);
// Note: Containers can't be added to Groups in Phaser 3 easily for visibility toggling without recursion,
// so we handle itemContainer visibility manually.
// Mask for scrolling
const maskShape = this.make.graphics();
maskShape.fillStyle(0xffffff);
maskShape.fillRect(PALETTE_X, contentY, SIDEBAR_W, VIEW_H - contentY);
const mask = maskShape.createGeometryMask();
itemContainer.setMask(mask);
// Prepare Palette Items
const paletteItems = [];
for (let i = 0; i < 16; i++) paletteItems.push(`path_tile_${i}`);
paletteItems.push('fence_sign_0');
paletteItems.push('eraser_icon');
for (let i = 0; i < 16; i++) paletteItems.push(`water_tile_${i}`);
for (let i = 0; i <= 2; i++) paletteItems.push(`fence_sign_${i}`);
paletteItems.push('sign_danger');
for (let i = 0; i <= 5; i++) paletteItems.push(`tree_adult_${i}`);
for (let i = 0; i <= 8; i++) paletteItems.push(`dead_nature_${i}`);
['bush_hiding_spot', 'drevo_faza_1', 'drevo_faza_2', 'drevo_srednje', 'drevo_veliko', 'grass_cluster_dense', 'grass_cluster_flowery', 'trava_sop'].forEach(k => paletteItems.push(k));
paletteItems.push('sotor', 'campfire', 'eraser_icon');
let px = 100;
let py = 100; // TOP
// Grid Layout
let col = 0, row = 0;
const CELL_SZ = 90;
// Selector Highlight (Border)
let selector = this.add.rectangle(0, 0, 80, 80).setStrokeStyle(4, 0x00ff00).setVisible(false);
itemContainer.add(selector);
const icons = []; // Store references for tinting
console.log("Palette View:", VIEW_W, VIEW_H); // Debug
paletteItems.forEach((key) => {
let icon = this.add.image(px, py, key).setScale(0.3).setInteractive({ useHandCursor: true });
icons.push(icon); // Track it
let ix = 50 + (col * CELL_SZ);
let iy = 50 + (row * CELL_SZ);
let icon = this.add.image(ix, iy, key).setScale(0.3).setInteractive({ useHandCursor: true });
itemContainer.add(icon);
icon.on('pointerover', () => {
if (this.selectedTile !== key) icon.setTint(0xFFFF00); // Yellow on hover
});
icon.on('pointerout', () => {
if (this.selectedTile !== key) icon.clearTint(); // Clear if not selected
else icon.setTint(0x00FF00); // Keep Green if selected
});
icon.on('pointerdown', () => {
this.selectedTile = key;
console.log("Selected Brush:", key);
// Visual feedback: Tint selected Green, clear others
icons.forEach(i => i.clearTint());
icon.setTint(0x00FF00);
selector.setPosition(ix, iy).setVisible(true);
// Update Ghost
if (key === 'eraser_icon') {
this.ghostSprite.setVisible(false);
} else {
this.ghostSprite.setTexture(key);
this.ghostSprite.setScale(key.includes('path') || key.includes('water') ? 0.5 : 1.0); // Simple scaling logic
}
});
this.paletteContainer.add(icon);
px += 80;
if (px > VIEW_W - 100) { px = 100; py += 80; } // wrap
col++;
if (col >= 3) { col = 0; row++; }
});
// Scroll Logic
let scrollY = 0;
const MAX_SCROLL = Math.max(0, (row * CELL_SZ) + 150 - (VIEW_H - contentY));
this.input.on('wheel', (ptr, gameObjs, dx, dy, dz) => {
if (this.editorEnabled && ptr.x > PALETTE_X) {
scrollY -= dy;
if (scrollY > 0) scrollY = 0;
if (scrollY < -MAX_SCROLL) scrollY = -MAX_SCROLL;
itemContainer.y = contentY + scrollY;
}
});
// 4. Ghost Cursor
this.ghostSprite = this.add.image(0, 0, this.selectedTile)
.setAlpha(0.6).setDepth(3000).setVisible(false); // Topmost
// Toggle Visibility Helpers
const toggleEditor = (state) => {
this.editorEnabled = state;
this.editorUI.setVisible(state);
itemContainer.setVisible(state);
this.ghostSprite.setVisible(state && this.selectedTile !== 'eraser_icon');
console.log("Editor:", state);
};
toggleEditor(false); // Start hidden
// Toggle Key
this.input.keyboard.on('keydown-E', () => {
toggleEditor(!this.editorEnabled);
});
this.input.on('pointermove', (pointer) => {
if (!this.editorEnabled) return;
// Hide Ghost if over UI
if (pointer.x > PALETTE_X) {
this.ghostSprite.setVisible(false);
return;
} else if (this.selectedTile !== 'eraser_icon') {
this.ghostSprite.setVisible(true);
}
// Snap calculation
const SNAP = 128;
const sx = Math.floor(pointer.worldX / SNAP) * SNAP + (SNAP / 2);
const sy = Math.floor(pointer.worldY / SNAP) * SNAP + (SNAP / 2);
this.ghostSprite.setPosition(sx, sy);
});
// Painting Logic
this.input.on('pointerdown', (pointer) => {
if (!this.editorEnabled) return;
// Ignore clicks on UI (Check Y < 200)
if (pointer.y < 200) return;
// Ignore UI clicks
if (pointer.x > PALETTE_X) return;
// ERASER MODE: Handled via object clicks
if (this.selectedTile === 'eraser_icon') return;
// ERASER
if (this.selectedTile === 'eraser_icon') return; // Handled by object click
// Snap to Grid (128px for finer control, or 256px for full tiles)
// Snap
const SNAP = 128;
const wx = pointer.worldX;
const wy = pointer.worldY;
const sx = Math.floor(wx / SNAP) * SNAP + (SNAP / 2);
const sy = Math.floor(wy / SNAP) * SNAP + (SNAP / 2);
const sx = Math.floor(pointer.worldX / SNAP) * SNAP + (SNAP / 2);
const sy = Math.floor(pointer.worldY / SNAP) * SNAP + (SNAP / 2);
let placedStub = this.add.image(sx, sy, this.selectedTile);
placedStub.setInteractive(); // Enable erase interaction
// Delete if clicked with eraser
placedStub.setInteractive();
placedStub.on('pointerdown', () => {
if (this.editorEnabled && this.selectedTile === 'eraser_icon') {
placedStub.destroy();
// Prevent click propagation?
}
if (this.editorEnabled && this.selectedTile === 'eraser_icon') placedStub.destroy();
});
if (this.selectedTile.includes('path')) {
placedStub.setDepth(-40); // Above ground
placedStub.setScale(0.5);
// Layer Logic
if (this.currentLayer === 'ground') {
placedStub.setDepth(-40);
placedStub.setScale(0.5); // Grid tiles
} else {
placedStub.setDepth(sy); // Y-sort
// placedStub.setInteractive({ draggable: true }); // Draggable requires careful event handling vs eraser