//import { battleAnimRawData } from "./battle-anim-raw-data"; import BattleScene from "../battle-scene"; import { AttackMove, ChargeAttr, DelayedAttackAttr, MoveFlags, SelfStatusMove, allMoves } from "./move"; import Pokemon from "../field/pokemon"; import * as Utils from "../utils"; import { BattlerIndex } from "../battle"; import stringify, { Element } from "json-stable-stringify"; import { Moves } from "./enums/moves"; import { getTypeRgb } from "./type"; //import fs from 'vite-plugin-fs/browser'; export enum AnimFrameTarget { USER, TARGET, GRAPHIC } enum AnimFocus { TARGET = 1, USER, USER_TARGET, SCREEN } enum AnimBlendType { NORMAL, ADD, SUBTRACT } export enum ChargeAnim { FLY_CHARGING = 1000, BOUNCE_CHARGING, DIG_CHARGING, FUTURE_SIGHT_CHARGING, DIVE_CHARGING, SOLAR_BEAM_CHARGING, SHADOW_FORCE_CHARGING, SKULL_BASH_CHARGING, FREEZE_SHOCK_CHARGING, SKY_DROP_CHARGING, SKY_ATTACK_CHARGING, ICE_BURN_CHARGING, DOOM_DESIRE_CHARGING, RAZOR_WIND_CHARGING, PHANTOM_FORCE_CHARGING, GEOMANCY_CHARGING, SHADOW_BLADE_CHARGING, SOLAR_BLADE_CHARGING, BEAK_BLAST_CHARGING, METEOR_BEAM_CHARGING, ELECTRO_SHOT_CHARGING } export enum CommonAnim { USE_ITEM = 2000, HEALTH_UP, POISON = 2010, TOXIC, PARALYSIS, SLEEP, FROZEN, BURN, CONFUSION, ATTRACT, BIND, WRAP, CURSE_NO_GHOST, LEECH_SEED, FIRE_SPIN, PROTECT, COVET, WHIRLPOOL, BIDE, SAND_TOMB, QUICK_GUARD, WIDE_GUARD, CURSE, MAGMA_STORM, CLAMP, THUNDER_CAGE, ORDER_UP_CURLY, ORDER_UP_DROOPY, ORDER_UP_STRETCHY, RAGING_BULL_FIRE, RAGING_BULL_WATER, SALT_CURE, SUNNY = 2100, RAIN, SANDSTORM, HAIL, WIND, HEAVY_RAIN, HARSH_SUN, STRONG_WINDS, MISTY_TERRAIN = 2110, ELECTRIC_TERRAIN, GRASSY_TERRAIN, PSYCHIC_TERRAIN, LOCK_ON = 2120 } export class AnimConfig { public id: integer; public graphic: string; public frames: AnimFrame[][]; public frameTimedEvents: Map; public position: integer; public hue: integer; constructor(source?: any) { this.frameTimedEvents = new Map; if (source) { this.id = source.id; this.graphic = source.graphic; const frames: any[][] = source.frames; frames.map(animFrames => { for (let f = 0; f < animFrames.length; f++) animFrames[f] = new ImportedAnimFrame(animFrames[f]); }); this.frames = frames; const frameTimedEvents = source.frameTimedEvents; for (let fte of Object.keys(frameTimedEvents)) { const timedEvents: AnimTimedEvent[] = []; for (let te of frameTimedEvents[fte]) { let timedEvent: AnimTimedEvent; switch (te.eventType) { case 'AnimTimedSoundEvent': timedEvent = new AnimTimedSoundEvent(te.frameIndex, te.resourceName, te); break; case 'AnimTimedAddBgEvent': timedEvent = new AnimTimedAddBgEvent(te.frameIndex, te.resourceName, te); break; case 'AnimTimedUpdateBgEvent': timedEvent = new AnimTimedUpdateBgEvent(te.frameIndex, te.resourceName, te); break; } timedEvents.push(timedEvent); } this.frameTimedEvents.set(parseInt(fte), timedEvents); } this.position = source.position; this.hue = source.hue; } else this.frames = []; } getSoundResourceNames(): string[] { const sounds = new Set(); for (let ftes of this.frameTimedEvents.values()) { for (let fte of ftes) { if (fte instanceof AnimTimedSoundEvent && fte.resourceName) sounds.add(fte.resourceName); } } return Array.from(sounds.values()); } getBackgroundResourceNames(): string[] { const backgrounds = new Set(); for (let ftes of this.frameTimedEvents.values()) { for (let fte of ftes) { if (fte instanceof AnimTimedAddBgEvent && fte.resourceName) backgrounds.add(fte.resourceName); } } return Array.from(backgrounds.values()); } } class AnimFrame { public x: number; public y: number; public zoomX: number; public zoomY: number; public angle: number; public mirror: boolean; public visible: boolean; public blendType: AnimBlendType; public target: AnimFrameTarget; public graphicFrame: integer; public opacity: integer; public color: integer[]; public tone: integer[]; public flash: integer[]; public locked: boolean; public priority: integer; public focus: AnimFocus; constructor(x: number, y: number, zoomX: number, zoomY: number, angle: number, mirror: boolean, visible: boolean, blendType: AnimBlendType, pattern: integer, opacity: integer, colorR: integer, colorG: integer, colorB: integer, colorA: integer, toneR: integer, toneG: integer, toneB: integer, toneA: integer, flashR: integer, flashG: integer, flashB: integer, flashA: integer, locked: boolean, priority: integer, focus: AnimFocus, init?: boolean) { this.x = !init ? ((x || 0) - 128) * 0.5 : x; this.y = !init ? ((y || 0) - 224) * 0.5 : y; if (zoomX) this.zoomX = zoomX; else if (init) this.zoomX = 0; if (zoomY) this.zoomY = zoomY; else if (init) this.zoomY = 0; if (angle) this.angle = angle; else if (init) this.angle = 0; if (mirror) this.mirror = mirror; else if (init) this.mirror = false; if (visible) this.visible = visible; else if (init) this.visible = false; if (blendType) this.blendType = blendType; else if (init) this.blendType = AnimBlendType.NORMAL; if (!init) { let target = AnimFrameTarget.GRAPHIC; switch (pattern) { case -2: target = AnimFrameTarget.TARGET; break; case -1: target = AnimFrameTarget.USER; break; } this.target = target; this.graphicFrame = pattern >= 0 ? pattern : 0; } if (opacity) this.opacity = opacity; else if (init) this.opacity = 0; if (colorR || colorG || colorB || colorA) this.color = [ colorR || 0, colorG || 0, colorB || 0, colorA || 0 ]; else if (init) this.color = [ 0, 0, 0, 0 ]; if (toneR || toneG || toneB || toneA) this.tone = [ toneR || 0, toneG || 0, toneB || 0, toneA || 0 ]; else if (init) this.tone = [ 0, 0, 0, 0 ]; if (flashR || flashG || flashB || flashA) this.flash = [ flashR || 0, flashG || 0, flashB || 0, flashA || 0 ]; else if (init) this.flash = [ 0, 0, 0, 0 ]; if (locked) this.locked = locked; else if (init) this.locked = false; if (priority) this.priority = priority; else if (init) this.priority = 0; this.focus = focus || AnimFocus.TARGET; } } class ImportedAnimFrame extends AnimFrame { constructor(source: any) { const color: integer[] = source.color || [ 0, 0, 0, 0 ]; const tone: integer[] = source.tone || [ 0, 0, 0, 0 ]; const flash: integer[] = source.flash || [ 0, 0, 0, 0 ]; super(source.x, source.y, source.zoomX, source.zoomY, source.angle, source.mirror, source.visible, source.blendType, source.graphicFrame, source.opacity, color[0], color[1], color[2], color[3], tone[0], tone[1], tone[2], tone[3], flash[0], flash[1], flash[2], flash[3], source.locked, source.priority, source.focus, true); this.target = source.target; this.graphicFrame = source.graphicFrame; } } abstract class AnimTimedEvent { public frameIndex: integer; public resourceName: string; constructor(frameIndex: integer, resourceName: string) { this.frameIndex = frameIndex; this.resourceName = resourceName; } abstract execute(scene: BattleScene, battleAnim: BattleAnim): integer; abstract getEventType(): string; } class AnimTimedSoundEvent extends AnimTimedEvent { public volume: number = 100; public pitch: number = 100; constructor(frameIndex: integer, resourceName: string, source?: any) { super(frameIndex, resourceName); if (source) { this.volume = source.volume; this.pitch = source.pitch; } } execute(scene: BattleScene, battleAnim: BattleAnim): integer { const soundConfig = { rate: (this.pitch * 0.01), volume: (this.volume * 0.01) }; if (this.resourceName) { try { scene.playSound(this.resourceName, soundConfig); } catch (err) { console.error(err); } return Math.ceil((scene.sound.get(this.resourceName).totalDuration * 1000) / 33.33); } else return Math.ceil((battleAnim.user.cry(soundConfig).totalDuration * 1000) / 33.33); } getEventType(): string { return 'AnimTimedSoundEvent'; } } abstract class AnimTimedBgEvent extends AnimTimedEvent { public bgX: number = 0; public bgY: number = 0; public opacity: integer = 0; /*public colorRed: integer = 0; public colorGreen: integer = 0; public colorBlue: integer = 0; public colorAlpha: integer = 0;*/ public duration: integer = 0; /*public flashScope: integer = 0; public flashRed: integer = 0; public flashGreen: integer = 0; public flashBlue: integer = 0; public flashAlpha: integer = 0; public flashDuration: integer = 0;*/ constructor(frameIndex: integer, resourceName: string, source: any) { super(frameIndex, resourceName); if (source) { this.bgX = source.bgX; this.bgY = source.bgY; this.opacity = source.opacity; /*this.colorRed = source.colorRed; this.colorGreen = source.colorGreen; this.colorBlue = source.colorBlue; this.colorAlpha = source.colorAlpha;*/ this.duration = source.duration; /*this.flashScope = source.flashScope; this.flashRed = source.flashRed; this.flashGreen = source.flashGreen; this.flashBlue = source.flashBlue; this.flashAlpha = source.flashAlpha; this.flashDuration = source.flashDuration;*/ } } } class AnimTimedUpdateBgEvent extends AnimTimedBgEvent { constructor(frameIndex: integer, resourceName: string, source?: any) { super(frameIndex, resourceName, source); } execute(scene: BattleScene, moveAnim: MoveAnim): integer { const tweenProps = {}; if (this.bgX !== undefined) tweenProps['x'] = (this.bgX * 0.5) - 320; if (this.bgY !== undefined) tweenProps['y'] = (this.bgY * 0.5) - 284; if (this.opacity !== undefined) tweenProps['alpha'] = (this.opacity || 0) / 255; if (Object.keys(tweenProps).length) { scene.tweens.add(Object.assign({ targets: moveAnim.bgSprite, duration: Utils.getFrameMs(this.duration * 3) }, tweenProps)); } return this.duration * 2; } getEventType(): string { return 'AnimTimedUpdateBgEvent'; } } class AnimTimedAddBgEvent extends AnimTimedBgEvent { constructor(frameIndex: integer, resourceName: string, source?: any) { super(frameIndex, resourceName, source); } execute(scene: BattleScene, moveAnim: MoveAnim): integer { if (moveAnim.bgSprite) moveAnim.bgSprite.destroy(); moveAnim.bgSprite = this.resourceName ? scene.add.tileSprite(this.bgX - 320, this.bgY - 284, 896, 576, this.resourceName) : scene.add.rectangle(this.bgX - 320, this.bgY - 284, 896, 576, 0); moveAnim.bgSprite.setOrigin(0, 0); moveAnim.bgSprite.setScale(1.25); moveAnim.bgSprite.setAlpha(this.opacity / 255); scene.field.add(moveAnim.bgSprite); const fieldPokemon = scene.getEnemyPokemon() || scene.getPlayerPokemon(); if (fieldPokemon?.isOnField()) scene.field.moveBelow(moveAnim.bgSprite as Phaser.GameObjects.GameObject, fieldPokemon); scene.tweens.add({ targets: moveAnim.bgSprite, duration: Utils.getFrameMs(this.duration * 3) }); return this.duration * 2; } getEventType(): string { return 'AnimTimedAddBgEvent'; } } export const moveAnims = new Map(); export const chargeAnims = new Map(); export const commonAnims = new Map(); export function initCommonAnims(): Promise { return new Promise(resolve => { const commonAnimNames = Utils.getEnumKeys(CommonAnim); const commonAnimIds = Utils.getEnumValues(CommonAnim); const commonAnimFetches = []; for (let ca = 0; ca < commonAnimIds.length; ca++) { const commonAnimId = commonAnimIds[ca]; commonAnimFetches.push(fetch(`./battle-anims/common-${commonAnimNames[ca].toLowerCase().replace(/\_/g, '-')}.json`) .then(response => response.json()) .then(cas => commonAnims.set(commonAnimId, new AnimConfig(cas)))); } Promise.allSettled(commonAnimFetches).then(() => resolve()); }); } export function initMoveAnim(move: Moves): Promise { return new Promise(resolve => { if (moveAnims.has(move)) { if (moveAnims.get(move) !== null) resolve(); else { let loadedCheckTimer = setInterval(() => { if (moveAnims.get(move) !== null) { const chargeAttr = allMoves[move].getAttrs(ChargeAttr).find(() => true) as ChargeAttr || allMoves[move].getAttrs(DelayedAttackAttr).find(() => true) as DelayedAttackAttr; if (chargeAttr && chargeAnims.get(chargeAttr.chargeAnim) === null) return; clearInterval(loadedCheckTimer); resolve(); } }, 50); } } else { moveAnims.set(move, null); const defaultMoveAnim = allMoves[move] instanceof AttackMove ? Moves.TACKLE : allMoves[move] instanceof SelfStatusMove ? Moves.FOCUS_ENERGY : Moves.TAIL_WHIP; const fetchAnimAndResolve = (move: Moves) => { fetch(`./battle-anims/${Moves[move].toLowerCase().replace(/\_/g, '-')}.json`) .then(response => { if (!response.ok) { console.error(response.statusText); if (move !== defaultMoveAnim) fetchAnimAndResolve(defaultMoveAnim); else resolve(); return; } return response.json(); }) .then(ba => { if (Array.isArray(ba)) { populateMoveAnim(move, ba[0]); populateMoveAnim(move, ba[1]); } else populateMoveAnim(move, ba); const chargeAttr = allMoves[move].getAttrs(ChargeAttr).find(() => true) as ChargeAttr || allMoves[move].getAttrs(DelayedAttackAttr).find(() => true) as DelayedAttackAttr; if (chargeAttr) initMoveChargeAnim(chargeAttr.chargeAnim).then(() => resolve()); else resolve(); }); }; fetchAnimAndResolve(move); } }); } export function initMoveChargeAnim(chargeAnim: ChargeAnim): Promise { return new Promise(resolve => { if (chargeAnims.has(chargeAnim)) { if (chargeAnims.get(chargeAnim) !== null) resolve(); else { let loadedCheckTimer = setInterval(() => { if (chargeAnims.get(chargeAnim) !== null) { clearInterval(loadedCheckTimer); resolve(); } }, 50); } } else { chargeAnims.set(chargeAnim, null); fetch(`./battle-anims/${ChargeAnim[chargeAnim].toLowerCase().replace(/\_/g, '-')}.json`) .then(response => response.json()) .then(ca => { if (Array.isArray(ca)) { populateMoveChargeAnim(chargeAnim, ca[0]); populateMoveChargeAnim(chargeAnim, ca[1]); } else populateMoveChargeAnim(chargeAnim, ca); resolve(); }); } }); } function populateMoveAnim(move: Moves, animSource: any): void { const moveAnim = new AnimConfig(animSource); if (moveAnims.get(move) === null) { moveAnims.set(move, moveAnim); return; } moveAnims.set(move, [ moveAnims.get(move) as AnimConfig, moveAnim ]); } function populateMoveChargeAnim(chargeAnim: ChargeAnim, animSource: any) { const moveChargeAnim = new AnimConfig(animSource); if (chargeAnims.get(chargeAnim) === null) { chargeAnims.set(chargeAnim, moveChargeAnim); return; } chargeAnims.set(chargeAnim, [ chargeAnims.get(chargeAnim) as AnimConfig, moveChargeAnim ]); } export function loadCommonAnimAssets(scene: BattleScene, startLoad?: boolean): Promise { return new Promise(resolve => { loadAnimAssets(scene, Array.from(commonAnims.values()), startLoad).then(() => resolve()); }); } export function loadMoveAnimAssets(scene: BattleScene, moveIds: Moves[], startLoad?: boolean): Promise { return new Promise(resolve => { const moveAnimations = moveIds.map(m => moveAnims.get(m) as AnimConfig).flat(); for (let moveId of moveIds) { const chargeAttr = allMoves[moveId].getAttrs(ChargeAttr).find(() => true) as ChargeAttr || allMoves[moveId].getAttrs(DelayedAttackAttr).find(() => true) as DelayedAttackAttr; if (chargeAttr) { const moveChargeAnims = chargeAnims.get(chargeAttr.chargeAnim); moveAnimations.push(moveChargeAnims instanceof AnimConfig ? moveChargeAnims : moveChargeAnims[0]); if (Array.isArray(moveChargeAnims)) moveAnimations.push(moveChargeAnims[1]); } } loadAnimAssets(scene, moveAnimations, startLoad).then(() => resolve()); }); } function loadAnimAssets(scene: BattleScene, anims: AnimConfig[], startLoad?: boolean): Promise { return new Promise(resolve => { const backgrounds = new Set(); const sounds = new Set(); for (let a of anims) { if (!a.frames?.length) continue; const animSounds = a.getSoundResourceNames(); for (let ms of animSounds) sounds.add(ms); const animBackgrounds = a.getBackgroundResourceNames(); for (let abg of animBackgrounds) backgrounds.add(abg); if (a.graphic) scene.loadSpritesheet(a.graphic, 'battle_anims', 96); } for (let bg of backgrounds) scene.loadImage(bg, 'battle_anims'); for (let s of sounds) scene.loadSe(s, 'battle_anims', s); if (startLoad) { scene.load.once(Phaser.Loader.Events.COMPLETE, () => resolve()); if (!scene.load.isLoading()) scene.load.start(); } else resolve(); }); } interface GraphicFrameData { x: number, y: number, scaleX: number, scaleY: number, angle: number } const userFocusX = 106; const userFocusY = 148 - 32; const targetFocusX = 234; const targetFocusY = 84 - 32; function transformPoint(x1: number, y1: number, x2: number, y2: number, x3: number, y3: number, x4: number, y4: number, px: number, py: number): [ x: number, y: number ] { const yIntersect = yAxisIntersect(x1, y1, x2, y2, px, py); return repositionY(x3, y3, x4, y4, yIntersect[0], yIntersect[1]); } function yAxisIntersect(x1: number, y1: number, x2: number, y2: number, px: number, py: number): [ x: number, y: number ] { const dx = x2 - x1; const dy = y2 - y1; const x = dx === 0 ? 0 : (px - x1) / dx; const y = dy === 0 ? 0 : (py - y1) / dy; return [ x, y ]; } function repositionY(x1: number, y1: number, x2: number, y2: number, tx: number, ty: number): [ x: number, y: number ] { const dx = x2 - x1; const dy = y2 - y1; const x = x1 + (tx * dx); const y = y1 + (ty * dy); return [ x, y ]; } function isReversed(src1: number, src2: number, dst1: number, dst2: number) { if (src1 === src2) return false; if (src1 < src2) return dst1 > dst2; return dst1 < dst2; } interface SpriteCache { [key: integer]: Phaser.GameObjects.Sprite[] } export abstract class BattleAnim { public user: Pokemon; public target: Pokemon; public sprites: Phaser.GameObjects.Sprite[]; public bgSprite: Phaser.GameObjects.TileSprite | Phaser.GameObjects.Rectangle; private srcLine: number[]; private dstLine: number[]; constructor(user: Pokemon, target: Pokemon) { this.user = user; this.target = target; this.sprites = []; } abstract getAnim(): AnimConfig; abstract isOppAnim(): boolean; protected isHideUser(): boolean { return false; } protected isHideTarget(): boolean { return false; } private getGraphicFrameData(scene: BattleScene, frames: AnimFrame[]): Map> { const ret: Map> = new Map([ [AnimFrameTarget.GRAPHIC, new Map() ], [AnimFrameTarget.USER, new Map() ], [AnimFrameTarget.TARGET, new Map() ] ]); const isOppAnim = this.isOppAnim(); const user = !isOppAnim ? this.user : this.target; const target = !isOppAnim ? this.target : this.user; const userInitialX = user.x; const userInitialY = user.y; const userHalfHeight = user.getSprite().displayHeight / 2; const targetInitialX = target.x; const targetInitialY = target.y; const targetHalfHeight = target.getSprite().displayHeight / 2; let g = 0; let u = 0; let t = 0; for (let frame of frames) { let x = frame.x + 106; let y = frame.y + 116; let scaleX = (frame.zoomX / 100) * (!frame.mirror ? 1 : -1); let scaleY = (frame.zoomY / 100); switch (frame.focus) { case AnimFocus.TARGET: x += targetInitialX - targetFocusX; y += (targetInitialY - targetHalfHeight) - targetFocusY; break; case AnimFocus.USER: x += userInitialX - userFocusX; y += (userInitialY - userHalfHeight) - userFocusY; break; case AnimFocus.USER_TARGET: const point = transformPoint(this.srcLine[0], this.srcLine[1], this.srcLine[2], this.srcLine[3], this.dstLine[0], this.dstLine[1] - userHalfHeight, this.dstLine[2], this.dstLine[3] - targetHalfHeight, x, y); x = point[0]; y = point[1]; if (frame.target === AnimFrameTarget.GRAPHIC && isReversed(this.srcLine[0], this.srcLine[2], this.dstLine[0], this.dstLine[2])) scaleX = scaleX * -1; break; } const angle = -frame.angle; const key = frame.target === AnimFrameTarget.GRAPHIC ? g++ : frame.target === AnimFrameTarget.USER ? u++ : t++; ret.get(frame.target).set(key, { x: x, y: y, scaleX: scaleX, scaleY: scaleY, angle: angle }); } return ret; } play(scene: BattleScene, callback?: Function) { const isOppAnim = this.isOppAnim(); const user = !isOppAnim ? this.user : this.target; const target = !isOppAnim ? this.target : this.user; if (!target.isOnField()) { if (callback) callback(); return; } const userSprite = user.getSprite(); const targetSprite = target.getSprite(); const anim = this.getAnim(); const userInitialX = user.x; const userInitialY = user.y; const targetInitialX = target.x; const targetInitialY = target.y; this.srcLine = [ userFocusX, userFocusY, targetFocusX, targetFocusY ]; this.dstLine = [ userInitialX, userInitialY, targetInitialX, targetInitialY ]; let r = anim.frames.length; let f = 0; const spriteCache: SpriteCache = { [AnimFrameTarget.GRAPHIC]: [], [AnimFrameTarget.USER]: [], [AnimFrameTarget.TARGET]: [] }; const spritePriorities: integer[] = []; scene.tweens.addCounter({ duration: Utils.getFrameMs(3), repeat: anim.frames.length, onRepeat: () => { if (!f) { userSprite.setVisible(false); targetSprite.setVisible(false); } const spriteFrames = anim.frames[f]; const frameData = this.getGraphicFrameData(scene, anim.frames[f]); let u = 0; let t = 0; let g = 0; for (let frame of spriteFrames) { if (frame.target !== AnimFrameTarget.GRAPHIC) { const isUser = frame.target === AnimFrameTarget.USER; if (isUser && target === user) continue; const sprites = spriteCache[isUser ? AnimFrameTarget.USER : AnimFrameTarget.TARGET]; const spriteSource = isUser ? userSprite : targetSprite; if ((isUser ? u : t) === sprites.length) { let sprite: Phaser.GameObjects.Sprite; sprite = scene.addPokemonSprite(isUser ? user : target, 0, 0, spriteSource.texture, spriteSource.frame.name, true); [ 'spriteColors', 'fusionSpriteColors' ].map(k => sprite.pipelineData[k] = (isUser ? user : target).getSprite().pipelineData[k]); sprite.pipelineData['ignoreFieldPos'] = true; spriteSource.on('animationupdate', (_anim, frame) => sprite.setFrame(frame.textureFrame)); scene.field.add(sprite); sprites.push(sprite); } const spriteIndex = isUser ? u++ : t++; const pokemonSprite = sprites[spriteIndex]; const graphicFrameData = frameData.get(frame.target).get(spriteIndex); pokemonSprite.setPosition(graphicFrameData.x, graphicFrameData.y - ((spriteSource.height / 2) * (spriteSource.parentContainer.scale - 1))); pokemonSprite.setAngle(graphicFrameData.angle); pokemonSprite.setScale(graphicFrameData.scaleX * spriteSource.parentContainer.scale, graphicFrameData.scaleY * spriteSource.parentContainer.scale); pokemonSprite.setData('locked', frame.locked); pokemonSprite.setAlpha(frame.opacity / 255); pokemonSprite.pipelineData['tone'] = frame.tone; pokemonSprite.setVisible(frame.visible && (isUser ? user.visible : target.visible)); pokemonSprite.setBlendMode(frame.blendType === AnimBlendType.NORMAL ? Phaser.BlendModes.NORMAL : frame.blendType === AnimBlendType.ADD ? Phaser.BlendModes.ADD : Phaser.BlendModes.DIFFERENCE); } else { const sprites = spriteCache[AnimFrameTarget.GRAPHIC]; if (g === sprites.length) { let newSprite: Phaser.GameObjects.Sprite = scene.addFieldSprite(0, 0, anim.graphic, 1); sprites.push(newSprite); scene.field.add(newSprite); spritePriorities.push(1); } const graphicIndex = g++; const moveSprite = sprites[graphicIndex]; if (spritePriorities[graphicIndex] !== frame.priority) { spritePriorities[graphicIndex] = frame.priority; const setSpritePriority = (priority: integer) => { switch (priority) { case 0: scene.field.moveBelow(moveSprite as Phaser.GameObjects.GameObject, scene.getEnemyPokemon() || scene.getPlayerPokemon()); break; case 1: scene.field.moveTo(moveSprite, scene.field.getAll().length - 1); break; case 2: switch (frame.focus) { case AnimFocus.USER: if (this.bgSprite) scene.field.moveAbove(moveSprite as Phaser.GameObjects.GameObject, this.bgSprite); else scene.field.moveBelow(moveSprite as Phaser.GameObjects.GameObject, this.user); break; case AnimFocus.TARGET: scene.field.moveBelow(moveSprite as Phaser.GameObjects.GameObject, this.target); break; default: setSpritePriority(1); break; } break; case 3: switch (frame.focus) { case AnimFocus.USER: scene.field.moveAbove(moveSprite as Phaser.GameObjects.GameObject, this.user); break; case AnimFocus.TARGET: scene.field.moveAbove(moveSprite as Phaser.GameObjects.GameObject, this.target); break; default: setSpritePriority(1); break; } break; default: setSpritePriority(1); } }; setSpritePriority(frame.priority); } moveSprite.setFrame(frame.graphicFrame); //console.log(AnimFocus[frame.focus]); const graphicFrameData = frameData.get(frame.target).get(graphicIndex); moveSprite.setPosition(graphicFrameData.x, graphicFrameData.y); moveSprite.setAngle(graphicFrameData.angle); moveSprite.setScale(graphicFrameData.scaleX, graphicFrameData.scaleY); moveSprite.setAlpha(frame.opacity / 255); moveSprite.setVisible(frame.visible); moveSprite.setBlendMode(frame.blendType === AnimBlendType.NORMAL ? Phaser.BlendModes.NORMAL : frame.blendType === AnimBlendType.ADD ? Phaser.BlendModes.ADD : Phaser.BlendModes.DIFFERENCE); } } if (anim.frameTimedEvents.has(f)) { for (let event of anim.frameTimedEvents.get(f)) r = Math.max((anim.frames.length - f) + event.execute(scene, this), r); } const targets = Utils.getEnumValues(AnimFrameTarget); for (let i of targets) { const count = i === AnimFrameTarget.GRAPHIC ? g : i === AnimFrameTarget.USER ? u : t; if (count < spriteCache[i].length) { const spritesToRemove = spriteCache[i].slice(count, spriteCache[i].length); for (let rs of spritesToRemove) { if (!rs.getData('locked') as boolean) { const spriteCacheIndex = spriteCache[i].indexOf(rs); spriteCache[i].splice(spriteCacheIndex, 1); if (i === AnimFrameTarget.GRAPHIC) spritePriorities.splice(spriteCacheIndex, 1); rs.destroy(); } } } } f++; r--; }, onComplete: () => { const cleanUpAndComplete = () => { userSprite.setPosition(0, 0); userSprite.setScale(1); userSprite.setAlpha(1); userSprite.pipelineData['tone'] = [ 0.0, 0.0, 0.0, 0.0 ]; userSprite.setAngle(0); targetSprite.setPosition(0, 0); targetSprite.setScale(1); targetSprite.setAlpha(1); targetSprite.pipelineData['tone'] = [ 0.0, 0.0, 0.0, 0.0 ]; targetSprite.setAngle(0); if (!this.isHideUser()) userSprite.setVisible(true); if (!this.isHideTarget() && (targetSprite !== userSprite || !this.isHideUser())) targetSprite.setVisible(true); for (let ms of Object.values(spriteCache).flat()) { if (ms) ms.destroy(); } if (this.bgSprite) this.bgSprite.destroy(); if (callback) callback(); }; for (let ms of Object.values(spriteCache).flat()) { if (ms && !ms.getData('locked')) ms.destroy(); } if (r) { scene.tweens.addCounter({ duration: Utils.getFrameMs(r), onComplete: () => cleanUpAndComplete() }); } else cleanUpAndComplete(); } }); } } export class CommonBattleAnim extends BattleAnim { public commonAnim: CommonAnim; constructor(commonAnim: CommonAnim, user: Pokemon, target?: Pokemon) { super(user, target || user); this.commonAnim = commonAnim; } getAnim(): AnimConfig { return commonAnims.get(this.commonAnim); } isOppAnim(): boolean { return false; } } export class MoveAnim extends BattleAnim { public move: Moves; constructor(move: Moves, user: Pokemon, target: BattlerIndex) { super(user, user.scene.getField()[target]); this.move = move; } getAnim(): AnimConfig { return moveAnims.get(this.move) instanceof AnimConfig ? moveAnims.get(this.move) as AnimConfig : moveAnims.get(this.move)[this.user.isPlayer() ? 0 : 1] as AnimConfig; } isOppAnim(): boolean { return !this.user.isPlayer() && Array.isArray(moveAnims.get(this.move)); } protected isHideUser(): boolean { return allMoves[this.move].hasFlag(MoveFlags.HIDE_USER); } protected isHideTarget(): boolean { return allMoves[this.move].hasFlag(MoveFlags.HIDE_TARGET); } } export class MoveChargeAnim extends MoveAnim { private chargeAnim: ChargeAnim; constructor(chargeAnim: ChargeAnim, move: Moves, user: Pokemon) { super(move, user, 0); this.chargeAnim = chargeAnim; } isOppAnim(): boolean { return !this.user.isPlayer() && Array.isArray(chargeAnims.get(this.chargeAnim)); } getAnim(): AnimConfig { return chargeAnims.get(this.chargeAnim) instanceof AnimConfig ? chargeAnims.get(this.chargeAnim) as AnimConfig : chargeAnims.get(this.chargeAnim)[this.user.isPlayer() ? 0 : 1] as AnimConfig; } } export async function populateAnims() { const commonAnimNames = Utils.getEnumKeys(CommonAnim).map(k => k.toLowerCase()); const commonAnimMatchNames = commonAnimNames.map(k => k.replace(/\_/g, '')); const commonAnimIds = Utils.getEnumValues(CommonAnim) as CommonAnim[]; const chargeAnimNames = Utils.getEnumKeys(ChargeAnim).map(k => k.toLowerCase()); const chargeAnimMatchNames = chargeAnimNames.map(k => k.replace(/\_/g, ' ')); const chargeAnimIds = Utils.getEnumValues(ChargeAnim) as ChargeAnim[]; const commonNamePattern = /name: (?:Common:)?(Opp )?(.*)/; const moveNameToId = {}; for (let move of Utils.getEnumValues(Moves).slice(1)) { const moveName = Moves[move].toUpperCase().replace(/\_/g, ''); moveNameToId[moveName] = move; } const seNames = [];//(await fs.readdir('./public/audio/se/battle_anims/')).map(se => se.toString()); const animsData = [];//battleAnimRawData.split('!ruby/array:PBAnimation').slice(1); for (let a = 0; a < animsData.length; a++) { const fields = animsData[a].split('@').slice(1); const nameField = fields.find(f => f.startsWith('name: ')); let isOppMove: boolean; let commonAnimId: CommonAnim; let chargeAnimId: ChargeAnim; if (!nameField.startsWith('name: Move:') && !(isOppMove = nameField.startsWith('name: OppMove:'))) { const nameMatch = commonNamePattern.exec(nameField); const name = nameMatch[2].toLowerCase(); if (commonAnimMatchNames.indexOf(name) > -1) commonAnimId = commonAnimIds[commonAnimMatchNames.indexOf(name)]; else if (chargeAnimMatchNames.indexOf(name) > -1) { isOppMove = nameField.startsWith('name: Opp '); chargeAnimId = chargeAnimIds[chargeAnimMatchNames.indexOf(name)]; } } const nameIndex = nameField.indexOf(':', 5) + 1; const animName = nameField.slice(nameIndex, nameField.indexOf('\n', nameIndex)); if (!moveNameToId.hasOwnProperty(animName) && !commonAnimId && !chargeAnimId) continue; let anim = commonAnimId || chargeAnimId ? new AnimConfig() : new AnimConfig(); if (anim instanceof AnimConfig) (anim as AnimConfig).id = moveNameToId[animName]; if (commonAnimId) commonAnims.set(commonAnimId, anim); else if (chargeAnimId) chargeAnims.set(chargeAnimId, !isOppMove ? anim : [ chargeAnims.get(chargeAnimId) as AnimConfig, anim ]); else moveAnims.set(moveNameToId[animName], !isOppMove ? anim as AnimConfig : [ moveAnims.get(moveNameToId[animName]) as AnimConfig, anim as AnimConfig ]); for (let f = 0; f < fields.length; f++) { const field = fields[f]; const fieldName = field.slice(0, field.indexOf(':')); const fieldData = field.slice(fieldName.length + 1, field.lastIndexOf('\n')).trim(); switch (fieldName) { case 'array': const framesData = fieldData.split(' - - - ').slice(1); for (let fd = 0; fd < framesData.length; fd++) { anim.frames.push([]); const frameData = framesData[fd]; const focusFramesData = frameData.split(' - - '); for (let tf = 0; tf < focusFramesData.length; tf++) { const values = focusFramesData[tf].replace(/ \- /g, '').split('\n'); const targetFrame = new AnimFrame(parseFloat(values[0]), parseFloat(values[1]), parseFloat(values[2]), parseFloat(values[11]), parseFloat(values[3]), parseInt(values[4]) === 1, parseInt(values[6]) === 1, parseInt(values[5]), parseInt(values[7]), parseInt(values[8]), parseInt(values[12]), parseInt(values[13]), parseInt(values[14]), parseInt(values[15]), parseInt(values[16]), parseInt(values[17]), parseInt(values[18]), parseInt(values[19]), parseInt(values[21]), parseInt(values[22]), parseInt(values[23]), parseInt(values[24]), parseInt(values[20]) === 1, parseInt(values[25]), parseInt(values[26]) as AnimFocus); anim.frames[fd].push(targetFrame); } } break; case 'graphic': const graphic = fieldData !== "''" ? fieldData : ''; anim.graphic = graphic.indexOf('.') > -1 ? graphic.slice(0, fieldData.indexOf('.')) : graphic; break; case 'timing': const timingEntries = fieldData.split('- !ruby/object:PBAnimTiming ').slice(1); for (let t = 0; t < timingEntries.length; t++) { const timingData = timingEntries[t].replace(/\n/g, ' ').replace(/[ ]{2,}/g, ' ').replace(/[a-z]+: ! '', /ig, '').replace(/name: (.*?),/, 'name: "$1",') .replace(/flashColor: !ruby\/object:Color { alpha: ([\d\.]+), blue: ([\d\.]+), green: ([\d\.]+), red: ([\d\.]+)}/, 'flashRed: $4, flashGreen: $3, flashBlue: $2, flashAlpha: $1'); const frameIndex = parseInt(/frame: (\d+)/.exec(timingData)[1]); let resourceName = /name: "(.*?)"/.exec(timingData)[1].replace("''", ''); const timingType = parseInt(/timingType: (\d)/.exec(timingData)[1]); let timedEvent: AnimTimedEvent; switch (timingType) { case 0: if (resourceName && resourceName.indexOf('.') === -1) { let ext: string; [ 'wav', 'mp3', 'm4a' ].every(e => { if (seNames.indexOf(`${resourceName}.${e}`) > -1) { ext = e; return false; } return true; }); if (!ext) ext = '.wav'; resourceName += `.${ext}`; } timedEvent = new AnimTimedSoundEvent(frameIndex, resourceName); break; case 1: timedEvent = new AnimTimedAddBgEvent(frameIndex, resourceName.slice(0, resourceName.indexOf('.'))); break; case 2: timedEvent = new AnimTimedUpdateBgEvent(frameIndex, resourceName.slice(0, resourceName.indexOf('.'))); break; } if (!timedEvent) continue; const propPattern = /([a-z]+): (.*?)(?:,|\})/ig; let propMatch: RegExpExecArray; while ((propMatch = propPattern.exec(timingData))) { const prop = propMatch[1]; let value: any = propMatch[2]; switch (prop) { case 'bgX': case 'bgY': value = parseFloat(value); break; case 'volume': case 'pitch': case 'opacity': case 'colorRed': case 'colorGreen': case 'colorBlue': case 'colorAlpha': case 'duration': case 'flashScope': case 'flashRed': case 'flashGreen': case 'flashBlue': case 'flashAlpha': case 'flashDuration': value = parseInt(value); break; } if (timedEvent.hasOwnProperty(prop)) timedEvent[prop] = value; } if (!anim.frameTimedEvents.has(frameIndex)) anim.frameTimedEvents.set(frameIndex, []); anim.frameTimedEvents.get(frameIndex).push(timedEvent); } break; case 'position': anim.position = parseInt(fieldData); break; case 'hue': anim.hue = parseInt(fieldData); break; } } } const animReplacer = (k, v) => { if (k === 'id' && !v) return undefined; if (v instanceof Map) return Object.fromEntries(v); if (v instanceof AnimTimedEvent) v['eventType'] = v.getEventType(); return v; }; const animConfigProps = [ 'id', 'graphic', 'frames', 'frameTimedEvents', 'position', 'hue' ]; const animFrameProps = [ 'x', 'y', 'zoomX', 'zoomY', 'angle', 'mirror', 'visible', 'blendType', 'target', 'graphicFrame', 'opacity', 'color', 'tone', 'flash', 'locked', 'priority', 'focus' ]; const propSets = [ animConfigProps, animFrameProps ]; const animComparator = (a: Element, b: Element) => { let props: string[]; let p = 0; for (let p = 0; p < propSets.length; p++) { props = propSets[p]; let ai = props.indexOf(a.key); if (ai === -1) continue; let bi = props.indexOf(b.key); return ai < bi ? -1 : ai > bi ? 1 : 0; } return 0; }; /*for (let ma of moveAnims.keys()) { const data = moveAnims.get(ma); (async () => { await fs.writeFile(`../public/battle-anims/${Moves[ma].toLowerCase().replace(/\_/g, '-')}.json`, stringify(data, { replacer: animReplacer, cmp: animComparator, space: ' ' })); })(); } for (let ca of chargeAnims.keys()) { const data = chargeAnims.get(ca); (async () => { await fs.writeFile(`../public/battle-anims/${chargeAnimNames[chargeAnimIds.indexOf(ca)].replace(/\_/g, '-')}.json`, stringify(data, { replacer: animReplacer, cmp: animComparator, space: ' ' })); })(); } for (let cma of commonAnims.keys()) { const data = commonAnims.get(cma); (async () => { await fs.writeFile(`../public/battle-anims/common-${commonAnimNames[commonAnimIds.indexOf(cma)].replace(/\_/g, '-')}.json`, stringify(data, { replacer: animReplacer, cmp: animComparator, space: ' ' })); })(); }*/ }