diff --git a/src/editor/configs/voidstrike.ts b/src/editor/configs/voidstrike.ts index 2bc5f367..f127e052 100644 --- a/src/editor/configs/voidstrike.ts +++ b/src/editor/configs/voidstrike.ts @@ -570,6 +570,84 @@ export const VOIDSTRIKE_OBJECTS: ObjectTypeConfig[] = [ { key: 'rotation', name: 'Rotation', type: 'number', defaultValue: 0, min: 0, max: 360 }, ], }, + { + id: 'decoration_rock_single', + category: 'decorations', + name: 'Single Rock', + icon: '🪨', + color: '#78909c', + defaultRadius: 2, + movable: true, + properties: [ + { key: 'scale', name: 'Scale', type: 'number', defaultValue: 1, min: 0.25, max: 3 }, + { key: 'rotation', name: 'Rotation', type: 'number', defaultValue: 0, min: 0, max: 360 }, + ], + }, + { + id: 'decoration_tree_alien', + category: 'decorations', + name: 'Alien Tree', + icon: '🌴', + color: '#9c27b0', + defaultRadius: 2, + movable: true, + properties: [ + { key: 'scale', name: 'Scale', type: 'number', defaultValue: 1, min: 0.25, max: 3 }, + { key: 'rotation', name: 'Rotation', type: 'number', defaultValue: 0, min: 0, max: 360 }, + ], + }, + { + id: 'decoration_tree_palm', + category: 'decorations', + name: 'Palm Tree', + icon: '🌴', + color: '#4caf50', + defaultRadius: 2, + movable: true, + properties: [ + { key: 'scale', name: 'Scale', type: 'number', defaultValue: 1, min: 0.25, max: 3 }, + { key: 'rotation', name: 'Rotation', type: 'number', defaultValue: 0, min: 0, max: 360 }, + ], + }, + { + id: 'decoration_tree_mushroom', + category: 'decorations', + name: 'Giant Mushroom', + icon: '🍄', + color: '#e91e63', + defaultRadius: 2, + movable: true, + properties: [ + { key: 'scale', name: 'Scale', type: 'number', defaultValue: 1, min: 0.25, max: 3 }, + { key: 'rotation', name: 'Rotation', type: 'number', defaultValue: 0, min: 0, max: 360 }, + ], + }, + { + id: 'decoration_alien_tower', + category: 'decorations', + name: 'Alien Tower', + icon: '🗼', + color: '#ff5722', + defaultRadius: 3, + movable: true, + properties: [ + { key: 'scale', name: 'Scale', type: 'number', defaultValue: 1, min: 0.25, max: 3 }, + { key: 'rotation', name: 'Rotation', type: 'number', defaultValue: 0, min: 0, max: 360 }, + ], + }, + { + id: 'decoration_debris', + category: 'decorations', + name: 'Debris', + icon: '🔩', + color: '#546e7a', + defaultRadius: 1.5, + movable: true, + properties: [ + { key: 'scale', name: 'Scale', type: 'number', defaultValue: 1, min: 0.25, max: 3 }, + { key: 'rotation', name: 'Rotation', type: 'number', defaultValue: 0, min: 0, max: 360 }, + ], + }, ]; // ============================================ diff --git a/src/editor/rendering3d/EditorModelLoader.ts b/src/editor/rendering3d/EditorModelLoader.ts index 9127c750..e0d94921 100644 --- a/src/editor/rendering3d/EditorModelLoader.ts +++ b/src/editor/rendering3d/EditorModelLoader.ts @@ -1,86 +1,36 @@ /** - * EditorModelLoader - GLTF model loader for the map editor + * EditorModelLoader - Dynamic GLTF model loader for the map editor * - * Loads real 3D models for decorations and objects in the editor, - * replacing placeholder geometries with actual game assets. + * Loads real 3D models for decorations and objects in the editor. + * Configuration is read dynamically from assets.json to stay in sync + * with the game's asset definitions. This allows the editor to work + * with any game that follows the same asset configuration format. */ import * as THREE from 'three'; import { GLTFLoader, type GLTF } from 'three/examples/jsm/loaders/GLTFLoader.js'; import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'; -// Model configuration mapping decoration IDs to model paths and settings +// Model configuration loaded from assets.json export interface ModelConfig { path: string; scale: number; - rotationY?: number; // Base rotation offset in degrees + rotationY: number; } -// Maps editor object type IDs to their model configurations -const MODEL_CONFIGS: Record = { - // Trees - decoration_tree_pine_tall: { - path: '/models/decorations/tree_pine_tall_LOD2.glb', - scale: 14.0, - rotationY: -90, - }, - decoration_tree_pine_medium: { - path: '/models/decorations/tree_pine_tall_LOD2.glb', - scale: 10.0, - rotationY: -90, - }, - decoration_tree_dead: { - path: '/models/decorations/tree_dead_LOD2.glb', - scale: 9.0, - rotationY: -90, - }, - - // Rocks - decoration_rocks_large: { - path: '/models/decorations/rocks_large_LOD2.glb', - scale: 3.0, - rotationY: -90, - }, - decoration_rocks_small: { - path: '/models/decorations/rocks_small_LOD2.glb', - scale: 2.0, - rotationY: -90, - }, - - // Special decorations - decoration_crystal_formation: { - path: '/models/decorations/crystal_formation_LOD2.glb', - scale: 4.0, - rotationY: -90, - }, - decoration_bush: { - path: '/models/decorations/shrub_LOD2.glb', - scale: 1.5, - rotationY: -90, - }, - decoration_ruined_wall: { - path: '/models/decorations/ruined_wall_LOD2.glb', - scale: 5.0, - rotationY: -90, - }, - - // Objects (using decoration models as stand-ins) - watch_tower: { - path: '/models/decorations/alien_tower_LOD2.glb', - scale: 8.0, - rotationY: -90, - }, - destructible_rock: { - path: '/models/decorations/rock_single_LOD2.glb', - scale: 2.5, - rotationY: -90, - }, - destructible_debris: { - path: '/models/decorations/debris_LOD2.glb', - scale: 1.5, - rotationY: -90, - }, -}; +// Asset configuration structure from assets.json +interface AssetJsonConfig { + decorations?: Record; + resources?: Record; +} // DRACO loader for compressed meshes const dracoLoader = new DRACOLoader(); @@ -91,6 +41,9 @@ dracoLoader.setDecoderConfig({ type: 'js' }); const gltfLoader = new GLTFLoader(); gltfLoader.setDRACOLoader(dracoLoader); +// Dynamic model configurations loaded from assets.json +const modelConfigs = new Map(); + // Cache for loaded models (template instances) const modelCache = new Map(); @@ -100,6 +53,96 @@ const loadingPromises = new Map>(); // Track loading state let isInitialized = false; let initPromise: Promise | null = null; +let configLoaded = false; + +/** + * Load asset configuration from assets.json + */ +async function loadAssetConfig(): Promise { + if (configLoaded) return; + + try { + const response = await fetch('/config/assets.json'); + if (!response.ok) { + console.warn('[EditorModelLoader] assets.json not found, using fallback'); + return; + } + + const config: AssetJsonConfig = await response.json(); + + // Register decoration models + if (config.decorations) { + for (const [assetId, assetConfig] of Object.entries(config.decorations)) { + const editorId = `decoration_${assetId}`; + modelConfigs.set(editorId, { + path: assetConfig.model, + scale: assetConfig.scale ?? 1.0, + rotationY: assetConfig.rotation?.y ?? 0, + }); + } + } + + // Register resource models (minerals, vespene) + if (config.resources) { + for (const [assetId, assetConfig] of Object.entries(config.resources)) { + const editorId = `resource_${assetId}`; + modelConfigs.set(editorId, { + path: assetConfig.model, + scale: assetConfig.scale ?? 1.0, + rotationY: assetConfig.rotation?.y ?? 0, + }); + } + } + + // Add common editor type aliases that map to asset IDs + // This handles cases where editor uses different naming conventions + const aliases: Record = { + // Trees + 'decoration_tree_pine_tall': 'decoration_tree_pine_tall', + 'decoration_tree_pine_medium': 'decoration_tree_pine_tall', // Uses same model, different scale + 'decoration_tree_dead': 'decoration_tree_dead', + 'decoration_tree_alien': 'decoration_tree_alien', + 'decoration_tree_palm': 'decoration_tree_palm', + 'decoration_tree_mushroom': 'decoration_tree_mushroom', + // Rocks + 'decoration_rocks_large': 'decoration_rocks_large', + 'decoration_rocks_small': 'decoration_rocks_small', + 'decoration_rock_single': 'decoration_rock_single', + // Special + 'decoration_crystal_formation': 'decoration_crystal_formation', + 'decoration_bush': 'decoration_shrub', + 'decoration_ruined_wall': 'decoration_ruined_wall', + 'decoration_alien_tower': 'decoration_alien_tower', + 'decoration_debris': 'decoration_debris', + // Game objects using decoration models + 'watch_tower': 'decoration_alien_tower', + 'destructible_rock': 'decoration_rock_single', + 'destructible_debris': 'decoration_debris', + }; + + // Apply aliases - copy config from source to alias + for (const [alias, source] of Object.entries(aliases)) { + if (!modelConfigs.has(alias) && modelConfigs.has(source)) { + const sourceConfig = modelConfigs.get(source)!; + modelConfigs.set(alias, { ...sourceConfig }); + } + } + + // Special case: decoration_tree_pine_medium uses pine_tall model but smaller + if (modelConfigs.has('decoration_tree_pine_tall') && !modelConfigs.has('decoration_tree_pine_medium')) { + const pineConfig = modelConfigs.get('decoration_tree_pine_tall')!; + modelConfigs.set('decoration_tree_pine_medium', { + ...pineConfig, + scale: pineConfig.scale * 0.7, // 70% of tall pine + }); + } + + configLoaded = true; + console.log(`[EditorModelLoader] Loaded ${modelConfigs.size} model configurations from assets.json`); + } catch (error) { + console.warn('[EditorModelLoader] Failed to load assets.json:', error); + } +} /** * Normalize a model: scale to target height and ground to y=0 @@ -126,7 +169,7 @@ function normalizeModel(root: THREE.Object3D, targetScale: number, rotationY: nu // Update matrices after transform root.updateMatrixWorld(true); - // Recalculate bounds + // Recalculate bounds after scaling box.setFromObject(root); // Ground the model (set bottom at y=0) @@ -139,7 +182,7 @@ function normalizeModel(root: THREE.Object3D, targetScale: number, rotationY: nu * Load a single model by its type ID */ async function loadModel(typeId: string): Promise { - const config = MODEL_CONFIGS[typeId]; + const config = modelConfigs.get(typeId); if (!config) { return null; } @@ -161,7 +204,7 @@ async function loadModel(typeId: string): Promise { (gltf: GLTF) => { const model = gltf.scene; - // Normalize the model + // Normalize the model to target height normalizeModel(model, config.scale, config.rotationY); // Enable shadows and visibility @@ -194,6 +237,9 @@ async function loadModel(typeId: string): Promise { /** * Editor Model Loader - Manages 3D models for the map editor + * + * Dynamically loads model configurations from assets.json to stay + * in sync with the game's asset definitions. */ export class EditorModelLoader { /** @@ -204,8 +250,11 @@ export class EditorModelLoader { if (initPromise) return initPromise; initPromise = (async () => { - // Preload all decoration models - const modelTypes = Object.keys(MODEL_CONFIGS); + // Load configuration from assets.json + await loadAssetConfig(); + + // Preload all registered models + const modelTypes = Array.from(modelConfigs.keys()); await Promise.all(modelTypes.map((typeId) => loadModel(typeId))); isInitialized = true; })(); @@ -213,18 +262,42 @@ export class EditorModelLoader { return initPromise; } + /** + * Register a custom model configuration + * Useful for game-specific extensions or runtime additions + */ + static registerModel(typeId: string, config: ModelConfig): void { + modelConfigs.set(typeId, config); + } + + /** + * Register multiple model configurations at once + */ + static registerModels(configs: Record): void { + for (const [typeId, config] of Object.entries(configs)) { + modelConfigs.set(typeId, config); + } + } + /** * Check if a model is available for a given type */ static hasModel(typeId: string): boolean { - return MODEL_CONFIGS[typeId] !== undefined; + return modelConfigs.has(typeId); } /** * Get the model config for a type */ static getModelConfig(typeId: string): ModelConfig | null { - return MODEL_CONFIGS[typeId] || null; + return modelConfigs.get(typeId) || null; + } + + /** + * Get all registered model type IDs + */ + static getRegisteredTypes(): string[] { + return Array.from(modelConfigs.keys()); } /** @@ -275,7 +348,7 @@ export class EditorModelLoader { * Get loading progress (0-1) */ static getLoadingProgress(): number { - const total = Object.keys(MODEL_CONFIGS).length; + const total = modelConfigs.size; if (total === 0) return 1; return modelCache.size / total; } @@ -301,6 +374,17 @@ export class EditorModelLoader { isInitialized = false; initPromise = null; } + + /** + * Reset and reload configurations + * Useful when assets.json is updated + */ + static async reload(): Promise { + this.dispose(); + modelConfigs.clear(); + configLoaded = false; + await this.initialize(); + } } export default EditorModelLoader; diff --git a/src/editor/rendering3d/EditorObjects.ts b/src/editor/rendering3d/EditorObjects.ts index e9e83010..0a2708a7 100644 --- a/src/editor/rendering3d/EditorObjects.ts +++ b/src/editor/rendering3d/EditorObjects.ts @@ -25,15 +25,21 @@ const OBJECT_VISUALS: Record< destructible_debris: { color: 0x606060, height: 1.2, shape: 'sphere' }, mineral_patch: { color: 0x4169e1, height: 0.8, shape: 'box' }, vespene_geyser: { color: 0x32cd32, height: 1.2, shape: 'cylinder' }, - // Decorations fallback - decoration_tree_pine_tall: { color: 0x2e7d32, height: 6, shape: 'cone' }, - decoration_tree_pine_medium: { color: 0x388e3c, height: 4, shape: 'cone' }, - decoration_tree_dead: { color: 0x5d4037, height: 4, shape: 'cone' }, - decoration_rocks_large: { color: 0x757575, height: 2, shape: 'sphere' }, - decoration_rocks_small: { color: 0x616161, height: 1, shape: 'sphere' }, - decoration_crystal_formation: { color: 0x7c4dff, height: 3, shape: 'cone' }, - decoration_bush: { color: 0x66bb6a, height: 1, shape: 'sphere' }, - decoration_ruined_wall: { color: 0x8d6e63, height: 2, shape: 'box' }, + // Decorations fallback (heights match base scale from assets.json) + decoration_tree_pine_tall: { color: 0x2e7d32, height: 14, shape: 'cone' }, + decoration_tree_pine_medium: { color: 0x388e3c, height: 10, shape: 'cone' }, + decoration_tree_dead: { color: 0x5d4037, height: 9, shape: 'cone' }, + decoration_tree_alien: { color: 0x9c27b0, height: 11, shape: 'cone' }, + decoration_tree_palm: { color: 0x4caf50, height: 11, shape: 'cone' }, + decoration_tree_mushroom: { color: 0xe91e63, height: 8, shape: 'sphere' }, + decoration_rocks_large: { color: 0x757575, height: 3, shape: 'sphere' }, + decoration_rocks_small: { color: 0x616161, height: 2, shape: 'sphere' }, + decoration_rock_single: { color: 0x78909c, height: 2.5, shape: 'sphere' }, + decoration_crystal_formation: { color: 0x7c4dff, height: 4, shape: 'cone' }, + decoration_bush: { color: 0x66bb6a, height: 1.5, shape: 'sphere' }, + decoration_ruined_wall: { color: 0x8d6e63, height: 5, shape: 'box' }, + decoration_alien_tower: { color: 0xff5722, height: 14, shape: 'cylinder' }, + decoration_debris: { color: 0x546e7a, height: 1.5, shape: 'sphere' }, }; export interface EditorObjectInstance {