diff --git a/packages/api/src/room/object/visualization/IRoomPlane.ts b/packages/api/src/room/object/visualization/IRoomPlane.ts index 1cc6bc6..1b0d9bd 100644 --- a/packages/api/src/room/object/visualization/IRoomPlane.ts +++ b/packages/api/src/room/object/visualization/IRoomPlane.ts @@ -3,6 +3,7 @@ export interface IRoomPlane { uniqueId: number; + type: number; location: IVector3D; leftSide: IVector3D; rightSide: IVector3D; diff --git a/packages/room/src/object/visualization/furniture/FurnitureGuildIsometricBadgeVisualization.ts b/packages/room/src/object/visualization/furniture/FurnitureGuildIsometricBadgeVisualization.ts index 3bf2ed1..f21f495 100644 --- a/packages/room/src/object/visualization/furniture/FurnitureGuildIsometricBadgeVisualization.ts +++ b/packages/room/src/object/visualization/furniture/FurnitureGuildIsometricBadgeVisualization.ts @@ -51,6 +51,10 @@ export class FurnitureGuildIsometricBadgeVisualization extends IsometricImageFur protected generateTransformedThumbnail(texture: Texture, asset: IGraphicAsset): Texture { + // Render into a texture exactly matching the asset slot (e.g. 40×58 for guild_forum layer i). + // dScale is derived so the sheared content fills the slot top-to-bottom without overflow: + // shear contribution = 0.5 * renderWidth, vertical fill = dScale * texture.height + // => dScale = (renderHeight - 0.5 * renderWidth) / texture.height const renderWidth = asset.width || 64; const renderHeight = asset.height || renderWidth; const difference = (renderWidth / texture.width); @@ -85,6 +89,7 @@ export class FurnitureGuildIsometricBadgeVisualization extends IsometricImageFur matrix.ty = 0; } + // Pass the matrix directly as a render transform — preserves full skew/shear in Pixi.js v8. return TextureUtils.createAndWriteRenderTexture(renderWidth, renderHeight, new Sprite(texture), matrix); } diff --git a/packages/room/src/object/visualization/furniture/IsometricImageFurniVisualization.ts b/packages/room/src/object/visualization/furniture/IsometricImageFurniVisualization.ts index d66424d..3e5ad89 100644 --- a/packages/room/src/object/visualization/furniture/IsometricImageFurniVisualization.ts +++ b/packages/room/src/object/visualization/furniture/IsometricImageFurniVisualization.ts @@ -80,6 +80,9 @@ export class IsometricImageFurniVisualization extends FurnitureAnimatedVisualiza const asset = this.getAsset(assetName, layerId); const thumbnailAssetName = `${this.getThumbnailAssetName(scale)}-${this._uniqueId}`; const transformedTexture = this.generateTransformedThumbnail(k, asset || { width: 64, height: 64 }); + + // Use the original asset's registered offsets so the thumbnail is drawn at the + // furniture-defined sprite position. Fall back to centering when no asset exists. const offsetX = asset ? asset.offsetX : -Math.floor(transformedTexture.width / 2); const offsetY = asset ? asset.offsetY : -Math.floor(transformedTexture.height / 2); diff --git a/packages/room/src/object/visualization/room/RoomPlane.ts b/packages/room/src/object/visualization/room/RoomPlane.ts index 3817faf..a1391bd 100644 --- a/packages/room/src/object/visualization/room/RoomPlane.ts +++ b/packages/room/src/object/visualization/room/RoomPlane.ts @@ -225,8 +225,7 @@ export class RoomPlane implements IRoomPlane switch(this._type) { case RoomPlane.TYPE_FLOOR: { - const heightOffset = (this._location.z + Math.min(0, this._leftSide.z, this._rightSide.z)) * geometry.scale; - relativeDepth = (relativeDepth - heightOffset); + relativeDepth = (relativeDepth - ((this._location.z + Math.min(0, this._leftSide.z, this._rightSide.z)) * 8)); break; } case RoomPlane.TYPE_LANDSCAPE: diff --git a/packages/room/src/object/visualization/room/RoomVisualization.ts b/packages/room/src/object/visualization/room/RoomVisualization.ts index 52307bd..d536585 100644 --- a/packages/room/src/object/visualization/room/RoomVisualization.ts +++ b/packages/room/src/object/visualization/room/RoomVisualization.ts @@ -701,9 +701,11 @@ export class RoomVisualization extends RoomObjectSpriteVisualization implements { if(plane.visible) { - depth = ((plane.relativeDepth + this.floorRelativeDepth) + (id / 1000)); - - if(plane.type !== RoomPlane.TYPE_FLOOR) + if(plane.type === RoomPlane.TYPE_FLOOR) + { + depth = ((plane.relativeDepth + this.floorRelativeDepth) + (id / 1000)); + } + else { depth = ((plane.relativeDepth + this.wallRelativeDepth) + (id / 1000)); diff --git a/packages/room/src/renderer/RoomSpriteCanvas.ts b/packages/room/src/renderer/RoomSpriteCanvas.ts index a37a758..19a4b67 100644 --- a/packages/room/src/renderer/RoomSpriteCanvas.ts +++ b/packages/room/src/renderer/RoomSpriteCanvas.ts @@ -1,8 +1,8 @@ -import { IRoomCanvasMouseListener, IRoomGeometry, IRoomObject, IRoomObjectSprite, IRoomObjectSpriteVisualization, IRoomRenderingCanvas, IRoomSpriteCanvasContainer, IRoomSpriteMouseEvent, MouseEventType, RoomObjectSpriteData, RoomObjectSpriteType } from '@nitrots/api'; +import { IPlaneVisualization, IRoomCanvasMouseListener, IRoomGeometry, IRoomObject, IRoomObjectSprite, IRoomObjectSpriteVisualization, IRoomPlane, IRoomRenderingCanvas, IRoomSpriteCanvasContainer, IRoomSpriteMouseEvent, MouseEventType, RoomObjectSpriteData, RoomObjectSpriteType } from '@nitrots/api'; import { GetConfiguration } from '@nitrots/configuration'; import { RoomSpriteMouseEvent } from '@nitrots/events'; import { GetTicker, TextureUtils, Vector3d } from '@nitrots/utils'; -import { Container, Matrix, Point, Rectangle, Sprite, Texture } from 'pixi.js'; +import { Container, Graphics, Matrix, Point, Rectangle, Sprite, Texture } from 'pixi.js'; import { RoomEnterEffect, RoomGeometry, RoomRotatingEffect, RoomShakingEffect } from '../utils'; import { RoomObjectCache, RoomObjectCacheItem } from './cache'; import { ExtendedSprite, ObjectMouseData, SortableSprite } from './utils'; @@ -18,6 +18,11 @@ export class RoomSpriteCanvas implements IRoomRenderingCanvas private _master: Container = null; private _display: Container = null; private _mask: Sprite = null; + private _boundaryMask: Graphics = null; + private _lastBoundaryOffsetX: number = NaN; + private _lastBoundaryOffsetY: number = NaN; + private _lastBoundaryGeometryId: number = -1; + private _lastBoundaryScale: number = NaN; private _sortableSprites: SortableSprite[] = []; private _spriteCount: number = 0; @@ -91,6 +96,13 @@ export class RoomSpriteCanvas implements IRoomRenderingCanvas this._display = display; } + + if(!this._boundaryMask) + { + this._boundaryMask = new Graphics(); + + this._master.addChild(this._boundaryMask); + } } public dispose(): void @@ -106,6 +118,8 @@ export class RoomSpriteCanvas implements IRoomRenderingCanvas if(this._mask) this._mask = null; + if(this._boundaryMask) this._boundaryMask = null; + if(this._objectCache) { this._objectCache.dispose(); @@ -246,6 +260,170 @@ export class RoomSpriteCanvas implements IRoomRenderingCanvas this.screenOffsetY = (offsetPoint.y - (point.y * this._scale)); } + private updateBoundaryMask(): void + { + if(!this._boundaryMask || !this._display || !this._geometry) return; + + const geometryId = this._geometry.updateId; + const offsetX = this._screenOffsetX; + const offsetY = this._screenOffsetY; + const scale = this._scale; + + if(geometryId === this._lastBoundaryGeometryId && offsetX === this._lastBoundaryOffsetX && offsetY === this._lastBoundaryOffsetY && scale === this._lastBoundaryScale) return; + + this._lastBoundaryGeometryId = geometryId; + this._lastBoundaryOffsetX = offsetX; + this._lastBoundaryOffsetY = offsetY; + this._lastBoundaryScale = scale; + + const pts: { x: number; y: number }[] = []; + const w2 = this._width / 2; + const h2 = this._height / 2; + + for(const object of this._container.objects.values()) + { + if(!object) continue; + + const viz = object.visualization as unknown as IPlaneVisualization; + + if(!viz || !viz.planes) continue; + + for(const plane of (viz.planes as IRoomPlane[])) + { + if(!plane || plane.type === 3) continue; + + const loc = plane.location; + const ls = plane.leftSide; + const rs = plane.rightSide; + + const corners = [ + new Vector3d(loc.x, loc.y, loc.z), + new Vector3d(loc.x + rs.x, loc.y + rs.y, loc.z + rs.z), + new Vector3d(loc.x + ls.x + rs.x, loc.y + ls.y + rs.y, loc.z + ls.z + rs.z), + new Vector3d(loc.x + ls.x, loc.y + ls.y, loc.z + ls.z) + ]; + + for(const c of corners) + { + const sp = this._geometry.getScreenPosition(c); + + if(!sp) continue; + + pts.push({ x: (sp.x + w2) * scale + offsetX, y: (sp.y + h2) * scale + offsetY }); + } + } + + break; + } + + this._boundaryMask.clear(); + + if(pts.length < 3) + { + if(this._display.mask === this._boundaryMask) this._display.mask = this._mask ?? null; + + return; + } + + const hull = RoomSpriteCanvas.convexHull(pts); + const maskPolygon = RoomSpriteCanvas.createMaskPolygon(hull); + + this._boundaryMask.poly(maskPolygon.flatMap(p => [p.x, p.y])); + this._boundaryMask.fill(0xFFFFFF); + + if(this._display.mask !== this._boundaryMask) + { + this._display.mask = this._boundaryMask; + } + } + + private static convexHull(points: { x: number; y: number }[]): { x: number; y: number }[] + { + if(points.length < 3) return points; + + const sorted = [...points].sort((a, b) => (a.x !== b.x ? a.x - b.x : a.y - b.y)); + + const cross = (o: { x: number; y: number }, a: { x: number; y: number }, b: { x: number; y: number }) => + (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x); + + const lower: { x: number; y: number }[] = []; + + for(const p of sorted) + { + while(lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], p) <= 0) lower.pop(); + + lower.push(p); + } + + const upper: { x: number; y: number }[] = []; + + for(let i = sorted.length - 1; i >= 0; i--) + { + const p = sorted[i]; + + while(upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], p) <= 0) upper.pop(); + + upper.push(p); + } + + lower.pop(); + upper.pop(); + + return [...lower, ...upper]; + } + + private static createMaskPolygon(hull: { x: number; y: number }[], extension: number = 5000): { x: number; y: number }[] + { + if(hull.length < 3) return hull; + + let leftIdx = 0; + let rightIdx = 0; + + for(let i = 1; i < hull.length; i++) + { + if(hull[i].x < hull[leftIdx].x) leftIdx = i; + if(hull[i].x > hull[rightIdx].x) rightIdx = i; + } + + const n = hull.length; + + // Collect arc going CCW: leftIdx → rightIdx via increasing indices + const arcCCW: { x: number; y: number }[] = []; + let idx = leftIdx; + + while(idx !== rightIdx) + { + arcCCW.push(hull[idx]); + idx = (idx + 1) % n; + } + + arcCCW.push(hull[rightIdx]); + + // Collect arc going CW: leftIdx → rightIdx via decreasing indices + const arcCW: { x: number; y: number }[] = []; + idx = leftIdx; + + while(idx !== rightIdx) + { + arcCW.push(hull[idx]); + idx = (idx - 1 + n) % n; + } + + arcCW.push(hull[rightIdx]); + + // Bottom arc = the arc with larger average Y (floor/front tiles) + const avgCCW = arcCCW.reduce((s, p) => s + p.y, 0) / arcCCW.length; + const avgCW = arcCW.reduce((s, p) => s + p.y, 0) / arcCW.length; + const bottomArc = avgCCW >= avgCW ? arcCCW : arcCW; + + // Build polygon: extend upward far above walls, then trace bottom boundary + return [ + { x: hull[leftIdx].x, y: -extension }, + { x: hull[rightIdx].x, y: -extension }, + ...bottomArc.slice().reverse() + ]; + } + public render(time: number, update: boolean = false): void { this._canvasUpdated = false; @@ -319,6 +497,9 @@ export class RoomSpriteCanvas implements IRoomRenderingCanvas this.cleanSprites(spriteCount); + this.updateBoundaryMask(); + + if(update || updateVisuals) this._canvasUpdated = true; this._renderTimestamp = this._totalTimeRunning;