You've already forked Nitro_Render_V3
mirror of
https://github.com/duckietm/Nitro_Render_V3.git
synced 2026-06-19 15:06:20 +00:00
363 lines
14 KiB
TypeScript
363 lines
14 KiB
TypeScript
import { IGraphicAsset, IRoomObjectSprite, RoomObjectVariable } from '@nitrots/api';
|
|
import { GetConfiguration } from '@nitrots/configuration';
|
|
import { GetSessionDataManager } from '@nitrots/session';
|
|
import { AnimatedSprite, Texture } from 'pixi.js';
|
|
import { parseGIF, decompressFrames } from 'gifuct-js';
|
|
import { FurnitureAnimatedVisualization } from './FurnitureAnimatedVisualization';
|
|
|
|
export class FurnitureBadgeDisplayVisualization extends FurnitureAnimatedVisualization
|
|
{
|
|
private static readonly BADGE_TAG = 'BADGE';
|
|
private static readonly BADGE_LAYER_ID = 1;
|
|
|
|
private _badgeId = '';
|
|
private _badgeAssetNameNormalScale = '';
|
|
private _badgeAssetNameSmallScale = '';
|
|
private _badgeVisibleInState = -1;
|
|
private _frameTextures: Texture[] = null;
|
|
private _animatedSprite: AnimatedSprite = null;
|
|
private _lastFrameIndex = -1;
|
|
|
|
public dispose(): void
|
|
{
|
|
this.disposeAnimatedSprite();
|
|
super.dispose();
|
|
}
|
|
|
|
private disposeAnimatedSprite(): void
|
|
{
|
|
if(this._animatedSprite)
|
|
{
|
|
this._animatedSprite.stop();
|
|
this._animatedSprite.destroy();
|
|
this._animatedSprite = null;
|
|
}
|
|
|
|
if(this._frameTextures)
|
|
{
|
|
for(const texture of this._frameTextures)
|
|
{
|
|
texture.destroy(true);
|
|
}
|
|
this._frameTextures = null;
|
|
}
|
|
|
|
this._lastFrameIndex = -1;
|
|
}
|
|
|
|
public getTexture(scale: number, layerId: number, asset: IGraphicAsset): Texture
|
|
{
|
|
return super.getTexture(scale, layerId, asset);
|
|
}
|
|
|
|
public update(geometry: any, time: number, update: boolean, skipUpdate: boolean): void
|
|
{
|
|
super.update(geometry, time, update, skipUpdate);
|
|
|
|
// Update animated GIF using AnimatedSprite's current frame
|
|
if(this._animatedSprite && this._frameTextures && this._frameTextures.length > 1)
|
|
{
|
|
const currentFrameIndex = this._animatedSprite.currentFrame;
|
|
|
|
// Only update if frame has changed
|
|
if(currentFrameIndex !== this._lastFrameIndex)
|
|
{
|
|
this._lastFrameIndex = currentFrameIndex;
|
|
|
|
const sessionDataManager = GetSessionDataManager();
|
|
let tex = sessionDataManager.getBadgeImage(this._badgeId);
|
|
if(!tex) tex = sessionDataManager.getGroupBadgeImage(this._badgeId);
|
|
|
|
if(!tex) return;
|
|
|
|
const currentFrameTexture = this._frameTextures[currentFrameIndex];
|
|
if(!currentFrameTexture) return;
|
|
|
|
// Update the badge canvas with the current frame
|
|
const badgeCanvas = (tex.source as any).resource as HTMLCanvasElement;
|
|
const ctx = badgeCanvas.getContext('2d', { willReadFrequently: true });
|
|
|
|
// Create a temporary canvas to extract the frame texture data
|
|
const frameCanvas = document.createElement('canvas');
|
|
frameCanvas.width = currentFrameTexture.width;
|
|
frameCanvas.height = currentFrameTexture.height;
|
|
const frameCtx = frameCanvas.getContext('2d');
|
|
|
|
// Get the source from the texture
|
|
const frameSource = (currentFrameTexture.source as any).resource;
|
|
if(frameSource instanceof HTMLCanvasElement || frameSource instanceof HTMLImageElement)
|
|
{
|
|
ctx.clearRect(0, 0, badgeCanvas.width, badgeCanvas.height);
|
|
ctx.drawImage(frameSource, 0, 0);
|
|
tex.source.update();
|
|
|
|
const assetName = this._badgeAssetNameNormalScale;
|
|
if(this.asset && assetName)
|
|
{
|
|
const asset = this.getAsset(assetName, FurnitureBadgeDisplayVisualization.BADGE_LAYER_ID);
|
|
if(asset && asset.texture)
|
|
{
|
|
asset.texture.source.update();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
protected updateModel(scale: number): boolean
|
|
{
|
|
let needsUpdate = super.updateModel(scale);
|
|
|
|
const badgeStatus = this.object.model.getValue<number>(RoomObjectVariable.FURNITURE_BADGE_IMAGE_STATUS);
|
|
const badgeId = this.object.model.getValue<string>(RoomObjectVariable.FURNITURE_BADGE_ASSET_NAME);
|
|
|
|
if(badgeStatus === -1)
|
|
{
|
|
this._badgeId = '';
|
|
this._badgeAssetNameNormalScale = '';
|
|
this._badgeAssetNameSmallScale = '';
|
|
this._badgeVisibleInState = -1;
|
|
this.disposeAnimatedSprite();
|
|
|
|
return needsUpdate;
|
|
}
|
|
|
|
if((badgeStatus === 1) && badgeId && (badgeId !== this._badgeId))
|
|
{
|
|
this._badgeId = badgeId;
|
|
this._badgeAssetNameNormalScale = badgeId;
|
|
this._badgeAssetNameSmallScale = `${badgeId}_32`;
|
|
|
|
const visibleInState = this.object.model.getValue<number>(RoomObjectVariable.FURNITURE_BADGE_VISIBLE_IN_STATE);
|
|
this._badgeVisibleInState = isNaN(visibleInState) ? -1 : visibleInState;
|
|
|
|
this.disposeAnimatedSprite();
|
|
this.addBadgeToAssetCollection(badgeId);
|
|
|
|
const layerId = FurnitureBadgeDisplayVisualization.BADGE_LAYER_ID;
|
|
this._assetNames[layerId] = undefined;
|
|
this._updatedLayers[layerId] = undefined;
|
|
this._spriteTags[layerId] = undefined;
|
|
|
|
this.updateObjectCounter = -1;
|
|
needsUpdate = true;
|
|
}
|
|
|
|
return needsUpdate;
|
|
}
|
|
|
|
protected getSpriteAssetName(scale: number, layerId: number): string
|
|
{
|
|
const tag = this.getLayerTag(scale, this.direction, layerId);
|
|
|
|
if(tag !== FurnitureBadgeDisplayVisualization.BADGE_TAG)
|
|
{
|
|
return super.getSpriteAssetName(scale, layerId);
|
|
}
|
|
|
|
if((this._badgeVisibleInState !== -1) && (this.object.getState(0) !== this._badgeVisibleInState))
|
|
{
|
|
return super.getSpriteAssetName(scale, layerId);
|
|
}
|
|
|
|
const assetName = (scale === 32) ? this._badgeAssetNameSmallScale : this._badgeAssetNameNormalScale;
|
|
if(!assetName) return super.getSpriteAssetName(scale, layerId);
|
|
|
|
const a = this.getAsset(assetName, layerId);
|
|
if(!a || !a.texture) return super.getSpriteAssetName(scale, layerId);
|
|
|
|
return assetName;
|
|
}
|
|
|
|
protected updateSprite(sprite: IRoomObjectSprite, asset: IGraphicAsset, scale: number, layerId: number): void
|
|
{
|
|
super.updateSprite(sprite, asset, scale, layerId);
|
|
|
|
const tag = this.getLayerTag(scale, this.direction, layerId);
|
|
|
|
if(tag === FurnitureBadgeDisplayVisualization.BADGE_TAG)
|
|
{
|
|
sprite.visible = true;
|
|
sprite.alpha = 255;
|
|
sprite.color = 0xFFFFFF;
|
|
}
|
|
}
|
|
|
|
protected getLayerXOffset(scale: number, direction: number, layerId: number): number
|
|
{
|
|
let offset = super.getLayerXOffset(scale, direction, layerId);
|
|
|
|
if(this.getLayerTag(scale, direction, layerId) === FurnitureBadgeDisplayVisualization.BADGE_TAG)
|
|
{
|
|
const assetName = (scale === 32) ? this._badgeAssetNameSmallScale : this._badgeAssetNameNormalScale;
|
|
if(!assetName) return offset;
|
|
|
|
const a = this.getAsset(assetName, layerId);
|
|
if(!a) return offset;
|
|
|
|
const targetW = (scale === 64) ? 40 : 20;
|
|
offset += ((targetW - a.width) / 2);
|
|
}
|
|
|
|
return offset;
|
|
}
|
|
|
|
protected getLayerYOffset(scale: number, direction: number, layerId: number): number
|
|
{
|
|
let offset = super.getLayerYOffset(scale, direction, layerId);
|
|
|
|
if(this.getLayerTag(scale, direction, layerId) === FurnitureBadgeDisplayVisualization.BADGE_TAG)
|
|
{
|
|
const assetName = (scale === 32) ? this._badgeAssetNameSmallScale : this._badgeAssetNameNormalScale;
|
|
if(!assetName) return offset;
|
|
|
|
const a = this.getAsset(assetName, layerId);
|
|
if(!a) return offset;
|
|
|
|
const targetH = (scale === 64) ? 40 : 20;
|
|
offset += ((targetH - a.height) / 2);
|
|
}
|
|
|
|
return offset;
|
|
}
|
|
|
|
private async addBadgeToAssetCollection(badgeId: string): Promise<void>
|
|
{
|
|
const sessionDataManager = GetSessionDataManager();
|
|
|
|
let tex = sessionDataManager.getBadgeImage(badgeId);
|
|
if(!tex) tex = sessionDataManager.getGroupBadgeImage(badgeId);
|
|
|
|
if(!tex || !this.asset) return;
|
|
|
|
const badgeCanvas = (tex.source as any).resource as HTMLCanvasElement;
|
|
const ctx = badgeCanvas.getContext('2d', { willReadFrequently: true });
|
|
const imageData = ctx.getImageData(0, 0, 1, 1);
|
|
const isEmpty = imageData.data[3] === 0;
|
|
|
|
if(isEmpty || !this._frameTextures)
|
|
{
|
|
const badgeUrl = GetConfiguration().getValue<string>('badge.asset.url', '').replace('%badgename%', badgeId);
|
|
|
|
try
|
|
{
|
|
const response = await fetch(badgeUrl);
|
|
const arrayBuffer = await response.arrayBuffer();
|
|
const gif = parseGIF(arrayBuffer);
|
|
const frames = decompressFrames(gif, true);
|
|
|
|
if(frames && frames.length > 0)
|
|
{
|
|
this._frameTextures = [];
|
|
const frameDelays: number[] = [];
|
|
|
|
const accCanvas = document.createElement('canvas');
|
|
accCanvas.width = gif.lsd.width;
|
|
accCanvas.height = gif.lsd.height;
|
|
const accCtx = accCanvas.getContext('2d', { willReadFrequently: true });
|
|
|
|
for(let i = 0; i < frames.length; i++)
|
|
{
|
|
const frame = frames[i];
|
|
|
|
if(i > 0)
|
|
{
|
|
const prevDisposal = frames[i - 1].disposalType;
|
|
if(prevDisposal === 2)
|
|
{
|
|
accCtx.clearRect(0, 0, gif.lsd.width, gif.lsd.height);
|
|
}
|
|
}
|
|
|
|
const tempCanvas = document.createElement('canvas');
|
|
tempCanvas.width = frame.dims.width;
|
|
tempCanvas.height = frame.dims.height;
|
|
const tempCtx = tempCanvas.getContext('2d');
|
|
|
|
const patchData = new ImageData(
|
|
new Uint8ClampedArray(frame.patch),
|
|
frame.dims.width,
|
|
frame.dims.height
|
|
);
|
|
tempCtx.putImageData(patchData, 0, 0);
|
|
|
|
accCtx.drawImage(tempCanvas, frame.dims.left, frame.dims.top);
|
|
tempCanvas.width = 0;
|
|
tempCanvas.height = 0;
|
|
|
|
// Create a new canvas for this frame and create a texture from it
|
|
const frameCanvas = document.createElement('canvas');
|
|
frameCanvas.width = gif.lsd.width;
|
|
frameCanvas.height = gif.lsd.height;
|
|
const frameCtx = frameCanvas.getContext('2d');
|
|
frameCtx.drawImage(accCanvas, 0, 0);
|
|
|
|
// Create texture from canvas
|
|
const frameTexture = Texture.from(frameCanvas);
|
|
this._frameTextures.push(frameTexture);
|
|
|
|
// GIF delays are in centiseconds (1/100th of a second)
|
|
frameDelays.push(frame.delay || 10);
|
|
}
|
|
|
|
accCanvas.width = 0;
|
|
accCanvas.height = 0;
|
|
|
|
// Create AnimatedSprite with frame textures
|
|
if(this._frameTextures.length > 1)
|
|
{
|
|
this._animatedSprite = new AnimatedSprite(this._frameTextures);
|
|
|
|
// Calculate average delay and set animation speed
|
|
const avgDelay = frameDelays.reduce((a, b) => a + b, 0) / frameDelays.length;
|
|
// Convert centiseconds to seconds, then to animation speed
|
|
// AnimatedSprite.animationSpeed is frames per ticker update (60fps default)
|
|
// delay in centiseconds * 10 = milliseconds, / 1000 = seconds
|
|
// At 60fps, 1 frame = ~16.67ms
|
|
const delayMs = (avgDelay > 50 ? 10 : avgDelay) * 10;
|
|
const framesPerTick = 16.67 / delayMs;
|
|
this._animatedSprite.animationSpeed = framesPerTick;
|
|
|
|
this._animatedSprite.loop = true;
|
|
this._animatedSprite.play();
|
|
this._lastFrameIndex = -1;
|
|
}
|
|
|
|
// Draw first frame to the badge canvas
|
|
const firstFrameSource = (this._frameTextures[0].source as any).resource;
|
|
if(firstFrameSource instanceof HTMLCanvasElement)
|
|
{
|
|
ctx.clearRect(0, 0, badgeCanvas.width, badgeCanvas.height);
|
|
ctx.drawImage(firstFrameSource, 0, 0);
|
|
tex.source.update();
|
|
}
|
|
}
|
|
}
|
|
catch(err)
|
|
{
|
|
console.error('Failed to parse GIF, using static image:', err);
|
|
|
|
const img = new Image();
|
|
img.crossOrigin = 'anonymous';
|
|
img.onload = () =>
|
|
{
|
|
ctx.clearRect(0, 0, badgeCanvas.width, badgeCanvas.height);
|
|
ctx.drawImage(img, 0, 0, badgeCanvas.width, badgeCanvas.height);
|
|
tex.source.update();
|
|
img.onload = null;
|
|
img.onerror = null;
|
|
};
|
|
img.onerror = () =>
|
|
{
|
|
img.onload = null;
|
|
img.onerror = null;
|
|
};
|
|
img.src = badgeUrl;
|
|
}
|
|
}
|
|
|
|
this.asset.addAsset(badgeId, tex, true, 0, 0, false, false);
|
|
}
|
|
}
|