☁️ Fase 1 done, adding animated landscapes

This commit is contained in:
DuckieTM
2026-02-01 16:42:27 +01:00
parent bd059d739d
commit c197de7ea2
7 changed files with 381 additions and 10 deletions
+2 -1
View File
@@ -14,10 +14,11 @@ export class RoomContentLoader implements IRoomContentLoader
private static PLACE_HOLDER_PET: string = 'place_holder_pet';
private static PLACE_HOLDER_DEFAULT: string = RoomContentLoader.PLACE_HOLDER;
private static ROOM: string = 'room';
private static LANDSCAPE: string = 'landscape';
private static TILE_CURSOR: string = 'tile_cursor';
private static SELECTION_ARROW: string = 'selection_arrow';
public static MANDATORY_LIBRARIES: string[] = [RoomContentLoader.PLACE_HOLDER, RoomContentLoader.PLACE_HOLDER_WALL, RoomContentLoader.PLACE_HOLDER_PET, RoomContentLoader.ROOM, RoomContentLoader.TILE_CURSOR, RoomContentLoader.SELECTION_ARROW];
public static MANDATORY_LIBRARIES: string[] = [RoomContentLoader.PLACE_HOLDER, RoomContentLoader.PLACE_HOLDER_WALL, RoomContentLoader.PLACE_HOLDER_PET, RoomContentLoader.ROOM, RoomContentLoader.LANDSCAPE, RoomContentLoader.TILE_CURSOR, RoomContentLoader.SELECTION_ARROW];
private _iconListener: IRoomContentListener;
private _images: Map<string, HTMLImageElement> = new Map();
@@ -1,8 +1,9 @@
import { IAssetPlaneVisualizationLayer, IAssetRoomVisualizationData, IRoomGeometry, IRoomPlane, IVector3D } from '@nitrots/api';
import { IAssetPlaneVisualizationAnimatedLayer, IAssetPlaneVisualizationLayer, IAssetRoomVisualizationData, IRoomGeometry, IRoomPlane, IVector3D } from '@nitrots/api';
import { GetAssetManager } from '@nitrots/assets';
import { GetRenderer, GetTexturePool, PlaneMaskFilter, Vector3d } from '@nitrots/utils';
import { Container, Filter, Matrix, Point, Sprite, Texture, TilingSprite } from 'pixi.js';
import { Container, Filter, Graphics, Matrix, Point, RenderTexture, Sprite, Texture, TilingSprite } from 'pixi.js';
import { RoomGeometry } from '../../../utils';
import { PlaneVisualizationAnimationLayer } from './animated';
import { RoomPlaneBitmapMask } from './RoomPlaneBitmapMask';
import { RoomPlaneRectangleMask } from './RoomPlaneRectangleMask';
import { PlaneMaskManager } from './mask';
@@ -17,6 +18,7 @@ export class RoomPlane implements IRoomPlane
'64': new RoomGeometry(64, new Vector3d(RoomPlane.HORIZONTAL_ANGLE_DEFAULT, RoomPlane.VERTICAL_ANGLE_DEFAULT), new Vector3d(-10, 0, 0))
};
private static LANDSCAPE_COLOR: number = 0x0082F0;
private static ANIMATION_UPDATE_INTERVAL: number = 500;
public static TYPE_UNDEFINED: number = 0;
public static TYPE_WALL: number = 1;
@@ -67,6 +69,18 @@ export class RoomPlane implements IRoomPlane
private _planeTexture: Texture = null;
private _maskFilter: Filter = null;
private _animationLayers: PlaneVisualizationAnimationLayer[] = [];
private _isAnimated: boolean = false;
private _lastAnimationUpdate: number = 0;
private _animationCanvasWidth: number = 0;
private _animationCanvasHeight: number = 0;
private _landscapeRenderWidth: number = 0;
private _landscapeRenderHeight: number = 0;
private _landscapeOffsetX: number = 0;
private _landscapeOffsetY: number = 0;
private _hasWindowMask: boolean = false;
private _windowMasks: { leftSideLoc: number; rightSideLoc: number }[] = [];
constructor(origin: IVector3D, location: IVector3D, leftSide: IVector3D, rightSide: IVector3D, type: number, usesMask: boolean, secondaryNormals: IVector3D[], randomSeed: number, textureOffsetX: number = 0, textureOffsetY: number = 0, textureMaxX: number = 0, textureMaxY: number = 0)
{
this._randomSeed = randomSeed;
@@ -122,6 +136,15 @@ export class RoomPlane implements IRoomPlane
this._planeTexture = null;
}
if(this._animationLayers)
{
for(const layer of this._animationLayers)
{
if(layer) layer.dispose();
}
this._animationLayers = [];
}
this._disposed = true;
}
@@ -136,9 +159,12 @@ export class RoomPlane implements IRoomPlane
needsUpdate = true;
}
if(!needsUpdate || !this._canBeVisible)
const needsAnimationUpdate = this._isAnimated && this._type === RoomPlane.TYPE_LANDSCAPE &&
(timeSinceStartMs - this._lastAnimationUpdate) >= RoomPlane.ANIMATION_UPDATE_INTERVAL;
if(!needsUpdate && !needsAnimationUpdate)
{
if(!this.visible) return false;
if(!this._canBeVisible || !this.visible) return false;
}
if(needsUpdate)
@@ -208,16 +234,43 @@ export class RoomPlane implements IRoomPlane
const dataType: keyof IAssetRoomVisualizationData = (planeType === RoomPlane.TYPE_FLOOR) ? 'floorData' : (planeType === RoomPlane.TYPE_WALL) ? 'wallData' : 'landscapeData';
const roomCollection = GetAssetManager().getCollection('room');
const planeVisualizationData = roomCollection?.data?.roomVisualization?.[dataType];
const plane = planeVisualizationData?.planes?.find(plane => (plane.id === planeId));
let planeVisualizationData = roomCollection?.data?.roomVisualization?.[dataType];
let plane = planeVisualizationData?.planes?.find(plane => (plane.id === planeId));
let assetCollection = roomCollection;
if(!plane && planeType === RoomPlane.TYPE_LANDSCAPE)
{
const landscapeCollection = GetAssetManager().getCollection('landscape');
if(landscapeCollection?.data?.roomVisualization?.landscapeData)
{
planeVisualizationData = landscapeCollection.data.roomVisualization.landscapeData;
plane = planeVisualizationData?.planes?.find(p => (p.id === planeId));
if(plane) assetCollection = landscapeCollection;
}
}
const planeVisualization = ((dataType === 'landscapeData') ? plane?.animatedVisualization : plane?.visualizations)?.find(visualization => (visualization.size === planeGeometry.scale)) ?? null;
const planeLayer = planeVisualization?.allLayers?.[0] as IAssetPlaneVisualizationLayer;
const planeMaterialId = planeLayer?.materialId;
const planeColor = planeLayer?.color;
const planeAssetName = planeVisualizationData?.textures?.find(texture => (texture.id === planeMaterialId))?.bitmaps?.[0]?.assetName;
const texture = GetAssetManager().getAsset(planeAssetName)?.texture;
const texture = assetCollection ? GetAssetManager().getAsset(planeAssetName)?.texture : null;
return { texture, color: planeColor };
const animationLayers: PlaneVisualizationAnimationLayer[] = [];
if(planeType === RoomPlane.TYPE_LANDSCAPE && planeVisualization?.allLayers && assetCollection)
{
for(const layer of planeVisualization.allLayers)
{
const animatedLayer = layer as IAssetPlaneVisualizationAnimatedLayer;
if(animatedLayer?.items && animatedLayer.items.length > 0)
{
const animLayer = new PlaneVisualizationAnimationLayer(animatedLayer.items, assetCollection);
if(animLayer.hasItems) animationLayers.push(animLayer);
}
}
}
return { texture, color: planeColor, animationLayers };
};
const planeData = getTextureAndColorForPlane(this._id, this._type);
@@ -304,6 +357,23 @@ export class RoomPlane implements IRoomPlane
const renderOffsetX = Math.trunc(this._textureOffsetX * Math.abs((_local_13.x - _local_15.x)));
const renderOffsetY = Math.trunc(this._textureOffsetY * Math.abs((_local_13.y - _local_14.y)));
this._landscapeRenderWidth = width;
this._landscapeRenderHeight = height;
this._animationCanvasWidth = renderMaxX;
this._animationCanvasHeight = renderMaxY;
this._landscapeOffsetX = renderOffsetX;
this._landscapeOffsetY = renderOffsetY;
if(this._animationLayers)
{
for(const layer of this._animationLayers)
{
if(layer) layer.dispose();
}
}
this._animationLayers = planeData.animationLayers || [];
this._isAnimated = this._animationLayers.length > 0;
this._planeSprite = new TilingSprite({
texture,
width,
@@ -349,7 +419,18 @@ export class RoomPlane implements IRoomPlane
this._planeTexture.source.label = `room_plane_${ this._uniqueId.toString() }`;
if(needsUpdate)
let animationUpdate = false;
if(this._isAnimated && this._type === RoomPlane.TYPE_LANDSCAPE)
{
const timeSinceLastUpdate = timeSinceStartMs - this._lastAnimationUpdate;
if(timeSinceLastUpdate >= RoomPlane.ANIMATION_UPDATE_INTERVAL || needsUpdate)
{
animationUpdate = true;
this._lastAnimationUpdate = timeSinceStartMs;
}
}
if(needsUpdate || animationUpdate)
{
GetRenderer().render({
target: this._planeTexture,
@@ -357,11 +438,78 @@ export class RoomPlane implements IRoomPlane
transform: this.getMatrixForDimensions(this._planeSprite.width, this._planeSprite.height),
clear: true
});
if(this._isAnimated && this._type === RoomPlane.TYPE_LANDSCAPE && this._animationLayers.length > 0 && this._hasWindowMask)
{
this.renderAnimationLayers(timeSinceStartMs, geometry);
}
}
return true;
}
private renderAnimationLayers(timeSinceStartMs: number, geometry: IRoomGeometry): void
{
if(!this._planeTexture || this._animationCanvasWidth <= 0 || this._animationCanvasHeight <= 0) return;
const canvasWidth = this._landscapeRenderWidth;
const canvasHeight = this._landscapeRenderHeight;
if(canvasWidth <= 0 || canvasHeight <= 0) return;
const animationCanvas = RenderTexture.create({ width: canvasWidth, height: canvasHeight });
for(const layer of this._animationLayers)
{
if(!layer) continue;
layer.render(
animationCanvas,
this._landscapeOffsetX,
this._landscapeOffsetY,
this._animationCanvasWidth,
this._animationCanvasHeight,
this._leftSide.length,
this._rightSide.length,
timeSinceStartMs
);
}
const animContainer = new Container();
const animSprite = new Sprite(animationCanvas);
animContainer.addChild(animSprite);
if(this._maskFilter)
{
animContainer.filters = [this._maskFilter];
}
if(this._planeSprite && this._planeSprite.children)
{
for(const child of this._planeSprite.children)
{
if(child instanceof Sprite)
{
const maskClone = new Sprite(child.texture);
maskClone.position.copyFrom(child.position);
maskClone.scale.copyFrom(child.scale);
animContainer.addChild(maskClone);
}
}
}
const transform = this.getMatrixForDimensions(canvasWidth, canvasHeight);
GetRenderer().render({
target: this._planeTexture,
container: animContainer,
transform,
clear: false
});
animationCanvas.destroy(true);
}
private updateCorners(geometry: IRoomGeometry): void
{
this._cornerA.assign(geometry.getScreenPosition(this._location));
@@ -434,6 +582,9 @@ export class RoomPlane implements IRoomPlane
public resetBitmapMasks(): void
{
this._hasWindowMask = false;
this._windowMasks = [];
if(this._disposed || !this._useMask || !this._bitmapMasks.length) return;
this._maskChanged = true;
@@ -459,6 +610,12 @@ export class RoomPlane implements IRoomPlane
return true;
}
public addWindowMask(leftSideLoc: number, rightSideLoc: number): void
{
this._windowMasks.push({ leftSideLoc, rightSideLoc });
this._hasWindowMask = true;
}
public resetRectangleMasks(): void
{
if(!this._useMask || !this._rectangleMasks.length) return;
@@ -653,4 +810,14 @@ export class RoomPlane implements IRoomPlane
{
this._isHighlighter = flag;
}
public get hasWindowMask(): boolean
{
return this._hasWindowMask;
}
public set hasWindowMask(flag: boolean)
{
this._hasWindowMask = flag;
}
}
@@ -817,6 +817,7 @@ export class RoomVisualization extends RoomObjectSpriteVisualization implements
if(!plane.canBeVisible) _local_6 = true;
plane.canBeVisible = true;
plane.addWindowMask(leftSideLoc, rightSideLoc);
_local_5.push(i);
}
@@ -0,0 +1,49 @@
import { IGraphicAsset } from '@nitrots/api';
import { Point } from 'pixi.js';
export class AnimationItem
{
private _x: number;
private _y: number;
private _speedX: number;
private _speedY: number;
private _asset: IGraphicAsset;
constructor(x: number, y: number, speedX: number, speedY: number, asset: IGraphicAsset)
{
this._x = x;
this._y = y;
this._speedX = speedX;
this._speedY = speedY;
this._asset = asset;
if(isNaN(this._x)) this._x = 0;
if(isNaN(this._y)) this._y = 0;
if(isNaN(this._speedX)) this._speedX = 0;
if(isNaN(this._speedY)) this._speedY = 0;
}
public get bitmapData(): IGraphicAsset
{
return this._asset;
}
public dispose(): void
{
this._asset = null;
}
public getPosition(maxX: number, maxY: number, dimensionX: number, dimensionY: number, timeSinceStartMs: number): Point
{
let x = this._x;
let y = this._y;
if(dimensionX > 0) x = (x + (((this._speedX / dimensionX) * timeSinceStartMs) / 1000));
if(dimensionY > 0) y = (y + (((this._speedY / dimensionY) * timeSinceStartMs) / 1000));
const localX = Math.trunc((x % 1) * maxX);
const localY = Math.trunc((y % 1) * maxY);
return new Point(localX, localY);
}
}
@@ -0,0 +1,150 @@
import { IAssetPlaneVisualizationAnimatedLayerItem, IGraphicAssetCollection } from '@nitrots/api';
import { TextureUtils } from '@nitrots/utils';
import { RenderTexture, Sprite } from 'pixi.js';
import { AnimationItem } from './AnimationItem';
export class PlaneVisualizationAnimationLayer
{
private _isDisposed: boolean = false;
private _items: AnimationItem[] = [];
constructor(items: IAssetPlaneVisualizationAnimatedLayerItem[], assets: IGraphicAssetCollection)
{
if(items && assets)
{
for(const item of items)
{
if(!item) continue;
const assetName = item.assetId;
if(assetName)
{
const asset = assets.getAsset(assetName);
if(asset)
{
const x = this.parseCoordinate(item.x, item.randomX);
const y = this.parseCoordinate(item.y, item.randomY);
this._items.push(new AnimationItem(x, y, item.speedX || 0, item.speedY || 0, asset));
}
}
}
}
}
private parseCoordinate(value: string, randomValue: string): number
{
let result = 0;
if(value)
{
if(value.includes('%'))
{
result = parseFloat(value.replace('%', '')) / 100;
}
else
{
result = parseFloat(value);
}
}
if(randomValue)
{
const random = parseFloat(randomValue);
if(!isNaN(random)) result += (Math.random() * random);
}
return result;
}
public get disposed(): boolean
{
return this._isDisposed;
}
public get hasItems(): boolean
{
return this._items.length > 0;
}
public dispose(): void
{
this._isDisposed = true;
if(this._items)
{
for(const item of this._items)
{
if(item) item.dispose();
}
this._items = [];
}
}
public render(
canvas: RenderTexture,
offsetX: number,
offsetY: number,
maxX: number,
maxY: number,
dimensionX: number,
dimensionY: number,
timeSinceStartMs: number
): RenderTexture
{
if(maxX <= 0 || maxY <= 0) return canvas;
for(const item of this._items)
{
if(!item || !item.bitmapData) continue;
const point = item.getPosition(maxX, maxY, dimensionX, dimensionY, timeSinceStartMs);
point.x = Math.trunc(point.x - offsetX);
point.y = Math.trunc(point.y - offsetY);
const assetWidth = item.bitmapData.width;
const assetHeight = item.bitmapData.height;
// Render at primary position
if(this.isVisible(point.x, point.y, assetWidth, assetHeight, canvas.width, canvas.height))
{
this.renderSprite(item, point.x, point.y, canvas);
}
// Wrap horizontally (left side)
if(this.isVisible(point.x - maxX, point.y, assetWidth, assetHeight, canvas.width, canvas.height))
{
this.renderSprite(item, point.x - maxX, point.y, canvas);
}
// Wrap vertically (top side)
if(this.isVisible(point.x, point.y - maxY, assetWidth, assetHeight, canvas.width, canvas.height))
{
this.renderSprite(item, point.x, point.y - maxY, canvas);
}
// Wrap both (top-left corner)
if(this.isVisible(point.x - maxX, point.y - maxY, assetWidth, assetHeight, canvas.width, canvas.height))
{
this.renderSprite(item, point.x - maxX, point.y - maxY, canvas);
}
}
return canvas;
}
private isVisible(x: number, y: number, width: number, height: number, canvasWidth: number, canvasHeight: number): boolean
{
return (x > -width) && (x < canvasWidth) && (y > -height) && (y < canvasHeight);
}
private renderSprite(item: AnimationItem, x: number, y: number, canvas: RenderTexture): void
{
const sprite = new Sprite(item.bitmapData.texture);
sprite.position.set(x, y);
TextureUtils.writeToTexture(sprite, canvas, false);
}
}
@@ -0,0 +1,2 @@
export * from './AnimationItem';
export * from './PlaneVisualizationAnimationLayer';
@@ -1,3 +1,4 @@
export * from './animated';
export * from './PlaneDrawingData';
export * from './RoomPlane';
export * from './RoomPlaneBitmapMask';