🆙 Update

- Animation canvas uses full landscape height for proper vertical positioning
- Animation assets always loaded from room collection
- Fallback to default plane when specific plane not found
This commit is contained in:
duckietm
2026-02-05 08:32:02 +01:00
parent e056013d5d
commit 120a502a7d
4 changed files with 108 additions and 13 deletions
@@ -4,4 +4,5 @@ export interface IAssetPlaneVisualizationLayer
color?: number; color?: number;
offset?: number; offset?: number;
align?: string; align?: string;
backgroundColor?: string;
} }
+24 -1
View File
@@ -99,6 +99,7 @@ export class AssetManager implements IAssetManager
{ {
if(!url || !url.length) return false; if(!url || !url.length) return false;
// ✅ NEW: Local bundled assets (no generic.asset.url)
if(url.startsWith('local://')) if(url.startsWith('local://'))
{ {
const key = url.substring('local://'.length); const key = url.substring('local://'.length);
@@ -172,32 +173,52 @@ export class AssetManager implements IAssetManager
} }
} }
/**
* ✅ Loads room assets from bundled code:
* - /packages/assets/src/assets/room/room.asset.json
* - /packages/assets/src/assets/room/images/*.png
*
* This loads individual PNG files instead of a spritesheet, then uses
* the JSON asset definitions for proper x, y offsets, flipH settings, etc.
*/
private async loadLocalRoom(): Promise<void> private async loadLocalRoom(): Promise<void>
{ {
// 1) Load room JSON locally (bundled)
const roomDataModule = await import('./assets/room/room.asset.json'); const roomDataModule = await import('./assets/room/room.asset.json');
const roomData = (roomDataModule.default ?? roomDataModule) as IAssetData; const roomData = (roomDataModule.default ?? roomDataModule) as IAssetData;
// 2) Create collection WITHOUT spritesheet
// Note: Constructor calls define(), but assets won't be created since textures don't exist yet
const collection = this.createCollection(roomData, null) as GraphicAssetCollection; const collection = this.createCollection(roomData, null) as GraphicAssetCollection;
if(!collection) return; if(!collection) return;
// 3) Load all images from the bundled assets
const roomImages = import.meta.glob('./assets/room/*.png', { eager: true }); const roomImages = import.meta.glob('./assets/room/*.png', { eager: true });
const roomImagesSub = import.meta.glob('./assets/room/images/*.png', { eager: true }); const roomImagesSub = import.meta.glob('./assets/room/images/*.png', { eager: true });
const merged = { ...roomImages, ...roomImagesSub }; const merged = { ...roomImages, ...roomImagesSub };
// 4) Register textures in the collection's _textures map
// getLibraryAsset() prepends collection name, so for asset 'wall_texture_64_0_wall_white'
// it looks for 'room_wall_texture_64_0_wall_white' in _textures
for(const path in merged) for(const path in merged)
{ {
const mod = merged[path]; const mod = merged[path];
const imageUrl = (mod.default ?? mod) as string; const imageUrl = (mod.default ?? mod) as string;
const file = path.split('/').pop()!; const file = path.split('/').pop()!;
const rawName = file.replace(/\.png$/i, ''); const rawName = file.replace(/\.png$/i, ''); // e.g., "room_wall_texture_64_0_wall_white"
const texture = await Assets.load<Texture>(imageUrl); const texture = await Assets.load<Texture>(imageUrl);
if(!texture) continue; if(!texture) continue;
// Register in AssetManager's global _textures for direct lookups
this.setTexture(rawName, texture); this.setTexture(rawName, texture);
// Register in collection's _textures with the full name (room_...)
// This is what getLibraryAsset() will look for when defining assets
collection.textures.set(rawName, texture); collection.textures.set(rawName, texture);
// Also register without the "room_" prefix for direct lookups
if(rawName.startsWith('room_')) if(rawName.startsWith('room_'))
{ {
const normalizedName = rawName.substring('room_'.length); const normalizedName = rawName.substring('room_'.length);
@@ -206,6 +227,8 @@ export class AssetManager implements IAssetManager
} }
} }
// 5) Now call define() again to process asset definitions with correct x, y offsets
// Assets that were skipped before (no textures) will now be created properly
collection.define(roomData); collection.define(roomData);
} }
@@ -5729,7 +5729,8 @@
"allLayers": [ "allLayers": [
{ {
"materialId": "landscape_64_background_1", "materialId": "landscape_64_background_1",
"align": "bottom" "align": "bottom",
"backgroundColor": "#FEFEFE"
}, },
{ {
"items": [ "items": [
@@ -83,6 +83,7 @@ export class RoomPlane implements IRoomPlane
private _landscapeForegroundTint: number = 0xffffff; private _landscapeForegroundTint: number = 0xffffff;
private _landscapeBaseAlignBottom: boolean = false; private _landscapeBaseAlignBottom: boolean = false;
private _landscapeForegroundAlignBottom: boolean = false; private _landscapeForegroundAlignBottom: boolean = false;
private _landscapeBackgroundColor: number = null;
private _hasWindowMask: boolean = false; private _hasWindowMask: boolean = false;
private _windowMasks: { leftSideLoc: number; rightSideLoc: number }[] = []; private _windowMasks: { leftSideLoc: number; rightSideLoc: number }[] = [];
@@ -256,6 +257,19 @@ export class RoomPlane implements IRoomPlane
} }
} }
// Fall back to "default" plane if the requested landscape plane wasn't found
if(!plane && planeType === RoomPlane.TYPE_LANDSCAPE)
{
const roomCollection2 = GetAssetManager().getCollection('room');
const defaultPlaneData = roomCollection2?.data?.roomVisualization?.landscapeData;
plane = defaultPlaneData?.planes?.find(p => (p.id === 'default'));
if(plane)
{
planeVisualizationData = defaultPlaneData;
assetCollection = roomCollection2;
}
}
let planeVisualization = null; let planeVisualization = null;
if(dataType === 'landscapeData') if(dataType === 'landscapeData')
{ {
@@ -280,6 +294,15 @@ export class RoomPlane implements IRoomPlane
const baseAlignBottom = materialLayers[0]?.align === 'bottom'; const baseAlignBottom = materialLayers[0]?.align === 'bottom';
const foregroundAlignBottom = materialLayers[1]?.align === 'bottom'; const foregroundAlignBottom = materialLayers[1]?.align === 'bottom';
// Parse backgroundColor from the first material layer (background layer)
const backgroundColorStr = materialLayers[0]?.backgroundColor;
let backgroundColor: number = null;
if(backgroundColorStr)
{
// Convert hex string like "#FEFEFE" to number
backgroundColor = parseInt(backgroundColorStr.replace('#', ''), 16);
}
const selectMaterialMatrixForNormal = (matrices = [], normal = null) => const selectMaterialMatrixForNormal = (matrices = [], normal = null) =>
{ {
if(!matrices.length) return null; if(!matrices.length) return null;
@@ -351,20 +374,22 @@ export class RoomPlane implements IRoomPlane
const foregroundTexture = resolveTextureForMaterial(foregroundMaterialId); const foregroundTexture = resolveTextureForMaterial(foregroundMaterialId);
const animationLayers: PlaneVisualizationAnimationLayer[] = []; const animationLayers: PlaneVisualizationAnimationLayer[] = [];
if(planeType === RoomPlane.TYPE_LANDSCAPE && planeVisualization?.allLayers && assetCollection) if(planeType === RoomPlane.TYPE_LANDSCAPE && planeVisualization?.allLayers)
{ {
// Always use the room collection for animation assets (clouds etc.) since they are stored there
const animationAssetCollection = roomCollection;
for(const layer of planeVisualization.allLayers) for(const layer of planeVisualization.allLayers)
{ {
const animatedLayer = layer as IAssetPlaneVisualizationAnimatedLayer; const animatedLayer = layer as IAssetPlaneVisualizationAnimatedLayer;
if(animatedLayer?.items && animatedLayer.items.length > 0) if(animatedLayer?.items && animatedLayer.items.length > 0)
{ {
const animLayer = new PlaneVisualizationAnimationLayer(animatedLayer.items, assetCollection); const animLayer = new PlaneVisualizationAnimationLayer(animatedLayer.items, animationAssetCollection);
if(animLayer.hasItems) animationLayers.push(animLayer); if(animLayer.hasItems) animationLayers.push(animLayer);
} }
} }
} }
return { texture, foregroundTexture, color: planeColor, baseAlignBottom, foregroundAlignBottom, animationLayers }; return { texture, foregroundTexture, color: planeColor, baseAlignBottom, foregroundAlignBottom, animationLayers, backgroundColor };
}; };
const planeData = getTextureAndColorForPlane(this._id, this._type, normal); const planeData = getTextureAndColorForPlane(this._id, this._type, normal);
@@ -453,8 +478,10 @@ export class RoomPlane implements IRoomPlane
this._landscapeRenderWidth = width; this._landscapeRenderWidth = width;
this._landscapeRenderHeight = height; this._landscapeRenderHeight = height;
this._animationCanvasWidth = renderMaxX; // Use total landscape width for animation canvas, but always use actual height
this._animationCanvasHeight = renderMaxY; // The renderMaxY is often too small (e.g., 160) while actual height is much larger (e.g., 755)
this._animationCanvasWidth = renderMaxX || width;
this._animationCanvasHeight = height; // Always use actual landscape height for animations
this._landscapeOffsetX = renderOffsetX; this._landscapeOffsetX = renderOffsetX;
this._landscapeOffsetY = renderOffsetY; this._landscapeOffsetY = renderOffsetY;
@@ -475,6 +502,7 @@ export class RoomPlane implements IRoomPlane
this._landscapeForegroundTint = landscapeTint; this._landscapeForegroundTint = landscapeTint;
this._landscapeBaseAlignBottom = planeData.baseAlignBottom ?? false; this._landscapeBaseAlignBottom = planeData.baseAlignBottom ?? false;
this._landscapeForegroundAlignBottom = planeData.foregroundAlignBottom ?? false; this._landscapeForegroundAlignBottom = planeData.foregroundAlignBottom ?? false;
this._landscapeBackgroundColor = planeData.backgroundColor ?? null;
this._planeSprite = new TilingSprite({ this._planeSprite = new TilingSprite({
texture: Texture.WHITE, texture: Texture.WHITE,
@@ -535,24 +563,37 @@ export class RoomPlane implements IRoomPlane
if(needsUpdate || animationUpdate) if(needsUpdate || animationUpdate)
{ {
// For landscapes with a custom backgroundColor, render it first
if(this._type === RoomPlane.TYPE_LANDSCAPE && this._landscapeBackgroundColor !== null)
{
this.renderBackgroundColor();
}
GetRenderer().render({ GetRenderer().render({
target: this._planeTexture, target: this._planeTexture,
container: this._planeSprite, container: this._planeSprite,
transform: this.getMatrixForDimensions(this._planeSprite.width, this._planeSprite.height), transform: this.getMatrixForDimensions(this._planeSprite.width, this._planeSprite.height),
clear: true clear: this._landscapeBackgroundColor === null
}); });
if(this._isAnimated && this._type === RoomPlane.TYPE_LANDSCAPE && this._animationLayers.length > 0 && this._hasWindowMask) // Layer order for landscapes:
{ // 1. Background color (rendered above)
this.renderAnimationLayers(timeSinceStartMs, geometry); // 2. Background texture
} // 3. Animation layers (clouds)
// 4. Foreground texture
if(this._type === RoomPlane.TYPE_LANDSCAPE && this._landscapeBackgroundTexture) if(this._type === RoomPlane.TYPE_LANDSCAPE && this._landscapeBackgroundTexture)
{ {
this.renderLandscapeLayer(this._landscapeBackgroundTexture, this._landscapeBackgroundTint, this._landscapeBaseAlignBottom); this.renderLandscapeLayer(this._landscapeBackgroundTexture, this._landscapeBackgroundTint, this._landscapeBaseAlignBottom);
} }
// Render foreground layer for landscapes on top of background/animation // Render animation layers (clouds) - between background and foreground
if(this._isAnimated && this._type === RoomPlane.TYPE_LANDSCAPE && this._animationLayers.length > 0)
{
this.renderAnimationLayers(timeSinceStartMs, geometry);
}
// Render foreground layer for landscapes on top of background and clouds
if(this._type === RoomPlane.TYPE_LANDSCAPE && this._landscapeForegroundTexture) if(this._type === RoomPlane.TYPE_LANDSCAPE && this._landscapeForegroundTexture)
{ {
this.renderLandscapeLayer(this._landscapeForegroundTexture, this._landscapeForegroundTint, this._landscapeForegroundAlignBottom); this.renderLandscapeLayer(this._landscapeForegroundTexture, this._landscapeForegroundTint, this._landscapeForegroundAlignBottom);
@@ -682,6 +723,35 @@ export class RoomPlane implements IRoomPlane
layerSprite.destroy(); layerSprite.destroy();
} }
private renderBackgroundColor(): void
{
if(!this._planeTexture || this._landscapeBackgroundColor === null) return;
const canvasWidth = this._landscapeRenderWidth;
const canvasHeight = this._landscapeRenderHeight;
if(canvasWidth <= 0 || canvasHeight <= 0) return;
// Create a solid color rectangle to fill the background
const colorGraphics = new Graphics();
colorGraphics.rect(0, 0, canvasWidth, canvasHeight);
colorGraphics.fill(this._landscapeBackgroundColor);
const colorContainer = new Container();
colorContainer.addChild(colorGraphics);
const transform = this.getMatrixForDimensions(canvasWidth, canvasHeight);
GetRenderer().render({
target: this._planeTexture,
container: colorContainer,
transform,
clear: true
});
colorGraphics.destroy();
}
private updateCorners(geometry: IRoomGeometry): void private updateCorners(geometry: IRoomGeometry): void
{ {
this._cornerA.assign(geometry.getScreenPosition(this._location)); this._cornerA.assign(geometry.getScreenPosition(this._location));