feat: Add Editor Mode (Palette, Eraser, Path Tiles), generate assets, enable WASD
BIN
assets/.DS_Store
vendored
BIN
assets/DEMO_FAZA1/.DS_Store
vendored
BIN
assets/DEMO_FAZA1/Environment/dead_nature_0.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
assets/DEMO_FAZA1/Environment/dead_nature_1.png
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
assets/DEMO_FAZA1/Environment/dead_nature_2.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
assets/DEMO_FAZA1/Environment/dead_nature_3.png
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
assets/DEMO_FAZA1/Environment/dead_nature_4.png
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
assets/DEMO_FAZA1/Environment/dead_nature_5.png
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
assets/DEMO_FAZA1/Environment/dead_nature_6.png
Normal file
|
After Width: | Height: | Size: 816 KiB |
BIN
assets/DEMO_FAZA1/Environment/dead_nature_7.png
Normal file
|
After Width: | Height: | Size: 213 KiB |
BIN
assets/DEMO_FAZA1/Environment/dead_nature_8.png
Normal file
|
After Width: | Height: | Size: 285 KiB |
BIN
assets/DEMO_FAZA1/Environment/fence_sign_0.png
Normal file
|
After Width: | Height: | Size: 671 KiB |
BIN
assets/DEMO_FAZA1/Environment/fence_sign_1.png
Normal file
|
After Width: | Height: | Size: 212 KiB |
BIN
assets/DEMO_FAZA1/Environment/fence_sign_2.png
Normal file
|
After Width: | Height: | Size: 278 KiB |
|
Before Width: | Height: | Size: 222 KiB |
BIN
assets/DEMO_FAZA1/Ground/path_mud_0.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
assets/DEMO_FAZA1/Ground/path_tile_0.png
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
assets/DEMO_FAZA1/Ground/path_tile_1.png
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
assets/DEMO_FAZA1/Ground/path_tile_10.png
Normal file
|
After Width: | Height: | Size: 115 KiB |
BIN
assets/DEMO_FAZA1/Ground/path_tile_11.png
Normal file
|
After Width: | Height: | Size: 119 KiB |
BIN
assets/DEMO_FAZA1/Ground/path_tile_12.png
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
assets/DEMO_FAZA1/Ground/path_tile_13.png
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
assets/DEMO_FAZA1/Ground/path_tile_14.png
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
assets/DEMO_FAZA1/Ground/path_tile_15.png
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
assets/DEMO_FAZA1/Ground/path_tile_2.png
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
assets/DEMO_FAZA1/Ground/path_tile_3.png
Normal file
|
After Width: | Height: | Size: 123 KiB |
BIN
assets/DEMO_FAZA1/Ground/path_tile_4.png
Normal file
|
After Width: | Height: | Size: 121 KiB |
BIN
assets/DEMO_FAZA1/Ground/path_tile_5.png
Normal file
|
After Width: | Height: | Size: 115 KiB |
BIN
assets/DEMO_FAZA1/Ground/path_tile_6.png
Normal file
|
After Width: | Height: | Size: 113 KiB |
BIN
assets/DEMO_FAZA1/Ground/path_tile_7.png
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
assets/DEMO_FAZA1/Ground/path_tile_8.png
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
assets/DEMO_FAZA1/Ground/path_tile_9.png
Normal file
|
After Width: | Height: | Size: 114 KiB |
BIN
assets/DEMO_FAZA1/Obstacles/trnje.png
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
assets/DEMO_FAZA1/Trees/tree_adult_0.png
Normal file
|
After Width: | Height: | Size: 178 KiB |
BIN
assets/DEMO_FAZA1/Trees/tree_adult_1.png
Normal file
|
After Width: | Height: | Size: 190 KiB |
BIN
assets/DEMO_FAZA1/Trees/tree_adult_2.png
Normal file
|
After Width: | Height: | Size: 271 KiB |
BIN
assets/DEMO_FAZA1/Trees/tree_adult_3.png
Normal file
|
After Width: | Height: | Size: 233 KiB |
BIN
assets/DEMO_FAZA1/Trees/tree_adult_4.png
Normal file
|
After Width: | Height: | Size: 241 KiB |
BIN
assets/DEMO_FAZA1/Trees/tree_adult_5.png
Normal file
|
After Width: | Height: | Size: 217 KiB |
142
scripts/slice_assets.py
Normal 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)
|
||||
@@ -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;
|
||||
|
||||