import { clamp } from 'lodash'; import * as sprites from '../generated/sprites'; import { EntityPart, Sprite, Rect, SpriteBatch, PaletteManager, Palette, PaletteRenderable, PaletteSpriteBatch, DrawOptions, getAnimationFromEntityState, EntityState, SignEntityOptions, EntityFlags, Collider, MixinEntity, Season, } from './interfaces'; import { at, att, hasFlag, invalidEnum } from './utils'; import { WHITE, BLACK, RED } from './colors'; import { toScreenX, toWorldX, toWorldY, toScreenYWithZ } from './positionUtils'; import { rect, addRects, addRect } from './rect'; import { SECOND } from './constants'; import { mockPaletteManager } from './ponyInfo'; import { releasePalette } from '../graphics/paletteManager'; interface Renderable { color?: Sprite; shadow?: Sprite; } export interface AnimatedRenderable { frames: Sprite[]; shadow?: Sprite; palette: Uint32Array; } export interface AnimatedRenderable1 { frames: (Sprite | undefined)[]; } const predefinedSteps = [ [], [1], [3, 1], [3, 2, 1], [4, 2, 1, 1], [5, 3, 2, 1, 1], [9, 5, 3, 2, 1, 1], [14, 9, 5, 3, 2, 1, 1], ]; let paletteManager: PaletteManager | undefined; export function createPalette(palette: Uint32Array | undefined): Palette | undefined { return palette && paletteManager && paletteManager.addArray(palette); } export function setPaletteManager(manager: PaletteManager | undefined) { paletteManager = manager; } export function fakePaletteManager(action: () => T): T { const tempPaletteManager = paletteManager; paletteManager = mockPaletteManager; const result = action(); paletteManager = tempPaletteManager; return result; } function getBounds(sprite: Sprite | undefined, ox: number, oy: number): Rect { return sprite ? rect(sprite.ox + ox, sprite.oy + oy, sprite.w, sprite.h) : rect(0, 0, 0, 0); } export function getRenderableBounds({ color, shadow }: Renderable, dx: number, dy: number): Rect { if (color && shadow) { return addRects(getBounds(color, -dx, -dy), getBounds(shadow, -dx, -dy)); } else if (color) { return getBounds(color, -dx, -dy); } else if (shadow) { return getBounds(shadow, -dx, -dy); } else { return rect(0, 0, 0, 0); } } function getBoundsForFrames(frames: (Sprite | undefined)[], dx: number, dy: number) { return frames.reduce((bounds, f) => f ? addRects(bounds, getBounds(f, dx, dy)) : bounds, rect(0, 0, 0, 0)); } export function pickable(pickableX: number, pickableY: number): EntityPart { return { pickableX, pickableY }; } export function mixPickable(pickableX: number, pickableY: number): MixinEntity { return base => { base.pickableX = pickableX; base.pickableY = pickableY; }; } export function mixTrigger(tileX: number, tileY: number, tileW: number, tileH: number, tall: boolean): MixinEntity { const x = toWorldX(tileX); const y = toWorldY(tileY); const w = toWorldX(tileW); const h = toWorldY(tileH); const bounds = rect(x, y, w, h); return base => { base.triggerBounds = bounds; base.triggerTall = tall; base.triggerOn = false; }; } export function collider(x: number, y: number, w: number, h: number, tall = true, exact = false): Collider { return { x, y, w, h, tall, exact }; } export const ponyColliders = roundedColliderList(-12, -4, 25, 7, 2); export const ponyCollidersBounds = getColliderBounds(ponyColliders); function getColliderBounds(colliders: Collider[]) { const bounds = rect(0, 0, 0, 0); for (const collider of colliders) { addRect(bounds, collider); } return bounds; } function roundedColliderList(x: number, y: number, w: number, h: number, stepsCount: number, tall = true) { const list: Collider[] = []; const steps = predefinedSteps[stepsCount]; if (DEVELOPMENT && !steps) { console.error('Invalid step count', steps); } for (let i = 0; i < steps.length; i++) { list.push(collider(x + steps[i], y + i, w - steps[i] * 2, 1, tall)); } list.push(collider(x, y + steps.length, w, h - steps.length * 2, tall)); for (let i = 0; i < steps.length; i++) { const ii = steps.length - (i + 1); list.push(collider(x + steps[ii], y + h - steps.length + i, w - steps[ii] * 2, 1, tall)); } return list; } export function mixColliderRect(x: number, y: number, w: number, h: number, tall = true, exact = false): MixinEntity { return mixColliders(collider(x, y, w, h, tall, exact)); } export function mixColliderRounded(x: number, y: number, w: number, h: number, stepsCount: number, tall = true): MixinEntity { return mixColliders(...roundedColliderList(x, y, w, h, stepsCount, tall)); } export function mixColliders(...list: Collider[]): MixinEntity { const bounds = getColliderBounds(list); return base => { base.flags |= EntityFlags.CanCollideWith; base.colliders = list; base.collidersBounds = bounds; }; } export function taperColliderSE(x: number, y: number, w: number, h: number, tall?: boolean) { const colliders: Collider[] = []; for (let iy = 0, ix = w - 2; iy < h; iy++ , ix -= ((iy % 3) ? 1 : 2)) { colliders.push(collider(x + ix, y + iy, w - ix, 1, tall)); } return colliders; } export function taperColliderSW(x: number, y: number, w: number, h: number, tall?: boolean) { const colliders: Collider[] = []; for (let iy = 0, ix = w - 2; iy < h; iy++ , ix -= ((iy % 3) ? 1 : 2)) { colliders.push(collider(x, y + iy, w - ix, 1, tall)); } return colliders; } export function taperColliderNW(x: number, y: number, w: number, h: number, tall?: boolean) { const colliders: Collider[] = []; for (let iy = 0, ix = 2; iy < h; iy++ , ix += ((iy % 3) ? 1 : 2)) { colliders.push(collider(x, y + iy, w - ix, 1, tall)); } return colliders; } export function taperColliderNE(x: number, y: number, w: number, h: number, tall?: boolean) { const colliders: Collider[] = []; for (let iy = 0, ix = 2; iy < h; iy++ , ix += ((iy % 3) ? 1 : 2)) { colliders.push(collider(x + ix, y + iy, w - ix, 1, tall)); } return colliders; } export function skewColliderNW(x: number, y: number, w: number, h: number, tall?: boolean) { const colliders: Collider[] = []; for (let iy = 0, ix = 2; iy < h; iy++ , ix += ((iy % 3) ? 1 : 2)) { colliders.push(collider(x - ix, y + iy, w, 1, tall)); } return colliders; } export function skewColliderNE(x: number, y: number, w: number, h: number, tall?: boolean) { const colliders: Collider[] = []; for (let iy = 0, ix = 2; iy < h; iy++ , ix += ((iy % 3) ? 1 : 2)) { colliders.push(collider(x + ix, y + iy, w, 1, tall)); } return colliders; } export function triangleColliderNW(x: number, y: number, w: number, h: number, tall?: boolean) { const colliders: Collider[] = []; for (let iy = 0, ix = 2; iy < h; iy++ , ix += ((iy % 3) ? 1 : 2)) { colliders.push(collider(x - ix, y + iy, w + ix, 1, tall)); } return colliders; } export function triangleColliderNE(x: number, y: number, w: number, h: number, tall?: boolean) { const colliders: Collider[] = []; for (let iy = 0, ix = 2; iy < h; iy++ , ix += ((iy % 3) ? 1 : 2)) { colliders.push(collider(x, y + iy, w + ix, 1, tall)); } return colliders; } export function mixInteract(x: number, y: number, w: number, h: number, interactRange?: number): MixinEntity { const interactBounds = rect(x, y, w, h); return base => { base.flags |= EntityFlags.Interactive; base.interactBounds = interactBounds; base.interactRange = interactRange; }; } export function mixInteractAt(interactRange?: number): MixinEntity { return base => { base.flags |= EntityFlags.Interactive; base.interactRange = interactRange; }; } export function mixMinimap(color: number, rect: Rect, order = 1): MixinEntity { const minimap = { color, rect, order }; return base => base.minimap = minimap; } export interface AnimatedMixinOptions { color?: number; repeat?: boolean; animations?: number[][]; lightSprite?: AnimatedRenderable1; useGameTime?: boolean; flipped?: boolean; } export function mixAnimation( anim: AnimatedRenderable, fps: number, dx: number, dy: number, { color = WHITE, repeat = true, animations, lightSprite, useGameTime, flipped = false }: AnimatedMixinOptions = {} ): MixinEntity { const bounds = getBoundsForFrames(anim.frames, -dx, -dy); const lightSpriteBounds = lightSprite ? getBoundsForFrames(lightSprite.frames, -dx, -dy) : rect(0, 0, 0, 0); if (SERVER && !TESTS) { return base => base.bounds = bounds; } return base => { const defaultPalette = anim.shadow && createPalette(sprites.defaultPalette); const palette = createPalette(anim.palette); let time = repeat ? Math.random() * 5 : 0; let animation = 0; let lastFrame = 0; const getFrame = (options: DrawOptions) => { let frameNumber = Math.floor(time * fps); if (useGameTime) { frameNumber = Math.floor((options.gameTime / 1000) * fps); } if (animations) { if (repeat) { frameNumber = frameNumber % animations[animation].length; } return at(animations[animation], frameNumber) || 0; } else { return repeat ? (frameNumber % anim.frames.length) : Math.min(frameNumber, anim.frames.length - 1); } }; base.bounds = bounds; base.palettes = []; defaultPalette && base.palettes.push(defaultPalette); palette && base.palettes.push(palette); base.update = function (delta: number) { time += delta; const anim = getAnimationFromEntityState(this.state); if (animations && anim !== animation) { animation = anim; time = 0; } const frameNumber = Math.floor(time * fps); if (lastFrame !== frameNumber) { lastFrame = frameNumber; return true; } else { return false; } }; base.draw = function (batch: PaletteSpriteBatch, options: DrawOptions) { const frame = getFrame(options); const frameSprite = anim.frames[frame]; const x = toScreenX(this.x); const y = toScreenYWithZ(this.y, this.z); batch.save(); batch.translate(x, y); if (hasFlag(this.state, EntityState.FacingRight) || flipped) { batch.scale(-1, 1); } batch.translate(-dx, -dy); anim.shadow && batch.drawSprite(anim.shadow, options.shadowColor, defaultPalette, 0, 0); frameSprite && batch.drawSprite(frameSprite, color, palette, 0, 0); batch.restore(); }; if (lightSprite) { base.lightSpriteColor = WHITE; base.lightSpriteBounds = lightSpriteBounds; base.drawLightSprite = function (batch, options) { const frame = getFrame(options); const frameSprite = lightSprite.frames[frame]; const x = toScreenX(this.x); const y = toScreenYWithZ(this.y, this.z); batch.save(); batch.translate(x, y); if (hasFlag(this.state, EntityState.FacingRight) || flipped) { batch.scale(-1, 1); } batch.translate(-dx, -dy); batch.drawSprite(frameSprite, this.lightSpriteColor!, 0, 0); batch.restore(); }; } }; } export function mixDrawWindow( sprite: PaletteRenderable, dx: number, dy: number, paletteIndex: number, padLeft: number, padTop: number, padRight: number, padBottom: number, ): MixinEntity { const bounds = getRenderableBounds(sprite, dx, dy); return base => { base.bounds = bounds; if (!SERVER || TESTS) { const defaultPalette = sprite.shadow && createPalette(sprites.defaultPalette); const palette = createPalette(att(sprite.palettes, paletteIndex)); base.palettes = []; defaultPalette && base.palettes.push(defaultPalette); palette && base.palettes.push(palette); base.draw = function (batch, options) { const baseX = toScreenX(this.x + (this.ox || 0)); const baseY = toScreenYWithZ(this.y + (this.oy || 0), this.z + (this.oz || 0)); const x = baseX - dx; const y = baseY - dy; if (sprite.shadow !== undefined) { batch.drawSprite(sprite.shadow, options.shadowColor, defaultPalette, x, y); } if (sprite.color !== undefined) { batch.drawRect(options.lightColor, x + padLeft, y + padTop, sprite.color.w - (padLeft + padRight), sprite.color.h - (padTop + padBottom)); batch.drawSprite(sprite.color, WHITE, palette, x, y); } }; } }; } export function mixDraw(sprite: PaletteRenderable, dx: number, dy: number, paletteIndex = 0): MixinEntity { const bounds = getRenderableBounds(sprite, dx, dy); return base => { base.bounds = bounds; if (!SERVER || TESTS) { const defaultPalette = sprite.shadow && createPalette(sprites.defaultPalette); const palette = createPalette(att(sprite.palettes, paletteIndex)); base.palettes = []; defaultPalette && base.palettes.push(defaultPalette); palette && base.palettes.push(palette); base.draw = function (batch: PaletteSpriteBatch, options: DrawOptions) { const x = toScreenX(this.x + (this.ox || 0)) - dx; const y = toScreenYWithZ(this.y + (this.oy || 0), this.z + (this.oz || 0)) - dy; const opacity = 1 - 0.6 * (this.coverLifting || 0); if (sprite.shadow !== undefined) { batch.drawSprite(sprite.shadow, options.shadowColor, defaultPalette, x, y); } batch.globalAlpha = opacity; if (sprite.color !== undefined) { batch.drawSprite(sprite.color, WHITE, palette, x, y); } batch.globalAlpha = 1; }; } }; } export interface MixDraw { sprite: PaletteRenderable; dx: number; dy: number; palette: number; } export interface MixDrawSeasonal { summer: MixDraw; autumn?: Partial; winter?: Partial; spring?: Partial; } function addBounds(bounds: Rect, setup: MixDraw) { addRect(bounds, getRenderableBounds(setup.sprite, setup.dx, setup.dy)); } export function mixDrawSeasonal(setup: MixDrawSeasonal): MixinEntity { const bounds = rect(0, 0, 0, 0); const summer = setup.summer; const autumn = { ...summer, ...setup.autumn }; const winter = { ...summer, ...setup.winter }; const spring = { ...summer, ...setup.spring }; addBounds(bounds, summer); addBounds(bounds, autumn); addBounds(bounds, winter); addBounds(bounds, spring); return (base, _, worldState) => { base.bounds = bounds; if (!SERVER || TESTS) { let season = Season.Summer; let { sprite, dx, dy, palette: paletteIndex } = setup.summer; let defaultPalette: Palette | undefined = undefined; let palette: Palette | undefined = undefined; const setupSeason = (newSeason: Season) => { season = newSeason; let set: MixDraw; switch (season) { case Season.Summer: set = summer; break; case Season.Autumn: set = autumn; break; case Season.Winter: set = winter; break; case Season.Spring: set = spring; break; default: invalidEnum(season); return; } sprite = set.sprite; dx = set.dx; dy = set.dy; paletteIndex = set.palette; if (base.palettes) { for (const palette of base.palettes) { releasePalette(palette); } } defaultPalette = sprite.shadow && createPalette(sprites.defaultPalette); palette = createPalette(att(sprite.palettes, paletteIndex)); base.palettes = []; defaultPalette && base.palettes.push(defaultPalette); palette && base.palettes.push(palette); }; setupSeason(worldState.season); base.draw = function (batch: PaletteSpriteBatch, options: DrawOptions) { const x = toScreenX(this.x + (this.ox || 0)) - dx; const y = toScreenYWithZ(this.y + (this.oy || 0), this.z + (this.oz || 0)) - dy; const opacity = 1 - 0.6 * (this.coverLifting || 0); if (sprite.shadow !== undefined) { batch.drawSprite(sprite.shadow, options.shadowColor, defaultPalette, x, y); } batch.globalAlpha = opacity; if (sprite.color !== undefined) { batch.drawSprite(sprite.color, WHITE, palette, x, y); } batch.globalAlpha = 1; if (season !== options.season) { setupSeason(options.season); } }; } }; } function splitSprite(sprite: Sprite, x: number, w: number, h: number) { const result: Sprite[] = []; for (let y = 0; y < sprite.h; y += h) { result.push({ x: sprite.x + x, y: sprite.y + y, w, h, ox: sprite.ox, oy: sprite.oy, type: sprite.type }); } return result; } const poles = [ { sprite: sprites.direction_pole_3, dy: -39 }, { sprite: sprites.direction_pole_4, dy: -50 }, { sprite: sprites.direction_pole_5, dy: -61 }, ]; const shadowLeft = sprites.direction_shadow_left.shadow; const shadowRight = sprites.direction_shadow_right.shadow; const leftSprites = splitSprite(sprites.direction_left_right.color, 0, 17, 10); const rightSprites = splitSprite(sprites.direction_left_right.color, 17, 17, 10); const dirUpDown = [ { shadowUp: sprites.direction_shadow_up_left.shadow, shadowDown: sprites.direction_shadow_down_right.shadow, spriteUp: sprites.direction_up_left.color, spriteDown: sprites.direction_down_right.color, shadowUpDX: -6, shadowUpDY: -9, shadowDownDX: -1, shadowDownDY: 3, upDX: -6, upDY: -7, downDX: -1, downDY: 5, }, { shadowUp: sprites.direction_shadow_up_right.shadow, shadowDown: sprites.direction_shadow_down_left.shadow, spriteUp: sprites.direction_up_right.color, spriteDown: sprites.direction_down_left.color, shadowUpDX: 1, shadowUpDY: -9, shadowDownDX: -4, shadowDownDY: 3, upDX: 1, upDY: -7, downDX: -5, downDY: 4, }, ]; export function mixDrawDirectionSign(): MixinEntity { const poleDX = -4; const leftDX = -20; const rightDX = 3; const plateDY = 2; const leftRightStep = 11; const upDownStep = 11; return (base, options = {}) => { const { sign: { r = 0, w = [], e = [], s = [], n = [] } = {} } = options as SignEntityOptions; const max = clamp(Math.max(w.length, e.length, s.length, n.length), 3, 5); const boundsH = 7 + max * 11; base.bounds = rect(-20, -boundsH, 40, boundsH); base.options = options; if (SERVER && !TESTS) return; const { shadowUp, shadowDown, spriteUp, spriteDown, upDX, upDY, downDX, downDY, shadowUpDX, shadowUpDY, shadowDownDX, shadowDownDY, } = dirUpDown[r]; const leftShadow = !!w.length; const rightShadow = !!e.length; const upShadow = !!n.length; const downShadow = !!s.length; const pole = poles[max - 3]; const defaultPalette = pole.sprite.shadow && createPalette(sprites.defaultPalette); const palette = createPalette(att(pole.sprite.palettes, 0)); base.palettes = []; defaultPalette && base.palettes.push(defaultPalette); palette && base.palettes.push(palette); base.draw = function (batch, options) { const x = toScreenX(this.x); const y = toScreenYWithZ(this.y, this.z); batch.drawSprite(pole.sprite.shadow, options.shadowColor, defaultPalette, x + poleDX, y + pole.dy); leftShadow && batch.drawSprite(shadowLeft, options.shadowColor, defaultPalette, x - 18, y - 1); rightShadow && batch.drawSprite(shadowRight, options.shadowColor, defaultPalette, x + 4, y - 1); upShadow && batch.drawSprite(shadowUp, options.shadowColor, defaultPalette, x + shadowUpDX, y + shadowUpDY); downShadow && batch.drawSprite(shadowDown, options.shadowColor, defaultPalette, x + shadowDownDX, y + shadowDownDY); for (let i = n.length - 1; i >= 0; i--) { if (n[i] !== -1) { batch.drawSprite(spriteUp, WHITE, palette, x + upDX, y + pole.dy + upDY + i * upDownStep); } } batch.drawSprite(pole.sprite.color, WHITE, palette, x + poleDX, y + pole.dy); for (let i = 0; i < w.length; i++) { if (w[i] !== -1) { const sprite = leftSprites[w[i]]; sprite && batch.drawSprite(sprite, WHITE, palette, x + leftDX, y + pole.dy + plateDY + i * leftRightStep); } } for (let i = 0; i < e.length; i++) { if (e[i] !== -1) { const sprite = rightSprites[e[i]]; sprite && batch.drawSprite(rightSprites[e[i]], WHITE, palette, x + rightDX, y + pole.dy + plateDY + i * leftRightStep); } } for (let i = s.length - 1; i >= 0; i--) { if (s[i] !== -1) { batch.drawSprite(spriteDown, WHITE, palette, x + downDX, y + pole.dy + downDY + i * upDownStep); } } }; }; } export function mixLight(color: number, dx: number, dy: number, w: number, h: number): MixinEntity { return base => { if (!SERVER || TESTS) { base.lightOn = true; base.lightColor = color; base.lightScale = 1; base.lightTarget = 1; base.lightScaleAdjust = 1; base.lightBounds = rect(-(dx + w / 2), -(dy + h / 2), w, h); base.drawLight = function (batch: SpriteBatch) { if (!this.lightOn) return; const x = toScreenX(this.x); const y = toScreenYWithZ(this.y, this.z); const s = this.lightScale! * this.lightScaleAdjust!; const width = w * s; const height = h * s; const color = this.lightColor!; batch.drawImage(color, -1, -1, 2, 2, x - (dx + width / 2), y - (dy + height / 2), width, height); }; } }; } export function mixLightSprite(sprite: Sprite, color: number, dx: number, dy: number): MixinEntity { return base => { if (!SERVER || TESTS) { base.lightSpriteOn = true; base.lightSpriteX = dx; base.lightSpriteY = dy; base.lightSpriteColor = color; base.lightSpriteBounds = getBounds(sprite, -dx, -dy); base.drawLightSprite = function (batch: SpriteBatch) { if (!this.lightSpriteOn) return; const x = toScreenX(this.x) - this.lightSpriteX!; const y = toScreenYWithZ(this.y, this.z) - this.lightSpriteY!; batch.drawSprite(sprite, this.lightSpriteColor || BLACK, x, y); }; } }; } export function mixDrawRain(): MixinEntity { const sprite = sprites.rainfall.color; // 110x477 const bounds = rect(toScreenX(-4), -sprite.h, toScreenX(8), sprite.h); return base => { base.bounds = bounds; if (SERVER && !TESTS) return; let time = 0; const palette = createPalette(sprites.defaultPalette); base.palettes = [palette]; // update(delta: number) { // time += delta * 1000; // if (time > 200) { // time -= 200; // } // }, base.draw = function (batch: PaletteSpriteBatch) { const x = toScreenX(this.x) + bounds.x; const y = toScreenYWithZ(this.y, this.z) - sprite.h + Math.floor(time); batch.drawImage(sprite.type, RED, palette, sprite.x, sprite.y, sprite.w, sprite.h, x, y, sprite.w, sprite.h); }; }; } export function mixDrawShadow(sprite: PaletteRenderable, dx: number, dy: number, shadowColor?: number): MixinEntity { const bounds = getRenderableBounds(sprite, dx, dy); return base => { base.bounds = bounds; if (!SERVER || TESTS) { const defaultPalette = createPalette(sprites.defaultPalette); base.palettes = [defaultPalette]; base.draw = function (batch: PaletteSpriteBatch, options: DrawOptions) { const x = toScreenX(this.x + (this.ox || 0)) - dx; const y = toScreenYWithZ(this.y + (this.oy || 0), this.z) - dy; const color = shadowColor === undefined ? options.shadowColor : shadowColor; sprite.shadow && batch.drawSprite(sprite.shadow, color, defaultPalette, x, y); }; } }; } export function mixBobbing(bobsFps: number, bobs: number[]): MixinEntity { return base => { base.flags |= EntityFlags.Bobbing; base.bobsFps = bobsFps; base.bobs = bobs; }; } let fullWalls = true; export function toggleWalls() { fullWalls = !fullWalls; } export function mixDrawWall( full: PaletteRenderable, half: PaletteRenderable, dx: number, dy: number, dy2: number ): MixinEntity { const fullBounds = getRenderableBounds(full, dx, dy); // const halfBounds = getRenderableBounds(half, dx, dy2); return base => { base.bounds = fullBounds; // fullWalls ? fullBounds : halfBounds if (SERVER && !TESTS) return; const fullPalette = createPalette(att(full.palettes, 0)); const halfPalette = createPalette(att(half.palettes, 0)); base.palettes = []; fullPalette && base.palettes.push(fullPalette); halfPalette && base.palettes.push(halfPalette); base.draw = function (batch: PaletteSpriteBatch) { const sprite = fullWalls ? full : half; const palette = fullWalls ? fullPalette : halfPalette; const x = toScreenX(this.x) - dx; const y = toScreenYWithZ(this.y, this.z) - (fullWalls ? dy : dy2); sprite.color && batch.drawSprite(sprite.color, WHITE, palette, x, y); }; }; } export function mixDrawSpider( sprite: PaletteRenderable, dx: number, dy: number ): MixinEntity { const heightOffset = 30; const spriteColor = sprite.color; const baseBounds = getRenderableBounds(sprite, dx, dy); if (!spriteColor) throw new Error('Missing sprite'); return base => { const { height, time } = base.options as { height: number; time: number; }; const bounds = { ...baseBounds }; bounds.y -= (height + heightOffset); bounds.h += height; base.bounds = bounds; if (SERVER && !TESTS) return; const palette = createPalette(sprite.palettes && sprite.palettes[0]); base.palettes = [palette]; base.draw = function (batch: PaletteSpriteBatch, options: DrawOptions) { const t = options.gameTime / SECOND - time; const h = clamp(Math.sin(t / 4) * 4, 0, 1) * height; if (h < height) { const lineLength = height - h - 4; const x = toScreenX(this.x) - dx; const y = toScreenYWithZ(this.y, this.z) - dy - heightOffset - h; batch.drawRect(0x181818ff, x + 2, y - lineLength, 1, lineLength + 1); batch.drawSprite(spriteColor, WHITE, palette, x, y); } }; }; }