import { PONY_WIDTH, PONY_HEIGHT, BLINK_FRAMES, canFly } from '../common/ponyUtils'; import { stand, sneeze } from '../common/ponyAnimations'; import { PaletteSpriteBatch, Pony, BodyAnimation, EntityState, SpriteBatch, ExpressionExtra, HeadAnimation, Palette, PaletteManager, DrawOptions, Rect, EntityFlags, IMap, Entity, DoAction, Muzzle, Expression, getEyeOpenness, Iris, EntityPlayerState, } from '../common/interfaces'; import { hasFlag, setFlag } from '../common/utils'; import { blinkFps, PONY_TYPE } from '../common/constants'; import { createAnEntity, boopSplashRight, boopSplashLeft } from '../common/entities'; import { createAnimationPlayer, isAnimationPlaying, drawAnimation, playAnimation, updateAnimation, playOneOfAnimations } from '../common/animationPlayer'; import { blushColor, WHITE, MAGIC_ALPHA, HEARTS_COLOR } from '../common/colors'; import { encodeExpression, decodeExpression } from '../common/encoders/expressionEncoder'; import { toScreenX, toWorldX, toWorldY, toScreenYWithZ } from '../common/positionUtils'; import { drawPony, getPonyHeadPosition, createHeadTransform } from './ponyDraw'; import { isPonySitting, isPonyFlying, isPonyLying, isPonyStanding, isPonyLandedOrCanLand, isIdle, isIdleAnimation, isFacingRight, releaseEntity, addOrRemoveFromEntityList, releasePalettePonyInfo } from '../common/entityUtils'; import { getAnimation, getAnimationFrame, setAnimatorState, updateAnimator, createAnimator, AnimatorState, resetAnimatorState } from '../common/animator'; import { trotting, flying, hovering, toBoopState, isFlyingUpOrDown, isFlyingDown, isSittingDown, isSittingUp, swinging, standing, sitting, lying, swimming, isSwimmingState, swimmingToFlying, toKissState, } from '../common/ponyStates'; import { decodePonyInfo } from '../common/compressPony'; import { defaultPonyState, defaultDrawPonyOptions, isStateEqual } from '../common/ponyHelpers'; import { sneezeAnimation, holdPoofAnimation, heartsAnimation, tearsAnimation, cryAnimation, zzzAnimations, magicAnimation } from './spriteAnimations'; import { rect } from '../common/rect'; import { hasDrawLight, hasLightSprite } from './draw'; import { ponyColliders, ponyCollidersBounds } from '../common/mixins'; import { PonyTownGame } from './game'; import { playEffect } from './handlers'; import * as sprites from '../generated/sprites'; import { withAlpha } from '../common/color'; const flyY = 15; const lightExtentX = 100; const lightExtentY = 70; const emptyBounds = rect(0, 0, 0, 0); const bounds = rect(-PONY_WIDTH / 2, -PONY_HEIGHT, PONY_WIDTH, PONY_HEIGHT + 5); const boundsFly = rect(bounds.x, bounds.y - flyY, bounds.w, bounds.h + flyY); const lightBounds = makeLightBounds(bounds); const lightBoundsFly = makeLightBounds(boundsFly); const interactBounds = rect(-20, -50, 40, 50); const interactBoundsFly = rect(interactBounds.x, interactBounds.y - flyY, interactBounds.w, interactBounds.h); const defaultExpr = encodeExpression(undefined); export function createPony( id: number, state: EntityState, info: string | Uint8Array | undefined, defaultPalette: Palette, paletteManager: PaletteManager ): Pony { const pony: Pony = { id, state, playerState: EntityPlayerState.None, type: PONY_TYPE, flags: EntityFlags.Movable | EntityFlags.CanCollide | EntityFlags.Interactive, expr: defaultExpr, ponyState: defaultPonyState(), x: 0, y: 0, z: 0, vx: 0, vy: 0, depth: 0, info, order: 0, timestamp: 0, colliders: ponyColliders, collidersBounds: ponyCollidersBounds, selected: false, extra: false, toy: 0, swimming: false, ex: false, // extended data indicator, sent in extended option inTheAirDelay: 0, name: undefined, tag: undefined, site: undefined, modInfo: undefined, hold: 0, palettePonyInfo: undefined, headAnimation: undefined, batch: undefined, discardBatch: false, headTime: Math.random() * 5, blinkTime: 0, nextBlink: Math.random() * 5, currentExpression: defaultExpr, drawingOptions: { ...defaultDrawPonyOptions(), shadow: true, bounce: BETA }, zzzEffect: createAnimationPlayer(defaultPalette), cryEffect: createAnimationPlayer(defaultPalette), sneezeEffect: createAnimationPlayer(defaultPalette), holdPoofEffect: createAnimationPlayer(defaultPalette), heartsEffect: createAnimationPlayer(defaultPalette), magicEffect: createAnimationPlayer(paletteManager.addArray(sprites.magic2.palette)), animator: createAnimator(), lastX: 0, lastY: 0, lastRight: false, lastState: defaultPonyState(), initialized: false, doAction: DoAction.None, bounds: bounds, interactBounds: interactBounds, chatBounds: interactBounds, lightBounds: emptyBounds, lightSpriteBounds: emptyBounds, paletteManager, lastBoopSplash: 0, magicColor: 0, }; pony.ponyState.drawFaceExtra = batch => drawFaceExtra(batch, pony); return pony; } export function isPonyOnTheGround(pony: Pony) { return !isPonyFlying(pony) && !isFlyingUpOrDown(pony.animator.state); } export function getPaletteInfo(pony: Pony) { return ensurePonyInfoDecoded(pony); } export function canPonyFly(pony: Pony) { return !!pony.palettePonyInfo && canFly(pony.palettePonyInfo); } export function canPonyLie(pony: Pony, map: IMap) { return !isPonyLying(pony) && (isIdle(pony) || isSittingDown(pony.animator.state) || isFlyingDown(pony.animator.state)) && isPonyLandedOrCanLand(pony, map); } export function canPonySit(pony: Pony, map: IMap) { return !isPonySitting(pony) && (isIdle(pony) || isFlyingDown(pony.animator.state)) && isPonyLandedOrCanLand(pony, map); } export function canPonyStand(pony: Pony, map: IMap) { return !isPonyStanding(pony) && (isIdleAnimation(pony.ponyState.animation) || isSittingUp(pony.animator.state)) && isPonyLandedOrCanLand(pony, map); } export function canPonyFlyUp(pony: Pony) { return !isPonyFlying(pony) && canPonyFly(pony) && !isFlyingUpOrDown(pony.animator.state); } export function updatePonyInfo(pony: Pony, info: string | Uint8Array, apply: () => void) { pony.info = info; if (pony.palettePonyInfo !== undefined) { releasePalettePonyInfo(pony); ensurePonyInfoDecoded(pony); pony.discardBatch = true; if (isPonyFlying(pony) && !canPonyFly(pony)) { if (DEVELOPMENT) { console.warn('Force land'); } pony.state = setFlag(pony.state, EntityState.PonyFlying, false); resetAnimatorState(pony.animator); } apply(); } } export function isPonyBug(pony: Pony) { if (pony.info === undefined || pony.palettePonyInfo === undefined) { return false; } const wingType = pony.palettePonyInfo.wings && pony.palettePonyInfo.wings.type || 0; return wingType === 4; } export function ensurePonyInfoDecoded(pony: Pony) { if (pony.info !== undefined && pony.palettePonyInfo === undefined) { pony.palettePonyInfo = decodePonyInfo(pony.info, pony.paletteManager); const wingType = pony.palettePonyInfo.wings && pony.palettePonyInfo.wings.type || 0; pony.animator.variant = wingType === 4 ? 'bug' : ''; pony.ponyState.blushColor = blushColor(pony.palettePonyInfo.coatPalette.colors[1]); pony.magicColor = withAlpha(pony.palettePonyInfo.magicColorValue, MAGIC_ALPHA); } return pony.palettePonyInfo!; } export function invalidatePalettesForPony(pony: Pony) { pony.discardBatch = true; } export function doBoopPonyAction(game: PonyTownGame, pony: Pony) { doPonyAction(pony, DoAction.Boop); if (pony.swimming && pony.lastBoopSplash < performance.now()) { if (isFacingRight(pony)) { playEffect(game, pony, boopSplashRight.type); } else { playEffect(game, pony, boopSplashLeft.type); } pony.lastBoopSplash = performance.now() + 800; } } export function doPonyAction(pony: Pony, action: DoAction) { pony.doAction = action; } export function setPonyExpression(pony: Pony, expr: number) { pony.expr = expr; } export function hasExtendedInfo(pony: Pony) { return pony.ex; } export function hasHeadAnimation(pony: Pony) { return pony.headAnimation !== undefined; } export function setHeadAnimation(pony: Pony, headAnimation: HeadAnimation | undefined) { if (pony.headAnimation !== headAnimation) { pony.headTime = 0; pony.headAnimation = headAnimation; } } export function drawPonyEntity(batch: PaletteSpriteBatch, pony: Pony, drawOptions: DrawOptions) { if (pony.discardBatch && pony.batch !== undefined) { batch.releaseBatch(pony.batch); pony.batch = undefined; pony.discardBatch = false; } if (pony.batch !== undefined) { batch.patchBatchDepth(pony.batch); batch.drawBatch(pony.batch); } else if (pony.palettePonyInfo !== undefined) { let swimming = false; if (isSwimmingState(pony.animator.state)) { if (pony.animator.state === swimmingToFlying) { swimming = pony.animator.time < 0.4; } else { swimming = true; } } const createBatch = pony.vx === 0 && pony.vy === 0 && !swimming; const right = isFacingRight(pony); if (createBatch) { batch.startBatch(); } batch.save(); transformBatch(batch, pony); const options = pony.drawingOptions; options.flipped = right; options.selected = pony.selected === true; options.extra = pony.extra; options.toy = pony.toy; options.swimming = swimming; options.shadow = !pony.swimming; options.gameTime = drawOptions.gameTime + pony.id * 0.1; options.shadowColor = drawOptions.shadowColor; const ponyState = pony.ponyState; drawPony(batch, pony.palettePonyInfo, ponyState, 0, 0, options); if ( isAnimationPlaying(pony.zzzEffect) || isAnimationPlaying(pony.sneezeEffect) || isAnimationPlaying(pony.holdPoofEffect) || isAnimationPlaying(pony.heartsEffect) || isAnimationPlaying(pony.magicEffect) ) { const { x, y } = getPonyHeadPosition(pony.ponyState, 0, 0); const right = isFacingRight(pony); const flip = right ? !ponyState.headTurned : ponyState.headTurned; batch.multiplyTransform(createHeadTransform(undefined, x, y, ponyState)); drawAnimation(batch, pony.zzzEffect, 0, 0, WHITE, flip); drawAnimation(batch, pony.sneezeEffect, 0, 0, WHITE, flip); drawAnimation(batch, pony.holdPoofEffect, 0, 0, WHITE, flip); drawAnimation(batch, pony.heartsEffect, 0, 0, HEARTS_COLOR, flip); if (pony.magicEffect.currentAnimation !== undefined) { drawAnimation(batch, pony.magicEffect, 0, 0, pony.magicColor, flip); const sprite = sprites.magic3.frames[pony.magicEffect.frame]; if (sprite) { batch.drawSprite(sprite, WHITE, pony.heartsEffect.palette, 0, 0); } } } batch.restore(); if (createBatch) { pony.batch = batch.finishBatch(); pony.lastX = toScreenX(pony.x); pony.lastY = toScreenYWithZ(pony.y, pony.z); pony.lastRight = right; pony.zzzEffect.dirty = false; pony.cryEffect.dirty = false; pony.sneezeEffect.dirty = false; pony.holdPoofEffect.dirty = false; pony.heartsEffect.dirty = false; pony.magicEffect.dirty = false; Object.assign(pony.lastState, ponyState); } } } const magickLightSizes = [ 0, 1.02, // fade-in 0.97, 0.94, 0.91, 0.94, 0.97, 1.00, // loop 0.97, 0.94, 0.91, // fade-out ]; export function drawPonyEntityLight(batch: SpriteBatch, pony: Pony, options: DrawOptions) { const ponyState = pony.ponyState; const holding = ponyState.holding; const drawHolding = holding !== undefined && holding.drawLight !== undefined; const drawMagic = pony.magicEffect.currentAnimation !== undefined; const draw = drawHolding || drawMagic; if (draw) { batch.save(); transformBatch(batch, pony); const { x, y } = getPonyHeadPosition(ponyState, 0, 0); batch.multiplyTransform(createHeadTransform(undefined, x, y, ponyState)); if (drawHolding) { holding!.x = toWorldX(holding!.pickableX!); holding!.y = toWorldY(holding!.pickableY!); holding!.drawLight!(batch, options); } if (drawMagic) { const size = 200 * (magickLightSizes[pony.magicEffect.frame] || 0); batch.drawImage(WHITE, -1, -1, 2, 2, 30 - size / 2, 27 - size / 2, size, size); } batch.restore(); } } export function drawPonyEntityLightSprite(batch: SpriteBatch, pony: Pony, options: DrawOptions) { const ponyState = pony.ponyState; const holding = ponyState.holding; const drawHolding = holding !== undefined && holding.drawLightSprite !== undefined; // const drawMagic = pony.magicEffect.currentAnimation !== undefined; const draw = drawHolding; // || drawMagic; if (draw) { batch.save(); transformBatch(batch, pony); const { x, y } = getPonyHeadPosition(ponyState, 0, 0); batch.multiplyTransform(createHeadTransform(undefined, x, y, ponyState)); if (drawHolding) { holding!.x = toWorldX(holding!.pickableX!); holding!.y = toWorldY(holding!.pickableY!); holding!.drawLightSprite!(batch, options); } // if (drawMagic) { // const frame = pony.magicEffect.frame; // const sprite = sprites.magic2_light.frames[frame]; // batch.drawSprite(sprite, WHITE, 0, 0); // } batch.restore(); } } export function flagsToState(state: EntityState, moving: boolean, isSwimming: boolean): AnimatorState { const ponyState = state & EntityState.PonyStateMask; if (isSwimming) { return swimming; } else if (moving) { if (ponyState === EntityState.PonyFlying) { return flying; } else { return trotting; } } else { switch (ponyState) { case EntityState.PonyStanding: return standing; case EntityState.PonyWalking: return trotting; case EntityState.PonyTrotting: return trotting; case EntityState.PonySitting: return sitting; case EntityState.PonyLying: return lying; case EntityState.PonyFlying: return hovering; default: throw new Error(`Invalid pony state (${ponyState})`); } } } export function updatePonyEntity(pony: Pony, delta: number, gameTime: number, safe: boolean) { // update state const state = pony.ponyState; const walking = pony.vx !== 0 || pony.vy !== 0; const animationState = flagsToState(pony.state, walking, pony.swimming); if (pony.inTheAirDelay > 0) { pony.inTheAirDelay -= delta; } if (pony.doAction !== DoAction.None) { switch (pony.doAction) { case DoAction.Boop: setAnimatorState(pony.animator, toBoopState(animationState) || animationState); break; case DoAction.Swing: setAnimatorState(pony.animator, swinging); break; case DoAction.HoldPoof: playAnimation(pony.holdPoofEffect, holdPoofAnimation); break; case DoAction.Kiss: setAnimatorState(pony.animator, toKissState(animationState) || animationState); break; default: if (DEVELOPMENT) { console.error(`Invalid DoAction: ${pony.doAction}`); } } pony.doAction = DoAction.None; } else { setAnimatorState(pony.animator, animationState); } // head pony.headTime += delta; if (pony.headAnimation !== undefined) { const frame = Math.floor(pony.headTime * pony.headAnimation.fps); if (frame >= pony.headAnimation.frames.length && !pony.headAnimation.loop) { pony.headAnimation = undefined; state.headAnimationFrame = 0; } else { state.headAnimationFrame = frame % pony.headAnimation.frames.length; } } if (state.headAnimation !== pony.headAnimation) { state.headAnimation = pony.headAnimation; if (pony.headAnimation === sneeze) { playAnimation(pony.sneezeEffect, sneezeAnimation); } } // effects / expressions if (pony.currentExpression !== pony.expr) { updatePonyExpression(pony, pony.expr, safe); } if ((pony.state & EntityState.Magic) !== 0) { playAnimation(pony.magicEffect, magicAnimation); } else { playAnimation(pony.magicEffect, undefined); } updateAnimation(pony.zzzEffect, delta); updateAnimation(pony.cryEffect, delta); updateAnimation(pony.sneezeEffect, delta); updateAnimation(pony.holdPoofEffect, delta); updateAnimation(pony.heartsEffect, delta); updateAnimation(pony.magicEffect, delta); // holding const holdingUpdated = state.holding !== undefined && state.holding.update !== undefined && state.holding.update(delta, gameTime); // blink pony.blinkTime += delta; if ((pony.blinkTime - pony.nextBlink) > 1) { pony.nextBlink = pony.blinkTime + Math.random() * 2 + 3; } // update animator updateAnimator(pony.animator, delta); // update state const blinkFrame = Math.floor((pony.blinkTime - pony.nextBlink) * blinkFps); state.blinkFrame = blinkFrame < BLINK_FRAMES.length ? BLINK_FRAMES[blinkFrame] : 1; state.headTurned = (pony.state & EntityState.HeadTurned) !== 0; state.animation = getAnimation(pony.animator) || stand; state.animationFrame = getAnimationFrame(pony.animator); // randomize animator time at startup if (!pony.initialized) { pony.initialized = true; updateAnimator(pony.animator, Math.random() * 2); } // discard batch if outdated if (pony.batch !== undefined) { const options = pony.drawingOptions; const right = isFacingRight(pony); if ( holdingUpdated || toScreenX(pony.x) !== pony.lastX || toScreenYWithZ(pony.y, pony.z) !== pony.lastY || pony.lastRight !== right || pony.zzzEffect.dirty || pony.cryEffect.dirty || pony.sneezeEffect.dirty || pony.holdPoofEffect.dirty || pony.heartsEffect.dirty || pony.magicEffect.dirty || options.flipped !== right || options.selected !== pony.selected || options.extra !== pony.extra || options.toy !== pony.toy || !isStateEqual(pony.lastState, state) ) { pony.discardBatch = true; } } // update bounds const flying = isPonyFlying(pony); const flyingUpOrDown = isFlyingUpOrDown(pony.animator.state); const flyingOrFlyingUpOrDown = flying || flyingUpOrDown; pony.bounds = flyingOrFlyingUpOrDown ? boundsFly : bounds; pony.interactBounds = flying ? interactBoundsFly : interactBounds; pony.lightBounds = flyingOrFlyingUpOrDown ? lightBoundsFly : lightBounds; pony.lightSpriteBounds = flyingOrFlyingUpOrDown ? lightBoundsFly : lightBounds; } export function updatePonyHold(pony: Pony, game: PonyTownGame) { const ponyState = pony.ponyState; const hadLight = hasDrawLight(pony); const hadLightSprite = hasLightSprite(pony); if (pony.hold !== 0) { if (ponyState.holding === undefined) { ponyState.holding = createAnEntity(pony.hold, 0, 0, 0, {}, pony.paletteManager, game); } else if (ponyState.holding.type !== pony.hold) { releaseEntity(ponyState.holding); ponyState.holding = createAnEntity(pony.hold, 0, 0, 0, {}, pony.paletteManager, game); } } else if (ponyState.holding !== undefined) { releaseEntity(ponyState.holding); ponyState.holding = undefined; } const hasLight = hasDrawLight(pony); const hasLightSprite1 = hasLightSprite(pony); addOrRemoveFromEntityList(game.map.entitiesLight, pony, hadLight, hasLight); addOrRemoveFromEntityList(game.map.entitiesLightSprite, pony, hadLightSprite, hasLightSprite1); } function filterExpression(expression: Expression) { const extra = expression.extra; const blush = hasFlag(extra, ExpressionExtra.Blush); if ( blush || hasFlag(extra, ExpressionExtra.Hearts) || hasFlag(extra, ExpressionExtra.Cry) || (getEyeOpenness(expression.left) === 0) || (getEyeOpenness(expression.right) === 0) ) { if (expression.muzzle === Muzzle.SmilePant || expression.muzzle === Muzzle.NeutralPant) { expression.muzzle = Muzzle.Neutral; } } if ( blush || expression.muzzle === Muzzle.SmilePant || expression.muzzle === Muzzle.NeutralPant ) { if (expression.leftIris === Iris.Up || expression.rightIris === Iris.Up) { expression.leftIris = Iris.Forward; expression.rightIris = Iris.Forward; } if (expression.muzzle === Muzzle.SmilePant) { expression.muzzle = Muzzle.SmileOpen; } else if (expression.muzzle === Muzzle.NeutralPant) { expression.muzzle = Muzzle.NeutralOpen2; } } if (blush) { if (expression.muzzle === Muzzle.SmileOpen2) { expression.muzzle = Muzzle.SmileOpen; } else if (expression.muzzle === Muzzle.FrownOpen) { expression.muzzle = Muzzle.ConcernedOpen; } else if (expression.muzzle === Muzzle.NeutralOpen2) { expression.muzzle = Muzzle.Oh; } } } function updatePonyExpression(pony: Pony, expr: number, safe: boolean) { const expression = decodeExpression(expr); pony.currentExpression = pony.expr; pony.ponyState.expression = expression; if (expression && safe) { filterExpression(expression); } const extra = (expression && expression.extra) || 0; if (hasFlag(extra, ExpressionExtra.Cry)) { playAnimation(pony.cryEffect, cryAnimation); } else if (hasFlag(extra, ExpressionExtra.Tears)) { playAnimation(pony.cryEffect, tearsAnimation); } else { playAnimation(pony.cryEffect, undefined); } if (hasFlag(extra, ExpressionExtra.Zzz)) { playOneOfAnimations(pony.zzzEffect, zzzAnimations); } else { playAnimation(pony.zzzEffect, undefined); } if (hasFlag(extra, ExpressionExtra.Hearts)) { playAnimation(pony.heartsEffect, heartsAnimation); } else { playAnimation(pony.heartsEffect, undefined); } } function transformBatch(batch: SpriteBatch | PaletteSpriteBatch, entity: Entity) { batch.translate(toScreenX(entity.x), toScreenYWithZ(entity.y, entity.z)); batch.scale(isFacingRight(entity) ? -1 : 1, 1); } function makeLightBounds({ x, y, w, h }: Rect) { return rect(x - lightExtentX, y - lightExtentY, w + lightExtentX * 2, h + lightExtentY * 2); } function drawFaceExtra(batch: PaletteSpriteBatch, pony: Pony) { if (isAnimationPlaying(pony.cryEffect)) { const flip = isFacingRight(pony) ? !pony.ponyState.headTurned : pony.ponyState.headTurned; const maxY = isPonyLying(pony) ? 62 : (isPonySitting(pony) ? 65 : 0); drawAnimation(batch, pony.cryEffect, 0, 0, WHITE, flip, maxY); } }