From eedad7d6784c68b10954225032412fa76e59fd80 Mon Sep 17 00:00:00 2001 From: Flashfyre Date: Sun, 7 Jan 2024 23:17:24 -0500 Subject: [PATCH] Add boss health bars --- public/images/ui/overlay_hp_boss.json | 83 +++++++++++ public/images/ui/overlay_hp_boss.png | Bin 0 -> 139 bytes public/images/ui/pbinfo_enemy_boss.png | Bin 0 -> 630 bytes src/arena.ts | 2 +- src/battle-phases.ts | 40 ++++-- src/battle-scene.ts | 51 ++++++- src/data/battler-tag.ts | 2 +- src/data/move.ts | 158 +++++++++++++-------- src/data/trainer-type.ts | 21 +-- src/egg-hatch-phase.ts | 4 +- src/main.ts | 1 + src/pokemon.ts | 188 +++++++++++++++++++++---- src/system/pokemon-data.ts | 8 +- src/trainer.ts | 2 +- src/ui/battle-info.ts | 47 ++++++- src/ui/text.ts | 2 +- src/utils.ts | 2 +- 17 files changed, 496 insertions(+), 115 deletions(-) create mode 100644 public/images/ui/overlay_hp_boss.json create mode 100644 public/images/ui/overlay_hp_boss.png create mode 100644 public/images/ui/pbinfo_enemy_boss.png diff --git a/public/images/ui/overlay_hp_boss.json b/public/images/ui/overlay_hp_boss.json new file mode 100644 index 000000000..06313756e --- /dev/null +++ b/public/images/ui/overlay_hp_boss.json @@ -0,0 +1,83 @@ +{ + "textures": [ + { + "image": "overlay_hp_boss.png", + "format": "RGBA8888", + "size": { + "w": 96, + "h": 12 + }, + "scale": 1, + "frames": [ + { + "filename": "high", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 86, + "h": 4 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 86, + "h": 4 + }, + "frame": { + "x": 0, + "y": 0, + "w": 86, + "h": 4 + } + }, + { + "filename": "medium", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 86, + "h": 4 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 98, + "h": 4 + }, + "frame": { + "x": 0, + "y": 4, + "w": 86, + "h": 4 + } + }, + { + "filename": "low", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 86, + "h": 4 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 86, + "h": 4 + }, + "frame": { + "x": 0, + "y": 8, + "w": 86, + "h": 4 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:0ab3d30defc8fe4d0802d006063b09c5:9cc1fb380aa2908ff6c9e92f310fde62:e6c4614fcfcf040f918551c90d4448f7$" + } +} diff --git a/public/images/ui/overlay_hp_boss.png b/public/images/ui/overlay_hp_boss.png new file mode 100644 index 0000000000000000000000000000000000000000..d7dc3b74e0f3edcfa0b531cb63218c6f747f3f81 GIT binary patch literal 139 zcmeAS@N?(olHy`uVBq!ia0vp^VL;5o!2~4B74I4XsUS}m#}JR>$vwNS9WvO&$Sp4a z--DmYFK*vNIhLFn8)X{~iSuipO1xa@5a3d=guSuB;D3fd&kuzNab1f`%?{JPz6UCo m?7OdQ%b~J!fBy#s1_rqw9CuiSn7VJP)RLTsxi4)UYygC3QvS9Ca5N$+?`P4rJ=U1$U8=kv(^jS6Af9$i=q;mx}9h zA#*9Y<5H6HKDG0*!!XOo&h8Jp7D~**AXh2_A#FfP8(**I)}ek@SzxU*Y;?*IBqwJe z90+08lDTgrPSX?{onjyy#K9B;;cz6$$=K)=)72VU*IOO*0YIsTSvU@+_~iH;MRIa% zDY3)~-h2rmCI|cXW?_(PonasxN=#a}w5UtZ%*IQ9$dMzRrS}s132Cj!givHcC^8`w znGlLhC^8`wnGlLh2t_9UEB!9LS+&kEk0O3*9cAHru%$e2k&VR3*xt+C+sD886`xh* z#b=dy7}L527`YdSgJ~l&cShv$OzhrfNX9mjvAt{mEiY4VwZVJSLOgXIjg>}TyK+Ig$#ctZBlijbYMrF+7wvDm$Z}{S2?H8& z27FIf*yt3)j}He=mC7&+gS<*+>(Za(=E(KZAM(f(IVt8E0CP2k5c_4)s+rm1#Vn+4 zJSFfUnJs|VWRdPsmbn!LQ+|=iu`;)&@=uw`F*3LAC#+c* { applyAbAttrs(SyncEncounterNatureAbAttr, playerPokemon, null, battle.enemyParty[e]); }); @@ -386,8 +385,10 @@ export class EncounterPhase extends BattlePhase { this.scene.gameData.setPokemonSeen(enemyPokemon); } - if (this.scene.gameMode === GameMode.CLASSIC && (battle.waveIndex === 200 || !(battle.waveIndex % 250)) && enemyPokemon.species.speciesId === Species.ETERNATUS) + if (this.scene.gameMode === GameMode.CLASSIC && (battle.waveIndex === 200 || !(battle.waveIndex % 250)) && enemyPokemon.species.speciesId === Species.ETERNATUS) { enemyPokemon.formIndex = 1; + enemyPokemon.setBoss(); + } loadEnemyAssets.push(enemyPokemon.loadAssets()); @@ -1246,15 +1247,21 @@ export class CommandPhase extends FieldPhase { this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); }, null, true); } else if (cursor < 4) { - this.scene.currentBattle.turnCommands[this.fieldIndex] = { command: Command.BALL, cursor: cursor }; - if (targets.length > 1) - this.scene.unshiftPhase(new SelectTargetPhase(this.scene, this.fieldIndex)); - else { + const targetPokemon = this.scene.getEnemyField().find(p => p.isActive(true)); + if (targetPokemon.isBoss() && targetPokemon.getBossSegmentIndex()) { + this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); + this.scene.ui.setMode(Mode.MESSAGE); + this.scene.ui.showText(`The target Pokémon is too strong to be caught!\nYou need to weaken it first!`, null, () => { + this.scene.ui.showText(null, 0); + this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); + }, null, true); + } else { + this.scene.currentBattle.turnCommands[this.fieldIndex] = { command: Command.BALL, cursor: cursor }; this.scene.currentBattle.turnCommands[this.fieldIndex].targets = targets; if (this.fieldIndex) this.scene.currentBattle.turnCommands[this.fieldIndex - 1].skip = true; + success = true; } - success = true; } } break; @@ -1741,6 +1748,9 @@ export class MovePhase extends BattlePhase { if (!lastMove.length || lastMove[0].move !== this.move.getMove().id || lastMove[0].result !== MoveResult.OTHER) return; } + + if (this.pokemon.getTag(BattlerTagType.RECHARGING)) + return; this.scene.queueMessage(getPokemonMessage(this.pokemon, ` used\n${this.move.getName()}!`), 500); } @@ -2951,7 +2961,7 @@ export class PokemonHealPhase extends CommonAnimPhase { if (!this.revive) this.scene.applyModifiers(HealingBoosterModifier, this.player, hpRestoreMultiplier); const healAmount = new Utils.NumberHolder(this.hpHealed * hpRestoreMultiplier.value); - pokemon.heal(healAmount.value); + healAmount.value = pokemon.heal(healAmount.value); this.scene.validateAchvs(HealAchv, healAmount); pokemon.updateInfo().then(() => super.end()); } else if (this.showFullHpMessage) @@ -2979,7 +2989,7 @@ export class AttemptCapturePhase extends PokemonPhase { start() { super.start(); - const pokemon = this.getPokemon(); + const pokemon = this.getPokemon() as EnemyPokemon; if (!pokemon?.hp) return this.end(); @@ -2988,7 +2998,11 @@ export class AttemptCapturePhase extends PokemonPhase { this.originalY = pokemon.y; - const _3m = 3 * pokemon.getMaxHp(); + const relMaxHp = !pokemon.isBoss() + ? pokemon.getMaxHp() + : Math.round(pokemon.getMaxHp() / pokemon.bossSegments); + + const _3m = 3 * relMaxHp; const _2h = 2 * pokemon.hp; const catchRate = pokemon.species.catchRate; const pokeballMultiplier = getPokeballCatchMultiplier(this.pokeballType); diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 7f22d400a..c2c12db85 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -41,6 +41,8 @@ import { Voucher, vouchers } from './system/voucher'; import { Gender } from './data/gender'; import UIPlugin from 'phaser3-rex-plugins/templates/ui/ui-plugin'; import { WindowVariant, getWindowVariantSuffix } from './ui/window'; +import PokemonData from './system/pokemon-data'; +import { Nature } from './data/nature'; const enableAuto = true; const quickStart = false; @@ -207,10 +209,12 @@ export default class BattleScene extends Phaser.Scene { this.loadImage('pbinfo_player', 'ui'); this.loadImage('pbinfo_player_mini', 'ui'); this.loadImage('pbinfo_enemy_mini', 'ui'); + this.loadImage('pbinfo_enemy_boss', 'ui'); this.loadImage('overlay_lv', 'ui'); this.loadAtlas('numbers', 'ui'); this.loadAtlas('numbers_red', 'ui'); this.loadAtlas('overlay_hp', 'ui'); + this.loadAtlas('overlay_hp_boss', 'ui'); this.loadImage('overlay_exp', 'ui'); this.loadImage('icon_owned', 'ui'); this.loadImage('ability_bar', 'ui'); @@ -526,7 +530,7 @@ export default class BattleScene extends Phaser.Scene { for (let s = 0; s < 3; s++) { const playerSpecies = this.randomSpecies(startingWave, startingLevel); - const playerPokemon = new PlayerPokemon(this, playerSpecies, startingLevel, 0, 0); + const playerPokemon = this.addPlayerPokemon(playerSpecies, startingLevel, 0, 0); playerPokemon.setVisible(false); this.party.push(playerPokemon); @@ -637,6 +641,22 @@ export default class BattleScene extends Phaser.Scene { return findInParty(this.getParty()) || findInParty(this.getEnemyParty()); } + addPlayerPokemon(species: PokemonSpecies, level: integer, abilityIndex: integer, formIndex: integer, gender?: Gender, shiny?: boolean, ivs?: integer[], nature?: Nature, dataSource?: Pokemon | PokemonData, postProcess?: (playerPokemon: PlayerPokemon) => void): PlayerPokemon { + const pokemon = new PlayerPokemon(this, species, level, abilityIndex, formIndex, gender, shiny, ivs, nature, dataSource); + if (postProcess) + postProcess(pokemon); + pokemon.init(); + return pokemon; + } + + addEnemyPokemon(species: PokemonSpecies, level: integer, trainer: boolean, boss: boolean = false, dataSource?: PokemonData, postProcess?: (enemyPokemon: EnemyPokemon) => void): EnemyPokemon { + const pokemon = new EnemyPokemon(this, species, level, trainer, boss, dataSource); + if (postProcess) + postProcess(pokemon); + pokemon.init(); + return pokemon; + } + reset(clearScene?: boolean): void { this.seed = Utils.randomString(16); console.log('Seed:', this.seed); @@ -715,7 +735,7 @@ export default class BattleScene extends Phaser.Scene { if (this.gameMode !== GameMode.CLASSIC) newBattleType = BattleType.WILD; else if (battleType === undefined) { - if ((newWaveIndex % 30) === 20) + if ((newWaveIndex % 30) === 20 && newWaveIndex !== 200) newBattleType = BattleType.TRAINER; else if (newWaveIndex % 10 !== 1 && newWaveIndex % 10) { const trainerChance = this.arena.getTrainerChance(); @@ -860,6 +880,33 @@ export default class BattleScene extends Phaser.Scene { return ret; } + getEncounterBossSegments(waveIndex: integer, level: integer, species?: PokemonSpecies, forceBoss: boolean = false): integer { + let isBoss: boolean; + if (forceBoss || (species && (species.pseudoLegendary || species.legendary || species.mythical))) + isBoss = true; + else { + this.executeWithSeedOffset(() => { + isBoss = waveIndex % 10 === 0 || (this.gameMode !== GameMode.CLASSIC && Utils.randSeedInt(100) < Math.min(Math.max(Math.ceil((waveIndex - 250) / 50), 0) * 2, 30)); + }, waveIndex << 2); + } + if (!isBoss) + return 0; + + let ret: integer = 2; + + if (level >= 100) + ret++; + if (species) { + if (species.baseTotal >= 670) + ret++; + if (species.legendary) + ret++; + } + ret += Math.floor(waveIndex / 250); + + return ret; + } + trySpreadPokerus(): void { const party = this.getParty(); const infectedIndexes: integer[] = []; diff --git a/src/data/battler-tag.ts b/src/data/battler-tag.ts index 4c64b71eb..705792b97 100644 --- a/src/data/battler-tag.ts +++ b/src/data/battler-tag.ts @@ -610,7 +610,7 @@ export class PerishSongTag extends BattlerTag { pokemon.scene.queueMessage(getPokemonMessage(pokemon, `\'s perish count fell to ${this.turnCount}.`)); else { pokemon.scene.unshiftPhase(new DamagePhase(pokemon.scene, pokemon.getBattlerIndex(), HitResult.ONE_HIT_KO)); - pokemon.damage(pokemon.hp); + pokemon.damage(pokemon.hp, true, true); } return ret; diff --git a/src/data/move.ts b/src/data/move.ts index 684ee0e1a..90ebd99cc 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -58,8 +58,8 @@ export enum MoveFlags { WIND_MOVE = 8192 } -type MoveCondition = (user: Pokemon, target: Pokemon, move: Move) => boolean; -type UserMoveCondition = (user: Pokemon, move: Move) => boolean; +type MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => boolean; +type UserMoveConditionFunc = (user: Pokemon, move: Move) => boolean; export default class Move { public id: Moves; @@ -111,18 +111,24 @@ export default class Move { attr MoveAttr>(AttrType: T, ...args: ConstructorParameters): this { const attr = new AttrType(...args); this.attrs.push(attr); - const attrCondition = attr.getCondition(); - if (attrCondition) + let attrCondition = attr.getCondition(); + if (attrCondition) { + if (typeof attrCondition === 'function') + attrCondition = new MoveCondition(attrCondition); this.conditions.push(attrCondition); + } return this; } addAttr(attr: MoveAttr): this { this.attrs.push(attr); - const attrCondition = attr.getCondition(); - if (attrCondition) + let attrCondition = attr.getCondition(); + if (attrCondition) { + if (typeof attrCondition === 'function') + attrCondition = new MoveCondition(attrCondition); this.conditions.push(attrCondition); + } return this; } @@ -136,7 +142,9 @@ export default class Move { return !!(this.flags & flag); } - condition(condition: MoveCondition): this { + condition(condition: MoveCondition | MoveConditionFunc): this { + if (typeof condition === 'function') + condition = new MoveCondition(condition as MoveConditionFunc); this.conditions.push(condition); return this; @@ -232,7 +240,7 @@ export default class Move { applyConditions(user: Pokemon, target: Pokemon, move: Move): boolean { for (let condition of this.conditions) { - if (!condition(user, target, move)) + if (!condition.apply(user, target, move)) return false; } @@ -245,6 +253,9 @@ export default class Move { for (let attr of this.attrs) score += attr.getUserBenefitScore(user, target, move); + for (let condition of this.conditions) + score += condition.getUserBenefitScore(user, target, move); + return score; } @@ -252,7 +263,7 @@ export default class Move { let score = 0; for (let attr of this.attrs) - score += attr.getTargetBenefitScore(user, target, move); + score += attr.getTargetBenefitScore(user, !attr.selfTarget ? target : user, move) * (target !== user && attr.selfTarget ? -1 : 1); return score; } @@ -1237,11 +1248,17 @@ export enum Moves { }; export abstract class MoveAttr { + public selfTarget: boolean; + + constructor(selfTarget: boolean = false) { + this.selfTarget = selfTarget; + } + apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean | Promise { return true; } - getCondition(): MoveCondition { + getCondition(): MoveCondition | MoveConditionFunc { return null; } @@ -1261,13 +1278,10 @@ export enum MoveEffectTrigger { } export class MoveEffectAttr extends MoveAttr { - public selfTarget: boolean; public trigger: MoveEffectTrigger; constructor(selfTarget?: boolean, trigger?: MoveEffectTrigger) { - super(); - - this.selfTarget = !!selfTarget; + super(selfTarget); this.trigger = trigger !== undefined ? trigger : MoveEffectTrigger.POST_APPLY; } @@ -1360,7 +1374,7 @@ export class MatchHpAttr extends FixedDamageAttr { return true; } - getCondition(): MoveCondition { + getCondition(): MoveConditionFunc { return (user, target, move) => user.hp <= target.hp; } @@ -1388,7 +1402,7 @@ export class CounterDamageAttr extends FixedDamageAttr { return true; } - getCondition(): MoveCondition { + getCondition(): MoveConditionFunc { return (user, target, move) => !!user.turnData.attacksReceived.filter(ar => this.moveFilter(allMoves[ar.move])).length; } } @@ -1440,7 +1454,7 @@ export class RecoilAttr extends MoveEffectAttr { user.scene.unshiftPhase(new DamagePhase(user.scene, user.getBattlerIndex(), HitResult.OTHER)); user.scene.queueMessage(getPokemonMessage(user, ' is hit\nwith recoil!')); - user.damage(recoilDamage); + user.damage(recoilDamage, true); return true; } @@ -1460,7 +1474,7 @@ export class SacrificialAttr extends MoveEffectAttr { return false; user.scene.unshiftPhase(new DamagePhase(user.scene, user.getBattlerIndex(), HitResult.OTHER)); - user.damage(user.getMaxHp()); + user.damage(user.getMaxHp(), true, true); return true; } @@ -1500,7 +1514,7 @@ export class HealAttr extends MoveEffectAttr { } getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer { - return (1 - (this.selfTarget ? user : target).getHpRatio()) * 20; + return Math.round((1 - (this.selfTarget ? user : target).getHpRatio()) * 20); } } @@ -1772,7 +1786,7 @@ export class WeatherChangeAttr extends MoveEffectAttr { return user.scene.arena.trySetWeather(this.weatherType, true); } - getCondition(): MoveCondition { + getCondition(): MoveConditionFunc { return (user, target, move) => !user.scene.arena.weather || (user.scene.arena.weather.weatherType !== this.weatherType && !user.scene.arena.weather.isImmutable()); } } @@ -1804,7 +1818,7 @@ export class OneHitKOAttr extends MoveAttr { return true; } - getCondition(): MoveCondition { + getCondition(): MoveConditionFunc { return (user, target, move) => user.level >= target.level; } } @@ -1918,9 +1932,9 @@ export class DelayedAttackAttr extends OverrideMoveEffectAttr { export class StatChangeAttr extends MoveEffectAttr { public stats: BattleStat[]; public levels: integer; - private condition: MoveCondition; + private condition: MoveConditionFunc; - constructor(stats: BattleStat | BattleStat[], levels: integer, selfTarget?: boolean, condition?: MoveCondition) { + constructor(stats: BattleStat | BattleStat[], levels: integer, selfTarget?: boolean, condition?: MoveConditionFunc) { super(selfTarget, MoveEffectTrigger.HIT); this.stats = typeof(stats) === 'number' ? [ stats as BattleStat ] @@ -1980,13 +1994,13 @@ export class HpSplitAttr extends MoveEffectAttr { if (user.hp < hpValue) user.heal(hpValue - user.hp); else if (user.hp > hpValue) - user.damage(user.hp - hpValue); + user.damage(user.hp - hpValue, true); infoUpdates.push(user.updateInfo()); if (target.hp < hpValue) target.heal(hpValue - target.hp); else if (target.hp > hpValue) - target.damage(target.hp - hpValue); + target.damage(target.hp - hpValue, true); infoUpdates.push(target.updateInfo()); return Promise.all(infoUpdates).then(() => resolve(true)); @@ -2232,9 +2246,9 @@ export class OneHitKOAccuracyAttr extends MoveAttr { } export class MissEffectAttr extends MoveAttr { - private missEffectFunc: UserMoveCondition; + private missEffectFunc: UserMoveConditionFunc; - constructor(missEffectFunc: UserMoveCondition) { + constructor(missEffectFunc: UserMoveConditionFunc) { super(); this.missEffectFunc = missEffectFunc; @@ -2280,7 +2294,7 @@ export class DisableMoveAttr extends MoveEffectAttr { return false; } - getCondition(): MoveCondition { + getCondition(): MoveConditionFunc { return (user, target, move) => { if (target.summonData.disabledMove) return false; @@ -2335,7 +2349,7 @@ export class FrenzyAttr extends MoveEffectAttr { } } -export const frenzyMissFunc: UserMoveCondition = (user: Pokemon, move: Move) => { +export const frenzyMissFunc: UserMoveConditionFunc = (user: Pokemon, move: Move) => { while (user.getMoveQueue().length && user.getMoveQueue()[0].move === move.id) user.getMoveQueue().shift(); user.lapseTag(BattlerTagType.FRENZY); @@ -2375,7 +2389,7 @@ export class AddBattlerTagAttr extends MoveEffectAttr { return move.chance; } - getCondition(): MoveCondition { + getCondition(): MoveConditionFunc { return this.failOnOverlap ? (user, target, move) => !(this.selfTarget ? user : target).getTag(this.tagType) : null; @@ -2383,6 +2397,8 @@ export class AddBattlerTagAttr extends MoveEffectAttr { getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer { switch (this.tagType) { + case BattlerTagType.RECHARGING: + return -16; case BattlerTagType.FLINCHED: return -5; case BattlerTagType.CONFUSED: @@ -2394,7 +2410,7 @@ export class AddBattlerTagAttr extends MoveEffectAttr { case BattlerTagType.NIGHTMARE: return -5; case BattlerTagType.FRENZY: - return -2; + return -3; case BattlerTagType.ENCORE: return -2; case BattlerTagType.INGRAIN: @@ -2415,7 +2431,7 @@ export class AddBattlerTagAttr extends MoveEffectAttr { case BattlerTagType.PROTECTED: return 5; case BattlerTagType.PERISH_SONG: - return -8; + return -16; case BattlerTagType.FLYING: return 5; case BattlerTagType.CRIT_BOOST: @@ -2460,6 +2476,12 @@ export class ConfuseAttr extends AddBattlerTagAttr { } } +export class RechargeAttr extends AddBattlerTagAttr { + constructor() { + super(BattlerTagType.RECHARGING, true); + } +} + export class TrapAttr extends AddBattlerTagAttr { constructor(tagType: BattlerTagType) { super(tagType, false, false, 3, 6); @@ -2471,7 +2493,7 @@ export class ProtectAttr extends AddBattlerTagAttr { super(BattlerTagType.PROTECTED, true); } - getCondition(): MoveCondition { + getCondition(): MoveConditionFunc { return ((user, target, move): boolean => { let timesUsed = 0; const moveHistory = user.getLastXMoves(); @@ -2514,7 +2536,7 @@ export class FaintCountdownAttr extends AddBattlerTagAttr { return true; } - getCondition(): MoveCondition { + getCondition(): MoveConditionFunc { return (user, target, move) => super.getCondition()(user, target, move) && !target.isBossImmune(); } } @@ -2560,7 +2582,7 @@ export class AddArenaTagAttr extends MoveEffectAttr { } export class AddArenaTrapTagAttr extends AddArenaTagAttr { - getCondition(): MoveCondition { + getCondition(): MoveConditionFunc { return (user, target, move) => { if (move.category !== MoveCategory.STATUS || !user.scene.arena.getTag(this.tagType)) return true; @@ -2619,11 +2641,11 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { }); } - getCondition(): MoveCondition { + getCondition(): MoveConditionFunc { return (user, target, move) => move.category !== MoveCategory.STATUS || this.getSwitchOutCondition()(user, target, move); } - getSwitchOutCondition(): MoveCondition { + getSwitchOutCondition(): MoveConditionFunc { return (user, target, move) => { const switchOutTarget = (this.user ? user : target); const player = switchOutTarget instanceof PlayerPokemon; @@ -2736,7 +2758,7 @@ export class RandomMoveAttr extends OverrideMoveEffectAttr { } } -const lastMoveCopiableCondition: MoveCondition = (user, target, move) => { +const lastMoveCopiableCondition: MoveConditionFunc = (user, target, move) => { const copiableMove = user.scene.currentBattle.lastMove; if (!copiableMove) @@ -2770,13 +2792,13 @@ export class CopyMoveAttr extends OverrideMoveEffectAttr { return true; } - getCondition(): MoveCondition { + getCondition(): MoveConditionFunc { return lastMoveCopiableCondition; } } // TODO: Review this -const targetMoveCopiableCondition: MoveCondition = (user, target, move) => { +const targetMoveCopiableCondition: MoveConditionFunc = (user, target, move) => { const targetMoves = target.getMoveHistory().filter(m => !m.virtual); if (!targetMoves.length) return false; @@ -2815,7 +2837,7 @@ export class MovesetCopyMoveAttr extends OverrideMoveEffectAttr { return true; } - getCondition(): MoveCondition { + getCondition(): MoveConditionFunc { return targetMoveCopiableCondition; } } @@ -2847,7 +2869,7 @@ export class SketchAttr extends MoveEffectAttr { return true; } - getCondition(): MoveCondition { + getCondition(): MoveConditionFunc { return (user, target, move) => { if (!targetMoveCopiableCondition(user, target, move)) return false; @@ -2891,7 +2913,7 @@ export class TransformAttr extends MoveEffectAttr { } } -const failOnGravityCondition: MoveCondition = (user, target, move) => !user.scene.arena.getTag(ArenaTagType.GRAVITY); +const failOnGravityCondition: MoveConditionFunc = (user, target, move) => !user.scene.arena.getTag(ArenaTagType.GRAVITY); export type MoveAttrFilter = (attr: MoveAttr) => boolean; @@ -2916,6 +2938,32 @@ export function applyFilteredMoveAttrs(attrFilter: MoveAttrFilter, user: Pokemon return applyMoveAttrsInternal(attrFilter, user, target, move, args); } +export class MoveCondition { + protected func: MoveConditionFunc; + + constructor(func: MoveConditionFunc) { + this.func = func; + } + + apply(user: Pokemon, target: Pokemon, move: Move): boolean { + return this.func(user, target, move); + } + + getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer { + return 0; + } +} + +export class FirstMoveCondition extends MoveCondition { + constructor() { + super((user, target, move) => !user.getMoveHistory().length); + } + + getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer { + return this.apply(user, target, move) ? 10 : -20; + } +} + export type MoveTargetSet = { targets: BattlerIndex[]; multiple: boolean; @@ -3127,7 +3175,7 @@ export function initMoves() { new AttackMove(Moves.AURORA_BEAM, "Aurora Beam", Type.ICE, MoveCategory.SPECIAL, 65, 100, 20, -1, "The target is hit with a rainbow-colored beam. This may also lower the target's Attack stat.", 10, 0, 1) .attr(StatChangeAttr, BattleStat.ATK, -1), new AttackMove(Moves.HYPER_BEAM, "Hyper Beam", Type.NORMAL, MoveCategory.SPECIAL, 150, 90, 5, 163, "The target is attacked with a powerful beam. The user can't move on the next turn.", -1, 0, 1) - .attr(AddBattlerTagAttr, BattlerTagType.RECHARGING, true), + .attr(RechargeAttr), new AttackMove(Moves.PECK, "Peck", Type.FLYING, MoveCategory.PHYSICAL, 35, 100, 35, -1, "The target is jabbed with a sharply pointed beak or horn.", -1, 0, 1), new AttackMove(Moves.DRILL_PECK, "Drill Peck", Type.FLYING, MoveCategory.PHYSICAL, 80, 100, 20, -1, "A corkscrewing attack that strikes the target with a sharp beak acting as a drill.", -1, 0, 1), new AttackMove(Moves.SUBMISSION, "Submission", Type.FIGHTING, MoveCategory.PHYSICAL, 80, 80, 20, -1, "The user grabs the target and recklessly dives for the ground. This also damages the user a little.", -1, 0, 1) @@ -3597,7 +3645,7 @@ export function initMoves() { .makesContact(false), new AttackMove(Moves.FAKE_OUT, "Fake Out", Type.NORMAL, MoveCategory.PHYSICAL, 40, 100, 10, -1, "This attack hits first and makes the target flinch. It only works the first turn each time the user enters battle.", 100, 3, 3) .attr(FlinchAttr) - .condition((user, target, move) => !user.getMoveHistory().length), + .condition(new FirstMoveCondition()), new AttackMove(Moves.UPROAR, "Uproar (N)", Type.NORMAL, MoveCategory.SPECIAL, 90, 100, 10, -1, "The user attacks in an uproar for three turns. During that time, no Pokémon can fall asleep.", -1, 0, 3) .ignoresVirtual() .soundBased() @@ -3712,9 +3760,9 @@ export function initMoves() { new AttackMove(Moves.CRUSH_CLAW, "Crush Claw", Type.NORMAL, MoveCategory.PHYSICAL, 75, 95, 10, -1, "The user slashes the target with hard and sharp claws. This may also lower the target's Defense stat.", 50, 0, 3) .attr(StatChangeAttr, BattleStat.DEF, -1), new AttackMove(Moves.BLAST_BURN, "Blast Burn", Type.FIRE, MoveCategory.SPECIAL, 150, 90, 5, 153, "The target is razed by a fiery explosion. The user can't move on the next turn.", -1, 0, 3) - .attr(AddBattlerTagAttr, BattlerTagType.RECHARGING, true), + .attr(RechargeAttr), new AttackMove(Moves.HYDRO_CANNON, "Hydro Cannon", Type.WATER, MoveCategory.SPECIAL, 150, 90, 5, 154, "The target is hit with a watery blast. The user can't move on the next turn.", -1, 0, 3) - .attr(AddBattlerTagAttr, BattlerTagType.RECHARGING, true), + .attr(RechargeAttr), new AttackMove(Moves.METEOR_MASH, "Meteor Mash", Type.STEEL, MoveCategory.PHYSICAL, 90, 90, 10, -1, "The target is hit with a hard punch fired like a meteor. This may also raise the user's Attack stat.", 20, 0, 3) .attr(StatChangeAttr, BattleStat.ATK, 1, true) .punchingMove(), @@ -3791,7 +3839,7 @@ export function initMoves() { .target(MoveTarget.USER_AND_ALLIES), new AttackMove(Moves.DRAGON_CLAW, "Dragon Claw", Type.DRAGON, MoveCategory.PHYSICAL, 80, 100, 15, 78, "The user slashes the target with huge sharp claws.", -1, 0, 3), new AttackMove(Moves.FRENZY_PLANT, "Frenzy Plant", Type.GRASS, MoveCategory.SPECIAL, 150, 90, 5, 155, "The user slams the target with the roots of an enormous tree. The user can't move on the next turn.", -1, 0, 3) - .attr(AddBattlerTagAttr, BattlerTagType.RECHARGING, true), + .attr(RechargeAttr), new SelfStatusMove(Moves.BULK_UP, "Bulk Up", Type.FIGHTING, -1, 20, 64, "The user tenses its muscles to bulk up its body, raising both its Attack and Defense stats.", -1, 0, 3) .attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.DEF ], 1, true), new AttackMove(Moves.BOUNCE, "Bounce", Type.FLYING, MoveCategory.PHYSICAL, 85, 85, 5, -1, "The user bounces up high, then drops on the target on the second turn. This may also leave the target with paralysis.", 30, 0, 3) @@ -3960,7 +4008,7 @@ export function initMoves() { .attr(StatChangeAttr, BattleStat.SPDEF, -1), new StatusMove(Moves.SWITCHEROO, "Switcheroo (N)", Type.DARK, 100, 10, -1, "The user trades held items with the target faster than the eye can follow.", -1, 0, 4), new AttackMove(Moves.GIGA_IMPACT, "Giga Impact", Type.NORMAL, MoveCategory.PHYSICAL, 150, 90, 5, 152, "The user charges at the target using every bit of its power. The user can't move on the next turn.", -1, 0, 4) - .attr(AddBattlerTagAttr, BattlerTagType.RECHARGING, true), + .attr(RechargeAttr), new SelfStatusMove(Moves.NASTY_PLOT, "Nasty Plot", Type.DARK, -1, 20, 140, "The user stimulates its brain by thinking bad thoughts. This sharply raises the user's Sp. Atk stat.", -1, 0, 4) .attr(StatChangeAttr, BattleStat.SPATK, 2, true), new AttackMove(Moves.BULLET_PUNCH, "Bullet Punch", Type.STEEL, MoveCategory.PHYSICAL, 40, 100, 30, -1, "The user strikes the target with tough punches as fast as bullets. This move always goes first.", -1, 1, 4) @@ -4018,7 +4066,7 @@ export function initMoves() { .attr(StatChangeAttr, BattleStat.SPATK, -2, true), new AttackMove(Moves.POWER_WHIP, "Power Whip", Type.GRASS, MoveCategory.PHYSICAL, 120, 85, 10, -1, "The user violently whirls its vines, tentacles, or the like to harshly lash the target.", -1, 0, 4), new AttackMove(Moves.ROCK_WRECKER, "Rock Wrecker", Type.ROCK, MoveCategory.PHYSICAL, 150, 90, 5, -1, "The user launches a huge boulder at the target to attack. The user can't move on the next turn.", -1, 0, 4) - .attr(AddBattlerTagAttr, BattlerTagType.RECHARGING, true) + .attr(RechargeAttr) .makesContact(false) .ballBombMove(), new AttackMove(Moves.CROSS_POISON, "Cross Poison", Type.POISON, MoveCategory.PHYSICAL, 70, 100, 20, -1, "A slashing attack with a poisonous blade that may also poison the target. Critical hits land more easily.", 10, 0, 4) @@ -4068,7 +4116,7 @@ export function initMoves() { new AttackMove(Moves.DOUBLE_HIT, "Double Hit", Type.NORMAL, MoveCategory.PHYSICAL, 35, 90, 10, -1, "The user slams the target with a long tail, vines, or a tentacle. The target is hit twice in a row.", -1, 0, 4) .attr(MultiHitAttr, MultiHitType._2), new AttackMove(Moves.ROAR_OF_TIME, "Roar of Time", Type.DRAGON, MoveCategory.SPECIAL, 150, 90, 5, -1, "The user blasts the target with power that distorts even time. The user can't move on the next turn.", -1, 0, 4) - .attr(AddBattlerTagAttr, BattlerTagType.RECHARGING, true), + .attr(RechargeAttr), new AttackMove(Moves.SPACIAL_REND, "Spacial Rend", Type.DRAGON, MoveCategory.SPECIAL, 100, 95, 5, -1, "The user tears the target along with the space around it. Critical hits land more easily.", -1, 0, 4) .attr(HighCritAttr), new SelfStatusMove(Moves.LUNAR_DANCE, "Lunar Dance (N)", Type.PSYCHIC, -1, 10, -1, "The user faints. In return, the Pokémon taking its place will have its status and HP fully restored.", -1, 0, 4) @@ -4465,7 +4513,7 @@ export function initMoves() { new SelfStatusMove(Moves.SHORE_UP, "Shore Up", Type.GROUND, -1, 10, -1, "The user regains up to half of its max HP. It restores more HP in a sandstorm.", -1, 0, 7) .attr(SandHealAttr), new AttackMove(Moves.FIRST_IMPRESSION, "First Impression", Type.BUG, MoveCategory.PHYSICAL, 90, 100, 10, -1, "Although this move has great power, it only works the first turn each time the user enters battle.", -1, 2, 7) - .condition((user, target, move) => !user.getMoveHistory().length), + .condition(new FirstMoveCondition()), new SelfStatusMove(Moves.BANEFUL_BUNKER, "Baneful Bunker (N)", Type.POISON, -1, 10, -1, "In addition to protecting the user from attacks, this move also poisons any attacker that makes direct contact.", -1, 4, 7), new AttackMove(Moves.SPIRIT_SHACKLE, "Spirit Shackle (N)", Type.GHOST, MoveCategory.PHYSICAL, 80, 100, 10, -1, "The user attacks while simultaneously stitching the target's shadow to the ground to prevent the target from escaping.", -1, 0, 7), new AttackMove(Moves.DARKEST_LARIAT, "Darkest Lariat (N)", Type.DARK, MoveCategory.PHYSICAL, 85, 100, 10, -1, "The user swings both arms and hits the target. The target's stat changes don't affect this attack's damage.", -1, 0, 7), @@ -4551,7 +4599,7 @@ export function initMoves() { new AttackMove(Moves.LIQUIDATION, "Liquidation", Type.WATER, MoveCategory.PHYSICAL, 85, 100, 10, -1, "The user slams into the target using a full-force blast of water. This may also lower the target's Defense stat.", 20, 0, 7) .attr(StatChangeAttr, BattleStat.DEF, -1), new AttackMove(Moves.PRISMATIC_LASER, "Prismatic Laser", Type.PSYCHIC, MoveCategory.SPECIAL, 160, 100, 10, -1, "The user shoots powerful lasers using the power of a prism. The user can't move on the next turn.", -1, 0, 7) - .attr(AddBattlerTagAttr, BattlerTagType.RECHARGING, true), + .attr(RechargeAttr), new AttackMove(Moves.SPECTRAL_THIEF, "Spectral Thief (N)", Type.GHOST, MoveCategory.PHYSICAL, 90, 100, 10, -1, "The user hides in the target's shadow, steals the target's stat boosts, and then attacks.", -1, 0, 7), new AttackMove(Moves.SUNSTEEL_STRIKE, "Sunsteel Strike (N)", Type.STEEL, MoveCategory.PHYSICAL, 100, 100, 5, -1, "The user slams into the target with the force of a meteor. This move can be used on the target regardless of its Abilities.", -1, 0, 7), new AttackMove(Moves.MOONGEIST_BEAM, "Moongeist Beam (N)", Type.GHOST, MoveCategory.SPECIAL, 100, 100, 5, -1, "The user emits a sinister ray to attack the target. This move can be used on the target regardless of its Abilities.", -1, 0, 7), @@ -4702,9 +4750,9 @@ export function initMoves() { .attr(ProtectAttr), new AttackMove(Moves.FALSE_SURRENDER, "False Surrender", Type.DARK, MoveCategory.PHYSICAL, 80, -1, 10, -1, "The user pretends to bow its head, but then it stabs the target with its disheveled hair. This attack never misses.", -1, 0, 8), new AttackMove(Moves.METEOR_ASSAULT, "Meteor Assault", Type.FIGHTING, MoveCategory.PHYSICAL, 150, 100, 5, -1, "The user attacks wildly with its thick leek. The user can't move on the next turn, because the force of this move makes it stagger.", -1, 0, 8) - .attr(AddBattlerTagAttr, BattlerTagType.RECHARGING, true), + .attr(RechargeAttr), new AttackMove(Moves.ETERNABEAM, "Eternabeam", Type.DRAGON, MoveCategory.SPECIAL, 160, 90, 5, -1, "This is Eternatus's most powerful attack in its original form. The user can't move on the next turn.", -1, 0, 8) - .attr(AddBattlerTagAttr, BattlerTagType.RECHARGING, true), + .attr(RechargeAttr), new AttackMove(Moves.STEEL_BEAM, "Steel Beam (N)", Type.STEEL, MoveCategory.SPECIAL, 140, 95, 5, -1, "The user fires a beam of steel that it collected from its entire body. This also damages the user.", -1, 0, 8), new AttackMove(Moves.EXPANDING_FORCE, "Expanding Force (N)", Type.PSYCHIC, MoveCategory.SPECIAL, 80, 100, 10, -1, "The user attacks the target with its psychic power. This move's power goes up and damages all opposing Pokémon on Psychic Terrain.", -1, 0, 8), new AttackMove(Moves.STEEL_ROLLER, "Steel Roller (N)", Type.STEEL, MoveCategory.PHYSICAL, 130, 100, 5, -1, "The user attacks while destroying the terrain. This move fails when the ground hasn't turned into a terrain.", -1, 0, 8), diff --git a/src/data/trainer-type.ts b/src/data/trainer-type.ts index 9c7d7a30a..d2f74ff02 100644 --- a/src/data/trainer-type.ts +++ b/src/data/trainer-type.ts @@ -3,6 +3,7 @@ import { ModifierTypeFunc, modifierTypes } from "../modifier/modifier-type"; import { EnemyPokemon } from "../pokemon"; import * as Utils from "../utils"; import { Moves } from "./move"; +import { PokeballType } from "./pokeball"; import { pokemonEvolutions, pokemonPrevolutions } from "./pokemon-evolutions"; import PokemonSpecies, { PokemonSpeciesFilter, getPokemonSpecies } from "./pokemon-species"; import { Species } from "./species"; @@ -630,10 +631,7 @@ function getGymLeaderPartyTemplate(scene: BattleScene) { function getRandomPartyMemberFunc(speciesPool: Species[], postProcess?: (enemyPokemon: EnemyPokemon) => void): PartyMemberFunc { return (scene: BattleScene, level: integer) => { const species = getPokemonSpecies(Phaser.Math.RND.pick(speciesPool)).getSpeciesForLevel(level, true, true, scene.currentBattle.trainer.config.isBoss); - const ret = new EnemyPokemon(scene, getPokemonSpecies(species), level, true); - if (postProcess) - postProcess(ret); - return ret; + return scene.addEnemyPokemon(getPokemonSpecies(species), level, true, undefined, undefined, postProcess); }; } @@ -641,9 +639,7 @@ function getSpeciesFilterRandomPartyMemberFunc(speciesFilter: PokemonSpeciesFilt const originalSpeciesFilter = speciesFilter; speciesFilter = (species: PokemonSpecies) => allowLegendaries || (!species.legendary && !species.pseudoLegendary && !species.mythical) && originalSpeciesFilter(species); return (scene: BattleScene, level: integer) => { - const ret = new EnemyPokemon(scene, getPokemonSpecies(scene.randomSpecies(scene.currentBattle.waveIndex, level, false, speciesFilter).getSpeciesForLevel(level, true, true, scene.currentBattle.trainer.config.isBoss)), level, true); - if (postProcess) - postProcess(ret); + const ret = scene.addEnemyPokemon(getPokemonSpecies(scene.randomSpecies(scene.currentBattle.waveIndex, level, false, speciesFilter).getSpeciesForLevel(level, true, true, scene.currentBattle.trainer.config.isBoss)), level, true, undefined, undefined, postProcess); return ret; }; } @@ -1111,11 +1107,18 @@ export const trainerConfigs: TrainerConfigs = { .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.PIDGEOT, Species.NOCTOWL, Species.SWELLOW, Species.STARAPTOR, Species.UNFEZANT, Species.TALONFLAME, Species.TOUCANNON, Species.CORVIKNIGHT ])) .setPartyMemberFunc(2, getSpeciesFilterRandomPartyMemberFunc((species: PokemonSpecies) => !pokemonEvolutions.hasOwnProperty(species.speciesId) && !pokemonPrevolutions.hasOwnProperty(species.speciesId) && species.baseTotal >= 450)) .setSpeciesFilter(species => species.baseTotal >= 540) - .setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.RAYQUAZA ])), + .setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.RAYQUAZA ], p => { + p.setBoss(); + p.pokeball = PokeballType.MASTER_BALL; + })), [TrainerType.RIVAL_6]: new TrainerConfig(++t).setBoss().setStaticParty().setMoneyMultiplier(3).setEncounterBgm('final').setBattleBgm('battle_rival_3').setPartyTemplates(trainerPartyTemplates.RIVAL_6) .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.VENUSAUR, Species.CHARIZARD, Species.BLASTOISE, Species.MEGANIUM, Species.TYPHLOSION, Species.FERALIGATR, Species.SCEPTILE, Species.BLAZIKEN, Species.SWAMPERT, Species.TORTERRA, Species.INFERNAPE, Species.EMPOLEON, Species.SERPERIOR, Species.EMBOAR, Species.SAMUROTT, Species.CHESNAUGHT, Species.DELPHOX, Species.GRENINJA, Species.DECIDUEYE, Species.INCINEROAR, Species.PRIMARINA, Species.RILLABOOM, Species.CINDERACE, Species.INTELEON ])) .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.PIDGEOT, Species.NOCTOWL, Species.SWELLOW, Species.STARAPTOR, Species.UNFEZANT, Species.TALONFLAME, Species.TOUCANNON, Species.CORVIKNIGHT ])) .setPartyMemberFunc(2, getSpeciesFilterRandomPartyMemberFunc((species: PokemonSpecies) => !pokemonEvolutions.hasOwnProperty(species.speciesId) && !pokemonPrevolutions.hasOwnProperty(species.speciesId) && species.baseTotal >= 450)) .setSpeciesFilter(species => species.baseTotal >= 540) - .setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.RAYQUAZA ], p => p.formIndex = 1)), + .setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.RAYQUAZA ], p => { + p.setBoss(); + p.pokeball = PokeballType.MASTER_BALL; + p.formIndex = 1; + })), } \ No newline at end of file diff --git a/src/egg-hatch-phase.ts b/src/egg-hatch-phase.ts index f1b9848b5..8cc57ac6c 100644 --- a/src/egg-hatch-phase.ts +++ b/src/egg-hatch-phase.ts @@ -350,7 +350,7 @@ export class EggHatchPhase extends BattlePhase { if (speciesOverride) { this.scene.executeWithSeedOffset(() => { const pokemonSpecies = getPokemonSpecies(speciesOverride); - ret = new PlayerPokemon(this.scene, pokemonSpecies, 5, undefined, undefined, undefined, false); + ret = this.scene.addPlayerPokemon(pokemonSpecies, 5, undefined, undefined, undefined, false); }, this.egg.id, EGG_SEED.toString()); } else { let minStarterValue: integer; @@ -422,7 +422,7 @@ export class EggHatchPhase extends BattlePhase { const pokemonSpecies = getPokemonSpecies(species); - ret = new PlayerPokemon(this.scene, pokemonSpecies, 5, undefined, undefined, undefined, false); + ret = this.scene.addPlayerPokemon(pokemonSpecies, 5, undefined, undefined, undefined, false); }, this.egg.id, EGG_SEED.toString()); } diff --git a/src/main.ts b/src/main.ts index 3c82a180c..3a9242e8f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -62,6 +62,7 @@ Phaser.GameObjects.Sprite.prototype.setPositionRelative = setPositionRelative; Phaser.GameObjects.Image.prototype.setPositionRelative = setPositionRelative; Phaser.GameObjects.NineSlice.prototype.setPositionRelative = setPositionRelative; Phaser.GameObjects.Text.prototype.setPositionRelative = setPositionRelative; +Phaser.GameObjects.Rectangle.prototype.setPositionRelative = setPositionRelative; document.fonts.load('16px emerald').then(() => document.fonts.load('10px pkmnems')); diff --git a/src/pokemon.ts b/src/pokemon.ts index a824d1baf..1e92f90dc 100644 --- a/src/pokemon.ts +++ b/src/pokemon.ts @@ -14,7 +14,7 @@ import { initMoveAnim, loadMoveAnimAssets } from './data/battle-anims'; import { Status, StatusEffect } from './data/status-effect'; import { reverseCompatibleTms, tmSpecies } from './data/tms'; import { pokemonEvolutions, pokemonPrevolutions, SpeciesEvolution, SpeciesEvolutionCondition } from './data/pokemon-evolutions'; -import { DamagePhase, FaintPhase, SwitchSummonPhase } from './battle-phases'; +import { DamagePhase, FaintPhase, StatChangePhase, SwitchSummonPhase } from './battle-phases'; import { BattleStat } from './data/battle-stat'; import { BattlerTag, BattlerTagLapseType, BattlerTagType, EncoreTag, TypeBoostTag, getBattlerTag } from './data/battler-tag'; import { Species } from './data/species'; @@ -104,9 +104,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const randAbilityIndex = Utils.randSeedInt(2); this.species = species; - this.battleInfo = this.isPlayer() - ? new PlayerBattleInfo(scene) - : new EnemyBattleInfo(scene); this.pokeball = dataSource?.pokeball || PokeballType.POKEBALL; this.level = level; this.abilityIndex = abilityIndex !== undefined @@ -190,12 +187,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.shiny = false; this.calculateStats(); + } + init(): void { this.fieldPosition = FieldPosition.CENTER; - scene.fieldUI.addAt(this.battleInfo, 0); - - this.battleInfo.initInfo(this); + this.initBattleInfo(); + + this.scene.fieldUI.addAt(this.battleInfo, 0); const getSprite = (hasShadow?: boolean) => { const ret = this.scene.addFieldSprite(0, 0, `pkmn__${this.isPlayer() ? 'back__' : ''}sub`); @@ -216,6 +215,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.initShinySparkle(); } + abstract initBattleInfo(): void; + isOnField(): boolean { if (!this.scene) return false; @@ -570,6 +571,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return this.shiny || (this.fusionSpecies && this.fusionShiny); } + abstract isBoss(): boolean; + getMoveset(ignoreOverride?: boolean): PokemonMove[] { const ret = !ignoreOverride && this.summonData?.moveset ? this.summonData.moveset @@ -852,18 +855,18 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return move?.isUsable(this, ignorePp); } - showInfo() { + showInfo(): void { if (!this.battleInfo.visible) { const otherBattleInfo = this.scene.fieldUI.getAll().slice(0, 4).filter(ui => ui instanceof BattleInfo && ((ui as BattleInfo) instanceof PlayerBattleInfo) === this.isPlayer()).find(() => true); if (!otherBattleInfo || !this.getFieldIndex()) this.scene.fieldUI.sendToBack(this.battleInfo); else this.scene.fieldUI.moveAbove(this.battleInfo, otherBattleInfo); - this.battleInfo.setX(this.battleInfo.x + (this.isPlayer() ? 150 : -150)); + this.battleInfo.setX(this.battleInfo.x + (this.isPlayer() ? 150 : !this.isBoss() ? -150 : -198)); this.battleInfo.setVisible(true); this.scene.tweens.add({ targets: this.battleInfo, - x: this.isPlayer() ? '-=150' : '+=150', + x: this.isPlayer() ? '-=150' : `+=${!this.isBoss() ? 150 : 246}`, duration: 1000, ease: 'Sine.easeOut' }); @@ -875,12 +878,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (this.battleInfo.visible) { this.scene.tweens.add({ targets: this.battleInfo, - x: this.isPlayer() ? '+=150' : '-=150', + x: this.isPlayer() ? '+=150' : `-=${!this.isBoss() ? 150 : 198}`, duration: 500, ease: 'Sine.easeIn', onComplete: () => { this.battleInfo.setVisible(false); - this.battleInfo.setX(this.battleInfo.x - (this.isPlayer() ? 150 : -150)); + this.battleInfo.setX(this.battleInfo.x - (this.isPlayer() ? 150 : !this.isBoss() ? -150 : -198)); resolve(); } }); @@ -1039,8 +1042,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (isCritical) this.scene.queueMessage('A critical hit!'); this.scene.setPhaseQueueSplice(); - damage.value = Math.min(damage.value, this.hp); - this.damage(damage.value); + damage.value = this.damage(damage.value); if (source.isPlayer()) this.scene.validateAchvs(DamageAchv, damage); source.turnData.damageDealt += damage.value; @@ -1083,9 +1085,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return result; } - damage(damage: integer, preventEndure?: boolean): void { + damage(damage: integer, ignoreSegments: boolean = false, preventEndure: boolean = false): integer { if (this.isFainted()) - return; + return 0; if (this.hp > 1 && this.hp - damage <= 0 && !preventEndure) { const surviveDamage = new Utils.BooleanHolder(false); @@ -1094,19 +1096,25 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { damage = this.hp - 1; } - this.hp = Math.max(this.hp - damage, 0); + damage = Math.min(damage, this.hp); + + this.hp = this.hp - damage; if (this.isFainted()) { this.scene.unshiftPhase(new FaintPhase(this.scene, this.getBattlerIndex(), preventEndure)); this.resetSummonData(); } + + return damage; } - heal(amount: integer): void { - this.hp = Math.min(this.hp + amount, this.getMaxHp()); + heal(amount: integer): integer { + const healAmount = Math.min(amount, this.getMaxHp() - this.hp); + this.hp += healAmount; + return healAmount; } isBossImmune(): boolean { - return this.species.speciesId === Species.ETERNATUS && this.formIndex === 1; + return this.isBoss(); } addTag(tagType: BattlerTagType, turnCount: integer = 0, sourceMove?: Moves, sourceId?: integer): boolean { @@ -1742,12 +1750,17 @@ export default interface Pokemon { export class PlayerPokemon extends Pokemon { public compatibleTms: Moves[]; - constructor(scene: BattleScene, species: PokemonSpecies, level: integer, abilityIndex: integer, formIndex: integer, gender?: Gender, shiny?: boolean, ivs?: integer[], nature?: Nature, dataSource?: Pokemon | PokemonData) { + constructor(scene: BattleScene, species: PokemonSpecies, level: integer, abilityIndex: integer, formIndex: integer, gender: Gender, shiny: boolean, ivs: integer[], nature: Nature, dataSource: Pokemon | PokemonData) { super(scene, 106, 148, species, level, abilityIndex, formIndex, gender, shiny, ivs, nature, dataSource); this.generateCompatibleTms(); } + initBattleInfo(): void { + this.battleInfo = new PlayerBattleInfo(this.scene); + this.battleInfo.initInfo(this); + } + isPlayer(): boolean { return true; } @@ -1756,6 +1769,10 @@ export class PlayerPokemon extends Pokemon { return true; } + isBoss(): boolean { + return false; + } + getFieldIndex(): integer { return this.scene.getPlayerField().indexOf(this); } @@ -1808,7 +1825,7 @@ export class PlayerPokemon extends Pokemon { return new Promise(resolve => { const species = getPokemonSpecies(evolution.speciesId); const formIndex = Math.max(this.species.forms.findIndex(f => f.formKey === evolution.evoFormKey), 0); - const ret = new PlayerPokemon(this.scene, species, this.level, this.abilityIndex, formIndex, this.gender, this.shiny, this.ivs, this.nature, this); + const ret = this.scene.addPlayerPokemon(species, this.level, this.abilityIndex, formIndex, this.gender, this.shiny, this.ivs, this.nature, this); ret.loadAssets().then(() => resolve(ret)); }); } @@ -1839,7 +1856,7 @@ export class PlayerPokemon extends Pokemon { if (this.species.speciesId === Species.NINCADA && evolution.speciesId === Species.NINJASK) { const newEvolution = pokemonEvolutions[this.species.speciesId][1]; if (newEvolution.condition.predicate(this)) { - const newPokemon = new PlayerPokemon(this.scene, this.species, this.level, this.abilityIndex, this.formIndex, this.gender, this.shiny, this.ivs, this.nature); + const newPokemon = this.scene.addPlayerPokemon(this.species, this.level, this.abilityIndex, this.formIndex, this.gender, this.shiny, this.ivs, this.nature); this.scene.getParty().push(newPokemon); newPokemon.evolve(newEvolution); const modifiers = this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier @@ -1911,12 +1928,16 @@ export class PlayerPokemon extends Pokemon { export class EnemyPokemon extends Pokemon { public trainer: boolean; public aiType: AiType; + public bossSegments: integer; + public bossSegmentIndex: integer; - constructor(scene: BattleScene, species: PokemonSpecies, level: integer, trainer: boolean, dataSource?: PokemonData) { + constructor(scene: BattleScene, species: PokemonSpecies, level: integer, trainer: boolean, boss: boolean, dataSource: PokemonData) { super(scene, 236, 84, species, level, dataSource?.abilityIndex, dataSource?.formIndex, dataSource?.gender, dataSource ? dataSource.shiny : false, null, dataSource ? dataSource.nature : undefined, dataSource); this.trainer = trainer; + if (boss) + this.setBoss(); if (!dataSource) { this.trySetShiny(); @@ -1934,6 +1955,22 @@ export class EnemyPokemon extends Pokemon { this.aiType = AiType.SMART_RANDOM; } + initBattleInfo(): void { + this.battleInfo = new EnemyBattleInfo(this.scene); + this.battleInfo.updateBossSegments(this); + this.battleInfo.initInfo(this); + } + + setBoss(boss: boolean = true): void { + if (boss) { + this.bossSegments = this.scene.getEncounterBossSegments(this.scene.currentBattle.waveIndex, this.level, this.species, true); + this.bossSegmentIndex = this.bossSegments - 1; + } else { + this.bossSegments = 0; + this.bossSegmentIndex = 0; + } + } + generateAndPopulateMoveset(): void { switch (true) { case (this.species.speciesId === Species.SMEARGLE): @@ -2097,6 +2134,107 @@ export class EnemyPokemon extends Pokemon { return this.trainer; } + isBoss(): boolean { + return !!this.bossSegments; + } + + getBossSegmentIndex(): integer { + const segments = (this as EnemyPokemon).bossSegments; + const segmentSize = this.getMaxHp() / segments; + for (let s = segments - 1; s > 0; s--) { + const hpThreshold = Math.round(segmentSize * s); + if (this.hp > hpThreshold) { + return s; + } + } + + return 0; + } + + damage(damage: integer, ignoreSegments: boolean = false, preventEndure: boolean = false): integer { + if (this.isFainted()) + return 0; + + let clearedSegment = false; + + if (!ignoreSegments && this.isBoss()) { + const segmentSize = this.getMaxHp() / this.bossSegments; + for (let s = this.bossSegments - 1; s > 0; s--) { + const hpThreshold = Math.round(segmentSize * s); + if (this.hp > hpThreshold) { + if (this.hp - damage < hpThreshold) { + damage = this.hp - hpThreshold; + clearedSegment = true; + this.handleBossSegmentCleared(s); + } + break; + } + } + } + + return super.damage(damage, ignoreSegments, preventEndure); + } + + handleBossSegmentCleared(segmentIndex: integer): void { + while (segmentIndex - 1 < this.bossSegmentIndex) { + let boostedStat = BattleStat.RAND; + + const battleStats = Utils.getEnumValues(BattleStat).slice(0, -2); + const statWeights = new Array().fill(battleStats.length).filter((bs: BattleStat) => this.summonData.battleStats[bs] < 6).map((bs: BattleStat) => this.getStat(bs + 1)); + const statThresholds: integer[] = []; + let totalWeight = 0; + for (let bs of battleStats) { + totalWeight += statWeights[bs]; + statThresholds.push(totalWeight); + } + + const randInt = Utils.randSeedInt(totalWeight); + + for (let bs of battleStats) { + if (randInt < statThresholds[bs]) { + boostedStat = bs; + break; + } + } + + let statLevels = 1; + + switch (segmentIndex) { + case 1: + if (this.bossSegments >= 3) + statLevels++; + break; + case 2: + if (this.bossSegments >= 5) + statLevels++; + break; + } + + + this.scene.unshiftPhase(new StatChangePhase(this.scene, this.getBattlerIndex(), true, [ boostedStat ], statLevels)); + + this.bossSegmentIndex--; + } + } + + heal(amount: integer): integer { + if (this.isBoss()) { + let amountRatio = amount / this.getMaxHp(); + let segmentBypassCount = Math.floor(amountRatio / (1 / this.bossSegments)); + const segmentSize = this.getMaxHp() / this.bossSegments; + for (let s = 1; s < this.bossSegments; s++) { + const hpThreshold = Math.round(segmentSize * s); + if (this.hp <= hpThreshold) { + const healAmount = Math.min(amount, this.getMaxHp() - this.hp, Math.round(hpThreshold + (segmentSize * segmentBypassCount) - this.hp)); + this.hp += healAmount; + return healAmount; + } + } + } + + return super.heal(amount); + } + getFieldIndex(): integer { return this.scene.getEnemyField().indexOf(this); } @@ -2113,7 +2251,7 @@ export class EnemyPokemon extends Pokemon { this.pokeball = pokeballType; this.metLevel = this.level; this.metBiome = this.scene.arena.biomeType; - const newPokemon = new PlayerPokemon(this.scene, this.species, this.level, this.abilityIndex, this.formIndex, this.gender, this.shiny, this.ivs, this.nature, this); + const newPokemon = this.scene.addPlayerPokemon(this.species, this.level, this.abilityIndex, this.formIndex, this.gender, this.shiny, this.ivs, this.nature, this); party.push(newPokemon); ret = newPokemon; } diff --git a/src/system/pokemon-data.ts b/src/system/pokemon-data.ts index 02fbf2866..f513848ba 100644 --- a/src/system/pokemon-data.ts +++ b/src/system/pokemon-data.ts @@ -39,6 +39,8 @@ export default class PokemonData { public fusionShiny: boolean; public fusionGender: Gender; + public boss: boolean; + public summonData: PokemonSummonData; constructor(source: Pokemon | any) { @@ -70,6 +72,8 @@ export default class PokemonData { this.fusionShiny = source.fusionShiny; this.fusionGender = source.fusionGender; + this.boss = (source instanceof EnemyPokemon && !!source.bossSegments) || (!this.player && !!source.boss); + if (sourcePokemon) { this.moveset = sourcePokemon.moveset; this.status = sourcePokemon.status; @@ -95,7 +99,7 @@ export default class PokemonData { toPokemon(scene: BattleScene, battleType?: BattleType): Pokemon { const species = getPokemonSpecies(this.species); if (this.player) - return new PlayerPokemon(scene, species, this.level, this.abilityIndex, this.formIndex, this.gender, this.shiny, this.ivs, this.nature, this); - return new EnemyPokemon(scene, species, this.level, battleType === BattleType.TRAINER, this); + return scene.addPlayerPokemon(species, this.level, this.abilityIndex, this.formIndex, this.gender, this.shiny, this.ivs, this.nature, this); + return scene.addEnemyPokemon(species, this.level, battleType === BattleType.TRAINER, this.boss, this); } } \ No newline at end of file diff --git a/src/trainer.ts b/src/trainer.ts index 2a560eaf7..bd9c6bb92 100644 --- a/src/trainer.ts +++ b/src/trainer.ts @@ -137,7 +137,7 @@ export default class Trainer extends Phaser.GameObjects.Container { ? getPokemonSpecies(battle.enemyParty[offset].species.getSpeciesForLevel(level, false, true, this.config.isBoss)) : this.genNewPartyMemberSpecies(level); - ret = new EnemyPokemon(this.scene, species, level, true); + ret = this.scene.addEnemyPokemon(species, level, true); }, this.config.hasStaticParty ? this.config.getDerivedType() + ((index + 1) << 8) : this.scene.currentBattle.waveIndex + (this.config.getDerivedType() << 10) + (((!this.config.useSameSeedForAllMembers ? index : 0) + 1) << 8)); return ret; diff --git a/src/ui/battle-info.ts b/src/ui/battle-info.ts index 45402d44b..098514fe0 100644 --- a/src/ui/battle-info.ts +++ b/src/ui/battle-info.ts @@ -1,4 +1,4 @@ -import { default as Pokemon } from '../pokemon'; +import { EnemyPokemon, default as Pokemon } from '../pokemon'; import { getLevelTotalExp, getLevelRelExp } from '../data/exp'; import * as Utils from '../utils'; import { addTextObject, TextStyle } from './text'; @@ -9,6 +9,8 @@ import BattleScene from '../battle-scene'; export default class BattleInfo extends Phaser.GameObjects.Container { private player: boolean; private mini: boolean; + private boss: boolean; + private bossSegments: integer; private offset: boolean; private lastName: string; private lastStatus: StatusEffect; @@ -29,6 +31,7 @@ export default class BattleInfo extends Phaser.GameObjects.Container { private statusIndicator: Phaser.GameObjects.Sprite; private levelContainer: Phaser.GameObjects.Container; private hpBar: Phaser.GameObjects.Image; + private hpBarSegmentDividers: Phaser.GameObjects.Rectangle[]; private levelNumbersContainer: Phaser.GameObjects.Container; private hpNumbersContainer: Phaser.GameObjects.Container; private expBar: Phaser.GameObjects.Image; @@ -37,6 +40,7 @@ export default class BattleInfo extends Phaser.GameObjects.Container { super(scene, x, y); this.player = player; this.mini = !player; + this.boss = false; this.offset = false; this.lastName = null; this.lastStatus = StatusEffect.NONE; @@ -100,6 +104,8 @@ export default class BattleInfo extends Phaser.GameObjects.Container { this.hpBar.setOrigin(0); this.add(this.hpBar); + this.hpBarSegmentDividers = []; + this.levelNumbersContainer = this.scene.add.container(9.5, 0); this.levelContainer.add(this.levelNumbersContainer); @@ -137,6 +143,9 @@ export default class BattleInfo extends Phaser.GameObjects.Container { const dexAttr = pokemon.getDexAttr(); if ((dexEntry.caughtAttr & dexAttr) < dexAttr) this.ownedIcon.setTint(0x808080); + + if (this.boss) + this.updateBossSegmentDividers(pokemon.getMaxHp()); } this.hpBar.setScale(pokemon.getHpRatio(), 1); @@ -158,7 +167,7 @@ export default class BattleInfo extends Phaser.GameObjects.Container { } getTextureName(): string { - return `pbinfo_${this.player ? 'player' : 'enemy'}${this.mini ? '_mini' : ''}`; + return `pbinfo_${this.player ? 'player' : 'enemy'}${!this.player && this.boss ? '_boss' : this.mini ? '_mini' : ''}`; } setMini(mini: boolean): void { @@ -181,6 +190,40 @@ export default class BattleInfo extends Phaser.GameObjects.Container { const toggledElements = [ this.hpNumbersContainer, this.expBar ]; toggledElements.forEach(el => el.setVisible(!mini)); } + + updateBossSegments(pokemon: EnemyPokemon): void { + const boss = !!pokemon.bossSegments; + + if (boss !== this.boss) { + this.boss = boss; + + [ this.nameText, this.genderText, this.splicedIcon, this.ownedIcon, this.statusIndicator, this.levelContainer ].map(e => e.x += 48 * (boss ? -1 : 1)); + this.hpBar.x += 38 * (boss ? -1 : 1); + this.hpBar.y += 2 * (this.boss ? -1 : 1); + this.hpBar.setTexture(`overlay_hp${boss ? '_boss' : ''}`); + this.box.setTexture(this.getTextureName()); + } + + this.bossSegments = boss ? pokemon.bossSegments : 0; + this.updateBossSegmentDividers(pokemon.hp); + } + + updateBossSegmentDividers(hp: number): void { + while (this.hpBarSegmentDividers.length) + this.hpBarSegmentDividers.pop().destroy(); + + if (this.boss && this.bossSegments > 1) { + for (let s = 1; s < this.bossSegments; s++) { + const dividerX = (Math.round((hp / this.bossSegments) * s) / hp) * this.hpBar.width; + const divider = this.scene.add.rectangle(0, 0, 1, this.hpBar.height, 0xffffff); + divider.setOrigin(0.5, 0); + this.add(divider); + + divider.setPositionRelative(this.hpBar, dividerX, 0); + this.hpBarSegmentDividers.push(divider); + } + } + } setOffset(offset: boolean): void { if (this.offset === offset) diff --git a/src/ui/text.ts b/src/ui/text.ts index 29a9c4cf2..5b33a2559 100644 --- a/src/ui/text.ts +++ b/src/ui/text.ts @@ -109,7 +109,7 @@ function getTextStyleOptions(style: TextStyle, extraStyleOptions?: Phaser.Types. } export function getBBCodeFrag(content: string, textStyle: TextStyle): string { - return `[color=${getTextColor(textStyle)}][shadow=${getTextColor(textStyle, true)}]${content}[/shadow][/color]`; + return `[color=${getTextColor(textStyle)}][shadow=${getTextColor(textStyle, true)}]${content}`; } export function getTextColor(textStyle: TextStyle, shadow?: boolean): string { diff --git a/src/utils.ts b/src/utils.ts index 300dd6011..c4612798c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -63,7 +63,7 @@ export function randInt(range: integer, min: integer = 0): integer { } export function randSeedInt(range: integer, min: integer = 0): integer { - if (range === 1) + if (range <= 1) return min; return Phaser.Math.RND.integerInRange(min, (range - 1) + min); }