From c197de7ea2783f38295fb92accf5e6d6a7a2b0bc Mon Sep 17 00:00:00 2001 From: DuckieTM Date: Sun, 1 Feb 2026 16:42:27 +0100 Subject: [PATCH] =?UTF-8?q?=E2=98=81=EF=B8=8F=20Fase=201=20done,=20adding?= =?UTF-8?q?=20animated=20landscapes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/room/src/RoomContentLoader.ts | 3 +- .../object/visualization/room/RoomPlane.ts | 185 +++++++++++++++++- .../visualization/room/RoomVisualization.ts | 1 + .../room/animated/AnimationItem.ts | 49 +++++ .../PlaneVisualizationAnimationLayer.ts | 150 ++++++++++++++ .../visualization/room/animated/index.ts | 2 + .../src/object/visualization/room/index.ts | 1 + 7 files changed, 381 insertions(+), 10 deletions(-) create mode 100644 packages/room/src/object/visualization/room/animated/AnimationItem.ts create mode 100644 packages/room/src/object/visualization/room/animated/PlaneVisualizationAnimationLayer.ts create mode 100644 packages/room/src/object/visualization/room/animated/index.ts diff --git a/packages/room/src/RoomContentLoader.ts b/packages/room/src/RoomContentLoader.ts index f182036..399cf84 100644 --- a/packages/room/src/RoomContentLoader.ts +++ b/packages/room/src/RoomContentLoader.ts @@ -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 = new Map(); diff --git a/packages/room/src/object/visualization/room/RoomPlane.ts b/packages/room/src/object/visualization/room/RoomPlane.ts index 830efe1..52378f8 100644 --- a/packages/room/src/object/visualization/room/RoomPlane.ts +++ b/packages/room/src/object/visualization/room/RoomPlane.ts @@ -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; + } } diff --git a/packages/room/src/object/visualization/room/RoomVisualization.ts b/packages/room/src/object/visualization/room/RoomVisualization.ts index 101ce23..c29b96b 100644 --- a/packages/room/src/object/visualization/room/RoomVisualization.ts +++ b/packages/room/src/object/visualization/room/RoomVisualization.ts @@ -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); } diff --git a/packages/room/src/object/visualization/room/animated/AnimationItem.ts b/packages/room/src/object/visualization/room/animated/AnimationItem.ts new file mode 100644 index 0000000..9f43a8a --- /dev/null +++ b/packages/room/src/object/visualization/room/animated/AnimationItem.ts @@ -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); + } +} diff --git a/packages/room/src/object/visualization/room/animated/PlaneVisualizationAnimationLayer.ts b/packages/room/src/object/visualization/room/animated/PlaneVisualizationAnimationLayer.ts new file mode 100644 index 0000000..bf599a8 --- /dev/null +++ b/packages/room/src/object/visualization/room/animated/PlaneVisualizationAnimationLayer.ts @@ -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); + } +} diff --git a/packages/room/src/object/visualization/room/animated/index.ts b/packages/room/src/object/visualization/room/animated/index.ts new file mode 100644 index 0000000..0b25b63 --- /dev/null +++ b/packages/room/src/object/visualization/room/animated/index.ts @@ -0,0 +1,2 @@ +export * from './AnimationItem'; +export * from './PlaneVisualizationAnimationLayer'; diff --git a/packages/room/src/object/visualization/room/index.ts b/packages/room/src/object/visualization/room/index.ts index 37a12a9..a05b951 100644 --- a/packages/room/src/object/visualization/room/index.ts +++ b/packages/room/src/object/visualization/room/index.ts @@ -1,3 +1,4 @@ +export * from './animated'; export * from './PlaneDrawingData'; export * from './RoomPlane'; export * from './RoomPlaneBitmapMask';