From b0ecb9d5f5ed52f30c4d19338b60d768ee4ccccf Mon Sep 17 00:00:00 2001 From: Flashfyre Date: Tue, 11 Apr 2023 19:08:03 -0400 Subject: [PATCH] Add status effects --- README.md | 3 - public/images/statuses.json | 51 ++++++--- public/images/statuses.png | Bin 395 -> 400 bytes public/images/statuses/toxic.png | Bin 0 -> 174 bytes src/battle-anims.ts | 177 +++++++++++++++++++++++-------- src/battle-info.ts | 19 ++++ src/battle-phases.ts | 160 +++++++++++++++++++++++----- src/battle-scene.ts | 4 +- src/move.ts | 61 ++++++----- src/pokemon.ts | 57 +++++++--- src/status-effect.ts | 93 +++++++++++++++- 11 files changed, 489 insertions(+), 136 deletions(-) create mode 100644 public/images/statuses/toxic.png diff --git a/README.md b/README.md index a1c57ce50..42eaddb56 100644 --- a/README.md +++ b/README.md @@ -9,13 +9,10 @@ - Moves - Move logic - Can't use when PP consumed - - Multi hit moves should still show 1 time(s) when hitting once - Abilities - Ability logic - Ability activation indicator (?) - Natures -- Status effects - - Status effect animation - EXP logic - Fix algorithm (currently inaccurate) - Pokemon summary screen diff --git a/public/images/statuses.json b/public/images/statuses.json index b893554b1..019c2dd6d 100644 --- a/public/images/statuses.json +++ b/public/images/statuses.json @@ -4,8 +4,8 @@ "image": "statuses.png", "format": "RGBA8888", "size": { - "w": 44, - "h": 30 + "w": 20, + "h": 56 }, "scale": 1, "frames": [ @@ -24,8 +24,8 @@ "h": 8 }, "frame": { - "x": 1, - "y": 1, + "x": 0, + "y": 0, "w": 20, "h": 8 } @@ -45,8 +45,8 @@ "h": 8 }, "frame": { - "x": 1, - "y": 11, + "x": 0, + "y": 8, "w": 20, "h": 8 } @@ -66,8 +66,8 @@ "h": 8 }, "frame": { - "x": 1, - "y": 21, + "x": 0, + "y": 16, "w": 20, "h": 8 } @@ -87,8 +87,8 @@ "h": 8 }, "frame": { - "x": 23, - "y": 1, + "x": 0, + "y": 24, "w": 20, "h": 8 } @@ -108,8 +108,8 @@ "h": 8 }, "frame": { - "x": 23, - "y": 11, + "x": 0, + "y": 32, "w": 20, "h": 8 } @@ -129,8 +129,29 @@ "h": 8 }, "frame": { - "x": 23, - "y": 21, + "x": 0, + "y": 40, + "w": 20, + "h": 8 + } + }, + { + "filename": "toxic", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 20, + "h": 8 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 20, + "h": 8 + }, + "frame": { + "x": 0, + "y": 48, "w": 20, "h": 8 } @@ -141,6 +162,6 @@ "meta": { "app": "https://www.codeandweb.com/texturepacker", "version": "3.0", - "smartupdate": "$TexturePacker:SmartUpdate:34eb38966145af4ef9cffbaaa8d07ea3:fba056f9a49345436b0820757c78f418:e6649238c018d3630e55681417c698ca$" + "smartupdate": "$TexturePacker:SmartUpdate:b1f59475db34388a7177fe541f95d9e8:47a06b96ac24cc4249538472b2604202:e6649238c018d3630e55681417c698ca$" } } diff --git a/public/images/statuses.png b/public/images/statuses.png index 8820b55689490e22d80bbf70ecb6fccab3060f79..defa8cae33e824b1b68002e731e73c397794856e 100644 GIT binary patch delta 339 zcmV-Z0j&Ou1CRqRiBL{Q4GJ0x0000DNk~Le0000K0000u2m=5B0LnQiasU7TEl^BU zMF0Q*gf@JUEGGokd#2fuS0#V^Nkl`!?PTD;+!MOvDqC2#E$9HIA@SN=4XgLxcpAd&J@$-k})J&b_A4$;8G zlT~}xNUyps$@x2UC%e5@=DD&xVOx(xwS=f(^7*1F8>>?B)C6Z4uz2-=u#ehNIlf`A zI-D9UN1}HM@ixXRJ0W4vggZO5NAmm$(`-)6(L#=`&9#O?;m l3{n0s*zMKf)Mz!*FBlVSB8pXYjg9~S002ovPDHLkV1i9tmD>OS delta 334 zcmV-U0kQs&1B(MJiBL{Q4GJ0x0000DNk~Le0000i0000U2m=5B0AT10l>h($Do{*R zMF0Q*kuN6%mMid3ky<5x`bk7VR5;7EkUS*as{n4*1+X6Qx&B!f9TD@?AIy ziysK{>XlLk%Ph~L1-6_CQS8R^MG-8XmFNO%t*`=LVDDhE1~2C}F3HYb+b~!M{J@t1 gk!ofXbKL`@a%MF_r}R1v5B2yO9Ru7X3j`mC2V3o(Ym%scrK(j~-&tE?^Z3$mKZ9AjGD0 U)cMGpFF@T4p00i_>zopr09%?pQ~&?~ literal 0 HcmV?d00001 diff --git a/src/battle-anims.ts b/src/battle-anims.ts index a20dc25f4..f2082ad6f 100644 --- a/src/battle-anims.ts +++ b/src/battle-anims.ts @@ -35,12 +35,12 @@ export enum ChargeAnim { export enum CommonAnim { HEALTH_UP = 2000, - SLEEP, POISON, TOXIC, - BURN, PARALYSIS, + SLEEP, FROZEN, + BURN, CONFUSION, ATTRACT, BIND, @@ -209,7 +209,7 @@ abstract class AnimTimedEvent { this.resourceName = resourceName; } - abstract execute(scene: BattleScene, moveAnim: MoveAnim): void; + abstract execute(scene: BattleScene, battleAnim: BattleAnim): integer; abstract getEventType(): string; } @@ -228,12 +228,13 @@ class AnimTimedSoundEvent extends AnimTimedEvent { this.pitch = 100; } - execute(scene: BattleScene, moveAnim: MoveAnim): void { + execute(scene: BattleScene, battleAnim: BattleAnim): integer { const soundConfig = { rate: (this.pitch * 0.01), volume: (this.volume * 0.01) }; - if (this.resourceName) + if (this.resourceName) { scene.sound.play(this.resourceName, soundConfig); - else - moveAnim.user.cry(soundConfig); + return Math.ceil((scene.sound.get(this.resourceName).totalDuration * 1000) / 33.33); + } else + return Math.ceil(battleAnim.user.cry(soundConfig) / 33.33); } getEventType(): string { @@ -284,7 +285,7 @@ class AnimTimedUpdateBgEvent extends AnimTimedBgEvent { super(frameIndex, resourceName, source); } - execute(scene: BattleScene, moveAnim: MoveAnim): void { + execute(scene: BattleScene, moveAnim: MoveAnim): integer { const tweenProps = {}; if (this.bgX !== undefined) tweenProps['x'] = (this.bgX * 0.5) - 256; @@ -299,6 +300,7 @@ class AnimTimedUpdateBgEvent extends AnimTimedBgEvent { useFrames: true }, tweenProps)) } + return this.duration * 2; } getEventType(): string { @@ -311,7 +313,7 @@ class AnimTimedAddBgEvent extends AnimTimedBgEvent { super(frameIndex, resourceName, source); } - execute(scene: BattleScene, moveAnim: MoveAnim): void { + execute(scene: BattleScene, moveAnim: MoveAnim): integer { moveAnim.bgSprite = scene.add.tileSprite(this.bgX - 256, this.bgY - 284, 768, 576, this.resourceName); moveAnim.bgSprite.setOrigin(0, 0); moveAnim.bgSprite.setScale(1.25); @@ -325,6 +327,8 @@ class AnimTimedAddBgEvent extends AnimTimedBgEvent { duration: this.duration * 2, useFrames: true }); + + return this.duration * 2; } getEventType(): string { @@ -336,7 +340,22 @@ export const moveAnims = new Map(); export const chargeAnims = new Map(); export const commonAnims = new Map(); -export function initAnim(move: Moves): Promise { +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 Anim(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) @@ -374,6 +393,12 @@ function populateMoveAnim(move: Moves, animSource: any) { moveAnims.set(move, [ moveAnims.get(move) as Anim, moveAnim ]); } +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 => { @@ -382,17 +407,23 @@ export function loadMoveAnimAssets(scene: BattleScene, moveIds: Moves[], startLo return anims as Anim; return anims[0] as Anim; }); + loadAnimAssets(scene, moveAnimations, startLoad).then(() => resolve()); + }); +} + +function loadAnimAssets(scene: BattleScene, anims: Anim[], startLoad?: boolean): Promise { + return new Promise(resolve => { const backgrounds = new Set(); const sounds = new Set(); - for (let ma of moveAnimations) { - const moveSounds = ma.getSoundResourceNames(); - for (let ms of moveSounds) - sounds.add(ms); - const moveBackgrounds = ma.getBackgroundResourceNames(); - for (let mbg of moveBackgrounds) - backgrounds.add(mbg); - if (ma.graphic) - scene.loadSpritesheet(ma.graphic, 'battle_anims', 96); + for (let a of anims) { + 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'); @@ -404,25 +435,26 @@ export function loadMoveAnimAssets(scene: BattleScene, moveIds: Moves[], startLo }); } -export class MoveAnim { - public move: Moves; +export abstract class BattleAnim { public user: Pokemon; public target: Pokemon; - public moveSprites: Phaser.GameObjects.Sprite[]; + public sprites: Phaser.GameObjects.Sprite[]; public bgSprite: Phaser.GameObjects.TileSprite; - constructor(move: Moves, user: Pokemon, target: Pokemon) { - this.move = move; + constructor(user: Pokemon, target: Pokemon) { this.user = user; this.target = target; - this.moveSprites = []; + this.sprites = []; } + abstract getAnim(): Anim; + + abstract isOppAnim(): boolean; + + abstract isReverseCoords(): boolean; + play(scene: BattleScene, callback?: Function) { - const anim = moveAnims.get(this.move) instanceof Anim - ? moveAnims.get(this.move) as Anim - : moveAnims.get(this.move)[this.user instanceof PlayerPokemon ? 0 : 1] as Anim; - const isOppMove = Array.isArray(moveAnims.get(this.move)) && this.user instanceof EnemyPokemon; + const anim = this.getAnim(); const userInitialX = this.user.x; const userInitialY = this.user.y; @@ -431,11 +463,12 @@ export class MoveAnim { const targetInitialY = this.target.y; const targetHalfHeight = this.target.getSprite().displayHeight / 2; - const coordMultiplayer = this.user instanceof PlayerPokemon || isOppMove ? 1 : -1; - + const coordMultiplier = this.isReverseCoords() ? -1 : 1; + + let r = anim.frames.length; let f = 0; - const moveSprites: Phaser.GameObjects.Sprite[] = []; + const sprites: Phaser.GameObjects.Sprite[] = []; scene.tweens.addCounter({ useFrames: true, @@ -447,24 +480,25 @@ export class MoveAnim { for (let frame of spriteFrames) { switch (frame.target) { case AnimFrameTarget.USER: - this.user.setPosition(userInitialX + frame.x * coordMultiplayer, userInitialY + frame.y * coordMultiplayer); + this.user.setPosition(userInitialX + frame.x * coordMultiplier, userInitialY + frame.y * coordMultiplier); break; case AnimFrameTarget.TARGET: - this.target.setPosition(targetInitialX + frame.x * coordMultiplayer, targetInitialY + frame.y * coordMultiplayer); + this.target.setPosition(targetInitialX + frame.x * coordMultiplier, targetInitialY + frame.y * coordMultiplier); break; case AnimFrameTarget.GRAPHIC: - if (g === moveSprites.length) { + if (g === sprites.length) { const newSprite = scene.add.sprite(0, 0, anim.graphic, 1); scene.field.add(newSprite); - moveSprites.push(newSprite); + sprites.push(newSprite); } - const moveSprite = moveSprites[g++]; + const moveSprite = sprites[g++]; moveSprite.setFrame(frame.graphicFrame); const xProgress = Math.min(Math.max(frame.x, 0) / 128, 1); const yOffset = ((userHalfHeight * (1 - xProgress)) + (targetHalfHeight * xProgress)) * -1; - moveSprite.setPosition((!isOppMove ? userInitialX : targetInitialX) + frame.x * coordMultiplayer, (!isOppMove ? userInitialY : targetInitialY) + yOffset + frame.y * coordMultiplayer); + const isOppAnim = this.isOppAnim(); + moveSprite.setPosition((!isOppAnim ? userInitialX : targetInitialX) + frame.x * coordMultiplier, (!isOppAnim ? userInitialY : targetInitialY) + yOffset + frame.y * coordMultiplier); moveSprite.setAlpha(frame.opacity); - moveSprite.setAngle(-frame.angle * coordMultiplayer); + moveSprite.setAngle(-frame.angle * coordMultiplier); break; } if (frame.target !== AnimFrameTarget.GRAPHIC) { @@ -472,7 +506,7 @@ export class MoveAnim { ? this.user : this.target; pokemon.setAlpha(frame.opacity); - pokemon.setAngle(-frame.angle * coordMultiplayer); + pokemon.setAngle(-frame.angle * coordMultiplier); const zoomScaleX = frame.zoomX / 100; const zoomScaleY = frame.zoomY / 100; const zoomSprite = pokemon.getZoomSprite(); @@ -482,14 +516,15 @@ export class MoveAnim { } if (anim.frameTimedEvents.has(f)) { for (let event of anim.frameTimedEvents.get(f)) - event.execute(scene, this); + r = Math.max((anim.frames.length - f) + event.execute(scene, this), r); } - if (g < moveSprites.length) { - const removedSprites = moveSprites.splice(g, moveSprites.length - g); + if (g < sprites.length) { + const removedSprites = sprites.splice(g, sprites.length - g); for (let rs of removedSprites) rs.destroy(); } f++; + r--; }, onComplete: () => { this.user.setPosition(userInitialX, userInitialY); @@ -498,15 +533,67 @@ export class MoveAnim { this.target.setPosition(targetInitialX, targetInitialY); this.target.setAlpha(1); this.target.setAngle(0); - for (let ms of moveSprites) + for (let ms of sprites) ms.destroy(); - if (callback) + if (r && callback) { + scene.tweens.addCounter({ + duration: r, + useFrames: true, + onComplete: () => callback() + }); + } else if (callback) callback(); } }); } } +export class CommonBattleAnim extends BattleAnim { + public commonAnim: CommonAnim; + + constructor(commonAnim: CommonAnim, user: Pokemon, target?: Pokemon) { + super(user, target || user); + + this.commonAnim = commonAnim; + } + + getAnim(): Anim { + return commonAnims.get(this.commonAnim); + } + + isOppAnim(): boolean { + return false; + } + + isReverseCoords(): boolean { + return false; + } +} + +export class MoveAnim extends BattleAnim { + public move: Moves; + + constructor(move: Moves, user: Pokemon, target: Pokemon) { + super(user, target); + + this.move = move; + } + + getAnim(): Anim { + return moveAnims.get(this.move) instanceof Anim + ? moveAnims.get(this.move) as Anim + : moveAnims.get(this.move)[this.user instanceof PlayerPokemon ? 0 : 1] as Anim; + } + + isOppAnim(): boolean { + return this.user instanceof EnemyPokemon && Array.isArray(moveAnims.get(this.move)); + } + + isReverseCoords(): boolean { + return this.user instanceof EnemyPokemon && !this.isOppAnim(); + } +} + export function populateAnims() { return; const commonAnimNames = Utils.getEnumKeys(CommonAnim).map(k => k.toLowerCase()); diff --git a/src/battle-info.ts b/src/battle-info.ts index 8aab1d98c..aabf3106a 100644 --- a/src/battle-info.ts +++ b/src/battle-info.ts @@ -3,10 +3,12 @@ import { getLevelTotalExp, getLevelRelExp } from './exp'; import * as Utils from './utils'; import { addTextObject, TextStyle } from './text'; import { getGenderSymbol, getGenderColor } from './gender'; +import { StatusEffect } from './status-effect'; export default class BattleInfo extends Phaser.GameObjects.Container { private player: boolean; private lastName: string; + private lastStatus: StatusEffect; private lastHp: integer; private lastMaxHp: integer; private lastHpFrame: string; @@ -16,6 +18,7 @@ export default class BattleInfo extends Phaser.GameObjects.Container { private nameText: Phaser.GameObjects.Text; private genderText: Phaser.GameObjects.Text; + private statusIndicator: Phaser.GameObjects.Sprite; private levelContainer: Phaser.GameObjects.Container; private hpBar: Phaser.GameObjects.Image; private levelNumbersContainer: Phaser.GameObjects.Container; @@ -25,6 +28,8 @@ export default class BattleInfo extends Phaser.GameObjects.Container { constructor(scene: Phaser.Scene, x: number, y: number, player: boolean) { super(scene, x, y); this.player = player; + this.lastName = null; + this.lastStatus = StatusEffect.NONE; this.lastHp = -1; this.lastMaxHp = -1; this.lastHpFrame = null; @@ -48,6 +53,12 @@ export default class BattleInfo extends Phaser.GameObjects.Container { this.genderText.setPositionRelative(this.nameText, 0, 2); this.add(this.genderText); + this.statusIndicator = this.scene.add.sprite(0, 0, 'statuses'); + this.statusIndicator.setVisible(false); + this.statusIndicator.setOrigin(0, 0); + this.statusIndicator.setPositionRelative(this.nameText, 0, 11.5); + this.add(this.statusIndicator); + this.levelContainer = this.scene.add.container(player ? -41 : -50, player ? -10 : -5); this.add(this.levelContainer); @@ -121,6 +132,14 @@ export default class BattleInfo extends Phaser.GameObjects.Container { this.genderText.setPositionRelative(this.nameText, nameTextWidth, 0); } + if (this.lastStatus !== (pokemon.status?.effect || StatusEffect.NONE)) { + this.lastStatus = pokemon.status?.effect || StatusEffect.NONE; + + if (this.lastStatus !== StatusEffect.NONE) + this.statusIndicator.setFrame(StatusEffect[this.lastStatus].toLowerCase()); + this.statusIndicator.setVisible(!!this.lastStatus); + } + const updatePokemonHp = () => { const duration = !instant ? Utils.clampInt(Math.abs((this.lastHp) - pokemon.hp) * 5, 250, 5000) : 0; this.scene.tweens.add({ diff --git a/src/battle-phases.ts b/src/battle-phases.ts index 6f93b7c64..3c13ca67c 100644 --- a/src/battle-phases.ts +++ b/src/battle-phases.ts @@ -8,8 +8,8 @@ import { Stat } from "./pokemon-stat"; import { ExpBoosterModifier, ExpShareModifier, ExtraModifierModifier, getModifierTypesForWave, ModifierType, PokemonModifierType, PokemonMoveModifierType, regenerateModifierPoolThresholds } from "./modifier"; import PartyUiHandler, { PartyOption, PartyUiMode } from "./ui/party-ui-handler"; import { doPokeballBounceAnim, getPokeballAtlasKey, getPokeballCatchMultiplier, getPokeballTintColor, PokeballType } from "./pokeball"; -import { MoveAnim, initAnim, loadMoveAnimAssets } from "./battle-anims"; -import { StatusEffect } from "./status-effect"; +import { CommonAnim, CommonBattleAnim, MoveAnim, initMoveAnim, loadMoveAnimAssets } from "./battle-anims"; +import { StatusEffect, getStatusEffectActivationText, getStatusEffectHealText, getStatusEffectObtainText, getStatusEffectOverlapText } from "./status-effect"; import { SummaryUiMode } from "./ui/summary-ui-handler"; import EvolutionSceneHandler from "./ui/evolution-scene-handler"; import { EvolutionPhase } from "./evolution-phase"; @@ -351,19 +351,20 @@ export class CommandPhase extends BattlePhase { }); } - handleCommand(command: Command, cursor: integer): boolean{ + handleCommand(command: Command, cursor: integer): boolean { const playerPokemon = this.scene.getPlayerPokemon(); const enemyPokemon = this.scene.getEnemyPokemon(); let success: boolean; - let delayed: boolean; + + const playerSpeed = playerPokemon.getBattleStat(Stat.SPD); + const enemySpeed = enemyPokemon.getBattleStat(Stat.SPD); + + const isDelayed = () => playerSpeed < enemySpeed || (playerSpeed === enemySpeed && Utils.randInt(2) === 1); switch (command) { case Command.FIGHT: if (playerPokemon.trySelectMove(cursor)) { const playerPhase = new PlayerMovePhase(this.scene, playerPokemon, playerPokemon.moveset[cursor]); - const playerSpeed = playerPokemon.getBattleStat(Stat.SPD); - const enemySpeed = enemyPokemon.getBattleStat(Stat.SPD); - delayed = playerSpeed < enemySpeed || (playerSpeed === enemySpeed && Utils.randInt(2) === 1) this.scene.pushPhase(playerPhase); success = true; } @@ -382,10 +383,24 @@ export class CommandPhase extends BattlePhase { if (success) { const enemyMove = enemyPokemon.getNextMove(); const enemyPhase = new EnemyMovePhase(this.scene, enemyPokemon, enemyMove); - if (delayed) + if (isDelayed()) this.scene.unshiftPhase(enemyPhase); else this.scene.pushPhase(enemyPhase); + + const statusEffectPhases: PostTurnStatusEffectPhase[] = []; + if (playerPokemon.status && playerPokemon.status.isPostTurn()) + statusEffectPhases.push(new PostTurnStatusEffectPhase(this.scene, true)); + if (enemyPokemon.status && enemyPokemon.status.isPostTurn()) { + const enemyStatusEffectPhase = new PostTurnStatusEffectPhase(this.scene, false); + if (isDelayed()) + statusEffectPhases.unshift(enemyStatusEffectPhase); + else + statusEffectPhases.push(enemyStatusEffectPhase); + } + for (let sef of statusEffectPhases) + this.scene.pushPhase(sef); + this.end(); } @@ -428,12 +443,14 @@ export abstract class PartyMemberPokemonPhase extends PokemonPhase { abstract class MovePhase extends BattlePhase { protected pokemon: Pokemon; protected move: PokemonMove; + protected cancelled: boolean; constructor(scene: BattleScene, pokemon: Pokemon, move: PokemonMove) { super(scene); this.pokemon = pokemon; this.move = move; + this.cancelled = false; } abstract getEffectPhase(): MoveEffectPhase; @@ -445,17 +462,61 @@ abstract class MovePhase extends BattlePhase { start() { super.start(); + const doMove = () => { + if (this.cancelled) { + this.end(); + return; + } + if (!this.move) + console.log(this.pokemon.moveset); + this.move.ppUsed++; + this.scene.unshiftPhase(new MessagePhase(this.scene, `${this.pokemon.name} used\n${this.move.getName()}!`, 500)); + this.scene.unshiftPhase(this.getEffectPhase()); + this.end(); + }; + if (!this.canMove()) { this.end(); return; } - if (!this.move) - console.log(this.pokemon.moveset); - this.scene.ui.showText(`${this.pokemon.name} used\n${this.move.getName()}!`, null, () => { - this.move.ppUsed++; - this.scene.unshiftPhase(this.getEffectPhase()); - this.end(); - }, 500); + + if (this.pokemon.status && !this.pokemon.status.isPostTurn()) { + this.pokemon.status.incrementTurn(); + let activated = false; + let healed = false; + switch (this.pokemon.status.effect) { + case StatusEffect.PARALYSIS: + if (Utils.randInt(4) === 0) { + activated = true; + this.cancelled = true; + } + break; + case StatusEffect.SLEEP: + healed = this.pokemon.status.turnCount === this.pokemon.status.cureTurn; + activated = !healed; + this.cancelled = activated; + break; + case StatusEffect.FREEZE: + healed = Utils.randInt(5) === 0; + activated = !healed; + this.cancelled = activated; + break; + } + if (activated) { + this.scene.unshiftPhase(new MessagePhase(this.scene, + `${this.pokemon instanceof PlayerPokemon ? '' : 'Foe '}${this.pokemon.name}${getStatusEffectActivationText(this.pokemon.status.effect)}`)); + new CommonBattleAnim(CommonAnim.POISON + (this.pokemon.status.effect - 1), this.pokemon).play(this.scene, () => doMove()); + } else { + if (healed) { + this.scene.unshiftPhase(new MessagePhase(this.scene, + `${this.pokemon instanceof PlayerPokemon ? '' : 'Foe '}${this.pokemon.name}${getStatusEffectHealText(this.pokemon.status.effect)}`)); + this.pokemon.resetStatus(); + this.pokemon.updateInfo(true); + } + doMove(); + } + } else + doMove(); } } @@ -498,7 +559,7 @@ abstract class MoveEffectPhase extends PokemonPhase { const hitCount = new Utils.IntegerHolder(1); applyMoveAttrs(MultiHitAttr, this.scene, user, target, this.move.getMove(), hitCount); user.turnData.hitCount = 0; - user.turnData.hitsLeft = hitCount.value; + user.turnData.hitsLeft = user.turnData.hitsTotal = hitCount.value; } if (!this.hitCheck()) { @@ -511,7 +572,8 @@ abstract class MoveEffectPhase extends PokemonPhase { new MoveAnim(this.move.getMove().id as Moves, user, target).play(this.scene, () => { this.getTargetPokemon().apply(this.getUserPokemon(), this.move, () => { ++user.turnData.hitCount; - applyMoveAttrs(MoveHitEffectAttr, this.scene, user, target, this.move.getMove()); + if (this.getTargetPokemon().hp) + applyMoveAttrs(MoveHitEffectAttr, this.scene, user, target, this.move.getMove()); this.end(); }); if (this.getUserPokemon().hp <= 0) { @@ -529,7 +591,7 @@ abstract class MoveEffectPhase extends PokemonPhase { const user = this.getUserPokemon(); if (--user.turnData.hitsLeft && this.getTargetPokemon().hp) this.scene.unshiftPhase(this.getNewHitPhase()); - else if (user.turnData.hitCount > 1) + else if (user.turnData.hitsTotal > 1) this.scene.unshiftPhase(new MessagePhase(this.scene, `Hit ${user.turnData.hitCount} time(s)!`)); super.end(); @@ -683,14 +745,56 @@ export class ObtainStatusEffectPhase extends PokemonPhase { constructor(scene: BattleScene, player: boolean, statusEffect: StatusEffect) { super(scene, player); + + this.statusEffect = statusEffect; } start() { const pokemon = this.getPokemon(); - if (pokemon.status === StatusEffect.NONE) { - pokemon.status = this.statusEffect; - // Show message and animation - this.end(); + if (!pokemon.status) { + if (pokemon.trySetStatus(this.statusEffect)) { + pokemon.updateInfo(true); + new CommonBattleAnim(CommonAnim.POISON + (this.statusEffect - 1), pokemon).play(this.scene, () => { + this.scene.unshiftPhase(new MessagePhase(this.scene, `${this.player ? '' : 'Foe '}${pokemon.name}${getStatusEffectObtainText(this.statusEffect)}`)); + if (pokemon.status.isPostTurn()) + this.scene.pushPhase(new PostTurnStatusEffectPhase(this.scene, this.player)); + this.end(); + }); + return; + } + } else if (pokemon.status.effect === this.statusEffect) + this.scene.unshiftPhase(new MessagePhase(this.scene, `${this.player ? '' : 'Foe '}${pokemon.name}${getStatusEffectOverlapText(this.statusEffect)}`)); + this.end(); + } +} + +export class PostTurnStatusEffectPhase extends PokemonPhase { + constructor(scene: BattleScene, player: boolean) { + super(scene, player); + } + + start() { + const pokemon = this.getPokemon(); + if (pokemon.hp && pokemon.status && pokemon.status.isPostTurn()) { + pokemon.status.incrementTurn(); + new CommonBattleAnim(CommonAnim.POISON + (pokemon.status.effect - 1), pokemon).play(this.scene, () => { + this.scene.unshiftPhase(new MessagePhase(this.scene, + `${pokemon instanceof PlayerPokemon ? '' : 'Foe '}${pokemon.name}${getStatusEffectActivationText(pokemon.status.effect)}`)); + switch (pokemon.status.effect) { + case StatusEffect.POISON: + case StatusEffect.BURN: + pokemon.hp = Math.max(pokemon.hp - Math.max(pokemon.getMaxHp() >> 3, 1), 0); + break; + case StatusEffect.TOXIC: + pokemon.hp = Math.max(pokemon.hp - Math.max(Math.floor((pokemon.getMaxHp() / 16) * pokemon.status.turnCount), 1), 0); + break; + } + if (pokemon.hp <= 0) { + this.scene.pushPhase(new FaintPhase(this.scene, this.player)); + (this.player ? this.scene.getEnemyPokemon() : this.scene.getPlayerPokemon()).resetBattleSummonData(); + } + pokemon.updateInfo().then(() => this.end()); + }); } else this.end(); } @@ -698,19 +802,21 @@ export class ObtainStatusEffectPhase extends PokemonPhase { export class MessagePhase extends BattlePhase { private text: string; + private callbackDelay: integer; private prompt: boolean; - constructor(scene: BattleScene, text: string, prompt?: boolean) { + constructor(scene: BattleScene, text: string, callbackDelay?: integer, prompt?: boolean) { super(scene); this.text = text; + this.callbackDelay = callbackDelay; this.prompt = prompt; } start() { super.start(); - this.scene.ui.showText(this.text, null, () => this.end(), this.prompt ? 0 : 1500, this.prompt); + this.scene.ui.showText(this.text, null, () => this.end(), this.callbackDelay || (this.prompt ? 0 : 1500), this.prompt); } } @@ -723,10 +829,10 @@ export class FaintPhase extends PokemonPhase { super.start(); if (this.player) { - this.scene.unshiftPhase(new MessagePhase(this.scene, `${this.getPokemon().name} fainted!`, true)); + this.scene.unshiftPhase(new MessagePhase(this.scene, `${this.getPokemon().name} fainted!`, null, true)); this.scene.unshiftPhase(new SwitchPhase(this.scene, true, false)); } else { - this.scene.unshiftPhase(new MessagePhase(this.scene, `Foe ${this.getPokemon().name} fainted!`, true)); + this.scene.unshiftPhase(new MessagePhase(this.scene, `Foe ${this.getPokemon().name} fainted!`, null, true)); this.scene.unshiftPhase(new VictoryPhase(this.scene)); } @@ -904,7 +1010,7 @@ export class LearnMovePhase extends PartyMemberPokemonPhase { if (emptyMoveIndex > -1) { pokemon.moveset[emptyMoveIndex] = new PokemonMove(this.moveId, 0, 0); - initAnim(this.moveId).then(() => { + initMoveAnim(this.moveId).then(() => { loadMoveAnimAssets(this.scene, [ this.moveId ], true) .then(() => { this.scene.ui.setMode(messageMode).then(() => { diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 6f6a0f9b5..ad082d788 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -10,7 +10,7 @@ import { PokeballType } from './pokeball'; import { Species } from './species'; import { initAutoPlay } from './auto-play'; import { Battle } from './battle'; -import { populateAnims } from './battle-anims'; +import { initCommonAnims, loadCommonAnimAssets, populateAnims } from './battle-anims'; import { BattlePhase } from './battle-phase'; const enableAuto = true; @@ -321,7 +321,7 @@ export default class BattleScene extends Phaser.Scene { ui.setup(); - Promise.all(loadPokemonAssets).then(() => { + Promise.all([ Promise.all(loadPokemonAssets), initCommonAnims().then(() => loadCommonAnimAssets(this, true)) ]).then(() => { if (enableAuto) initAutoPlay.apply(this); diff --git a/src/move.ts b/src/move.ts index 589e066cd..04ddf5cbb 100644 --- a/src/move.ts +++ b/src/move.ts @@ -2,8 +2,7 @@ import { MessagePhase, ObtainStatusEffectPhase, StatChangePhase } from "./battle import BattleScene from "./battle-scene"; import { BattleStat } from "./battle-stat"; import Pokemon, { PlayerPokemon } from "./pokemon"; -import { Stat } from "./pokemon-stat"; -import { StatusEffect } from "./status-effect"; +import { StatusEffect, getStatusEffectOverlapText } from "./status-effect"; import { Type } from "./type"; import * as Utils from "./utils"; @@ -609,6 +608,10 @@ export enum Moves { FUSION_BOLT }; +const enum MoveEffectText { + BUT_IT_FAILED = 'But it failed!' +} + type MoveAttrFunc = (scene: BattleScene, user: Pokemon, target: Pokemon, move: Move) => void; export abstract class MoveAttr { @@ -624,7 +627,7 @@ export class HighCritAttr extends MoveAttr { apply(scene: BattleScene, user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { const critChance = args[0] as Utils.IntegerHolder; critChance.value /= 2; - + return true; } } @@ -681,8 +684,10 @@ class StatusEffectAttr extends MoveHitEffectAttr { apply(scene: BattleScene, user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { const statusCheck = move.chance < 0 || move.chance === 100 || Utils.randInt(100) < move.chance; - if (statusCheck) - scene.unshiftPhase(new ObtainStatusEffectPhase(scene, target instanceof PlayerPokemon, this.effect)); + if (statusCheck) { + if (!target.status || (target.status.effect === this.effect && move.chance < 0)) + scene.unshiftPhase(new ObtainStatusEffectPhase(scene, target instanceof PlayerPokemon, this.effect)); + } return true; } } @@ -794,32 +799,32 @@ export const allMoves = [ new Move(Moves.FURY_ATTACK, "Fury Attack", Type.NORMAL, MoveCategory.PHYSICAL, 15, 85, 20, -1, "Hits 2-5 times in one turn.", -1, 1, new MultiHitAttr()), new Move(Moves.HORN_DRILL, "Horn Drill", Type.NORMAL, MoveCategory.PHYSICAL, -1, 30, 5, -1, "One-Hit-KO, if it hits.", -1, 1, new OneHitKOAttr()), new Move(Moves.TACKLE, "Tackle", Type.NORMAL, MoveCategory.PHYSICAL, 40, 100, 35, -1, "", -1, 1), - new Move(Moves.BODY_SLAM, "Body Slam", Type.NORMAL, MoveCategory.PHYSICAL, 85, 100, 15, 66, "May paralyze opponent.", 30, 1), + new Move(Moves.BODY_SLAM, "Body Slam", Type.NORMAL, MoveCategory.PHYSICAL, 85, 100, 15, 66, "May paralyze opponent.", 30, 1, new StatusEffectAttr(StatusEffect.PARALYSIS)), new Move(Moves.WRAP, "Wrap", Type.NORMAL, MoveCategory.PHYSICAL, 15, 90, 20, -1, "Traps opponent, damaging them for 4-5 turns.", 100, 1), new Move(Moves.TAKE_DOWN, "Take Down", Type.NORMAL, MoveCategory.PHYSICAL, 90, 85, 20, 1, "User receives recoil damage.", -1, 1), new Move(Moves.THRASH, "Thrash", Type.NORMAL, MoveCategory.PHYSICAL, 120, 100, 10, -1, "User attacks for 2-3 turns but then becomes confused.", -1, 1), new Move(Moves.DOUBLE_EDGE, "Double-Edge", Type.NORMAL, MoveCategory.PHYSICAL, 120, 100, 15, -1, "User receives recoil damage.", -1, 1), new Move(Moves.TAIL_WHIP, "Tail Whip", Type.NORMAL, MoveCategory.STATUS, -1, 100, 30, -1, "Lowers opponent's Defense.", -1, 1, new StatChangeAttr(BattleStat.DEF, -1)), - new Move(Moves.POISON_STING, "Poison Sting", Type.POISON, MoveCategory.PHYSICAL, 15, 100, 35, -1, "May poison the opponent.", 30, 1), - new Move(Moves.TWINEEDLE, "Twineedle", Type.BUG, MoveCategory.PHYSICAL, 25, 100, 20, -1, "Hits twice in one turn. May poison opponent.", 20, 1, new MultiHitAttr(MultiHitType._2)), + new Move(Moves.POISON_STING, "Poison Sting", Type.POISON, MoveCategory.PHYSICAL, 15, 100, 35, -1, "May poison the opponent.", 30, 1, new StatusEffectAttr(StatusEffect.POISON)), + new Move(Moves.TWINEEDLE, "Twineedle", Type.BUG, MoveCategory.PHYSICAL, 25, 100, 20, -1, "Hits twice in one turn. May poison opponent.", 20, 1, new MultiHitAttr(MultiHitType._2), new StatusEffectAttr(StatusEffect.POISON)), new Move(Moves.PIN_MISSILE, "Pin Missile", Type.BUG, MoveCategory.PHYSICAL, 25, 95, 20, -1, "Hits 2-5 times in one turn.", -1, 1,new MultiHitAttr()), new Move(Moves.LEER, "Leer", Type.NORMAL, MoveCategory.STATUS, -1, 100, 30, -1, "Lowers opponent's Defense.", 100, 1, new StatChangeAttr(BattleStat.DEF, -1)), new Move(Moves.BITE, "Bite", Type.DARK, MoveCategory.PHYSICAL, 60, 100, 25, -1, "May cause flinching.", 30, 1, new FlinchAttr()), new Move(Moves.GROWL, "Growl", Type.NORMAL, MoveCategory.STATUS, -1, 100, 40, -1, "Lowers opponent's Attack.", -1, 1, new StatChangeAttr(BattleStat.ATK, -1)), new Move(Moves.ROAR, "Roar", Type.NORMAL, MoveCategory.STATUS, -1, -1, 20, -1, "In battles, the opponent switches. In the wild, the Pokémon runs.", -1, 1), - new Move(Moves.SING, "Sing", Type.NORMAL, MoveCategory.STATUS, -1, 55, 15, -1, "Puts opponent to sleep.", -1, 1), + new Move(Moves.SING, "Sing", Type.NORMAL, MoveCategory.STATUS, -1, 55, 15, -1, "Puts opponent to sleep.", -1, 1, new StatusEffectAttr(StatusEffect.SLEEP)), new Move(Moves.SUPERSONIC, "Supersonic", Type.NORMAL, MoveCategory.STATUS, -1, 55, 20, -1, "Confuses opponent.", -1, 1), new Move(Moves.SONIC_BOOM, "Sonic Boom", Type.NORMAL, MoveCategory.SPECIAL, -1, 90, 20, -1, "Always inflicts 20 HP.", -1, 1), new Move(Moves.DISABLE, "Disable", Type.NORMAL, MoveCategory.STATUS, -1, 100, 20, -1, "Opponent can't use its last attack for a few turns.", -1, 1), new Move(Moves.ACID, "Acid", Type.POISON, MoveCategory.SPECIAL, 40, 100, 30, -1, "May lower opponent's Special Defense.", 10, 1, new StatChangeAttr(BattleStat.SPDEF, -1)), - new Move(Moves.EMBER, "Ember", Type.FIRE, MoveCategory.SPECIAL, 40, 100, 25, -1, "May burn opponent.", 10, 1), - new Move(Moves.FLAMETHROWER, "Flamethrower", Type.FIRE, MoveCategory.SPECIAL, 90, 100, 15, 125, "May burn opponent.", 10, 1), + new Move(Moves.EMBER, "Ember", Type.FIRE, MoveCategory.SPECIAL, 40, 100, 25, -1, "May burn opponent.", 10, 1, new StatusEffectAttr(StatusEffect.BURN)), + new Move(Moves.FLAMETHROWER, "Flamethrower", Type.FIRE, MoveCategory.SPECIAL, 90, 100, 15, 125, "May burn opponent.", 10, 1, new StatusEffectAttr(StatusEffect.BURN)), new Move(Moves.MIST, "Mist", Type.ICE, MoveCategory.STATUS, -1, -1, 30, -1, "User's stats cannot be changed for a period of time.", -1, 1), new Move(Moves.WATER_GUN, "Water Gun", Type.WATER, MoveCategory.SPECIAL, 40, 100, 25, -1, "", -1, 1), new Move(Moves.HYDRO_PUMP, "Hydro Pump", Type.WATER, MoveCategory.SPECIAL, 110, 80, 5, 142, "", -1, 1), new Move(Moves.SURF, "Surf", Type.WATER, MoveCategory.SPECIAL, 90, 100, 15, 123, "Hits all adjacent Pokémon.", -1, 1), - new Move(Moves.ICE_BEAM, "Ice Beam", Type.ICE, MoveCategory.SPECIAL, 90, 100, 10, 135, "May freeze opponent.", 10, 1), - new Move(Moves.BLIZZARD, "Blizzard", Type.ICE, MoveCategory.SPECIAL, 110, 70, 5, 143, "May freeze opponent.", 10, 1), + new Move(Moves.ICE_BEAM, "Ice Beam", Type.ICE, MoveCategory.SPECIAL, 90, 100, 10, 135, "May freeze opponent.", 10, 1, new StatusEffectAttr(StatusEffect.FREEZE)), + new Move(Moves.BLIZZARD, "Blizzard", Type.ICE, MoveCategory.SPECIAL, 110, 70, 5, 143, "May freeze opponent.", 10, 1, new StatusEffectAttr(StatusEffect.FREEZE)), new Move(Moves.PSYBEAM, "Psybeam", Type.PSYCHIC, MoveCategory.SPECIAL, 65, 100, 20, 16, "May confuse opponent.", 10, 1), new Move(Moves.BUBBLE_BEAM, "Bubble Beam", Type.WATER, MoveCategory.SPECIAL, 65, 100, 20, -1, "May lower opponent's Speed.", 10, 1, new StatChangeAttr(BattleStat.SPD, -1)), new Move(Moves.AURORA_BEAM, "Aurora Beam", Type.ICE, MoveCategory.SPECIAL, 65, 100, 20, -1, "May lower opponent's Attack.", 10, 1, new StatChangeAttr(BattleStat.ATK, -1)), @@ -838,9 +843,9 @@ export const allMoves = [ new StatChangeAttr([ BattleStat.ATK, BattleStat.SPATK ], 1, true)), new Move(Moves.RAZOR_LEAF, "Razor Leaf", Type.GRASS, MoveCategory.PHYSICAL, 55, 95, 25, -1, "High critical hit ratio.", -1, 1), new Move(Moves.SOLAR_BEAM, "Solar Beam", Type.GRASS, MoveCategory.SPECIAL, 120, 100, 10, 168, "Charges on first turn, attacks on second.", -1, 1), - new Move(Moves.POISON_POWDER, "Poison Powder", Type.POISON, MoveCategory.STATUS, -1, 75, 35, -1, "Poisons opponent.", -1, 1), - new Move(Moves.STUN_SPORE, "Stun Spore", Type.GRASS, MoveCategory.STATUS, -1, 75, 30, -1, "Paralyzes opponent.", -1, 1), - new Move(Moves.SLEEP_POWDER, "Sleep Powder", Type.GRASS, MoveCategory.STATUS, -1, 75, 15, -1, "Puts opponent to sleep.", -1, 1), + new Move(Moves.POISON_POWDER, "Poison Powder", Type.POISON, MoveCategory.STATUS, -1, 75, 35, -1, "Poisons opponent.", -1, 1, new StatusEffectAttr(StatusEffect.POISON)), + new Move(Moves.STUN_SPORE, "Stun Spore", Type.GRASS, MoveCategory.STATUS, -1, 75, 30, -1, "Paralyzes opponent.", -1, 1, new StatusEffectAttr(StatusEffect.PARALYSIS)), + new Move(Moves.SLEEP_POWDER, "Sleep Powder", Type.GRASS, MoveCategory.STATUS, -1, 75, 15, -1, "Puts opponent to sleep.", -1, 1, new StatusEffectAttr(StatusEffect.SLEEP)), new Move(Moves.PETAL_DANCE, "Petal Dance", Type.GRASS, MoveCategory.SPECIAL, 120, 100, 10, -1, "User attacks for 2-3 turns but then becomes confused.", -1, 1), new Move(Moves.STRING_SHOT, "String Shot", Type.BUG, MoveCategory.STATUS, -1, 95, 40, -1, "Sharply lowers opponent's Speed.", -1, 1, new StatChangeAttr(BattleStat.SPD, -2)), new Move(Moves.DRAGON_RAGE, "Dragon Rage", Type.DRAGON, MoveCategory.SPECIAL, -1, 100, 10, -1, "Always inflicts 40 HP.", -1, 1), @@ -944,7 +949,7 @@ export const allMoves = [ new Move(Moves.COTTON_SPORE, "Cotton Spore", Type.GRASS, MoveCategory.STATUS, -1, 100, 40, -1, "Sharply lowers opponent's Speed.", -1, 2, new StatChangeAttr(BattleStat.SPD, -2)), new Move(Moves.REVERSAL, "Reversal", Type.FIGHTING, MoveCategory.PHYSICAL, -1, 100, 15, 134, "The lower the user's HP, the higher the power.", -1, 2), new Move(Moves.SPITE, "Spite", Type.GHOST, MoveCategory.STATUS, -1, 100, 10, -1, "The opponent's last move loses 2-5 PP.", -1, 2), - new Move(Moves.POWDER_SNOW, "Powder Snow", Type.ICE, MoveCategory.SPECIAL, 40, 100, 25, -1, "May freeze opponent.", 10, 2), + new Move(Moves.POWDER_SNOW, "Powder Snow", Type.ICE, MoveCategory.SPECIAL, 40, 100, 25, -1, "May freeze opponent.", 10, 2, new StatusEffectAttr(StatusEffect.FREEZE)), new Move(Moves.PROTECT, "Protect", Type.NORMAL, MoveCategory.STATUS, -1, -1, 10, 7, "Protects the user, but may fail if used consecutively.", -1, 2), new Move(Moves.MACH_PUNCH, "Mach Punch", Type.FIGHTING, MoveCategory.PHYSICAL, 40, 100, 30, -1, "User attacks first.", -1, 2), new Move(Moves.SCARY_FACE, "Scary Face", Type.NORMAL, MoveCategory.STATUS, -1, 100, 10, 6, "Sharply lowers opponent's Speed.", -1, 2, new StatChangeAttr(BattleStat.SPD, -2)), @@ -1047,7 +1052,7 @@ export const allMoves = [ new Move(Moves.RECYCLE, "Recycle", Type.NORMAL, MoveCategory.STATUS, -1, -1, 10, -1, "User's used hold item is restored.", -1, 3), new Move(Moves.REVENGE, "Revenge", Type.FIGHTING, MoveCategory.PHYSICAL, 60, 100, 10, -1, "Power increases if user was hit first.", -1, 3), new Move(Moves.BRICK_BREAK, "Brick Break", Type.FIGHTING, MoveCategory.PHYSICAL, 75, 100, 15, 58, "Breaks through Reflect and Light Screen barriers.", -1, 3), - new Move(Moves.YAWN, "Yawn", Type.NORMAL, MoveCategory.STATUS, -1, -1, 10, -1, "Puts opponent to sleep in the next turn.", -1, 3), + new Move(Moves.YAWN, "Yawn", Type.NORMAL, MoveCategory.STATUS, -1, -1, 10, -1, "Puts opponent to sleep in the next turn.", -1, 3), // TODO new Move(Moves.KNOCK_OFF, "Knock Off", Type.DARK, MoveCategory.PHYSICAL, 65, 100, 20, -1, "Removes opponent's held item for the rest of the battle.", -1, 3), new Move(Moves.ENDEAVOR, "Endeavor", Type.NORMAL, MoveCategory.PHYSICAL, -1, 100, 5, -1, "Reduces opponent's HP to same as user's.", -1, 3), new Move(Moves.ERUPTION, "Eruption", Type.FIRE, MoveCategory.SPECIAL, 150, 100, 5, -1, "Stronger when the user's HP is higher.", -1, 3), @@ -1168,11 +1173,11 @@ export const allMoves = [ new Move(Moves.HEART_SWAP, "Heart Swap", Type.PSYCHIC, MoveCategory.STATUS, -1, -1, 10, -1, "Stat changes are swapped with the opponent.", -1, 4), new Move(Moves.AQUA_RING, "Aqua Ring", Type.WATER, MoveCategory.STATUS, -1, -1, 20, -1, "Restores a little HP each turn.", -1, 4), new Move(Moves.MAGNET_RISE, "Magnet Rise", Type.ELECTRIC, MoveCategory.STATUS, -1, -1, 10, -1, "User becomes immune to Ground-type moves for 5 turns.", -1, 4), - new Move(Moves.FLARE_BLITZ, "Flare Blitz", Type.FIRE, MoveCategory.PHYSICAL, 120, 100, 15, 165, "User receives recoil damage. May burn opponent.", 10, 4), - new Move(Moves.FORCE_PALM, "Force Palm", Type.FIGHTING, MoveCategory.PHYSICAL, 60, 100, 10, -1, "May paralyze opponent.", 30, 4), + new Move(Moves.FLARE_BLITZ, "Flare Blitz", Type.FIRE, MoveCategory.PHYSICAL, 120, 100, 15, 165, "User receives recoil damage. May burn opponent.", 10, 4, new StatusEffectAttr(StatusEffect.BURN)), // TODO + new Move(Moves.FORCE_PALM, "Force Palm", Type.FIGHTING, MoveCategory.PHYSICAL, 60, 100, 10, -1, "May paralyze opponent.", 30, 4, new StatusEffectAttr(StatusEffect.PARALYSIS)), new Move(Moves.AURA_SPHERE, "Aura Sphere", Type.FIGHTING, MoveCategory.SPECIAL, 80, 999, 20, 112, "Ignores Accuracy and Evasiveness.", -1, 4), new Move(Moves.ROCK_POLISH, "Rock Polish", Type.ROCK, MoveCategory.STATUS, -1, -1, 20, -1, "Sharply raises user's Speed.", -1, 4, new StatChangeAttr(BattleStat.SPD, 2, true)), - new Move(Moves.POISON_JAB, "Poison Jab", Type.POISON, MoveCategory.PHYSICAL, 80, 100, 20, 83, "May poison the opponent.", 30, 4), + new Move(Moves.POISON_JAB, "Poison Jab", Type.POISON, MoveCategory.PHYSICAL, 80, 100, 20, 83, "May poison the opponent.", 30, 4, new StatusEffectAttr(StatusEffect.POISON)), new Move(Moves.DARK_PULSE, "Dark Pulse", Type.DARK, MoveCategory.SPECIAL, 80, 100, 15, 94, "May cause flinching.", 20, 4, new FlinchAttr()), new Move(Moves.NIGHT_SLASH, "Night Slash", Type.DARK, MoveCategory.PHYSICAL, 70, 100, 15, -1, "High critical hit ratio.", -1, 4), new Move(Moves.AQUA_TAIL, "Aqua Tail", Type.WATER, MoveCategory.PHYSICAL, 90, 90, 10, -1, "", -1, 4), @@ -1209,8 +1214,8 @@ export const allMoves = [ new Move(Moves.DEFOG, "Defog", Type.FLYING, MoveCategory.STATUS, -1, -1, 15, -1, "Lowers opponent's Evasiveness and clears fog.", -1, 4, new StatChangeAttr(BattleStat.EVA, -1)), // TODO new Move(Moves.TRICK_ROOM, "Trick Room", Type.PSYCHIC, MoveCategory.STATUS, -1, -1, 5, 161, "Slower Pokémon move first in the turn for 5 turns.", -1, 4), new Move(Moves.DRACO_METEOR, "Draco Meteor", Type.DRAGON, MoveCategory.SPECIAL, 130, 90, 5, 169, "Sharply lowers user's Special Attack.", 100, 4, new StatChangeAttr(BattleStat.SPATK, -2, true)), - new Move(Moves.DISCHARGE, "Discharge", Type.ELECTRIC, MoveCategory.SPECIAL, 80, 100, 15, -1, "May paralyze opponent.", 30, 4), - new Move(Moves.LAVA_PLUME, "Lava Plume", Type.FIRE, MoveCategory.SPECIAL, 80, 100, 15, -1, "May burn opponent.", 30, 4), + new Move(Moves.DISCHARGE, "Discharge", Type.ELECTRIC, MoveCategory.SPECIAL, 80, 100, 15, -1, "May paralyze opponent.", 30, 4, new StatusEffectAttr(StatusEffect.PARALYSIS)), + new Move(Moves.LAVA_PLUME, "Lava Plume", Type.FIRE, MoveCategory.SPECIAL, 80, 100, 15, -1, "May burn opponent.", 30, 4, new StatusEffectAttr(StatusEffect.BURN)), new Move(Moves.LEAF_STORM, "Leaf Storm", Type.GRASS, MoveCategory.SPECIAL, 130, 90, 5, 159, "Sharply lowers user's Special Attack.", 100, 4, new StatChangeAttr(BattleStat.SPATK, -2, true)), new Move(Moves.POWER_WHIP, "Power Whip", Type.GRASS, MoveCategory.PHYSICAL, 120, 85, 10, -1, "", -1, 4), new Move(Moves.ROCK_WRECKER, "Rock Wrecker", Type.ROCK, MoveCategory.PHYSICAL, 150, 90, 5, -1, "User must recharge next turn.", -1, 4), @@ -1240,7 +1245,7 @@ export const allMoves = [ new Move(Moves.LUNAR_DANCE, "Lunar Dance", Type.PSYCHIC, MoveCategory.STATUS, -1, -1, 10, -1, "The user faints but the next Pokémon released is fully healed.", -1, 4), new Move(Moves.CRUSH_GRIP, "Crush Grip", Type.NORMAL, MoveCategory.PHYSICAL, -1, 100, 5, -1, "More powerful when opponent has higher HP.", -1, 4), new Move(Moves.MAGMA_STORM, "Magma Storm", Type.FIRE, MoveCategory.SPECIAL, 100, 75, 5, -1, "Traps opponent, damaging them for 4-5 turns.", 100, 4), - new Move(Moves.DARK_VOID, "Dark Void", Type.DARK, MoveCategory.STATUS, -1, 50, 10, -1, "Puts all adjacent opponents to sleep.", -1, 4), + new Move(Moves.DARK_VOID, "Dark Void", Type.DARK, MoveCategory.STATUS, -1, 50, 10, -1, "Puts all adjacent opponents to sleep.", -1, 4, new StatusEffectAttr(StatusEffect.SLEEP)), new Move(Moves.SEED_FLARE, "Seed Flare", Type.GRASS, MoveCategory.SPECIAL, 120, 85, 5, -1, "May lower opponent's Special Defense.", 40, 4, new StatChangeAttr(BattleStat.SPDEF, -1)), new Move(Moves.OMINOUS_WIND, "Ominous Wind", Type.GHOST, MoveCategory.SPECIAL, 60, 100, 5, -1, "May raise all user's stats at once.", 10, 4, new StatChangeAttr([ BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.SPD ], 1, true)), @@ -1283,7 +1288,7 @@ export const allMoves = [ new Move(Moves.STORED_POWER, "Stored Power", Type.PSYCHIC, MoveCategory.SPECIAL, 20, 100, 10, 41, "Power increases when user's stats have been raised.", -1, 5), new Move(Moves.QUICK_GUARD, "Quick Guard", Type.FIGHTING, MoveCategory.STATUS, -1, -1, 15, -1, "Protects the user's team from high-priority moves.", -1, 5), new Move(Moves.ALLY_SWITCH, "Ally Switch", Type.PSYCHIC, MoveCategory.STATUS, -1, -1, 15, -1, "User switches with opposite teammate.", -1, 5), - new Move(Moves.SCALD, "Scald", Type.WATER, MoveCategory.SPECIAL, 80, 100, 15, -1, "May burn opponent.", 30, 5), + new Move(Moves.SCALD, "Scald", Type.WATER, MoveCategory.SPECIAL, 80, 100, 15, -1, "May burn opponent.", 30, 5, new StatusEffectAttr(StatusEffect.BURN)), new Move(Moves.SHELL_SMASH, "Shell Smash", Type.NORMAL, MoveCategory.STATUS, -1, -1, 15, -1, "Sharply raises user's Attack, Special Attack and Speed but lowers Defense and Special Defense.", -1, 5, new StatChangeAttr([ BattleStat.ATK, BattleStat.SPATK ], 2, true), new StatChangeAttr([ BattleStat.DEF, BattleStat.SPDEF ], -1, true)), new Move(Moves.HEAL_PULSE, "Heal Pulse", Type.PSYCHIC, MoveCategory.STATUS, -1, -1, 10, -1, "Restores half the target's max HP.", -1, 5), @@ -1330,14 +1335,14 @@ export const allMoves = [ new Move(Moves.GEAR_GRIND, "Gear Grind", Type.STEEL, MoveCategory.PHYSICAL, 50, 85, 15, -1, "Hits twice in one turn.", -1, 5, new MultiHitAttr(MultiHitType._2)), new Move(Moves.SEARING_SHOT, "Searing Shot", Type.FIRE, MoveCategory.SPECIAL, 100, 100, 5, -1, "May burn opponent.", 30, 5, new StatusEffectAttr(StatusEffect.BURN)), new Move(Moves.TECHNO_BLAST, "Techno Blast", Type.NORMAL, MoveCategory.SPECIAL, 120, 100, 5, -1, "Type depends on the Drive being held.", -1, 5), - new Move(Moves.RELIC_SONG, "Relic Song", Type.NORMAL, MoveCategory.SPECIAL, 75, 100, 10, -1, "May put the target to sleep.", 10, 5), + new Move(Moves.RELIC_SONG, "Relic Song", Type.NORMAL, MoveCategory.SPECIAL, 75, 100, 10, -1, "May put the target to sleep.", 10, 5, new StatusEffectAttr(StatusEffect.SLEEP)), new Move(Moves.SECRET_SWORD, "Secret Sword", Type.FIGHTING, MoveCategory.SPECIAL, 85, 100, 10, -1, "Inflicts damage based on the target's Defense, not Special Defense.", -1, 5), new Move(Moves.GLACIATE, "Glaciate", Type.ICE, MoveCategory.SPECIAL, 65, 95, 10, -1, "Lowers opponent's Speed.", 100, 5, new StatChangeAttr(BattleStat.SPD, -1)), new Move(Moves.BOLT_STRIKE, "Bolt Strike", Type.ELECTRIC, MoveCategory.PHYSICAL, 130, 85, 5, -1, "May paralyze opponent.", 20, 5, new StatusEffectAttr(StatusEffect.PARALYSIS)), new Move(Moves.BLUE_FLARE, "Blue Flare", Type.FIRE, MoveCategory.SPECIAL, 130, 85, 5, -1, "May burn opponent.", 20, 5, new StatusEffectAttr(StatusEffect.BURN)), new Move(Moves.FIERY_DANCE, "Fiery Dance", Type.FIRE, MoveCategory.SPECIAL, 80, 100, 10, -1, "May raise user's Special Attack.", 50, 5, new StatChangeAttr(BattleStat.SPATK, 1, true)), - new Move(Moves.FREEZE_SHOCK, "Freeze Shock", Type.ICE, MoveCategory.PHYSICAL, 140, 90, 5, -1, "Charges on first turn, attacks on second. May paralyze opponent.", 30, 5), - new Move(Moves.ICE_BURN, "Ice Burn", Type.ICE, MoveCategory.SPECIAL, 140, 90, 5, -1, "Charges on first turn, attacks on second. May burn opponent.", 30, 5), + new Move(Moves.FREEZE_SHOCK, "Freeze Shock", Type.ICE, MoveCategory.PHYSICAL, 140, 90, 5, -1, "Charges on first turn, attacks on second. May paralyze opponent.", 30, 5, new StatusEffectAttr(StatusEffect.PARALYSIS)), + new Move(Moves.ICE_BURN, "Ice Burn", Type.ICE, MoveCategory.SPECIAL, 140, 90, 5, -1, "Charges on first turn, attacks on second. May burn opponent.", 30, 5, new StatusEffectAttr(StatusEffect.BURN)), new Move(Moves.SNARL, "Snarl", Type.DARK, MoveCategory.SPECIAL, 55, 95, 15, 30, "Lowers opponent's Special Attack.", 100, 5, new StatChangeAttr(BattleStat.SPATK, -1)), new Move(Moves.ICICLE_CRASH, "Icicle Crash", Type.ICE, MoveCategory.PHYSICAL, 85, 90, 10, -1, "May cause flinching.", 30, 5, new FlinchAttr()), new Move(Moves.V_CREATE, "V-create", Type.FIRE, MoveCategory.PHYSICAL, 180, 95, 5, -1, "Lowers user's Defense, Special Defense and Speed.", 100, 5, diff --git a/src/pokemon.ts b/src/pokemon.ts index 0672b1901..2871fd31a 100644 --- a/src/pokemon.ts +++ b/src/pokemon.ts @@ -5,14 +5,14 @@ import { default as Move, allMoves, MoveCategory, Moves, StatChangeAttr, applyMo import { pokemonLevelMoves } from './pokemon-level-moves'; import { default as PokemonSpecies, getPokemonSpecies } from './pokemon-species'; import * as Utils from './utils'; -import { getTypeDamageMultiplier } from './type'; +import { Type, getTypeDamageMultiplier } from './type'; import { getLevelTotalExp } from './exp'; import { Stat } from './pokemon-stat'; import { PokemonBaseStatModifier as PokemonBaseStatBoosterModifier, ShinyRateBoosterModifier } from './modifier'; import { PokeballType } from './pokeball'; import { Gender } from './gender'; -import { initAnim, loadMoveAnimAssets } from './battle-anims'; -import { StatusEffect } from './status-effect'; +import { initMoveAnim, loadMoveAnimAssets } from './battle-anims'; +import { Status, StatusEffect } from './status-effect'; import { tmSpecies } from './tms'; import { pokemonEvolutions, SpeciesEvolution, SpeciesEvolutionCondition } from './pokemon-evolutions'; import { MessagePhase, StatChangePhase } from './battle-phases'; @@ -33,7 +33,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { public stats: integer[]; public ivs: integer[]; public moveset: PokemonMove[]; - public status: StatusEffect; + public status: Status; public winCount: integer; public summonData: PokemonSummonData; @@ -109,8 +109,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { /*else this.shiny = Utils.randInt(16) === 0;*/ - this.status = StatusEffect.NONE; - this.winCount = 0; } @@ -162,7 +160,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { loadAssets(): Promise { return new Promise(resolve => { const moveIds = this.moveset.map(m => m.getMove().id); - Promise.allSettled(moveIds.map(m => initAnim(m))) + Promise.allSettled(moveIds.map(m => initMoveAnim(m))) .then(() => { loadMoveAnimAssets(this.scene as BattleScene, moveIds); (this.scene as BattleScene).loadAtlas(this.getSpriteKey(), 'pokemon', this.getSpriteAtlasPath()); @@ -258,7 +256,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (stat === Stat.HP) return this.stats[Stat.HP]; const statLevel = this.summonData.battleStats[(stat + 1) as BattleStat]; - return this.stats[stat] * (Math.max(2, 2 + statLevel) / Math.max(2, 2 - statLevel)); + let ret = this.stats[stat] * (Math.max(2, 2 + statLevel) / Math.max(2, 2 - statLevel)); + if (this.status && this.status.effect === StatusEffect.PARALYSIS) + ret >>= 2; + return ret; } calculateStats(): void { @@ -436,6 +437,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const typeMultiplier = getTypeDamageMultiplier(move.type, this.species.type1) * (this.species.type2 > -1 ? getTypeDamageMultiplier(move.type, this.species.type2) : 1); const criticalMultiplier = isCritical ? 2 : 1; damage = Math.ceil(((((2 * source.level / 5 + 2) * move.power * sourceAtk / targetDef) / 50) + 2) * stabMultiplier * typeMultiplier * ((Utils.randInt(15) + 85) / 100)) * criticalMultiplier; + if (isPhysical && source.status && source.status.effect === StatusEffect.BURN) + damage = Math.floor(damage / 2); console.log('damage', damage, move.name, move.power, sourceAtk, targetDef); if (damage) { this.hp = Math.max(this.hp - damage, 0); @@ -503,8 +506,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { callback(); } - cry(soundConfig?: Phaser.Types.Sound.SoundConfig) { + cry(soundConfig?: Phaser.Types.Sound.SoundConfig): integer { this.scene.sound.play(this.species.speciesId.toString(), soundConfig); + return this.scene.sound.get(this.species.speciesId.toString()).totalDuration * 1000; } faintCry(callback: Function) { @@ -555,7 +559,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } }); // Failsafe - this.scene.time.delayedCall(5000, () => { + this.scene.time.delayedCall(3000, () => { if (!faintCryTimer || !this.scene) return; const crySound = this.scene.sound.get(key); @@ -567,16 +571,42 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { }); } - resetSummonData() { + trySetStatus(effect: StatusEffect): boolean { + if (this.status) + return false; + switch (effect) { + case StatusEffect.POISON: + case StatusEffect.TOXIC: + if (this.species.isOfType(Type.POISON) || this.species.isOfType(Type.STEEL)) + return false; + break; + case StatusEffect.FREEZE: + if (this.species.isOfType(Type.ICE)) + return false; + break; + case StatusEffect.BURN: + if (this.species.isOfType(Type.FIRE)) + return false; + break; + } + this.status = new Status(effect); + return true; + } + + resetStatus(): void { + this.status = undefined; + } + + resetSummonData(): void { this.summonData = new PokemonSummonData(); this.resetBattleSummonData(); } - resetBattleSummonData() { + resetBattleSummonData(): void { this.battleSummonData = new PokemonBattleSummonData(); } - resetTurnData() { + resetTurnData(): void { this.turnData = new PokemonTurnData(); } @@ -815,6 +845,7 @@ export class PokemonTurnData { public flinched: boolean; public hitCount: integer; public hitsLeft: integer; + public hitsTotal: integer; } export enum AiType { diff --git a/src/status-effect.ts b/src/status-effect.ts index 0185ca7b9..a1426050a 100644 --- a/src/status-effect.ts +++ b/src/status-effect.ts @@ -1,3 +1,5 @@ +import * as Utils from "./utils"; + export enum StatusEffect { NONE, POISON, @@ -9,10 +11,95 @@ export enum StatusEffect { } export class Status { - public statusType: StatusEffect; + public effect: StatusEffect; public turnCount: integer; + public cureTurn: integer; - constructor(statusType: StatusEffect) { - this.statusType = statusType; + constructor(effect: StatusEffect) { + this.effect = effect; + this.turnCount = 0; + if (effect === StatusEffect.SLEEP) + this.cureTurn = Utils.randInt(3, 1); } + + incrementTurn(): void { + this.turnCount++; + } + + isPostTurn(): boolean { + return this.effect === StatusEffect.POISON || this.effect === StatusEffect.TOXIC || this.effect === StatusEffect.BURN; + } +} + +export function getStatusEffectObtainText(statusEffect: StatusEffect): string { + switch (statusEffect) { + case StatusEffect.POISON: + return '\nwas poisoned!'; + case StatusEffect.TOXIC: + return '\nwas badly poisoned!'; + case StatusEffect.PARALYSIS: + return ' is paralyzed!\nIt may be unable to move!'; + case StatusEffect.SLEEP: + return '\nfell asleep!'; + case StatusEffect.FREEZE: + return '\nwas frozen solid!'; + case StatusEffect.BURN: + return '\nwas burned!'; + } + + return ''; +} + +export function getStatusEffectActivationText(statusEffect: StatusEffect): string { + switch (statusEffect) { + case StatusEffect.POISON: + case StatusEffect.TOXIC: + return ' is hurt\nby poison!'; + case StatusEffect.PARALYSIS: + return ' is paralyzed!\nIt can\'t move!'; + case StatusEffect.SLEEP: + return ' is fast asleep.'; + case StatusEffect.FREEZE: + return ' is\nfrozen solid!'; + case StatusEffect.BURN: + return ' is hurt\nby its burn!'; + } + + return ''; +} + +export function getStatusEffectOverlapText(statusEffect: StatusEffect): string { + switch (statusEffect) { + case StatusEffect.POISON: + case StatusEffect.TOXIC: + return ' is\nalready poisoned!'; + case StatusEffect.PARALYSIS: + return ' is\nalready paralyzed!'; + case StatusEffect.SLEEP: + return ' is\nalready asleep!'; + case StatusEffect.FREEZE: + return ' is\nalready frozen!'; + case StatusEffect.BURN: + return ' is\nalready burned!'; + } + + return ''; +} + +export function getStatusEffectHealText(statusEffect: StatusEffect) { + switch (statusEffect) { + case StatusEffect.POISON: + case StatusEffect.TOXIC: + return ' was\ncured of its poison!'; + case StatusEffect.PARALYSIS: + return ' was\nhealed of paralysis!'; + case StatusEffect.SLEEP: + return ' woke up!'; + case StatusEffect.FREEZE: + return ' was\ndefrosted!'; + case StatusEffect.BURN: + return ' is hurt\nby its burn!'; + } + + return ''; } \ No newline at end of file