feat: Add Editor Mode (Palette, Eraser, Path Tiles), generate assets, enable WASD

This commit is contained in:
2026-01-29 20:21:50 +01:00
parent 177049e470
commit c70e651020
41 changed files with 323 additions and 8 deletions

BIN
assets/.DS_Store vendored

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 816 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 671 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

142
scripts/slice_assets.py Normal file
View File

@@ -0,0 +1,142 @@
import cv2
import numpy as np
import os
import glob
# Configuration
# Input files (Generated images)
INPUT_FILES = [
{
'path': '/Users/davidkotnik/.gemini/antigravity/brain/8233d64e-0c17-43b1-b8b5-fbc41754e56b/trees_adult_noir_1769706733583.png',
'prefix': 'tree_adult',
'output_folder': 'assets/DEMO_FAZA1/Trees'
},
{
'path': '/Users/davidkotnik/.gemini/antigravity/brain/8233d64e-0c17-43b1-b8b5-fbc41754e56b/dead_nature_noir_1769706761105.png',
'prefix': 'dead_nature',
'output_folder': 'assets/DEMO_FAZA1/Environment'
},
{
'path': '/Users/davidkotnik/.gemini/antigravity/brain/8233d64e-0c17-43b1-b8b5-fbc41754e56b/fence_sign_noir_1769706790281.png',
'prefix': 'fence_sign',
'output_folder': 'assets/DEMO_FAZA1/Environment'
},
{
'path': '/Users/davidkotnik/.gemini/antigravity/brain/8233d64e-0c17-43b1-b8b5-fbc41754e56b/muddy_path_noir_v2_1769706866951.png',
'prefix': 'path_tile',
'output_folder': 'assets/DEMO_FAZA1/Ground',
'mode': 'grid',
'grid_size': 256
}
]
BASE_DIR = '/Users/davidkotnik/repos/novafarma'
def process_and_slice(file_data):
full_path = file_data['path']
prefix = file_data['prefix']
out_dir_rel = file_data['output_folder']
mode = file_data.get('mode', 'contour') # Default to contour
out_dir = os.path.join(BASE_DIR, out_dir_rel)
if not os.path.exists(out_dir):
os.makedirs(out_dir)
print(f"Processing {prefix} from {full_path} in {mode} mode...")
img = cv2.imread(full_path, cv2.IMREAD_COLOR)
if img is None:
print(f"Error loading {full_path}")
return
h, w = img.shape[:2]
if mode == 'grid':
# Split into fixed grid cells
tile_size = file_data.get('grid_size', 256)
rows = h // tile_size
cols = w // tile_size
count = 0
for r in range(rows):
for c in range(cols):
y = r * tile_size
x = c * tile_size
roi = img[y:y+tile_size, x:x+tile_size]
# Check if tile is empty (all white/uniform?)
# Calculate variance or average color.
if np.mean(roi) > 250: # Mostly white
continue
# We can enable alpha here if needed, but for ground tiles usually we want Opaque?
# User wants "Tile Palette". Tiles usually have transparency if they are "Path on Grass".
# Let's clean white background using same threshold logic.
roi_gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
_, roi_mask = cv2.threshold(roi_gray, 240, 255, cv2.THRESH_BINARY_INV)
roi_bgra = cv2.cvtColor(roi, cv2.COLOR_BGR2BGRA)
roi_bgra[:, :, 3] = roi_mask
# Save
filename = f"{prefix}_{count}.png"
out_path = os.path.join(out_dir, filename)
cv2.imwrite(out_path, roi_bgra)
print(f" Saved Grid Tile {filename}")
count += 1
else: # Contour mode (old logic)
# 1. Remove Background (White) -> Alpha
# Assuming white background.
# Convert to Gray for thresholding
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Threshold: Close to white (240+) is background
_, mask = cv2.threshold(gray, 240, 255, cv2.THRESH_BINARY_INV)
# mask: White (255) is OBJECT, Black (0) is BACKGROUND
# Clean mask (remove noise)
kernel = np.ones((3,3), np.uint8)
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel, iterations=1)
# Dilate slightly to include outlines?
# mask = cv2.dilate(mask, kernel, iterations=1)
# 2. Find Contours (Separate Objects)
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
count = 0
for i, c in enumerate(contours):
area = cv2.contourArea(c)
if area < 500: # Filter small noise
continue
# Bounding box
x, y, w, h = cv2.boundingRect(c)
# Crop
roi = img[y:y+h, x:x+w]
# Create alpha channel for ROI
# We need a mask for this specific object in the ROI
roi_gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
# Re-threshold ROI to get clean alpha
_, roi_mask = cv2.threshold(roi_gray, 240, 255, cv2.THRESH_BINARY_INV)
# Convert ROI to BGRA
roi_bgra = cv2.cvtColor(roi, cv2.COLOR_BGR2BGRA)
roi_bgra[:, :, 3] = roi_mask
# Save
filename = f"{prefix}_{count}.png"
out_path = os.path.join(out_dir, filename)
cv2.imwrite(out_path, roi_bgra)
print(f" Saved {filename}")
count += 1
if __name__ == "__main__":
for f in INPUT_FILES:
process_and_slice(f)

View File

@@ -20,6 +20,19 @@ export default class GrassSceneClean extends Phaser.Scene {
// 4. Items & Charts
this.load.image('hay', 'DEMO_FAZA1/Items/hay_drop_0.png');
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
// Tileset (Grid Slices)
for (let i = 0; i < 16; i++) {
this.load.image(`path_tile_${i}`, `DEMO_FAZA1/Ground/path_tile_${i}.png`);
}
// REPLACED STATIC KAI WITH SPRITE SHEET
// Frame size 256x256 based on 1024x1024 sheet
this.load.spritesheet('kai', 'DEMO_FAZA1/Characters/kai_walk_sheet.png', {
@@ -73,6 +86,7 @@ export default class GrassSceneClean extends Phaser.Scene {
this.stream = this.physics.add.staticImage(startX, startY, 'stream_final_v7');
this.stream.setOrigin(0.5, 0.5);
this.stream.setDepth(-50);
this.stream.setInteractive({ draggable: true }); // Enable Dragging
// Physics Body for Main
this.stream.body.setSize(this.stream.width * 0.8, this.stream.height * 0.2);
@@ -86,8 +100,154 @@ export default class GrassSceneClean extends Phaser.Scene {
// --- 3. FOLIAGE (Trava - Šopi) ---
// Removed as requested
// --- 4. ITEMS (Seno) ---
// Removed as requested
// --- 4. ITEMS & OBSTACLES ---
// Trnje (Thorns) - Draggable
this.trnje = this.add.image(startX - 200, startY + 100, 'trnje');
this.trnje.setScale(0.5); // Adjust scale if needed
this.trnje.setInteractive({ draggable: true });
// General Drag Event
this.input.on('drag', function (pointer, gameObject, dragX, dragY) {
gameObject.x = dragX;
gameObject.y = dragY;
});
// --- EDITOR MODE SYSTEM ---
this.editorEnabled = false;
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);
});
// 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);
// 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);
// Generate Eraser Texture
let g = this.make.graphics().fillStyle(0xFF0000).fillRect(0, 0, 64, 64);
g.generateTexture('eraser_icon', 64, 64);
g.destroy();
// Populate Palette
// 16 Path Tiles + Fence + Stump + Eraser
const paletteItems = [];
for (let i = 0; i < 16; i++) paletteItems.push(`path_tile_${i}`);
paletteItems.push('fence_sign_0');
paletteItems.push('eraser_icon');
let px = 100;
let py = 100; // TOP
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
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);
});
this.paletteContainer.add(icon);
px += 80;
if (px > VIEW_W - 100) { px = 100; py += 80; } // wrap
});
// Painting Logic
this.input.on('pointerdown', (pointer) => {
if (!this.editorEnabled) return;
// Ignore clicks on UI (Check Y < 200)
if (pointer.y < 200) return;
// ERASER MODE: Handled via object clicks
if (this.selectedTile === 'eraser_icon') return;
// Snap to Grid (128px for finer control, or 256px for full tiles)
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);
let placedStub = this.add.image(sx, sy, this.selectedTile);
placedStub.setInteractive(); // Enable erase interaction
// Delete if clicked with eraser
placedStub.on('pointerdown', () => {
if (this.editorEnabled && this.selectedTile === 'eraser_icon') {
placedStub.destroy();
// Prevent click propagation?
}
});
if (this.selectedTile.includes('path')) {
placedStub.setDepth(-40); // Above ground
placedStub.setScale(0.5);
} else {
placedStub.setDepth(sy); // Y-sort
// placedStub.setInteractive({ draggable: true }); // Draggable requires careful event handling vs eraser
// Let's enable drag ONLY if not eraser?
// For now, prioritize Eraser. Dragging might conflict with 'pointerdown' erase.
// Or better: enable Drag, but if Eraser selected, destroy on click.
}
this.editorGroup.add(placedStub);
});
// --- PREVIOUSLY GENERATED PROPS (Draggable Example) ---
// Commented out per request "samo blatno potko"
/*
const propAssets = ['tree_adult_0', 'tree_adult_1', 'dead_nature_0'];
for (let i = 0; i < propAssets.length; i++) {
let item = this.add.image(startX + 200 + (i * 100), startY + 200, propAssets[i]);
item.setInteractive({ draggable: true });
}
*/
// --- RECONSTRUCT PATH (4x4 GRID) ---
// Image was 1024x1024, sliced into 256x256 (4 cols, 4 rows)
// Indices 0..15
let pIndex = 0;
const GRID_SZ = 256;
for (let r = 0; r < 4; r++) {
for (let c = 0; c < 4; c++) {
// Determine Tile Key
let key = `path_tile_${pIndex}`;
// Place it
// Center offset: -1.5 * size to center the 4x4 block
let px = startX + (c * GRID_SZ) + 200;
let py = startY + (r * GRID_SZ) + 200;
let tile = this.add.image(px, py, key);
tile.setDepth(-40); // Ground level
// Optional: make draggable? User said "naredi", maybe fixed?
// tile.setInteractive({ draggable: true });
pIndex++;
}
}
// --- 5. CHAR (Kai) ---
this.kai = this.physics.add.sprite(WORLD_W / 2, WORLD_H / 2, 'kai');
@@ -138,6 +298,13 @@ export default class GrassSceneClean extends Phaser.Scene {
// Camera setup logic
this.cameras.main.startFollow(this.kai, true, 0.1, 0.1);
this.cursors = this.input.keyboard.createCursorKeys();
// Add WASD keys
this.keys = this.input.keyboard.addKeys({
up: Phaser.Input.Keyboard.KeyCodes.W,
down: Phaser.Input.Keyboard.KeyCodes.S,
left: Phaser.Input.Keyboard.KeyCodes.A,
right: Phaser.Input.Keyboard.KeyCodes.D
});
@@ -196,25 +363,31 @@ export default class GrassSceneClean extends Phaser.Scene {
let moving = false;
if (this.cursors.left.isDown) {
// Input helpers
const left = this.cursors.left.isDown || this.keys.left.isDown;
const right = this.cursors.right.isDown || this.keys.right.isDown;
const up = this.cursors.up.isDown || this.keys.up.isDown;
const down = this.cursors.down.isDown || this.keys.down.isDown;
if (left) {
this.kai.setVelocityX(-speed);
this.kai.play('walk-left', true);
moving = true;
} else if (this.cursors.right.isDown) {
} else if (right) {
this.kai.setVelocityX(speed);
this.kai.play('walk-right', true);
moving = true;
}
if (this.cursors.up.isDown) {
if (up) {
this.kai.setVelocityY(-speed);
if (!this.cursors.left.isDown && !this.cursors.right.isDown) {
if (!left && !right) {
this.kai.play('walk-up', true);
}
moving = true;
} else if (this.cursors.down.isDown) {
} else if (down) {
this.kai.setVelocityY(speed);
if (!this.cursors.left.isDown && !this.cursors.right.isDown) {
if (!left && !right) {
this.kai.play('walk-down', true);
}
moving = true;