diff --git a/public/audio/bgm/evolution.mp3 b/public/audio/bgm/evolution.mp3 new file mode 100644 index 000000000..5f1b739f1 Binary files /dev/null and b/public/audio/bgm/evolution.mp3 differ diff --git a/public/audio/bgm/evolution_fanfare.mp3 b/public/audio/bgm/evolution_fanfare.mp3 new file mode 100644 index 000000000..165d1d2d8 Binary files /dev/null and b/public/audio/bgm/evolution_fanfare.mp3 differ diff --git a/public/images/effects/evo_bg.mp4 b/public/images/effects/evo_bg.mp4 new file mode 100644 index 000000000..48b561b77 Binary files /dev/null and b/public/images/effects/evo_bg.mp4 differ diff --git a/public/images/effects/evo_sparkle.png b/public/images/effects/evo_sparkle.png new file mode 100644 index 000000000..46e1aebba Binary files /dev/null and b/public/images/effects/evo_sparkle.png differ diff --git a/src/battle-info.ts b/src/battle-info.ts index ac9263109..b6b2143f9 100644 --- a/src/battle-info.ts +++ b/src/battle-info.ts @@ -101,7 +101,7 @@ export default class BattleInfo extends Phaser.GameObjects.Container { } } - updateInfo(pokemon: Pokemon): Promise { + updateInfo(pokemon: Pokemon, instant?: boolean): Promise { return new Promise(resolve => { if (!this.scene) { resolve(); @@ -109,7 +109,7 @@ export default class BattleInfo extends Phaser.GameObjects.Container { } const updatePokemonHp = () => { - const duration = Utils.clampInt(Math.abs((this.lastHp) - pokemon.hp) * 5, 250, 5000); + const duration = !instant ? Utils.clampInt(Math.abs((this.lastHp) - pokemon.hp) * 5, 250, 5000) : 0; this.scene.tweens.add({ targets: this.hpBar, ease: 'Sine.easeOut', @@ -154,13 +154,13 @@ export default class BattleInfo extends Phaser.GameObjects.Container { }); } - updatePokemonExp(battler: Pokemon): Promise { + updatePokemonExp(battler: Pokemon, instant?: boolean): Promise { return new Promise(resolve => { const levelUp = this.lastLevel < battler.level; const relLevelExp = getLevelRelExp(this.lastLevel + 1, battler.species.growthRate); const levelExp = levelUp ? relLevelExp : battler.levelExp; let ratio = levelExp / relLevelExp; - let duration = this.visible ? ((levelExp - this.lastLevelExp) / relLevelExp) * 1650 : 0; + let duration = this.visible && !instant ? ((levelExp - this.lastLevelExp) / relLevelExp) * 1650 : 0; if (duration) this.scene.sound.play('exp'); this.scene.tweens.add({ @@ -191,7 +191,7 @@ export default class BattleInfo extends Phaser.GameObjects.Container { this.setLevel(this.lastLevel); this.scene.time.delayedCall(500, () => { this.expBar.setScale(0, 1); - this.updateInfo(battler).then(() => resolve()); + this.updateInfo(battler, instant).then(() => resolve()); }); return; } else { diff --git a/src/battle-phase.ts b/src/battle-phase.ts index cd21eb0ed..aecdd4096 100644 --- a/src/battle-phase.ts +++ b/src/battle-phase.ts @@ -12,6 +12,8 @@ import { pokemonLevelMoves } from "./pokemon-level-moves"; import { MoveAnim, initAnim, loadMoveAnimAssets } from "./battle-anims"; import { StatusEffect } from "./status-effect"; import { SummaryUiMode } from "./ui/summary-ui-handler"; +import { Species } from "./species"; +import { SpeciesEvolution } from "./pokemon-evolutions"; export class BattlePhase { protected scene: BattleScene; @@ -1014,6 +1016,277 @@ export class AttemptCapturePhase extends BattlePhase { } } +export class EvolutionPhase extends BattlePhase { + private partyMemberIndex: integer; + private evolution: SpeciesEvolution; + + private evolutionContainer: Phaser.GameObjects.Container; + private evolutionBaseBg: Phaser.GameObjects.Image; + private evolutionBg: Phaser.GameObjects.Video; + private evolutionBgOverlay: Phaser.GameObjects.Rectangle; + private pokemonSprite: Phaser.GameObjects.Sprite; + private pokemonTintSprite: Phaser.GameObjects.Sprite; + private pokemonEvoSprite: Phaser.GameObjects.Sprite; + private pokemonEvoTintSprite: Phaser.GameObjects.Sprite; + + constructor(scene: BattleScene, partyMemberIndex: integer, evolution: SpeciesEvolution) { + super(scene); + + this.partyMemberIndex = partyMemberIndex; + this.evolution = evolution; + } + + start() { + super.start(); + + if (!this.evolution) { + this.end(); + return; + } + + this.scene.pauseBgm(); + + this.evolutionContainer = this.scene.add.container(0, 0); + this.scene.field.add(this.evolutionContainer); + + this.evolutionBaseBg = this.scene.add.image(0, 0, 'plains_bg'); + this.evolutionBaseBg.setOrigin(0, 0); + this.evolutionContainer.add(this.evolutionBaseBg); + + this.evolutionBg = this.scene.add.video(0, 0, 'evo_bg').stop(); + this.evolutionBg.setOrigin(0, 0); + this.evolutionBg.setScale(0.4359673025); + this.evolutionBg.setVisible(false); + this.evolutionContainer.add(this.evolutionBg); + + this.evolutionBgOverlay = this.scene.add.rectangle(0, 0, this.scene.game.canvas.width / 6, this.scene.game.canvas.height / 6, 0x262626); + this.evolutionBgOverlay.setOrigin(0, 0); + this.evolutionBgOverlay.setAlpha(0); + this.evolutionContainer.add(this.evolutionBgOverlay); + + const getPokemonSprite = () => { + return this.scene.add.sprite(this.evolutionBaseBg.displayWidth / 2, this.evolutionBaseBg.displayHeight / 2, `pkmn__sub`); + }; + + this.evolutionContainer.add((this.pokemonSprite = getPokemonSprite())); + this.evolutionContainer.add((this.pokemonTintSprite = getPokemonSprite())); + this.evolutionContainer.add((this.pokemonEvoSprite = getPokemonSprite())); + this.evolutionContainer.add((this.pokemonEvoTintSprite = getPokemonSprite())); + + this.pokemonTintSprite.setAlpha(0); + this.pokemonTintSprite.setTintFill(0xFFFFFF); + this.pokemonEvoSprite.setVisible(false); + this.pokemonEvoTintSprite.setVisible(false); + this.pokemonEvoTintSprite.setTintFill(0xFFFFFF); + + const pokemon = this.scene.getParty()[this.partyMemberIndex]; + + this.pokemonSprite.play(pokemon.getSpriteKey()); + this.pokemonTintSprite.play(pokemon.getSpriteKey()); + this.pokemonEvoSprite.play(pokemon.getSpriteKey()); + this.pokemonEvoTintSprite.play(pokemon.getSpriteKey()); + + this.scene.ui.showText(`What?\n${pokemon.name} is evolving!`, null, () => { + pokemon.cry(); + + pokemon.evolve(this.evolution).then(() => { + this.pokemonEvoSprite.play(pokemon.getSpriteKey()); + this.pokemonEvoTintSprite.play(pokemon.getSpriteKey()); + }); + + this.scene.time.delayedCall(1000, () => { + this.scene.tweens.add({ + targets: this.evolutionBgOverlay, + alpha: 1, + delay: 500, + duration: 1500, + ease: 'Sine.easeOut', + onComplete: () => { + this.scene.time.delayedCall(1000, () => { + this.scene.tweens.add({ + targets: this.evolutionBgOverlay, + alpha: 0, + duration: 250, + onComplete: () => this.evolutionBgOverlay.setVisible(false) + }); + this.evolutionBg.setVisible(true); + this.evolutionBg.play(); + }); + this.doSpiralUpward(); + this.scene.tweens.addCounter({ + from: 0, + to: 1, + duration: 2000, + onUpdate: t => { + this.pokemonTintSprite.setAlpha(t.getValue()); + }, + onComplete: () => { + this.pokemonSprite.setVisible(false); + this.scene.time.delayedCall(1000, () => { + this.doArcDownward(); + this.scene.time.delayedCall(1500, () => { + this.pokemonEvoTintSprite.setScale(0.25); + this.pokemonEvoTintSprite.setVisible(true); + this.doCycle(1).then(() => { + this.scene.sound.play('shiny'); + this.pokemonEvoSprite.setVisible(true); + }); + }); + }); + } + }) + } + }); + //this.scene.sound.play('evolution'); + }); + }, 1000); + } + + sin(index: integer, amplitude: integer) { + return amplitude * Math.sin(index * (Math.PI / 128)); + } + + cos(index: integer, amplitude: integer) { + return amplitude * Math.cos(index * (Math.PI / 128)); + } + + doSpiralUpward() { + let f = 0; + + this.scene.tweens.addCounter({ + repeat: 64, + duration: 1, + useFrames: true, + onRepeat: () => { + if (f < 64) { + if (!(f & 7)) { + for (let i = 0; i < 4; i++) + this.doSpiralUpwardParticle((f & 120) * 2 + i * 64); + } + f++; + } + } + }); + } + + doArcDownward() { + let f = 0; + + this.scene.tweens.addCounter({ + repeat: 96, + duration: 1, + useFrames: true, + onRepeat: () => { + if (f < 96) { + if (f < 6) { + for (let i = 0; i < 9; i++) + this.doArcDownParticle(i * 16); + } + f++; + } + } + }); + } + + doCycle(l: number): Promise { + return new Promise(resolve => { + const isLastCycle = l === 15; + this.scene.tweens.add({ + targets: this.pokemonTintSprite, + scale: 0.25, + ease: 'Cubic.easeInOut', + duration: 500 / l, + yoyo: !isLastCycle + }); + this.scene.tweens.add({ + targets: this.pokemonEvoTintSprite, + scale: 1, + ease: 'Cubic.easeInOut', + duration: 500 / l, + yoyo: !isLastCycle, + onComplete: () => { + if (l < 15) + this.doCycle(l + 0.5).then(() => resolve()); + else { + this.pokemonTintSprite.setVisible(false); + resolve(); + } + } + }); + }); + } + + doSpiralUpwardParticle(trigIndex: integer) { + const initialX = (this.scene.game.canvas.width / 6) / 2; + const particle = this.scene.add.image(initialX, 0, 'evo_sparkle'); + this.evolutionContainer.add(particle); + + let f = 0; + let amp = 48; + + const particleTimer = this.scene.tweens.addCounter({ + repeat: -1, + duration: 1, + useFrames: true, + onRepeat: () => { + updateParticle(); + } + }); + + const updateParticle = () => { + if (!f || particle.y > 8) { + particle.setPosition(initialX, 88 - (f * f) / 80); + particle.y += this.sin(trigIndex, amp) / 4; + particle.x += this.cos(trigIndex, amp); + particle.setScale(1 - (f / 80)); + trigIndex += 4; + if (f & 1) + amp--; + f++; + } else { + particle.destroy(); + particleTimer.remove(); + } + }; + + updateParticle(); + } + + doArcDownParticle(trigIndex: integer) { + const initialX = (this.scene.game.canvas.width / 6) / 2; + const particle = this.scene.add.image(initialX, 0, 'evo_sparkle'); + particle.setScale(0.5); + this.evolutionContainer.add(particle); + + let f = 0; + let amp = 8; + + const particleTimer = this.scene.tweens.addCounter({ + repeat: -1, + duration: 1, + useFrames: true, + onRepeat: () => { + updateParticle(); + } + }); + + const updateParticle = () => { + if (!f || particle.y < 88) { + particle.setPosition(initialX, 8 + (f * f) / 5); + particle.y += this.sin(trigIndex, amp) / 4; + particle.x += this.cos(trigIndex, amp); + amp = 8 + this.sin(f * 4, 40); + f++; + } else { + particle.destroy(); + particleTimer.remove(); + } + }; + + updateParticle(); + } +} + export class SelectModifierPhase extends BattlePhase { constructor(scene: BattleScene) { super(scene); diff --git a/src/battle-scene.ts b/src/battle-scene.ts index e4cd58a7b..d3ce92073 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -1,17 +1,16 @@ import Phaser from 'phaser'; import { Biome, BiomeArena } from './biome'; import UI from './ui/ui'; -import { BattlePhase, EncounterPhase, SummonPhase, CommandPhase, NextEncounterPhase, SwitchBiomePhase, NewBiomeEncounterPhase } from './battle-phase'; +import { BattlePhase, EncounterPhase, SummonPhase, CommandPhase, NextEncounterPhase, SwitchBiomePhase, NewBiomeEncounterPhase, EvolutionPhase } from './battle-phase'; import { PlayerPokemon, EnemyPokemon } from './pokemon'; import PokemonSpecies, { allSpecies, getPokemonSpecies } from './pokemon-species'; import * as Utils from './utils'; -import { Modifier, ModifierBar, ConsumablePokemonModifier, ConsumableModifier, PartyShareModifier, PokemonHpRestoreModifier, HealingBoosterModifier, PersistentModifier, PokemonBaseStatBoosterModifierType, PokemonBaseStatModifier } from './modifier'; +import { Modifier, ModifierBar, ConsumablePokemonModifier, ConsumableModifier, PartyShareModifier, PokemonHpRestoreModifier, HealingBoosterModifier, PersistentModifier } from './modifier'; import { PokeballType } from './pokeball'; import { Species } from './species'; import { initAutoPlay } from './auto-play'; import { Battle } from './battle'; import { populateAnims } from './battle-anims'; -import { Stat } from './pokemon-stat'; const enableAuto = true; @@ -164,6 +163,8 @@ export default class BattleScene extends Phaser.Scene { this.loadImage(`pkmn__back__sub`, 'pokemon/back', 'sub.png'); this.loadImage(`pkmn__sub`, 'pokemon', 'sub.png'); this.loadAtlas('shiny', 'effects'); + this.loadImage('evo_sparkle', 'effects'); + this.load.video('evo_bg', 'images/effects/evo_bg.mp4', null, false, true); this.loadAtlas('pb', ''); this.loadAtlas('items', ''); @@ -197,6 +198,8 @@ export default class BattleScene extends Phaser.Scene { this.loadSe('pb_lock'); this.loadBgm('level_up_fanfare'); + this.loadBgm('evolution'); + this.loadBgm('evolution_fanfare'); //this.load.glsl('sprite', 'shaders/sprite.frag'); @@ -254,7 +257,7 @@ export default class BattleScene extends Phaser.Scene { for (let s = 0; s < 3; s++) { const playerSpecies = getPokemonSpecies(s === 0 ? Species.TORCHIC : s === 1 ? Species.TREECKO : Species.MUDKIP); //this.randomSpecies(); - const playerPokemon = new PlayerPokemon(this, playerSpecies, 5); + const playerPokemon = new PlayerPokemon(this, playerSpecies, 16); playerPokemon.setVisible(false); loadPokemonAssets.push(playerPokemon.loadAssets()); @@ -302,10 +305,6 @@ export default class BattleScene extends Phaser.Scene { this.plusKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.PLUS); this.minusKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.MINUS); - for (let a = 0; a < 3; a++) { - this.addModifier(new PokemonBaseStatModifier(new PokemonBaseStatBoosterModifierType('HP-UP', Stat.HP), this.getParty()[0].id, Stat.HP)); - } - Promise.all(loadPokemonAssets).then(() => { if (enableAuto) initAutoPlay.apply(this); @@ -346,6 +345,7 @@ export default class BattleScene extends Phaser.Scene { this.unshiftPhase(new NewBiomeEncounterPhase(this)); } } else { + this.pushPhase(new EvolutionPhase(this, 0, this.getPlayerPokemon().getEvolution())); //this.pushPhase(new SelectStarterPhase(this)); this.pushPhase(new EncounterPhase(this)); this.pushPhase(new SummonPhase(this)); @@ -356,7 +356,7 @@ export default class BattleScene extends Phaser.Scene { } newBiome(): BiomeArena { - const biome = this.currentBattle ? Utils.randInt(20) as Biome : Biome.PLAINS; + const biome = this.currentBattle ? Utils.randInt(20) as Biome : Biome.LAKE; this.arena = new BiomeArena(this, biome, Biome[biome].toLowerCase()); return this.arena; } diff --git a/src/pokemon-evolutions.ts b/src/pokemon-evolutions.ts index 230ced81f..1f47033b8 100644 --- a/src/pokemon-evolutions.ts +++ b/src/pokemon-evolutions.ts @@ -36,7 +36,11 @@ export class SpeciesEvolutionCondition { } } -export const pokemonEvolutions = { +interface PokemonEvolutions { + [key: string]: SpeciesEvolution[] +} + +export const pokemonEvolutions: PokemonEvolutions = { [Species.BULBASAUR]: [ new SpeciesEvolution(Species.IVYSAUR, 16, null, null) ], diff --git a/src/pokemon.ts b/src/pokemon.ts index 81fbacffd..62cb94fe0 100644 --- a/src/pokemon.ts +++ b/src/pokemon.ts @@ -4,7 +4,7 @@ import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from './battle-info'; import { MessagePhase } from './battle-phase'; import { default as Move, allMoves, MoveCategory, Moves } from './move'; import { pokemonLevelMoves } from './pokemon-level-moves'; -import { default as PokemonSpecies } from './pokemon-species'; +import { default as PokemonSpecies, getPokemonSpecies } from './pokemon-species'; import * as Utils from './utils'; import { getTypeDamageMultiplier } from './type'; import { getLevelTotalExp } from './exp'; @@ -15,6 +15,7 @@ import { Gender } from './gender'; import { initAnim, loadMoveAnimAssets } from './battle-anims'; import { StatusEffect } from './status-effect'; import { tmSpecies } from './tms'; +import { pokemonEvolutions, SpeciesEvolution, SpeciesEvolutionCondition } from './pokemon-evolutions'; export default abstract class Pokemon extends Phaser.GameObjects.Container { public id: integer; @@ -241,13 +242,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return this.getAt(2) as Phaser.GameObjects.Sprite; } - playAnim() { + playAnim(): void{ this.getSprite().play(this.getBattleSpriteKey()); this.getTintSprite().play(this.getBattleSpriteKey()); this.getZoomSprite().play(this.getBattleSpriteKey()); } - calculateStats() { + calculateStats(): void { if (!this.stats) this.stats = [ 0, 0, 0, 0, 0, 0 ]; const baseStats = this.species.baseStats.slice(0); @@ -273,15 +274,42 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } } - getMaxHp() { + getMaxHp(): integer { return this.stats[Stat.HP]; } - getHpRatio() { + getHpRatio(): number { return Math.floor((this.hp / this.getMaxHp()) * 100) / 100; } - generateAndPopulateMoveset() { + getEvolution(): SpeciesEvolution { + if (!pokemonEvolutions.hasOwnProperty(this.species.speciesId)) + return null; + + const evolutions = pokemonEvolutions[this.species.speciesId]; + for (let e of evolutions) { + if (this.level >= e.level) { + // TODO: Remove string conditions + if (e.condition === null || typeof e.condition === 'string' || (e.condition as SpeciesEvolutionCondition).predicate(this)) + return e; + } + } + + return null; + } + + evolve(evolution: SpeciesEvolution): Promise { + return new Promise(resolve => { + console.log(evolution?.speciesId) + this.species = getPokemonSpecies(evolution.speciesId); + this.loadAssets().then(() => { + this.calculateStats(); + this.updateInfo().then(() => resolve()); + }); + }); + } + + generateAndPopulateMoveset(): void { this.moveset = []; const movePool = []; const allLevelMoves = pokemonLevelMoves[this.species.speciesId]; @@ -352,8 +380,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } } - updateInfo(): Promise { - return this.battleInfo.updateInfo(this); + updateInfo(instant?: boolean): Promise { + return this.battleInfo.updateInfo(this, instant); } addExp(exp: integer) { @@ -669,10 +697,9 @@ export class EnemyPokemon extends Pokemon { const scoreB = moveScores[movePool.indexOf(b)]; return scoreA < scoreB ? 1 : scoreA > scoreB ? -1 : 0; }); - let randInt: integer; let r = 0; if (this.aiType === AiType.SMART_RANDOM) { - while (r < sortedMovePool.length - 1 && (randInt = Utils.randInt(8)) >= 5) + while (r < sortedMovePool.length - 1 && Utils.randInt(8) >= 5) r++; } console.log(movePool.map(m => m.getName()), moveScores, r, sortedMovePool.map(m => m.getName())); diff --git a/src/ui/ui.ts b/src/ui/ui.ts index 1e48f4328..94fdc0670 100644 --- a/src/ui/ui.ts +++ b/src/ui/ui.ts @@ -10,6 +10,7 @@ import ModifierSelectUiHandler from './modifier-select-ui-handler'; import BallUiHandler from './ball-ui-handler'; import SummaryUiHandler from './summary-ui-handler'; import StarterSelectUiHandler from './starter-select-ui-handler'; +import EvolutionUiHandler from './evolution-ui-handler'; export enum Mode { MESSAGE = 0, @@ -20,7 +21,7 @@ export enum Mode { MODIFIER_SELECT, PARTY, SUMMARY, - STARTER_SELECT, + STARTER_SELECT }; const transitionModes = [