feat: complete Style 32 overhaul & Tiled integration fix
- Enforced 'Style 32 - Dark Chibi Vector' for all ground assets. - Fixed critical Prologue-to-Game crash (function renaming). - Implemented Tiled JSON/TMX auto-conversion. - Updated Asset Manager to visualize 1800+ assets. - Cleaned up project structure (new assets/grounds folder). - Auto-Ground logic added to GameScene.js.
This commit is contained in:
79
tools/create_tmx.js
Normal file
79
tools/create_tmx.js
Normal file
@@ -0,0 +1,79 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const TILED_DIR = path.join(__dirname, '../assets/tiled');
|
||||
const ASSETS_ROOT = path.join(__dirname, '../assets');
|
||||
|
||||
// Configuration
|
||||
const TMX_FILENAME = 'Faza1_Nova.tmx';
|
||||
const TSX_FILENAME = 'Buildings.tsx';
|
||||
const MAP_WIDTH = 80;
|
||||
const MAP_HEIGHT = 80;
|
||||
|
||||
// Gather all images
|
||||
const imageFiles = [];
|
||||
const sourceDirs = ['buildings', 'terrain', 'props'];
|
||||
|
||||
let tileIdCounter = 0;
|
||||
let tilesXml = '';
|
||||
|
||||
sourceDirs.forEach(dirName => {
|
||||
const fullPath = path.join(ASSETS_ROOT, dirName);
|
||||
if (fs.existsSync(fullPath)) {
|
||||
const files = fs.readdirSync(fullPath);
|
||||
files.forEach(file => {
|
||||
if (file.toLowerCase().endsWith('.png')) {
|
||||
// Determine relative path from assets/tiled/ to assets/dir/file
|
||||
// assets/tiled -> assets/dir = ../dir
|
||||
const relativePath = `../${dirName}/${file}`;
|
||||
|
||||
tilesXml += ` <tile id="${tileIdCounter}">\n`;
|
||||
tilesXml += ` <image source="${relativePath}"/>\n`; // Let Tiled detect size
|
||||
tilesXml += ` </tile>\n`;
|
||||
|
||||
tileIdCounter++;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 1. Generate TSX (Tileset)
|
||||
const tsxContent = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<tileset version="1.10" tiledversion="1.11.0" name="Buildings" tilewidth="256" tileheight="256" tilecount="${tileIdCounter}" columns="0">
|
||||
<grid orientation="orthogonal" width="1" height="1"/>
|
||||
${tilesXml}
|
||||
</tileset>`;
|
||||
|
||||
fs.writeFileSync(path.join(TILED_DIR, TSX_FILENAME), tsxContent);
|
||||
console.log(`✅ Created tileset: ${TSX_FILENAME} with ${tileIdCounter} tiles.`);
|
||||
|
||||
// 2. Generate TMX (Map)
|
||||
// Create empty CSV data (all zeros)
|
||||
const csvData = new Array(MAP_WIDTH * MAP_HEIGHT).fill(0).join(',');
|
||||
|
||||
const tmxContent = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<map version="1.10" tiledversion="1.11.0" orientation="orthogonal" renderorder="right-down" width="${MAP_WIDTH}" height="${MAP_HEIGHT}" tilewidth="48" tileheight="48" infinite="0" nextlayerid="5" nextobjectid="1">
|
||||
<properties>
|
||||
<property name="music" value="farm_theme"/>
|
||||
</properties>
|
||||
<tileset firstgid="1" source="${TSX_FILENAME}"/>
|
||||
<layer id="1" name="Ground" width="${MAP_WIDTH}" height="${MAP_HEIGHT}">
|
||||
<data encoding="csv">
|
||||
${csvData}
|
||||
</data>
|
||||
</layer>
|
||||
<layer id="2" name="Objects" width="${MAP_WIDTH}" height="${MAP_HEIGHT}">
|
||||
<data encoding="csv">
|
||||
${csvData}
|
||||
</data>
|
||||
</layer>
|
||||
<layer id="3" name="Collision" width="${MAP_WIDTH}" height="${MAP_HEIGHT}" opacity="0.5">
|
||||
<data encoding="csv">
|
||||
${csvData}
|
||||
</data>
|
||||
</layer>
|
||||
<objectgroup id="4" name="Audio_Zones"/>
|
||||
</map>`;
|
||||
|
||||
fs.writeFileSync(path.join(TILED_DIR, TMX_FILENAME), tmxContent);
|
||||
console.log(`✅ Created map: ${TMX_FILENAME} (${MAP_WIDTH}x${MAP_HEIGHT}) linked to ${TSX_FILENAME}.`);
|
||||
85
tools/generate_manifest.js
Normal file
85
tools/generate_manifest.js
Normal file
@@ -0,0 +1,85 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ASSETS_DIR = path.join(__dirname, '../assets');
|
||||
const OUTPUT_FILE = path.join(__dirname, '../src/AssetManifest.js');
|
||||
|
||||
const CATEGORY_MAP = {
|
||||
'buildings': 'farm',
|
||||
'crops': 'farm',
|
||||
'grounds': 'farm',
|
||||
'props': 'farm',
|
||||
'terrain': 'farm',
|
||||
'characters': 'common',
|
||||
'ui': 'common',
|
||||
'vfx': 'common',
|
||||
'images': 'common',
|
||||
'intro_assets': 'common',
|
||||
'sprites': 'common', // Treat as regular images for now unless frame config known
|
||||
'slike 🟢': 'common'
|
||||
};
|
||||
|
||||
const manifest = {
|
||||
phases: {
|
||||
farm: [],
|
||||
basement_mine: [],
|
||||
common: []
|
||||
},
|
||||
spritesheets: []
|
||||
};
|
||||
|
||||
function scanDir(dir, category) {
|
||||
if (!fs.existsSync(dir)) return;
|
||||
|
||||
const files = fs.readdirSync(dir);
|
||||
|
||||
files.forEach(file => {
|
||||
const fullPath = path.join(dir, file);
|
||||
const stat = fs.statSync(fullPath);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
// Recursive scan
|
||||
// Special handling: if dir is 'sprites', maybe subdirs are categories?
|
||||
// For now, simple recursion
|
||||
scanDir(fullPath, category);
|
||||
} else if (file.match(/\.(png|jpg|jpeg)$/i)) {
|
||||
const relativePath = 'assets/' + path.relative(ASSETS_DIR, fullPath);
|
||||
const key = path.parse(file).name; // Use filename without ext as key
|
||||
|
||||
// Avoid duplicates?
|
||||
// Just push for now
|
||||
|
||||
if (category === 'spritesheets') {
|
||||
// Try to guess definition or generic
|
||||
manifest.spritesheets.push({
|
||||
key: key,
|
||||
path: relativePath,
|
||||
frameConfig: { frameWidth: 32, frameHeight: 32 } // Default guess
|
||||
});
|
||||
} else {
|
||||
if (!manifest.phases[category]) manifest.phases[category] = [];
|
||||
manifest.phases[category].push({
|
||||
key: key,
|
||||
path: relativePath
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// MAIN SCAN LOOP
|
||||
Object.keys(CATEGORY_MAP).forEach(dirName => {
|
||||
const fullDir = path.join(ASSETS_DIR, dirName);
|
||||
const cat = CATEGORY_MAP[dirName];
|
||||
scanDir(fullDir, cat);
|
||||
});
|
||||
|
||||
// Generate File Content
|
||||
const fileContent = `window.AssetManifest = ${JSON.stringify(manifest, null, 4)};`;
|
||||
|
||||
fs.writeFileSync(OUTPUT_FILE, fileContent);
|
||||
|
||||
console.log('✅ AssetManifest.js regenerated!');
|
||||
console.log(`Farm: ${manifest.phases.farm.length}`);
|
||||
console.log(`Common: ${manifest.phases.common.length}`);
|
||||
console.log(`Mine: ${manifest.phases.basement_mine.length}`);
|
||||
118
tools/setup_tiled.js
Normal file
118
tools/setup_tiled.js
Normal file
@@ -0,0 +1,118 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const TILED_DIR = 'assets/tiled';
|
||||
const TARGET_MAPS = ['faza1_kmetija.json', 'demo_test.json'];
|
||||
const ASSET_DIRS = ['../buildings', '../terrain', '../props']; // Relative to TILED_DIR
|
||||
|
||||
// 1. Gather Images
|
||||
const tiles = []; // ARRAY for Tiled JSON compatibility
|
||||
let nextId = 0; // Start ID at 0
|
||||
let grassId = null;
|
||||
|
||||
ASSET_DIRS.forEach(dir => {
|
||||
const fullDir = path.join(__dirname, '..', 'assets', dir.replace('../', ''));
|
||||
if (fs.existsSync(fullDir)) {
|
||||
const files = fs.readdirSync(fullDir);
|
||||
files.forEach(file => {
|
||||
if (file.endsWith('.png')) {
|
||||
const relPath = path.join(dir, file);
|
||||
|
||||
// Add to array
|
||||
tiles.push({
|
||||
id: nextId,
|
||||
image: relPath,
|
||||
imageheight: 64, // Placeholder, Tiled will try to read actual
|
||||
imagewidth: 64
|
||||
});
|
||||
|
||||
// Identify Grass
|
||||
if (file.includes('grass') || file.includes('resource_pile_food')) { // Fallback
|
||||
if (grassId === null) grassId = nextId;
|
||||
}
|
||||
|
||||
nextId++;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`🔍 Found ${tiles.length} assets.`);
|
||||
console.log(`🌿 Grass ID: ${grassId}`);
|
||||
|
||||
// 2. Setup Tileset Object
|
||||
const autoTileset = {
|
||||
"columns": 0,
|
||||
"grid": { "height": 1, "orientation": "orthogonal", "width": 1 },
|
||||
"margin": 0,
|
||||
"name": "Auto_Assets",
|
||||
"spacing": 0,
|
||||
"tilecount": tiles.length,
|
||||
"tileheight": 256,
|
||||
"tiles": tiles, // Array!
|
||||
"tilewidth": 256,
|
||||
"type": "tileset_image_collection"
|
||||
};
|
||||
|
||||
// 3. Process Maps
|
||||
TARGET_MAPS.forEach(mapFile => {
|
||||
const mapPath = path.join(TILED_DIR, mapFile);
|
||||
if (!fs.existsSync(mapPath)) {
|
||||
console.warn(`⚠️ Map not found: ${mapPath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const mapData = JSON.parse(fs.readFileSync(mapPath, 'utf8'));
|
||||
|
||||
// A. Inject Tileset
|
||||
mapData.tilesets = [autoTileset];
|
||||
|
||||
// B. Auto-Ground (Fill Layer 1 with Grass)
|
||||
const mapWidth = 100;
|
||||
const mapHeight = 100;
|
||||
mapData.width = mapWidth;
|
||||
mapData.height = mapHeight;
|
||||
|
||||
// Find Ground Layer
|
||||
const groundLayer = mapData.layers.find(l => l.name === 'Ground');
|
||||
if (groundLayer && grassId !== null) {
|
||||
groundLayer.width = mapWidth;
|
||||
groundLayer.height = mapHeight;
|
||||
groundLayer.data = new Array(mapWidth * mapHeight).fill(grassId + 1); // +1 because GID is local ID + firstgid (1)
|
||||
// Wait, for image collections, keys match the ID.
|
||||
// But map data uses GID (Global Tile ID).
|
||||
// If firstgid is 1 (default for first tileset), then tile with ID 0 is GID 1.
|
||||
// So tile with ID X is GID X + 1.
|
||||
console.log(`✅ Filled Ground in ${mapFile} with GID ${grassId + 1}`);
|
||||
}
|
||||
|
||||
// C. Demo Setup (Landmarks)
|
||||
if (mapFile === 'demo_test.json') {
|
||||
const objectLayer = mapData.layers.find(l => l.name === 'Objects');
|
||||
if (objectLayer) {
|
||||
objectLayer.width = mapWidth;
|
||||
objectLayer.height = mapHeight;
|
||||
// Add 5 random buildings
|
||||
const randomIds = tiles.slice(0, 5).map(t => t.id);
|
||||
const objData = new Array(mapWidth * mapHeight).fill(0);
|
||||
|
||||
randomIds.forEach((tileId, index) => {
|
||||
const x = 10 + (index * 10);
|
||||
const y = 15;
|
||||
const idx = y * mapWidth + x;
|
||||
if (idx < objData.length) objData[idx] = tileId + 1; // +1 GID
|
||||
});
|
||||
objectLayer.data = objData;
|
||||
console.log(`✅ Placed 5 landmarks in ${mapFile}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Save
|
||||
fs.writeFileSync(mapPath, JSON.stringify(mapData, null, 4));
|
||||
console.log(`💾 Saved updated ${mapFile}`);
|
||||
|
||||
} catch (e) {
|
||||
console.error(`❌ Error processing ${mapFile}:`, e);
|
||||
}
|
||||
});
|
||||
120
tools/tmx_to_json.js
Normal file
120
tools/tmx_to_json.js
Normal file
@@ -0,0 +1,120 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const src = 'assets/tiled/ProbnaFarma.tmx';
|
||||
const dest = 'assets/maps/NovaFarma.json';
|
||||
|
||||
try {
|
||||
const xml = fs.readFileSync(src, 'utf8');
|
||||
|
||||
// Super simple Regex parser for Tiled TMX (Caveat: Fragile!)
|
||||
const parseAttr = (tag, attr) => {
|
||||
const match = tag.match(new RegExp(`${attr}="([^"]*)"`));
|
||||
return match ? match[1] : null;
|
||||
};
|
||||
|
||||
// Extract Map Info
|
||||
const mapMatch = xml.match(/<map [^>]+>/);
|
||||
if (!mapMatch) throw new Error("No <map> tag");
|
||||
|
||||
const mapTag = mapMatch[0];
|
||||
const width = parseInt(parseAttr(mapTag, 'width'));
|
||||
const height = parseInt(parseAttr(mapTag, 'height'));
|
||||
const tilewidth = parseInt(parseAttr(mapTag, 'tilewidth'));
|
||||
const tileheight = parseInt(parseAttr(mapTag, 'tileheight'));
|
||||
|
||||
const json = {
|
||||
width, height, tilewidth, tileheight,
|
||||
layers: [],
|
||||
tilesets: [],
|
||||
orientation: "orthogonal",
|
||||
renderorder: "right-down",
|
||||
type: "map"
|
||||
};
|
||||
|
||||
// Extract Layers
|
||||
const layerRegex = /<layer [^>]+>([\s\S]*?)<\/layer>/g;
|
||||
let layerMatch;
|
||||
let idCounter = 1;
|
||||
|
||||
while ((layerMatch = layerRegex.exec(xml)) !== null) {
|
||||
const fullLayer = layerMatch[0];
|
||||
const content = layerMatch[1];
|
||||
|
||||
const openTag = fullLayer.match(/<layer [^>]+>/)[0];
|
||||
const name = parseAttr(openTag, 'name');
|
||||
const visible = parseAttr(openTag, 'visible') !== '0';
|
||||
|
||||
let data = [];
|
||||
// Extract CSV Data
|
||||
const dataMatch = content.match(/<data encoding="csv">([\s\S]*?)<\/data>/);
|
||||
if (dataMatch) {
|
||||
data = dataMatch[1].replace(/\s/g, '').split(',').map(Number);
|
||||
}
|
||||
|
||||
json.layers.push({
|
||||
id: idCounter++,
|
||||
name: name,
|
||||
type: "tilelayer",
|
||||
width: width,
|
||||
height: height,
|
||||
visible: visible,
|
||||
opacity: 1,
|
||||
x: 0, y: 0,
|
||||
data: data
|
||||
});
|
||||
}
|
||||
|
||||
// Extract Object Groups
|
||||
const objGroupRegex = /<objectgroup [^>]+>([\s\S]*?)<\/objectgroup>/g;
|
||||
while ((layerMatch = objGroupRegex.exec(xml)) !== null) {
|
||||
const openTag = layerMatch[0].match(/<objectgroup [^>]+>/)[0];
|
||||
const name = parseAttr(openTag, 'name');
|
||||
|
||||
json.layers.push({
|
||||
id: idCounter++,
|
||||
name: name,
|
||||
type: "objectgroup",
|
||||
visible: true,
|
||||
opacity: 1,
|
||||
objects: [] // Parsing objects is harder, skipping for now as user just wants visuals mostly
|
||||
});
|
||||
}
|
||||
|
||||
// Extract Tilesets
|
||||
// <tileset firstgid="1" source="Buildings.tsx"/>
|
||||
const tilesetRegex = /<tileset [^>]+>/g;
|
||||
let tsMatch;
|
||||
while ((tsMatch = tilesetRegex.exec(xml)) !== null) {
|
||||
const tsTag = tsMatch[0];
|
||||
const firstgid = parseInt(parseAttr(tsTag, 'firstgid'));
|
||||
const source = parseAttr(tsTag, 'source');
|
||||
|
||||
// We need to bake the tileset data or link it.
|
||||
// Phaser supports linked tilesets if JSON.
|
||||
// But for simplicity, let's create a minimal embedded definition referring to image collection
|
||||
// Actually, Tiled JSON exports 'source' usually.
|
||||
|
||||
// Fix path: TMX is in assets/tiled/, references are relative.
|
||||
// JSON is in assets/maps/.
|
||||
// If source is "Buildings.tsx", in TMX it means "assets/tiled/Buildings.tsx".
|
||||
// In JSON (assets/maps), it should be "../tiled/Buildings.tsx".
|
||||
|
||||
let jsonSource = source;
|
||||
if (!source.includes('/')) jsonSource = '../tiled/' + source;
|
||||
|
||||
json.tilesets.push({
|
||||
firstgid: firstgid,
|
||||
source: jsonSource
|
||||
});
|
||||
}
|
||||
|
||||
// Write
|
||||
fs.writeFileSync(dest, JSON.stringify(json, null, 4));
|
||||
console.log(`✅ Converted TMX to JSON: ${dest}`);
|
||||
|
||||
} catch (e) {
|
||||
console.error("❌ Conversion failed:", e);
|
||||
// Create Dummy
|
||||
fs.writeFileSync(dest, JSON.stringify({ width: 10, height: 10, layers: [] }));
|
||||
}
|
||||
@@ -1,812 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="sl">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>🎨 Visual Asset Manager - DolinaSmrti</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', system-ui, sans-serif;
|
||||
background: linear-gradient(135deg, #0f0c1d 0%, #1a1333 50%, #2d1b3d 100%);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
.sidebar {
|
||||
width: 280px;
|
||||
background: rgba(20, 20, 40, 0.95);
|
||||
border-right: 2px solid #9D4EDD;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
box-shadow: 4px 0 20px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.sidebar h2 {
|
||||
color: #9D4EDD;
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.5em;
|
||||
text-shadow: 0 0 10px rgba(157, 78, 221, 0.5);
|
||||
}
|
||||
|
||||
.stats-panel {
|
||||
background: rgba(157, 78, 221, 0.1);
|
||||
border: 2px solid #9D4EDD;
|
||||
border-radius: 12px;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stat-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 8px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
color: #9D4EDD;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.filter-title {
|
||||
color: #9D4EDD;
|
||||
font-size: 14px;
|
||||
margin-bottom: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.filter-btn {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
margin: 5px 0;
|
||||
border: 2px solid rgba(157, 78, 221, 0.3);
|
||||
background: rgba(42, 42, 60, 0.6);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s;
|
||||
font-size: 13px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.filter-btn:hover {
|
||||
background: rgba(157, 78, 221, 0.2);
|
||||
border-color: #9D4EDD;
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
.filter-btn.active {
|
||||
background: #9D4EDD;
|
||||
color: #000;
|
||||
font-weight: bold;
|
||||
border-color: #9D4EDD;
|
||||
}
|
||||
|
||||
.filter-count {
|
||||
float: right;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
margin: 8px 0;
|
||||
border: 2px solid #9D4EDD;
|
||||
background: rgba(157, 78, 221, 0.2);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
font-weight: bold;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background: #9D4EDD;
|
||||
color: #000;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.action-btn.danger {
|
||||
border-color: #ff4444;
|
||||
background: rgba(255, 68, 68, 0.2);
|
||||
}
|
||||
|
||||
.action-btn.danger:hover {
|
||||
background: #ff4444;
|
||||
}
|
||||
|
||||
/* Main Content */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Toolbar */
|
||||
.toolbar {
|
||||
background: rgba(20, 20, 40, 0.95);
|
||||
border-bottom: 2px solid #9D4EDD;
|
||||
padding: 15px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
flex: 1;
|
||||
padding: 12px 20px;
|
||||
border: 2px solid #9D4EDD;
|
||||
background: rgba(42, 42, 60, 0.8);
|
||||
color: #fff;
|
||||
border-radius: 25px;
|
||||
font-size: 16px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.search-box:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 20px rgba(157, 78, 221, 0.5);
|
||||
}
|
||||
|
||||
.view-toggle {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.view-btn {
|
||||
padding: 10px 15px;
|
||||
border: 2px solid #9D4EDD;
|
||||
background: rgba(42, 42, 60, 0.8);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.view-btn.active {
|
||||
background: #9D4EDD;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/* Gallery */
|
||||
.gallery-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.gallery-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.asset-card {
|
||||
background: rgba(30, 30, 50, 0.9);
|
||||
border-radius: 16px;
|
||||
padding: 15px;
|
||||
border: 2px solid rgba(157, 78, 221, 0.2);
|
||||
transition: all 0.3s;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.asset-card:hover {
|
||||
transform: translateY(-8px);
|
||||
box-shadow: 0 12px 30px rgba(157, 78, 221, 0.4);
|
||||
border-color: #9D4EDD;
|
||||
}
|
||||
|
||||
.asset-thumbnail {
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
background: rgba(10, 10, 20, 0.8);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 12px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.asset-thumbnail img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.asset-thumbnail:hover img {
|
||||
transform: scale(1.15);
|
||||
}
|
||||
|
||||
.asset-filename {
|
||||
font-size: 13px;
|
||||
color: #ccc;
|
||||
margin-bottom: 10px;
|
||||
word-break: break-word;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.asset-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 11px;
|
||||
color: #888;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.asset-controls {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
padding: 8px;
|
||||
border: 1px solid;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
transition: all 0.2s;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.control-btn.delete {
|
||||
border-color: #ff4444;
|
||||
color: #ff4444;
|
||||
}
|
||||
|
||||
.control-btn.delete:hover {
|
||||
background: #ff4444;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.control-btn.reroll {
|
||||
border-color: #44ff44;
|
||||
color: #44ff44;
|
||||
}
|
||||
|
||||
.control-btn.reroll:hover {
|
||||
background: #44ff44;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.control-btn.view {
|
||||
border-color: #4488ff;
|
||||
color: #4488ff;
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.control-btn.view:hover {
|
||||
background: #4488ff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.95);
|
||||
z-index: 10000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
position: relative;
|
||||
max-width: 90%;
|
||||
max-height: 90%;
|
||||
background: rgba(30, 30, 50, 0.95);
|
||||
border-radius: 16px;
|
||||
padding: 30px;
|
||||
border: 2px solid #9D4EDD;
|
||||
}
|
||||
|
||||
.modal-image {
|
||||
max-width: 100%;
|
||||
max-height: 70vh;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.modal-info {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.modal-info h3 {
|
||||
color: #9D4EDD;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.close-modal {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: rgba(157, 78, 221, 0.3);
|
||||
border: 2px solid #9D4EDD;
|
||||
color: #fff;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.close-modal:hover {
|
||||
background: #9D4EDD;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
/* Loading & Toast */
|
||||
.loading-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
z-index: 9999;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.loading-overlay.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
border: 4px solid rgba(157, 78, 221, 0.3);
|
||||
border-top: 4px solid #9D4EDD;
|
||||
border-radius: 50%;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: 30px;
|
||||
right: 30px;
|
||||
padding: 15px 25px;
|
||||
background: rgba(157, 78, 221, 0.95);
|
||||
color: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
|
||||
transform: translateY(200px);
|
||||
transition: transform 0.3s;
|
||||
z-index: 10001;
|
||||
}
|
||||
|
||||
.toast.show {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba(20, 20, 40, 0.5);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #9D4EDD;
|
||||
border-radius: 5px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Sidebar -->
|
||||
<div class="sidebar">
|
||||
<h2>🎨 Asset Manager</h2>
|
||||
|
||||
<div class="stats-panel">
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">Skupaj:</span>
|
||||
<span class="stat-value" id="total-assets">Loading...</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">Prikazanih:</span>
|
||||
<span class="stat-value" id="visible-assets">0</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span class="stat-label">Velikost:</span>
|
||||
<span class="stat-value" id="total-size">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filter-section">
|
||||
<div class="filter-title">📁 Kategorije</div>
|
||||
<button class="filter-btn active" data-filter="all">
|
||||
Vse <span class="filter-count">1166</span>
|
||||
</button>
|
||||
<button class="filter-btn" data-filter="liki">
|
||||
👤 Liki <span class="filter-count">31</span>
|
||||
</button>
|
||||
<button class="filter-btn" data-filter="zgradbe">
|
||||
🏠 Zgradbe <span class="filter-count">54</span>
|
||||
</button>
|
||||
<button class="filter-btn" data-filter="oprema">
|
||||
⚔️ Oprema <span class="filter-count">48</span>
|
||||
</button>
|
||||
<button class="filter-btn" data-filter="narava">
|
||||
🌿 Narava <span class="filter-count">289</span>
|
||||
</button>
|
||||
<button class="filter-btn" data-filter="notranjost">
|
||||
🛋️ Notranjost <span class="filter-count">57</span>
|
||||
</button>
|
||||
<button class="filter-btn" data-filter="teren">
|
||||
🗺️ Teren <span class="filter-count">30</span>
|
||||
</button>
|
||||
<button class="filter-btn" data-filter="vmesnik">
|
||||
🎨 UI <span class="filter-count">34</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="action-buttons">
|
||||
<button class="action-btn" onclick="runCodeScan()">
|
||||
🔍 Code Deep Scan
|
||||
</button>
|
||||
<button class="action-btn" onclick="validatePaths()">
|
||||
✅ Validate Paths
|
||||
</button>
|
||||
<button class="action-btn" onclick="organizeAssets()">
|
||||
📂 Organize Assets
|
||||
</button>
|
||||
<button class="action-btn danger" onclick="deleteSelected()">
|
||||
🗑️ Delete Selected
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="main-content">
|
||||
<!-- Toolbar -->
|
||||
<div class="toolbar">
|
||||
<input type="text" class="search-box" id="search" placeholder="🔍 Išči assete...">
|
||||
<div class="view-toggle">
|
||||
<button class="view-btn active" onclick="setView('grid')">⊞ Grid</button>
|
||||
<button class="view-btn" onclick="setView('list')">☰ List</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gallery -->
|
||||
<div class="gallery-container">
|
||||
<div class="gallery-grid" id="gallery">
|
||||
<!-- Assets will be loaded here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal -->
|
||||
<div class="modal" id="modal">
|
||||
<div class="modal-content">
|
||||
<div class="close-modal" onclick="closeModal()">×</div>
|
||||
<img id="modal-img" class="modal-image" src="" alt="">
|
||||
<div class="modal-info">
|
||||
<h3 id="modal-filename"></h3>
|
||||
<p id="modal-path"></p>
|
||||
<p id="modal-size"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading Overlay -->
|
||||
<div class="loading-overlay" id="loading">
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
|
||||
<!-- Toast Notifications -->
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<script>
|
||||
let allAssets = [];
|
||||
let selectedAssets = new Set();
|
||||
let currentFilter = 'all';
|
||||
|
||||
// Initialize
|
||||
async function init() {
|
||||
await loadAssets();
|
||||
setupEventListeners();
|
||||
renderGallery(allAssets);
|
||||
}
|
||||
|
||||
async function loadAssets() {
|
||||
console.log('Loading assets from manifest...');
|
||||
try {
|
||||
const response = await fetch('asset_manifest.json');
|
||||
const manifest = await response.json();
|
||||
|
||||
allAssets = manifest.assets;
|
||||
|
||||
// Update stats
|
||||
document.getElementById('total-assets').textContent = manifest.total_assets;
|
||||
document.getElementById('visible-assets').textContent = manifest.total_assets;
|
||||
document.getElementById('total-size').textContent = manifest.total_size_mb;
|
||||
|
||||
console.log(`✅ Loaded ${allAssets.length} assets from manifest`);
|
||||
} catch (error) {
|
||||
console.error('❌ Error loading manifest:', error);
|
||||
alert('Error loading assets! Make sure you run:\npython3 ../scripts/generate_asset_manifest.py');
|
||||
}
|
||||
}
|
||||
|
||||
function renderGallery(assets) {
|
||||
const gallery = document.getElementById('gallery');
|
||||
document.getElementById('visible-assets').textContent = assets.length;
|
||||
|
||||
if (assets.length === 0) {
|
||||
gallery.innerHTML = '<p style="text-align:center;padding:60px;color:#888;">Ni rezultatov</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
gallery.innerHTML = assets.map(asset => `
|
||||
<div class="asset-card" data-id="${asset.id}">
|
||||
<div class="asset-thumbnail" onclick="viewAsset('${asset.id}')">
|
||||
<img src="${asset.path}" alt="${asset.name}" loading="lazy">
|
||||
</div>
|
||||
<div class="asset-filename">${asset.name}</div>
|
||||
<div class="asset-meta">
|
||||
<span>📁 ${asset.category}</span>
|
||||
<span>${asset.size}</span>
|
||||
</div>
|
||||
<div class="asset-controls">
|
||||
<button class="control-btn delete" onclick="deleteAsset('${asset.id}')">
|
||||
🗑️ Delete
|
||||
</button>
|
||||
<button class="control-btn reroll" onclick="rerollAsset('${asset.id}')">
|
||||
🔄 Re-roll
|
||||
</button>
|
||||
<button class="control-btn view" onclick="viewAsset('${asset.id}')">
|
||||
👁️ View Full
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function setupEventListeners() {
|
||||
// Search
|
||||
document.getElementById('search').addEventListener('input', (e) => {
|
||||
const query = e.target.value.toLowerCase();
|
||||
const filtered = allAssets.filter(a =>
|
||||
a.name.toLowerCase().includes(query) ||
|
||||
a.category.toLowerCase().includes(query)
|
||||
);
|
||||
renderGallery(filtered);
|
||||
});
|
||||
|
||||
// Filter buttons
|
||||
document.querySelectorAll('.filter-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
currentFilter = btn.dataset.filter;
|
||||
applyFilters();
|
||||
});
|
||||
});
|
||||
|
||||
// Modal close on outside click
|
||||
document.getElementById('modal').addEventListener('click', (e) => {
|
||||
if (e.target.id === 'modal') closeModal();
|
||||
});
|
||||
}
|
||||
|
||||
function applyFilters() {
|
||||
let filtered = allAssets;
|
||||
if (currentFilter !== 'all') {
|
||||
filtered = filtered.filter(a => a.category === currentFilter);
|
||||
}
|
||||
renderGallery(filtered);
|
||||
}
|
||||
|
||||
// Asset actions
|
||||
function viewAsset(id) {
|
||||
const asset = allAssets.find(a => a.id === id);
|
||||
if (!asset) return;
|
||||
|
||||
document.getElementById('modal-img').src = asset.path;
|
||||
document.getElementById('modal-filename').textContent = asset.name;
|
||||
document.getElementById('modal-path').textContent = `📁 ${asset.path}`;
|
||||
document.getElementById('modal-size').textContent = `💾 ${asset.size}`;
|
||||
document.getElementById('modal').classList.add('active');
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('modal').classList.remove('active');
|
||||
}
|
||||
|
||||
function deleteAsset(id) {
|
||||
const asset = allAssets.find(a => a.id === id);
|
||||
if (!asset) return;
|
||||
|
||||
if (!confirm(`Res želiš trajno izbrisati:\n${asset.name}\n\nDatoteka bo fizično izbrisana iz diska!`)) return;
|
||||
|
||||
showLoading();
|
||||
|
||||
// Call backend API
|
||||
fetch(`http://localhost:5001/api/asset/${id}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
hideLoading();
|
||||
if (data.success) {
|
||||
showToast(`✅ ${asset.name} izbrisan!`);
|
||||
// Reload assets from updated manifest
|
||||
setTimeout(() => {
|
||||
loadAssets().then(() => {
|
||||
applyFilters();
|
||||
});
|
||||
}, 500);
|
||||
} else {
|
||||
showToast(`❌ Napaka: ${data.error}`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
hideLoading();
|
||||
showToast(`❌ Backend error: ${error.message}`);
|
||||
console.error('Delete error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function rerollAsset(id) {
|
||||
const asset = allAssets.find(a => a.id === id);
|
||||
if (!asset) return;
|
||||
|
||||
if (!confirm(`Re-generate "${asset.name}" z novim promptom?\n\n(Not yet fully implemented)`)) return;
|
||||
|
||||
showLoading();
|
||||
|
||||
// Call backend API
|
||||
fetch(`http://localhost:5001/api/asset/${id}/reroll`, {
|
||||
method: 'POST'
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
hideLoading();
|
||||
if (data.success) {
|
||||
showToast(`🎨 ${data.message}`);
|
||||
if (data.note) {
|
||||
alert(data.note);
|
||||
}
|
||||
} else {
|
||||
showToast(`❌ Napaka: ${data.error}`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
hideLoading();
|
||||
showToast(`❌ Backend error: ${error.message}`);
|
||||
console.error('Re-roll error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// Bulk actions
|
||||
function runCodeScan() {
|
||||
showLoading();
|
||||
showToast('🔍 Running Deep Code Scan...');
|
||||
setTimeout(() => {
|
||||
hideLoading();
|
||||
showToast('✅ Code scan complete! 0 errors found.');
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function validatePaths() {
|
||||
showLoading();
|
||||
showToast('✅ Validating asset paths...');
|
||||
setTimeout(() => {
|
||||
hideLoading();
|
||||
showToast('✅ All paths valid!');
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
function organizeAssets() {
|
||||
if (!confirm('Start asset organization? This will move files.')) return;
|
||||
showLoading();
|
||||
showToast('📂 Organizing assets...');
|
||||
setTimeout(() => {
|
||||
hideLoading();
|
||||
showToast('✅ Assets organized!');
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function deleteSelected() {
|
||||
if (selectedAssets.size === 0) {
|
||||
alert('No assets selected');
|
||||
return;
|
||||
}
|
||||
if (!confirm(`Delete ${selectedAssets.size} selected assets?`)) return;
|
||||
showLoading();
|
||||
setTimeout(() => {
|
||||
hideLoading();
|
||||
selectedAssets.clear();
|
||||
showToast('✅ Selected assets deleted!');
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
// UI helpers
|
||||
function showLoading() {
|
||||
document.getElementById('loading').classList.add('active');
|
||||
}
|
||||
|
||||
function hideLoading() {
|
||||
document.getElementById('loading').classList.remove('active');
|
||||
}
|
||||
|
||||
function showToast(message) {
|
||||
const toast = document.getElementById('toast');
|
||||
toast.textContent = message;
|
||||
toast.classList.add('show');
|
||||
setTimeout(() => toast.classList.remove('show'), 3000);
|
||||
}
|
||||
|
||||
function setView(view) {
|
||||
document.querySelectorAll('.view-btn').forEach(b => b.classList.remove('active'));
|
||||
event.target.classList.add('active');
|
||||
// Implement list view if needed
|
||||
}
|
||||
|
||||
// Initialize on load
|
||||
window.addEventListener('DOMContentLoaded', init);
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user