feat: Upgrade Editor to v2 (Sidebar, Layers, Ghost), Tiled Setup, Asset Integration
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user