Implement Double Battles (#1)

* Add WiP logic for double battles

* Minor changes for double battles

* More fixes for double battles

* Show battle info for both in double battles

* Improvements to double battles

* Add double battle version of party UI

* Fix some issues with double battles

* Updates to double battles

* More work on double battles for stability

* Fix issues with ability bar and evolution screen

* Add chance for double battles
pull/2/head
Samuel H 2023-05-18 11:11:06 -04:00 committed by GitHub
parent e2d6890072
commit b9f7ba173d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 1583 additions and 702 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 B

View File

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -161,7 +161,7 @@ export class Arena {
this.weather = weather ? new Weather(weather, viaMove ? 5 : 0) : null;
if (this.weather) {
this.scene.unshiftPhase(new CommonAnimPhase(this.scene, true, CommonAnim.SUNNY + (weather - 1)));
this.scene.unshiftPhase(new CommonAnimPhase(this.scene, undefined, undefined, CommonAnim.SUNNY + (weather - 1)));
this.scene.queueMessage(getWeatherStartMessage(weather));
} else
this.scene.queueMessage(getWeatherClearMessage(oldWeatherType));

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,14 @@
import Phaser from 'phaser';
import { Biome } from './data/biome';
import UI from './ui/ui';
import { EncounterPhase, SummonPhase, CommandPhase, NextEncounterPhase, NewBiomeEncounterPhase, SelectBiomePhase, MessagePhase, CheckLoadPhase } from './battle-phases';
import Pokemon, { PlayerPokemon, EnemyPokemon } from './pokemon';
import { EncounterPhase, SummonPhase, NextEncounterPhase, NewBiomeEncounterPhase, SelectBiomePhase, MessagePhase, CheckLoadPhase, TurnInitPhase, ReturnPhase, ToggleDoublePositionPhase, CheckSwitchPhase } from './battle-phases';
import Pokemon, { PlayerPokemon, EnemyPokemon, FieldPosition } from './pokemon';
import PokemonSpecies, { allSpecies, getPokemonSpecies, initSpecies } from './data/pokemon-species';
import * as Utils from './utils';
import { Modifier, ModifierBar, ConsumablePokemonModifier, ConsumableModifier, PokemonHpRestoreModifier, HealingBoosterModifier, PersistentModifier, PokemonHeldItemModifier, ModifierPredicate } from './modifier/modifier';
import { PokeballType } from './data/pokeball';
import { Species } from './data/species';
import { initAutoPlay } from './system/auto-play';
import { Battle } from './battle';
import { initCommonAnims, initMoveAnim, loadCommonAnimAssets, loadMoveAnimAssets, populateAnims } from './data/battle-anims';
import { BattlePhase } from './battle-phase';
import { initGameSpeed } from './system/game-speed';
@ -20,7 +19,8 @@ import { TextStyle, addTextObject } from './ui/text';
import { Moves, initMoves } from './data/move';
import { getDefaultModifierTypeForTier, getEnemyModifierTypesForWave } from './modifier/modifier-type';
import AbilityBar from './ui/ability-bar';
import { BlockItemTheftAbAttr, applyAbAttrs, initAbilities } from './data/ability';
import { BlockItemTheftAbAttr, DoubleBattleChanceAbAttr, applyAbAttrs, initAbilities } from './data/ability';
import Battle from './battle';
const enableAuto = true;
const quickStart = false;
@ -156,7 +156,8 @@ export default class BattleScene extends Phaser.Scene {
this.loadAtlas('prompt', 'ui');
this.loadImage('cursor', 'ui');
this.loadImage('pbinfo_player', 'ui');
this.loadImage('pbinfo_enemy', 'ui');
this.loadImage('pbinfo_player_mini', 'ui');
this.loadImage('pbinfo_enemy_mini', 'ui');
this.loadImage('overlay_lv', 'ui');
this.loadAtlas('numbers', 'ui');
this.loadAtlas('overlay_hp', 'ui');
@ -168,6 +169,7 @@ export default class BattleScene extends Phaser.Scene {
this.loadImage('boolean_window', 'ui');
this.loadImage('party_bg', 'ui');
this.loadImage('party_bg_double', 'ui');
this.loadAtlas('party_slot_main', 'ui');
this.loadAtlas('party_slot', 'ui');
this.loadImage('party_slot_overlay_lv', 'ui');
@ -209,6 +211,8 @@ export default class BattleScene extends Phaser.Scene {
this.loadImage('starter_select_gen_cursor', 'ui');
this.loadImage('starter_select_gen_cursor_highlight', 'ui');
this.loadImage('default_bg', 'arenas');
// Load arena images
Utils.getEnumValues(Biome).map(bt => {
const btKey = Biome[bt].toLowerCase();
@ -446,21 +450,35 @@ export default class BattleScene extends Phaser.Scene {
return this.party;
}
getEnemyParty(): EnemyPokemon[] {
return this.getEnemyPokemon() ? [ this.getEnemyPokemon() ] : [];
getPlayerPokemon(): PlayerPokemon {
return this.getPlayerField().find(p => p.isActive());
}
getPlayerPokemon(): PlayerPokemon {
return this.getParty()[0];
getPlayerField(): PlayerPokemon[] {
const party = this.getParty();
return party.slice(0, Math.min(party.length, this.currentBattle?.double ? 2 : 1));
}
getEnemyPokemon(): EnemyPokemon {
return this.currentBattle?.enemyPokemon;
return this.getEnemyField().find(p => p.isActive());
}
getEnemyField(): EnemyPokemon[] {
return this.currentBattle?.enemyField || [];
}
getField(): Pokemon[] {
const ret = new Array(4).fill(null);
const playerField = this.getPlayerField();
const enemyField = this.getEnemyField();
ret.splice(0, playerField.length, ...playerField);
ret.splice(2, enemyField.length, ...enemyField);
return ret;
}
getPokemonById(pokemonId: integer): Pokemon {
const findInParty = (party: Pokemon[]) => party.find(p => p.id === pokemonId);
return findInParty(this.getParty()) || findInParty(this.getEnemyParty());
return findInParty(this.getParty()) || findInParty(this.getEnemyField());
}
reset(): void {
@ -475,7 +493,7 @@ export default class BattleScene extends Phaser.Scene {
for (let p of this.getParty())
p.destroy();
this.party = [];
for (let p of this.getEnemyParty())
for (let p of this.getEnemyField())
p.destroy();
this.currentBattle = null;
@ -493,11 +511,26 @@ export default class BattleScene extends Phaser.Scene {
this.trainer.setPosition(406, 132);
}
newBattle(waveIndex?: integer): Battle {
newBattle(waveIndex?: integer, double?: boolean): Battle {
let newWaveIndex = waveIndex || ((this.currentBattle?.waveIndex || (startingWave - 1)) + 1);
let newDouble: boolean;
if (double === undefined) {
const doubleChance = new Utils.IntegerHolder(newWaveIndex % 10 === 0 ? 32 : 8);
this.getPlayerField().forEach(p => applyAbAttrs(DoubleBattleChanceAbAttr, p, null, doubleChance));
newDouble = !Utils.randInt(doubleChance.value);
} else
newDouble = double;
const lastBattle = this.currentBattle;
this.currentBattle = new Battle(newWaveIndex, newDouble);
this.currentBattle.incrementTurn(this);
if (!waveIndex) {
if (this.currentBattle) {
this.getEnemyPokemon().destroy();
if (this.currentBattle.waveIndex % 10)
if (lastBattle) {
this.getEnemyField().forEach(enemyPokemon => enemyPokemon.destroy());
if (lastBattle.waveIndex % 10)
this.pushPhase(new NextEncounterPhase(this));
else {
this.pushPhase(new SelectBiomePhase(this));
@ -509,12 +542,29 @@ export default class BattleScene extends Phaser.Scene {
else {
this.arena.playBgm();
this.pushPhase(new EncounterPhase(this));
this.pushPhase(new SummonPhase(this));
}
this.pushPhase(new SummonPhase(this, 0));
}
}
this.currentBattle = new Battle(waveIndex || ((this.currentBattle?.waveIndex || (startingWave - 1)) + 1));
if ((lastBattle?.double || false) !== newDouble) {
const availablePartyMemberCount = this.getParty().filter(p => !p.isFainted()).length;
if (newDouble) {
this.pushPhase(new ToggleDoublePositionPhase(this, true));
if (availablePartyMemberCount > 1)
this.pushPhase(new SummonPhase(this, 1));
} else {
if (availablePartyMemberCount > 1)
this.pushPhase(new ReturnPhase(this, 1));
this.pushPhase(new ToggleDoublePositionPhase(this, false));
}
}
if (lastBattle) {
this.pushPhase(new CheckSwitchPhase(this, 0, newDouble));
if (newDouble)
this.pushPhase(new CheckSwitchPhase(this, 1, newDouble));
}
}
return this.currentBattle;
}
@ -699,7 +749,7 @@ export default class BattleScene extends Phaser.Scene {
}
populatePhaseQueue(): void {
this.phaseQueue.push(new CommandPhase(this));
this.phaseQueue.push(new TurnInitPhase(this));
}
addModifier(modifier: Modifier, playSound?: boolean, virtual?: boolean): Promise<void> {
@ -828,7 +878,9 @@ export default class BattleScene extends Phaser.Scene {
}
if (isBoss)
count = Math.max(count, Math.floor(chances / 2));
getEnemyModifierTypesForWave(waveIndex, count, this.getEnemyParty()).map(mt => mt.newModifier(this.getEnemyPokemon()).add(this.enemyModifiers, false));
const enemyField = this.getEnemyField();
getEnemyModifierTypesForWave(waveIndex, count, this.getEnemyField())
.map(mt => mt.newModifier(enemyField[Utils.randInt(enemyField.length)]).add(this.enemyModifiers, false));
this.updateModifiers(false).then(() => resolve());
});
@ -855,7 +907,7 @@ export default class BattleScene extends Phaser.Scene {
modifiers.splice(modifiers.indexOf(modifier), 1);
}
this.updatePartyForModifiers(player ? this.getParty() : this.getEnemyParty()).then(() => {
this.updatePartyForModifiers(player ? this.getParty() : this.getEnemyField().filter(p => p.isActive())).then(() => {
(player ? this.modifierBar : this.enemyModifierBar).updateModifiers(modifiers);
if (!player)
this.updateWaveCountPosition();

View File

@ -1,18 +1,44 @@
import { EnemyPokemon, PlayerPokemon } from "./pokemon";
import BattleScene, { PokeballCounts } from "./battle-scene";
import { EnemyPokemon, PlayerPokemon, QueuedMove } from "./pokemon";
import { Command } from "./ui/command-ui-handler";
import * as Utils from "./utils";
export class Battle {
export enum BattlerIndex {
PLAYER,
PLAYER_2,
ENEMY,
ENEMY_2
}
export interface TurnCommand {
command: Command;
cursor?: integer;
move?: QueuedMove;
targets?: BattlerIndex[];
args?: any[];
};
interface TurnCommands {
[key: integer]: TurnCommand
}
export default class Battle {
public waveIndex: integer;
public enemyLevel: integer;
public enemyPokemon: EnemyPokemon;
public enemyLevels: integer[];
public enemyField: EnemyPokemon[];
public double: boolean;
public turn: integer;
public turnCommands: TurnCommands;
public turnPokeballCounts: PokeballCounts;
public playerParticipantIds: Set<integer> = new Set<integer>();
public escapeAttempts: integer = 0;
constructor(waveIndex: integer) {
constructor(waveIndex: integer, double: boolean) {
this.waveIndex = waveIndex;
this.enemyLevel = this.getLevelForWave();
this.turn = 1;
this.enemyLevels = new Array(double ? 2 : 1).fill(null).map(() => this.getLevelForWave());
this.enemyField = [];
this.double = double;
this.turn = 0;
}
private getLevelForWave(): number {
@ -29,8 +55,14 @@ export class Battle {
return Math.max(Math.round(baseLevel + Math.abs(Utils.randGauss(deviation))), 1);
}
incrementTurn() {
getBattlerCount(): integer {
return this.double ? 2 : 1;
}
incrementTurn(scene: BattleScene): void {
this.turn++;
this.turnCommands = Object.fromEntries(Utils.getEnumValues(BattlerIndex).map(bt => [ bt, null ]));
this.turnPokeballCounts = Object.assign({}, scene.pokeballCounts);
}
addParticipant(playerPokemon: PlayerPokemon): void {

View File

@ -1,4 +1,4 @@
import Pokemon, { MoveResult, PokemonMove } from "../pokemon";
import Pokemon, { HitResult, MoveResult, PokemonMove } from "../pokemon";
import { Type } from "./type";
import * as Utils from "../utils";
import { BattleStat, getBattleStatName } from "./battle-stat";
@ -83,6 +83,18 @@ export class BlockRecoilDamageAttr extends AbAttr {
}
}
export class DoubleBattleChanceAbAttr extends AbAttr {
constructor() {
super(false);
}
apply(pokemon: Pokemon, cancelled: Utils.BooleanHolder, args: any[]): boolean {
const doubleChance = (args[0] as Utils.IntegerHolder);
doubleChance.value = Math.max(doubleChance.value / 2, 1);
return true;
}
}
export class PreDefendAbAttr extends AbAttr {
applyPreDefend(pokemon: Pokemon, attacker: Pokemon, move: PokemonMove, cancelled: Utils.BooleanHolder, args: any[]): boolean {
return false;
@ -155,9 +167,10 @@ export class TypeImmunityHealAbAttr extends TypeImmunityAbAttr {
applyPreDefend(pokemon: Pokemon, attacker: Pokemon, move: PokemonMove, cancelled: Utils.BooleanHolder, args: any[]): boolean {
const ret = super.applyPreDefend(pokemon, attacker, move, cancelled, args);
if (ret && pokemon.getHpRatio() < 1) {
const scene = pokemon.scene;
scene.unshiftPhase(new PokemonHealPhase(scene, pokemon.isPlayer(), Math.max(Math.floor(pokemon.getMaxHp() / 4), 1), getPokemonMessage(pokemon, `'s ${pokemon.getAbility().name}\nrestored its HP a little!`), true));
if (ret) {
if (pokemon.getHpRatio() < 1)
pokemon.scene.unshiftPhase(new PokemonHealPhase(pokemon.scene, pokemon.getBattlerIndex(),
Math.max(Math.floor(pokemon.getMaxHp() / 4), 1), getPokemonMessage(pokemon, `'s ${pokemon.getAbility().name}\nrestored its HP a little!`), true));
return true;
}
@ -181,7 +194,7 @@ class TypeImmunityStatChangeAbAttr extends TypeImmunityAbAttr {
if (ret) {
cancelled.value = true;
pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.isPlayer(), true, [ this.stat ], this.levels));
pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ this.stat ], this.levels));
}
return ret;
@ -232,14 +245,14 @@ export class NonSuperEffectiveImmunityAbAttr extends TypeImmunityAbAttr {
}
export class PostDefendAbAttr extends AbAttr {
applyPostDefend(pokemon: Pokemon, attacker: Pokemon, move: PokemonMove, moveResult: MoveResult, args: any[]): boolean {
applyPostDefend(pokemon: Pokemon, attacker: Pokemon, move: PokemonMove, hitResult: HitResult, args: any[]): boolean {
return false;
}
}
export class PostDefendTypeChangeAbAttr extends PostDefendAbAttr {
applyPostDefend(pokemon: Pokemon, attacker: Pokemon, move: PokemonMove, moveResult: MoveResult, args: any[]): boolean {
if (moveResult < MoveResult.NO_EFFECT) {
applyPostDefend(pokemon: Pokemon, attacker: Pokemon, move: PokemonMove, hitResult: HitResult, args: any[]): boolean {
if (hitResult < HitResult.NO_EFFECT) {
const type = move.getMove().type;
const pokemonTypes = pokemon.getTypes();
if (pokemonTypes.length !== 1 || pokemonTypes[0] !== type) {
@ -267,7 +280,7 @@ export class PostDefendContactApplyStatusEffectAbAttr extends PostDefendAbAttr {
this.effects = effects;
}
applyPostDefend(pokemon: Pokemon, attacker: Pokemon, move: PokemonMove, moveResult: MoveResult, args: any[]): boolean {
applyPostDefend(pokemon: Pokemon, attacker: Pokemon, move: PokemonMove, hitResult: HitResult, args: any[]): boolean {
if (move.getMove().hasFlag(MoveFlags.MAKES_CONTACT) && Utils.randInt(100) < this.chance) {
const effect = this.effects.length === 1 ? this.effects[0] : this.effects[Utils.randInt(this.effects.length)];
return attacker.trySetStatus(effect);
@ -290,7 +303,7 @@ export class PostDefendContactApplyTagChanceAbAttr extends PostDefendAbAttr {
this.turnCount = turnCount;
}
applyPostDefend(pokemon: Pokemon, attacker: Pokemon, move: PokemonMove, moveResult: MoveResult, args: any[]): boolean {
applyPostDefend(pokemon: Pokemon, attacker: Pokemon, move: PokemonMove, hitResult: HitResult, args: any[]): boolean {
if (move.getMove().hasFlag(MoveFlags.MAKES_CONTACT) && Utils.randInt(100) < this.chance)
return attacker.addTag(this.tagType, this.turnCount, move.moveId, pokemon.id);
@ -406,13 +419,21 @@ export class PostSummonStatChangeAbAttr extends PostSummonAbAttr {
}
applyPostSummon(pokemon: Pokemon, args: any[]): boolean {
const statChangePhase = new StatChangePhase(pokemon.scene, pokemon.isPlayer() === this.selfTarget, this.selfTarget, this.stats, this.levels);
const statChangePhases: StatChangePhase[] = [];
if (!this.selfTarget && !pokemon.getOpponent()?.summonData)
if (this.selfTarget)
statChangePhases.push(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, this.stats, this.levels));
else {
for (let opponent of pokemon.getOpponents())
statChangePhases.push(new StatChangePhase(pokemon.scene, opponent.getBattlerIndex(), false, this.stats, this.levels));
}
for (let statChangePhase of statChangePhases) {
if (!this.selfTarget && !statChangePhase.getPokemon().summonData)
pokemon.scene.pushPhase(statChangePhase); // TODO: This causes the ability bar to be shown at the wrong time
else
pokemon.scene.unshiftPhase(statChangePhase);
}
return true;
}
@ -576,7 +597,7 @@ export class PostTurnAbAttr extends AbAttr {
export class PostTurnSpeedBoostAbAttr extends PostTurnAbAttr {
applyPostTurn(pokemon: Pokemon, args: any[]): boolean {
pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.isPlayer(), true, [ BattleStat.SPD ], 1));
pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ BattleStat.SPD ], 1));
return true;
}
}
@ -594,7 +615,8 @@ export class PostTurnHealAbAttr extends PostTurnAbAttr {
applyPostTurn(pokemon: Pokemon, args: any[]): boolean {
if (pokemon.getHpRatio() < 1) {
const scene = pokemon.scene;
scene.unshiftPhase(new PokemonHealPhase(scene, pokemon.isPlayer(), Math.max(Math.floor(pokemon.getMaxHp() / 16), 1), getPokemonMessage(pokemon, `'s ${pokemon.getAbility().name}\nrestored its HP a little!`), true));
scene.unshiftPhase(new PokemonHealPhase(scene, pokemon.getBattlerIndex(),
Math.max(Math.floor(pokemon.getMaxHp() / 16), 1), getPokemonMessage(pokemon, `'s ${pokemon.getAbility().name}\nrestored its HP a little!`), true));
return true;
}
@ -632,7 +654,8 @@ export class PostWeatherLapseHealAbAttr extends PostWeatherLapseAbAttr {
applyPostWeatherLapse(pokemon: Pokemon, weather: Weather, args: any[]): boolean {
if (pokemon.getHpRatio() < 1) {
const scene = pokemon.scene;
scene.unshiftPhase(new PokemonHealPhase(scene, pokemon.isPlayer(), Math.max(Math.floor(pokemon.getMaxHp() / (16 / this.healFactor)), 1), getPokemonMessage(pokemon, `'s ${pokemon.getAbility().name}\nrestored its HP a little!`), true));
scene.unshiftPhase(new PokemonHealPhase(scene, pokemon.getBattlerIndex(),
Math.max(Math.floor(pokemon.getMaxHp() / (16 / this.healFactor)), 1), getPokemonMessage(pokemon, `'s ${pokemon.getAbility().name}\nrestored its HP a little!`), true));
return true;
}
@ -653,7 +676,7 @@ export class PostWeatherLapseDamageAbAttr extends PostWeatherLapseAbAttr {
if (pokemon.getHpRatio() < 1) {
const scene = pokemon.scene;
scene.queueMessage(getPokemonMessage(pokemon, ` is hurt\nby its ${pokemon.getAbility()}!`));
scene.unshiftPhase(new DamagePhase(pokemon.scene, pokemon.isPlayer(), MoveResult.OTHER));
scene.unshiftPhase(new DamagePhase(pokemon.scene, pokemon.getBattlerIndex(), HitResult.OTHER));
pokemon.damage(Math.ceil(pokemon.getMaxHp() * (16 / this.damageFactor)));
return true;
}
@ -685,7 +708,6 @@ export function applyAbAttrs(attrType: { new(...args: any[]): AbAttr }, pokemon:
const ability = pokemon.getAbility();
const attrs = ability.getAttrs(attrType) as AbAttr[];
console.log(attrs, ability);
for (let attr of attrs) {
if (!canApplyAttr(pokemon, attr))
continue;
@ -726,7 +748,7 @@ export function applyPreDefendAbAttrs(attrType: { new(...args: any[]): PreDefend
}
export function applyPostDefendAbAttrs(attrType: { new(...args: any[]): PostDefendAbAttr },
pokemon: Pokemon, attacker: Pokemon, move: PokemonMove, moveResult: MoveResult, ...args: any[]): void {
pokemon: Pokemon, attacker: Pokemon, move: PokemonMove, hitResult: HitResult, ...args: any[]): void {
if (!pokemon.canApplyAbility())
return;
@ -736,7 +758,7 @@ export function applyPostDefendAbAttrs(attrType: { new(...args: any[]): PostDefe
if (!canApplyAttr(pokemon, attr))
continue;
pokemon.scene.setPhaseQueueSplice();
if (attr.applyPostDefend(pokemon, attacker, move, moveResult, args)) {
if (attr.applyPostDefend(pokemon, attacker, move, hitResult, args)) {
if (attr.showAbility)
queueShowAbility(pokemon);
const message = attr.getTriggerMessage(pokemon, attacker, move);
@ -988,7 +1010,7 @@ function canApplyAttr(pokemon: Pokemon, attr: AbAttr): boolean {
}
function queueShowAbility(pokemon: Pokemon): void {
pokemon.scene.unshiftPhase(new ShowAbilityPhase(pokemon.scene, pokemon.isPlayer()));
pokemon.scene.unshiftPhase(new ShowAbilityPhase(pokemon.scene, pokemon.getBattlerIndex()));
pokemon.scene.clearPhaseQueueSplice();
}
@ -1210,7 +1232,8 @@ export function initAbilities() {
new Ability(Abilities.HUSTLE, "Hustle (N)", "Boosts the ATTACK stat, but lowers accuracy.", 3),
new Ability(Abilities.HYPER_CUTTER, "Hyper Cutter", "Prevents other POKéMON from lowering ATTACK stat.", 3)
.attr(ProtectStatAbAttr, BattleStat.ATK),
new Ability(Abilities.ILLUMINATE, "Illuminate (N)", "Raises the likelihood of meeting wild POKéMON.", 3),
new Ability(Abilities.ILLUMINATE, "Illuminate", "Raises the likelihood of an encounter being a double battle.", 3)
.attr(DoubleBattleChanceAbAttr),
new Ability(Abilities.IMMUNITY, "Immunity", "Prevents the POKéMON from getting poisoned.", 3)
.attr(StatusEffectImmunityAbAttr, StatusEffect.POISON),
new Ability(Abilities.INNER_FOCUS, "Inner Focus", "The POKéMON is protected from flinching.", 3)
@ -1232,8 +1255,8 @@ export function initAbilities() {
new Ability(Abilities.MAGMA_ARMOR, "Magma Armor", "Prevents the POKéMON from becoming frozen.", 3)
.attr(StatusEffectImmunityAbAttr, StatusEffect.FREEZE),
new Ability(Abilities.MAGNET_PULL, "Magnet Pull", "Prevents STEEL-type POKéMON from escaping.", 3)
.attr(ArenaTrapAbAttr)
.condition((pokemon: Pokemon) => pokemon.getOpponent()?.isOfType(Type.STEEL)),
/*.attr(ArenaTrapAbAttr)
.condition((pokemon: Pokemon) => pokemon.getOpponent()?.isOfType(Type.STEEL))*/, // TODO: Rework
new Ability(Abilities.MARVEL_SCALE, "Marvel Scale (N)", "Ups DEFENSE if there is a status problem.", 3),
new Ability(Abilities.MINUS, "Minus (N)", "Ups SP. ATK if another POKéMON has PLUS or MINUS.", 3),
new Ability(Abilities.NATURAL_CURE, "Natural Cure (N)", "All status problems heal when it switches out.", 3),

View File

@ -3,7 +3,7 @@ import { Type } from "./type";
import * as Utils from "../utils";
import { Moves, allMoves } from "./move";
import { getPokemonMessage } from "../messages";
import Pokemon, { DamageResult, MoveResult } from "../pokemon";
import Pokemon, { DamageResult, HitResult, MoveResult } from "../pokemon";
import { DamagePhase, ObtainStatusEffectPhase } from "../battle-phases";
import { StatusEffect } from "./status-effect";
import { BattlerTagType } from "./battler-tag";
@ -143,8 +143,7 @@ class SpikesTag extends ArenaTrapTag {
super.onAdd(arena);
const source = arena.scene.getPokemonById(this.sourceId);
const target = source.getOpponent();
arena.scene.queueMessage(`${this.getMoveName()} were scattered\nall around ${target.name}'s feet!`);
arena.scene.queueMessage(`${this.getMoveName()} were scattered\nall around ${source.getOpponentDescriptor()}'s feet!`);
}
activateTrap(pokemon: Pokemon): boolean {
@ -152,7 +151,7 @@ class SpikesTag extends ArenaTrapTag {
const damageHpRatio = 1 / (10 - 2 * this.layers);
pokemon.scene.queueMessage(getPokemonMessage(pokemon, ' is hurt\nby the spikes!'));
pokemon.scene.unshiftPhase(new DamagePhase(pokemon.scene, pokemon.isPlayer(), MoveResult.OTHER));
pokemon.scene.unshiftPhase(new DamagePhase(pokemon.scene, pokemon.getBattlerIndex(), HitResult.OTHER));
pokemon.damage(Math.ceil(pokemon.getMaxHp() * damageHpRatio));
return true;
}
@ -170,15 +169,14 @@ class ToxicSpikesTag extends ArenaTrapTag {
super.onAdd(arena);
const source = arena.scene.getPokemonById(this.sourceId);
const target = source.getOpponent();
arena.scene.queueMessage(`${this.getMoveName()} were scattered\nall around ${target.name}'s feet!`);
arena.scene.queueMessage(`${this.getMoveName()} were scattered\nall around ${source.getOpponentDescriptor()}'s feet!`);
}
activateTrap(pokemon: Pokemon): boolean {
if (!pokemon.status && (!pokemon.isOfType(Type.FLYING) || pokemon.getTag(BattlerTagType.IGNORE_FLYING) || pokemon.scene.arena.getTag(ArenaTagType.GRAVITY))) {
const toxic = this.layers > 1;
pokemon.scene.unshiftPhase(new ObtainStatusEffectPhase(pokemon.scene, pokemon.isPlayer(),
pokemon.scene.unshiftPhase(new ObtainStatusEffectPhase(pokemon.scene, pokemon.getBattlerIndex(),
!toxic ? StatusEffect.POISON : StatusEffect.TOXIC, null, `the ${this.getMoveName()}`));
return true;
}
@ -196,8 +194,7 @@ class StealthRockTag extends ArenaTrapTag {
super.onAdd(arena);
const source = arena.scene.getPokemonById(this.sourceId);
const target = source.getOpponent();
arena.scene.queueMessage(`Pointed stones float in the air\naround ${target.name}!`);
arena.scene.queueMessage(`Pointed stones float in the air\naround ${source.getOpponentDescriptor()}!`);
}
activateTrap(pokemon: Pokemon): boolean {
@ -228,7 +225,7 @@ class StealthRockTag extends ArenaTrapTag {
if (damageHpRatio) {
pokemon.scene.queueMessage(`Pointed stones dug into\n${pokemon.name}!`);
pokemon.scene.unshiftPhase(new DamagePhase(pokemon.scene, pokemon.isPlayer(), MoveResult.OTHER));
pokemon.scene.unshiftPhase(new DamagePhase(pokemon.scene, pokemon.getBattlerIndex(), HitResult.OTHER));
pokemon.damage(Math.ceil(pokemon.getMaxHp() * damageHpRatio));
}
@ -242,8 +239,8 @@ export class TrickRoomTag extends ArenaTag {
}
apply(args: any[]): boolean {
const speedDelayed = args[0] as Utils.BooleanHolder;
speedDelayed.value = !speedDelayed.value;
const speedReversed = args[0] as Utils.BooleanHolder;
speedReversed.value = !speedReversed.value;
return true;
}

View File

@ -1,8 +1,9 @@
//import { battleAnimRawData } from "./battle-anim-raw-data";
import BattleScene from "../battle-scene";
import { ChargeAttr, Moves, allMoves, getMoveTarget } from "./move";
import { ChargeAttr, Moves, allMoves } from "./move";
import Pokemon from "../pokemon";
import * as Utils from "../utils";
import { BattlerIndex } from "../battle";
//import fs from 'vite-plugin-fs/browser';
export enum AnimFrameTarget {
@ -831,8 +832,8 @@ export class CommonBattleAnim extends BattleAnim {
export class MoveAnim extends BattleAnim {
public move: Moves;
constructor(move: Moves, user: Pokemon) {
super(user, getMoveTarget(user, move));
constructor(move: Moves, user: Pokemon, target: BattlerIndex) {
super(user, user.scene.getField()[target]);
this.move = move;
}
@ -852,7 +853,7 @@ export class MoveChargeAnim extends MoveAnim {
private chargeAnim: ChargeAnim;
constructor(chargeAnim: ChargeAnim, move: Moves, user: Pokemon) {
super(move, user);
super(move, user, 0);
this.chargeAnim = chargeAnim;
}

View File

@ -103,7 +103,7 @@ export class RechargingTag extends BattlerTag {
onAdd(pokemon: Pokemon): void {
super.onAdd(pokemon);
pokemon.getMoveQueue().push({ move: Moves.NONE })
pokemon.getMoveQueue().push({ move: Moves.NONE, targets: [] })
}
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
@ -177,7 +177,7 @@ export class ConfusedTag extends BattlerTag {
onAdd(pokemon: Pokemon): void {
super.onAdd(pokemon);
pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, pokemon.isPlayer(), CommonAnim.CONFUSION));
pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, pokemon.getBattlerIndex(), undefined, CommonAnim.CONFUSION));
pokemon.scene.queueMessage(getPokemonMessage(pokemon, ' became\nconfused!'));
}
@ -198,14 +198,14 @@ export class ConfusedTag extends BattlerTag {
if (ret) {
pokemon.scene.queueMessage(getPokemonMessage(pokemon, ' is\nconfused!'));
pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, pokemon.isPlayer(), CommonAnim.CONFUSION));
pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, pokemon.getBattlerIndex(), undefined, CommonAnim.CONFUSION));
if (Utils.randInt(2)) {
const atk = pokemon.getBattleStat(Stat.ATK);
const def = pokemon.getBattleStat(Stat.DEF);
const damage = Math.ceil(((((2 * pokemon.level / 5 + 2) * 40 * atk / def) / 50) + 2) * ((Utils.randInt(15) + 85) / 100));
pokemon.scene.queueMessage('It hurt itself in its\nconfusion!');
pokemon.scene.unshiftPhase(new DamagePhase(pokemon.scene, pokemon.isPlayer()));
pokemon.scene.unshiftPhase(new DamagePhase(pokemon.scene, pokemon.getBattlerIndex()));
pokemon.damage(damage);
(pokemon.scene.getCurrentPhase() as MovePhase).cancel();
}
@ -245,7 +245,7 @@ export class InfatuatedTag extends BattlerTag {
if (ret) {
pokemon.scene.queueMessage(getPokemonMessage(pokemon, ` is in love\nwith ${pokemon.scene.getPokemonById(this.sourceId).name}!`));
pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, pokemon.isPlayer(), CommonAnim.ATTRACT));
pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, pokemon.getBattlerIndex(), undefined, CommonAnim.ATTRACT));
if (Utils.randInt(2)) {
pokemon.scene.queueMessage(getPokemonMessage(pokemon, ' is\nimmobilized by love!'));
@ -282,12 +282,13 @@ export class SeedTag extends BattlerTag {
const ret = lapseType !== BattlerTagLapseType.CUSTOM || super.lapse(pokemon, lapseType);
if (ret) {
pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, !pokemon.isPlayer(), CommonAnim.LEECH_SEED));
const source = pokemon.scene.getPokemonById(this.sourceId);
pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, source.getBattlerIndex(), pokemon.getBattlerIndex(), CommonAnim.LEECH_SEED));
const damage = Math.max(Math.floor(pokemon.getMaxHp() / 8), 1);
pokemon.scene.unshiftPhase(new DamagePhase(pokemon.scene, pokemon.isPlayer()));
pokemon.scene.unshiftPhase(new DamagePhase(pokemon.scene, pokemon.getBattlerIndex()));
pokemon.damage(damage);
pokemon.scene.unshiftPhase(new PokemonHealPhase(pokemon.scene, !pokemon.isPlayer(), damage, getPokemonMessage(pokemon, '\'s health is\nsapped by LEECH SEED!'), false, true));
pokemon.scene.unshiftPhase(new PokemonHealPhase(pokemon.scene, source.getBattlerIndex(), damage, getPokemonMessage(pokemon, '\'s health is\nsapped by LEECH SEED!'), false, true));
}
return ret;
@ -320,10 +321,10 @@ export class NightmareTag extends BattlerTag {
if (ret) {
pokemon.scene.queueMessage(getPokemonMessage(pokemon, ' is locked\nin a NIGHTMARE!'));
pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, pokemon.isPlayer(), CommonAnim.CURSE)); // TODO: Update animation type
pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, pokemon.getBattlerIndex(), undefined, CommonAnim.CURSE)); // TODO: Update animation type
const damage = Math.ceil(pokemon.getMaxHp() / 4);
pokemon.scene.unshiftPhase(new DamagePhase(pokemon.scene, pokemon.isPlayer()));
pokemon.scene.unshiftPhase(new DamagePhase(pokemon.scene, pokemon.getBattlerIndex()));
pokemon.damage(damage);
}
@ -344,7 +345,7 @@ export class IngrainTag extends TrappedTag {
const ret = lapseType !== BattlerTagLapseType.CUSTOM || super.lapse(pokemon, lapseType);
if (ret)
pokemon.scene.unshiftPhase(new PokemonHealPhase(pokemon.scene, pokemon.isPlayer(), Math.floor(pokemon.getMaxHp() / 16),
pokemon.scene.unshiftPhase(new PokemonHealPhase(pokemon.scene, pokemon.getBattlerIndex(), Math.floor(pokemon.getMaxHp() / 16),
getPokemonMessage(pokemon, ` absorbed\nnutrients with its roots!`), true));
return ret;
@ -374,7 +375,8 @@ export class AquaRingTag extends BattlerTag {
const ret = lapseType !== BattlerTagLapseType.CUSTOM || super.lapse(pokemon, lapseType);
if (ret)
pokemon.scene.unshiftPhase(new PokemonHealPhase(pokemon.scene, pokemon.isPlayer(), Math.floor(pokemon.getMaxHp() / 16), `${this.getMoveName()} restored\n${pokemon.name}\'s HP!`, true));
pokemon.scene.unshiftPhase(new PokemonHealPhase(pokemon.scene, pokemon.getBattlerIndex(),
Math.floor(pokemon.getMaxHp() / 16), `${this.getMoveName()} restored\n${pokemon.name}\'s HP!`, true));
return ret;
}
@ -393,7 +395,7 @@ export class DrowsyTag extends BattlerTag {
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
if (!super.lapse(pokemon, lapseType)) {
pokemon.scene.unshiftPhase(new ObtainStatusEffectPhase(pokemon.scene, pokemon.isPlayer(), StatusEffect.SLEEP));
pokemon.scene.unshiftPhase(new ObtainStatusEffectPhase(pokemon.scene, pokemon.getBattlerIndex(), StatusEffect.SLEEP));
return false;
}
@ -423,10 +425,10 @@ export abstract class DamagingTrapTag extends TrappedTag {
if (ret) {
pokemon.scene.queueMessage(getPokemonMessage(pokemon, ` is hurt\nby ${this.getMoveName()}!`));
pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, pokemon.isPlayer(), this.commonAnim));
pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, pokemon.getBattlerIndex(), undefined, this.commonAnim));
const damage = Math.ceil(pokemon.getMaxHp() / 16);
pokemon.scene.unshiftPhase(new DamagePhase(pokemon.scene, pokemon.isPlayer()));
pokemon.scene.unshiftPhase(new DamagePhase(pokemon.scene, pokemon.getBattlerIndex()));
pokemon.damage(damage);
}
@ -541,7 +543,7 @@ export class TruantTag extends BattlerTag {
if (lastMove && lastMove.move !== Moves.NONE) {
(pokemon.scene.getCurrentPhase() as MovePhase).cancel();
pokemon.scene.unshiftPhase(new ShowAbilityPhase(pokemon.scene, pokemon.isPlayer()));
pokemon.scene.unshiftPhase(new ShowAbilityPhase(pokemon.scene, pokemon.getBattlerIndex()));
pokemon.scene.queueMessage(getPokemonMessage(pokemon, ' is\nloafing around!'));
}

View File

@ -1,6 +1,6 @@
import { PokemonHealPhase, StatChangePhase } from "../battle-phases";
import { getPokemonMessage } from "../messages";
import Pokemon, { MoveResult } from "../pokemon";
import Pokemon, { HitResult, MoveResult } from "../pokemon";
import { getBattleStatName } from "./battle-stat";
import { BattleStat } from "./battle-stat";
import { BattlerTagType } from "./battler-tag";
@ -54,12 +54,7 @@ export function getBerryPredicate(berryType: BerryType): BerryPredicate {
case BerryType.LUM:
return (pokemon: Pokemon) => !!pokemon.status || !!pokemon.getTag(BattlerTagType.CONFUSED);
case BerryType.ENIGMA:
return (pokemon: Pokemon) => {
const opponent = pokemon.getOpponent();
const opponentLastMove = opponent ? opponent.getLastXMoves(1).find(() => true) : null; // TODO: Update so this works even if opponent has fainted
return opponentLastMove && opponentLastMove.turn === pokemon.scene.currentBattle?.turn - 1 && opponentLastMove.result === MoveResult.SUPER_EFFECTIVE;
};
return (pokemon: Pokemon) => !!pokemon.turnData.attacksReceived.filter(a => a.result === HitResult.SUPER_EFFECTIVE).length;
case BerryType.LIECHI:
case BerryType.GANLON:
case BerryType.SALAC:
@ -83,7 +78,8 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc {
case BerryType.SITRUS:
case BerryType.ENIGMA:
return (pokemon: Pokemon) => {
pokemon.scene.unshiftPhase(new PokemonHealPhase(pokemon.scene, pokemon.isPlayer(), Math.floor(pokemon.getMaxHp() / 4), getPokemonMessage(pokemon, `'s ${getBerryName(berryType)}\nrestored its HP!`), true));
pokemon.scene.unshiftPhase(new PokemonHealPhase(pokemon.scene, pokemon.getBattlerIndex(),
Math.floor(pokemon.getMaxHp() / 4), getPokemonMessage(pokemon, `'s ${getBerryName(berryType)}\nrestored its HP!`), true));
};
case BerryType.LUM:
return (pokemon: Pokemon) => {
@ -101,13 +97,13 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc {
case BerryType.APICOT:
return (pokemon: Pokemon) => {
const battleStat = (berryType - BerryType.LIECHI) as BattleStat;
pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.isPlayer(), true, [ battleStat ], 1));
pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ battleStat ], 1));
};
case BerryType.LANSAT:
return (pokemon: Pokemon) => {
pokemon.addTag(BattlerTagType.CRIT_BOOST);
};
case BerryType.STARF:
return (pokemon: Pokemon) => pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.isPlayer(), true, [ BattleStat.RAND ], 2));
return (pokemon: Pokemon) => pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ BattleStat.RAND ], 2));
}
}

View File

@ -1,9 +1,9 @@
import { ChargeAnim, MoveChargeAnim, initMoveAnim, loadMoveAnimAssets } from "./battle-anims";
import { DamagePhase, EnemyMovePhase, ObtainStatusEffectPhase, PlayerMovePhase, PokemonHealPhase, StatChangePhase } from "../battle-phases";
import { DamagePhase, MovePhase, ObtainStatusEffectPhase, PokemonHealPhase, StatChangePhase } from "../battle-phases";
import { BattleStat } from "./battle-stat";
import { BattlerTagType } from "./battler-tag";
import { getPokemonMessage } from "../messages";
import Pokemon, { AttackMoveResult, EnemyPokemon, MoveResult, PlayerPokemon, PokemonMove, TurnMove } from "../pokemon";
import Pokemon, { AttackMoveResult, EnemyPokemon, HitResult, MoveResult, PlayerPokemon, PokemonMove, TurnMove } from "../pokemon";
import { StatusEffect, getStatusEffectDescriptor } from "./status-effect";
import { Type } from "./type";
import * as Utils from "../utils";
@ -11,6 +11,8 @@ import { WeatherType } from "./weather";
import { ArenaTagType, ArenaTrapTag } from "./arena-tag";
import { BlockRecoilDamageAttr, applyAbAttrs } from "./ability";
import { PokemonHeldItemModifier } from "../modifier/modifier";
import { BattlerIndex } from "../battle";
import { Stat } from "./pokemon-stat";
export enum MoveCategory {
PHYSICAL,
@ -24,14 +26,13 @@ export enum MoveTarget {
ALL_OTHERS,
NEAR_OTHER,
ALL_NEAR_OTHERS,
ENEMY,
NEAR_ENEMY,
ALL_NEAR_ENEMIES,
RANDOM_NEAR_ENEMY,
ALL_ENEMIES,
ATTACKER,
ALLY,
NEAR_ALLY,
ALLY,
USER_OR_NEAR_ALLY,
USER_AND_ALLIES,
ALL,
@ -47,6 +48,7 @@ export enum MoveFlags {
}
type MoveCondition = (user: Pokemon, target: Pokemon, move: Move) => boolean;
type UserMoveCondition = (user: Pokemon, move: Move) => boolean;
export default class Move {
public id: Moves;
@ -159,12 +161,67 @@ export default class Move {
return true;
}
getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer {
let score = 0;
for (let attr of this.attrs)
score += attr.getUserBenefitScore(user, target, move);
return score;
}
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer {
let score = 0;
for (let attr of this.attrs)
score += attr.getTargetBenefitScore(user, target, move);
return score;
}
}
export class AttackMove extends Move {
constructor(id: Moves, name: string, type: Type, category: MoveCategory, power: integer, accuracy: integer, pp: integer, tm: integer, effect: string, chance: integer, priority: integer, generation: integer) {
super(id, name, type, category, MoveTarget.NEAR_OTHER, power, accuracy, pp, tm, effect, chance, priority, generation);
}
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer {
let ret = super.getTargetBenefitScore(user, target, move);
let attackScore = 0;
const effectiveness = target.getAttackMoveEffectiveness(this.type);
attackScore = Math.pow(effectiveness - 1, 2) * effectiveness < 1 ? -2 : 2;
if (attackScore) {
if (this.category === MoveCategory.PHYSICAL) {
if (user.getBattleStat(Stat.ATK) > user.getBattleStat(Stat.SPATK)) {
const statRatio = user.getBattleStat(Stat.SPATK) / user.getBattleStat(Stat.ATK);
if (statRatio <= 0.75)
attackScore *= 2;
else if (statRatio <= 0.875)
attackScore *= 1.5;
}
} else {
if (user.getBattleStat(Stat.SPATK) > user.getBattleStat(Stat.ATK)) {
const statRatio = user.getBattleStat(Stat.ATK) / user.getBattleStat(Stat.SPATK);
if (statRatio <= 0.75)
attackScore *= 2;
else if (statRatio <= 0.875)
attackScore *= 1.5;
}
}
const power = new Utils.NumberHolder(this.power);
applyMoveAttrs(VariablePowerAttr, user, target, move, power);
attackScore += Math.floor(power.value / 5);
}
ret -= attackScore;
return ret;
}
}
export class StatusMove extends Move {
@ -755,6 +812,14 @@ export abstract class MoveAttr {
getCondition(): MoveCondition {
return null;
}
getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer {
return 0;
}
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer {
return 0;
}
}
export class MoveEffectAttr extends MoveAttr {
@ -792,6 +857,10 @@ export class HighCritAttr extends MoveAttr {
return true;
}
getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer {
return 3;
}
}
export class CritOnlyAttr extends MoveAttr {
@ -800,6 +869,10 @@ export class CritOnlyAttr extends MoveAttr {
return true;
}
getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer {
return 5;
}
}
export class FixedDamageAttr extends MoveAttr {
@ -889,12 +962,16 @@ export class RecoilAttr extends MoveEffectAttr {
return false;
const recoilDamage = Math.max(Math.floor((!this.useHp ? user.turnData.damageDealt : user.getMaxHp()) / 4), 1);
user.scene.unshiftPhase(new DamagePhase(user.scene, user.isPlayer(), MoveResult.OTHER));
user.scene.unshiftPhase(new DamagePhase(user.scene, user.getBattlerIndex(), HitResult.OTHER));
user.scene.queueMessage(getPokemonMessage(user, ' is hit\nwith recoil!'));
user.damage(recoilDamage);
return true;
}
getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer {
return Math.floor((move.power / 5) / -4);
}
}
export class SacrificialAttr extends MoveEffectAttr {
@ -906,11 +983,15 @@ export class SacrificialAttr extends MoveEffectAttr {
if (!super.apply(user, target, move, args))
return false;
user.scene.unshiftPhase(new DamagePhase(user.scene, user.isPlayer(), MoveResult.OTHER));
user.scene.unshiftPhase(new DamagePhase(user.scene, user.getBattlerIndex(), HitResult.OTHER));
user.damage(user.getMaxHp());
return true;
}
getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer {
return Math.ceil((1 - user.getHpRatio()) * 10) - 10;
}
}
export enum MultiHitType {
@ -936,7 +1017,12 @@ export class HealAttr extends MoveEffectAttr {
}
addHealPhase(user: Pokemon, healRatio: number) {
user.scene.unshiftPhase(new PokemonHealPhase(user.scene, user.isPlayer(), Math.max(Math.floor(user.getMaxHp() * healRatio), 1), getPokemonMessage(user, ' regained\nhealth!'), true, !this.showAnim));
user.scene.unshiftPhase(new PokemonHealPhase(user.scene, user.getBattlerIndex(),
Math.max(Math.floor(user.getMaxHp() * healRatio), 1), getPokemonMessage(user, ' regained\nhealth!'), true, !this.showAnim));
}
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer {
return (1 - (this.selfTarget ? user : target).getHpRatio()) * 20;
}
}
@ -977,9 +1063,14 @@ export class HitHealAttr extends MoveHitEffectAttr {
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
user.scene.unshiftPhase(new PokemonHealPhase(user.scene, user.isPlayer(), Math.max(Math.floor(user.turnData.damageDealt * this.healRatio), 1), getPokemonMessage(target, ` had its\nenergy drained!`), false, true));
user.scene.unshiftPhase(new PokemonHealPhase(user.scene, user.getBattlerIndex(),
Math.max(Math.floor(user.turnData.damageDealt * this.healRatio), 1), getPokemonMessage(target, ` had its\nenergy drained!`), false, true));
return true;
}
getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer {
return Math.floor(Math.max((1 - user.getHpRatio()) - 0.33, 0) * ((move.power / 5) / 4));
}
}
export class MultiHitAttr extends MoveAttr {
@ -1016,6 +1107,10 @@ export class MultiHitAttr extends MoveAttr {
(args[0] as Utils.IntegerHolder).value = hitTimes;
return true;
}
getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
return 5;
}
}
export class StatusEffectAttr extends MoveHitEffectAttr {
@ -1034,12 +1129,16 @@ export class StatusEffectAttr extends MoveHitEffectAttr {
if (statusCheck) {
const pokemon = this.selfTarget ? user : target;
if (!pokemon.status || (pokemon.status.effect === this.effect && move.chance < 0)) {
user.scene.unshiftPhase(new ObtainStatusEffectPhase(user.scene, pokemon.isPlayer(), this.effect, this.cureTurn));
user.scene.unshiftPhase(new ObtainStatusEffectPhase(user.scene, pokemon.getBattlerIndex(), this.effect, this.cureTurn));
return true;
}
}
return false;
}
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
return !(this.selfTarget ? user : target).status ? Math.floor(move.chance * -0.1) : 0;
}
}
export class StealHeldItemAttr extends MoveHitEffectAttr {
@ -1048,8 +1147,7 @@ export class StealHeldItemAttr extends MoveHitEffectAttr {
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const heldItems = user.scene.findModifiers(m => m instanceof PokemonHeldItemModifier
&& (m as PokemonHeldItemModifier).pokemonId === target.id, target.isPlayer()) as PokemonHeldItemModifier[];
const heldItems = this.getTargetHeldItems(target);
if (heldItems.length) {
const stolenItem = heldItems[Utils.randInt(heldItems.length)];
user.scene.tryTransferHeldItemModifier(stolenItem, user, false, false);
@ -1060,6 +1158,21 @@ export class StealHeldItemAttr extends MoveHitEffectAttr {
return false;
}
getTargetHeldItems(target: Pokemon): PokemonHeldItemModifier[] {
return target.scene.findModifiers(m => m instanceof PokemonHeldItemModifier
&& (m as PokemonHeldItemModifier).pokemonId === target.id, target.isPlayer()) as PokemonHeldItemModifier[];
}
getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
const heldItems = this.getTargetHeldItems(target);
return heldItems.length ? 5 : 0;
}
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
const heldItems = this.getTargetHeldItems(target);
return heldItems.length ? -5 : 0;
}
}
export class HealStatusEffectAttr extends MoveEffectAttr {
@ -1086,6 +1199,10 @@ export class HealStatusEffectAttr extends MoveEffectAttr {
return false;
}
getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer {
return user.status ? 10 : 0;
}
}
export class BypassSleepAttr extends MoveAttr {
@ -1170,8 +1287,8 @@ export class ChargeAttr extends OverrideMoveEffectAttr {
user.addTag(this.tagType, 1, move.id, user.id);
if (this.chargeEffect)
applyMoveAttrs(MoveEffectAttr, user, target, move);
user.pushMoveHistory({ move: move.id, result: MoveResult.OTHER });
user.getMoveQueue().push({ move: move.id, ignorePP: true });
user.pushMoveHistory({ move: move.id, targets: [ target.getBattlerIndex() ], result: MoveResult.OTHER });
user.getMoveQueue().push({ move: move.id, targets: [ target.getBattlerIndex() ], ignorePP: true });
resolve(true);
});
} else
@ -1214,7 +1331,7 @@ export class StatChangeAttr extends MoveEffectAttr {
if (move.chance < 0 || move.chance === 100 || Utils.randInt(100) < move.chance) {
const levels = this.getLevels(user);
user.scene.unshiftPhase(new StatChangePhase(user.scene, user.isPlayer() === this.selfTarget, this.selfTarget, this.stats, levels));
user.scene.unshiftPhase(new StatChangePhase(user.scene, (this.selfTarget ? user : target).getBattlerIndex(), this.selfTarget, this.stats, levels));
return true;
}
@ -1224,6 +1341,12 @@ export class StatChangeAttr extends MoveEffectAttr {
getLevels(_user: Pokemon): integer {
return this.levels;
}
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer {
// TODO: Add awareness of level limits
const levels = this.getLevels(user);
return (levels * 4) + (levels > 0 ? -2 : 2);
}
}
export class GrowthStatChangeAttr extends StatChangeAttr {
@ -1273,7 +1396,7 @@ export abstract class ConsecutiveUsePowerMultiplierAttr extends MovePowerMultipl
let count = 0;
let turnMove: TurnMove;
while (((turnMove = moveHistory.shift())?.move === move.id || (comboMoves.length && comboMoves.indexOf(turnMove?.move) > -1)) && (!resetOnFail || turnMove.result < MoveResult.NO_EFFECT)) {
while (((turnMove = moveHistory.shift())?.move === move.id || (comboMoves.length && comboMoves.indexOf(turnMove?.move) > -1)) && (!resetOnFail || turnMove.result === MoveResult.SUCCESS)) {
if (count < (limit - 1))
count++;
else if (resetOnLimit)
@ -1458,16 +1581,16 @@ export class BlizzardAccuracyAttr extends VariableAccuracyAttr {
}
export class MissEffectAttr extends MoveAttr {
private missEffectFunc: MoveCondition;
private missEffectFunc: UserMoveCondition;
constructor(missEffectFunc: MoveCondition) {
constructor(missEffectFunc: UserMoveCondition) {
super();
this.missEffectFunc = missEffectFunc;
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
this.missEffectFunc(user, target, move);
this.missEffectFunc(user, move);
return true;
}
}
@ -1530,7 +1653,7 @@ export class FrenzyAttr extends MoveEffectAttr {
}
canApply(user: Pokemon, target: Pokemon, move: Move, args: any[]) {
return !!(this.selfTarget ? user.hp : target.hp);
return !(this.selfTarget ? user : target).isFainted();
}
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
@ -1540,7 +1663,7 @@ export class FrenzyAttr extends MoveEffectAttr {
if (!user.getMoveQueue().length) {
if (!user.getTag(BattlerTagType.FRENZY)) {
const turnCount = Utils.randInt(2) + 1;
new Array(turnCount).fill(null).map(() => user.getMoveQueue().push({ move: move.id, ignorePP: true }));
new Array(turnCount).fill(null).map(() => user.getMoveQueue().push({ move: move.id, targets: [ target.getBattlerIndex() ], ignorePP: true }));
user.addTag(BattlerTagType.FRENZY, 1, move.id, user.id);
} else {
applyMoveAttrs(AddBattlerTagAttr, user, target, move, args);
@ -1553,7 +1676,7 @@ export class FrenzyAttr extends MoveEffectAttr {
}
}
export const frenzyMissFunc: MoveCondition = (user: Pokemon, target: Pokemon, move: Move) => {
export const frenzyMissFunc: UserMoveCondition = (user: Pokemon, move: Move) => {
while (user.getMoveQueue().length && user.getMoveQueue()[0].move === move.id)
user.getMoveQueue().shift();
user.lapseTag(BattlerTagType.FRENZY);
@ -1596,6 +1719,48 @@ export class AddBattlerTagAttr extends MoveEffectAttr {
? (user: Pokemon, target: Pokemon, move: Move) => !(this.selfTarget ? user : target).getTag(this.tagType)
: null;
}
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer {
switch (this.tagType) {
case BattlerTagType.FLINCHED:
return -5;
case BattlerTagType.CONFUSED:
return -5;
case BattlerTagType.INFATUATED:
return -5;
case BattlerTagType.SEEDED:
return -3;
case BattlerTagType.NIGHTMARE:
return -5;
case BattlerTagType.FRENZY:
return -2;
case BattlerTagType.INGRAIN:
return 3;
case BattlerTagType.AQUA_RING:
return 3;
case BattlerTagType.DROWSY:
return -5;
case BattlerTagType.TRAPPED:
case BattlerTagType.BIND:
case BattlerTagType.WRAP:
case BattlerTagType.FIRE_SPIN:
case BattlerTagType.WHIRLPOOL:
case BattlerTagType.CLAMP:
case BattlerTagType.SAND_TOMB:
case BattlerTagType.MAGMA_STORM:
return -3;
case BattlerTagType.PROTECTED:
return 10;
case BattlerTagType.FLYING:
return 5;
case BattlerTagType.CRIT_BOOST:
return 5;
case BattlerTagType.NO_CRIT:
return -5;
case BattlerTagType.IGNORE_ACCURACY:
return 3;
}
}
}
export class LapseBattlerTagAttr extends MoveEffectAttr {
@ -1646,7 +1811,7 @@ export class ProtectAttr extends AddBattlerTagAttr {
let timesUsed = 0;
const moveHistory = user.getLastXMoves(-1);
let turnMove: TurnMove;
while (moveHistory.length && (turnMove = moveHistory.shift()).move === move.id && turnMove.result === MoveResult.STATUS)
while (moveHistory.length && (turnMove = moveHistory.shift()).move === move.id && turnMove.result === MoveResult.SUCCESS)
timesUsed++;
if (timesUsed)
return !Utils.randInt(Math.pow(2, timesUsed));
@ -1680,6 +1845,10 @@ export class HitsTagAttr extends MoveAttr {
this.tagType = tagType;
this.doubleDamage = !!doubleDamage;
}
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer {
return target.getTag(this.tagType) ? this.doubleDamage ? 10 : 5 : 0;
}
}
export class AddArenaTagAttr extends MoveEffectAttr {
@ -1768,10 +1937,16 @@ export class RandomMovesetMoveAttr extends OverrideMoveEffectAttr {
if (moves.length) {
const move = moves[Utils.randInt(moves.length)];
const moveIndex = moveset.findIndex(m => m.moveId === move.moveId);
user.getMoveQueue().push({ move: move.moveId, ignorePP: this.enemyMoveset });
user.scene.unshiftPhase(user.isPlayer()
? new PlayerMovePhase(user.scene, user as PlayerPokemon, moveset[moveIndex], true)
: new EnemyMovePhase(user.scene, user as EnemyPokemon, moveset[moveIndex], true));
const moveTargets = getMoveTargets(user, move.moveId);
if (!moveTargets.targets.length)
return false;
const targets = moveTargets.multiple || moveTargets.targets.length === 1
? moveTargets.targets
: moveTargets.targets.indexOf(target.getBattlerIndex()) > -1
? [ target.getBattlerIndex() ]
: [ moveTargets.targets[Utils.randInt(moveTargets.targets.length)] ];
user.getMoveQueue().push({ move: move.moveId, targets: targets, ignorePP: this.enemyMoveset });
user.scene.unshiftPhase(new MovePhase(user.scene, user, targets, moveset[moveIndex], true));
return true;
}
@ -1784,10 +1959,19 @@ export class RandomMoveAttr extends OverrideMoveEffectAttr {
return new Promise(resolve => {
const moveIds = Utils.getEnumValues(Moves).filter(m => !allMoves[m].hasFlag(MoveFlags.IGNORE_VIRTUAL));
const moveId = moveIds[Utils.randInt(moveIds.length)];
user.getMoveQueue().push({ move: moveId, ignorePP: true });
user.scene.unshiftPhase(user.isPlayer()
? new PlayerMovePhase(user.scene, user as PlayerPokemon, new PokemonMove(moveId, 0, 0, true), true)
: new EnemyMovePhase(user.scene, user as EnemyPokemon, new PokemonMove(moveId, 0, 0, true), true));
const moveTargets = getMoveTargets(user, moveId);
if (!moveTargets.targets.length) {
resolve(false);
return;
}
const targets = moveTargets.multiple || moveTargets.targets.length === 1
? moveTargets.targets
: moveTargets.targets.indexOf(target.getBattlerIndex()) > -1
? [ target.getBattlerIndex() ]
: [ moveTargets.targets[Utils.randInt(moveTargets.targets.length)] ];
user.getMoveQueue().push({ move: moveId, targets: targets, ignorePP: true });
user.scene.unshiftPhase(new MovePhase(user.scene, user, targets, new PokemonMove(moveId, 0, 0, true), true));
initMoveAnim(moveId).then(() => {
loadMoveAnimAssets(user.scene, [ moveId ], true)
.then(() => resolve(true));
@ -1822,10 +2006,18 @@ export class CopyMoveAttr extends OverrideMoveEffectAttr {
const copiedMove = targetMoves[0];
user.getMoveQueue().push({ move: copiedMove.move, ignorePP: true });
user.scene.unshiftPhase(user.isPlayer()
? new PlayerMovePhase(user.scene, user as PlayerPokemon, new PokemonMove(copiedMove.move, 0, 0, true), true)
: new EnemyMovePhase(user.scene, user as EnemyPokemon, new PokemonMove(copiedMove.move, 0, 0, true), true));
const moveTargets = getMoveTargets(user, copiedMove.move);
if (!moveTargets.targets.length)
return false;
const targets = moveTargets.multiple || moveTargets.targets.length === 1
? moveTargets.targets
: moveTargets.targets.indexOf(target.getBattlerIndex()) > -1
? [ target.getBattlerIndex() ]
: [ moveTargets.targets[Utils.randInt(moveTargets.targets.length)] ];
user.getMoveQueue().push({ move: copiedMove.move, targets: targets, ignorePP: true });
user.scene.unshiftPhase(new MovePhase(user.scene, user as PlayerPokemon, targets, new PokemonMove(copiedMove.move, 0, 0, true), true));
return true;
}
@ -1932,20 +2124,60 @@ export function applyFilteredMoveAttrs(attrFilter: MoveAttrFilter, user: Pokemon
return applyMoveAttrsInternal(attrFilter, user, target, move, args);
}
export function getMoveTarget(user: Pokemon, move: Moves): Pokemon {
const moveTarget = allMoves[move].moveTarget;
export type MoveTargetSet = {
targets: BattlerIndex[];
multiple: boolean;
}
const other = user.getOpponent();
export function getMoveTargets(user: Pokemon, move: Moves): MoveTargetSet {
const moveTarget = move ? allMoves[move].moveTarget : move === undefined ? MoveTarget.NEAR_ENEMY : [];
const opponents = user.getOpponents();
let set: BattlerIndex[] = [];
let multiple = false;
switch (moveTarget) {
case MoveTarget.USER:
set = [ user.getBattlerIndex() ];
break;
case MoveTarget.NEAR_OTHER:
case MoveTarget.OTHER:
case MoveTarget.ALL_NEAR_OTHERS:
case MoveTarget.ALL_OTHERS:
set = (opponents.concat([ user.getAlly() ])).map(p => p?.getBattlerIndex());
multiple = moveTarget === MoveTarget.ALL_NEAR_OTHERS || moveTarget === MoveTarget.ALL_OTHERS
break;
case MoveTarget.NEAR_ENEMY:
case MoveTarget.ALL_NEAR_ENEMIES:
case MoveTarget.ALL_ENEMIES:
case MoveTarget.ENEMY_SIDE:
set = opponents.map(p => p.getBattlerIndex());
multiple = moveTarget !== MoveTarget.NEAR_ENEMY;
break;
case MoveTarget.RANDOM_NEAR_ENEMY:
set = [ opponents[Utils.randInt(opponents.length)].getBattlerIndex() ];
break;
case MoveTarget.ATTACKER:
set = [ user.scene.getPokemonById(user.turnData.attacksReceived[0].sourceId).getBattlerIndex() ];
break;
case MoveTarget.NEAR_ALLY:
case MoveTarget.ALLY:
set = [ user.getAlly()?.getBattlerIndex() ];
break;
case MoveTarget.USER_OR_NEAR_ALLY:
case MoveTarget.USER_AND_ALLIES:
case MoveTarget.USER_SIDE:
return user;
default:
return other;
set = [ user, user.getAlly() ].map(p => p?.getBattlerIndex());
multiple = moveTarget !== MoveTarget.USER_OR_NEAR_ALLY;
break;
case MoveTarget.ALL:
case MoveTarget.BOTH_SIDES:
set = [ user, user.getAlly() ].concat(user.getOpponents()).map(p => p?.getBattlerIndex());
multiple = true;
break;
}
return { targets: set.filter(t => t !== undefined), multiple };
}
export const allMoves = [
@ -2002,7 +2234,7 @@ export function initMoves() {
.attr(MultiHitAttr, MultiHitType._2),
new AttackMove(Moves.MEGA_KICK, "Mega Kick", Type.NORMAL, MoveCategory.PHYSICAL, 120, 75, 5, -1, "", -1, 0, 1),
new AttackMove(Moves.JUMP_KICK, "Jump Kick", Type.FIGHTING, MoveCategory.PHYSICAL, 100, 95, 10, -1, "If it misses, the user loses half their HP.", -1, 0, 1)
.attr(MissEffectAttr, (user: Pokemon, target: Pokemon, move: Move) => { user.damage(Math.floor(user.getMaxHp() / 2)); return true; })
.attr(MissEffectAttr, (user: Pokemon, move: Move) => { user.damage(Math.floor(user.getMaxHp() / 2)); return true; })
.condition(failOnGravityCondition),
new AttackMove(Moves.ROLLING_KICK, "Rolling Kick", Type.FIGHTING, MoveCategory.PHYSICAL, 60, 85, 15, -1, "May cause flinching.", 30, 0, 1)
.attr(FlinchAttr),
@ -2070,13 +2302,15 @@ export function initMoves() {
.target(MoveTarget.USER_SIDE),
new AttackMove(Moves.WATER_GUN, "Water Gun", Type.WATER, MoveCategory.SPECIAL, 40, 100, 25, -1, "", -1, 0, 1),
new AttackMove(Moves.HYDRO_PUMP, "Hydro Pump", Type.WATER, MoveCategory.SPECIAL, 110, 80, 5, 142, "", -1, 0, 1),
new AttackMove(Moves.SURF, "Surf", Type.WATER, MoveCategory.SPECIAL, 90, 100, 15, 123, "Hits all adjacent Pokémon.", -1, 0, 1), // TODO
new AttackMove(Moves.SURF, "Surf", Type.WATER, MoveCategory.SPECIAL, 90, 100, 15, 123, "Hits all adjacent Pokémon.", -1, 0, 1)
.target(MoveTarget.ALL_NEAR_OTHERS),
new AttackMove(Moves.ICE_BEAM, "Ice Beam", Type.ICE, MoveCategory.SPECIAL, 90, 100, 10, 135, "May freeze opponent.", 10, 0, 1)
.attr(StatusEffectAttr, StatusEffect.FREEZE)
.target(MoveTarget.ALL_NEAR_OTHERS),
new AttackMove(Moves.BLIZZARD, "Blizzard", Type.ICE, MoveCategory.SPECIAL, 110, 70, 5, 143, "May freeze opponent.", 10, 0, 1)
.attr(BlizzardAccuracyAttr)
.attr(StatusEffectAttr, StatusEffect.FREEZE), // TODO: 30% chance to hit protect/detect in hail
.attr(StatusEffectAttr, StatusEffect.FREEZE) // TODO: 30% chance to hit protect/detect in hail
.target(MoveTarget.ALL_NEAR_ENEMIES),
new AttackMove(Moves.PSYBEAM, "Psybeam", Type.PSYCHIC, MoveCategory.SPECIAL, 65, 100, 20, 16, "May confuse opponent.", 10, 0, 1)
.attr(ConfuseAttr)
.target(MoveTarget.ALL_NEAR_ENEMIES),
@ -2253,7 +2487,7 @@ export function initMoves() {
new SelfStatusMove(Moves.SOFT_BOILED, "Soft-Boiled", Type.NORMAL, -1, 5, -1, "User recovers half its max HP.", -1, 0, 1)
.attr(HealAttr, 0.5),
new AttackMove(Moves.HIGH_JUMP_KICK, "High Jump Kick", Type.FIGHTING, MoveCategory.PHYSICAL, 130, 90, 10, -1, "If it misses, the user loses half their HP.", -1, 0, 1)
.attr(MissEffectAttr, (user: Pokemon, target: Pokemon, move: Move) => { user.damage(Math.floor(user.getMaxHp() / 2)); return true; })
.attr(MissEffectAttr, (user: Pokemon, move: Move) => { user.damage(Math.floor(user.getMaxHp() / 2)); return true; })
.condition(failOnGravityCondition),
new StatusMove(Moves.GLARE, "Glare", Type.NORMAL, 100, 30, -1, "Paralyzes opponent.", -1, 0, 1)
.attr(StatusEffectAttr, StatusEffect.PARALYSIS),
@ -2337,7 +2571,7 @@ export function initMoves() {
.ignoresVirtual(),
new AttackMove(Moves.TRIPLE_KICK, "Triple Kick", Type.FIGHTING, MoveCategory.PHYSICAL, 10, 90, 10, -1, "Hits thrice in one turn at increasing power.", -1, 0, 2)
.attr(MultiHitAttr, MultiHitType._3_INCR)
.attr(MissEffectAttr, (user: Pokemon, target: Pokemon, move: Move) => {
.attr(MissEffectAttr, (user: Pokemon, move: Move) => {
user.turnData.hitsLeft = 0;
return true;
}),

View File

@ -100,17 +100,10 @@ export class Weather {
}
isEffectSuppressed(scene: BattleScene): boolean {
const playerPokemon = scene.getPlayerPokemon();
const enemyPokemon = scene.getEnemyPokemon();
const field = scene.getField().filter(p => p);
if (playerPokemon) {
const suppressWeatherEffectAbAttr = playerPokemon.getAbility().getAttrs(SuppressWeatherEffectAbAttr).find(() => true) as SuppressWeatherEffectAbAttr;
if (suppressWeatherEffectAbAttr && (!this.isImmutable() || suppressWeatherEffectAbAttr.affectsImmutable))
return true;
}
if (enemyPokemon) {
const suppressWeatherEffectAbAttr = enemyPokemon.getAbility().getAttrs(SuppressWeatherEffectAbAttr).find(() => true) as SuppressWeatherEffectAbAttr;
for (let pokemon of field) {
const suppressWeatherEffectAbAttr = pokemon.getAbility().getAttrs(SuppressWeatherEffectAbAttr).find(() => true) as SuppressWeatherEffectAbAttr;
if (suppressWeatherEffectAbAttr && (!this.isImmutable() || suppressWeatherEffectAbAttr.affectsImmutable))
return true;
}

View File

@ -44,7 +44,7 @@ export class EvolutionPhase extends BattlePhase {
this.evolutionContainer = (this.scene.ui.getHandler() as EvolutionSceneHandler).evolutionContainer;
this.evolutionBaseBg = this.scene.add.image(0, 0, 'plains_bg');
this.evolutionBaseBg = this.scene.add.image(0, 0, 'default_bg');
this.evolutionBaseBg.setOrigin(0, 0);
this.evolutionContainer.add(this.evolutionBaseBg);
@ -98,6 +98,7 @@ export class EvolutionPhase extends BattlePhase {
const levelMoves = pokemon.getLevelMoves(this.lastLevel + 1);
for (let lm of levelMoves)
this.scene.unshiftPhase(new LearnMovePhase(this.scene, this.partyMemberIndex, lm));
this.scene.unshiftPhase(new EndEvolutionPhase(this.scene));
this.scene.time.delayedCall(1000, () => {
const evolutionBgm = this.scene.sound.add('evolution');
@ -444,3 +445,11 @@ export class EvolutionPhase extends BattlePhase {
updateParticle();
}
}
export class EndEvolutionPhase extends BattlePhase {
start() {
super.start();
this.scene.ui.setModeForceTransition(Mode.MESSAGE).then(() => this.end());
}
}

View File

@ -11,7 +11,6 @@ import * as Utils from '../utils';
import { TempBattleStat, getTempBattleStatBoosterItemName, getTempBattleStatName } from '../data/temp-battle-stat';
import { BerryType, getBerryEffectDescription, getBerryName } from '../data/berry';
import { Unlockables } from '../system/unlockables';
import { maxExpLevel } from '../battle-scene';
type Modifier = Modifiers.Modifier;
@ -128,7 +127,7 @@ export class PokemonReviveModifierType extends PokemonHpRestoreModifierType {
constructor(name: string, restorePercent: integer, iconImage?: string) {
super(name, restorePercent, true, (_type, args) => new Modifiers.PokemonHpRestoreModifier(this, (args[0] as PlayerPokemon).id, this.restorePoints, true, true),
((pokemon: PlayerPokemon) => {
if (pokemon.hp)
if (!pokemon.isFainted())
return PartyUiHandler.NoEffectMessage;
return null;
}), iconImage, 'revive');
@ -669,15 +668,15 @@ const modifierPool = {
return statusEffectPartyMemberCount * 6;
}),
new WeightedModifierType(modifierTypes.REVIVE, (party: Pokemon[]) => {
const faintedPartyMemberCount = Math.min(party.filter(p => !p.hp).length, 3);
const faintedPartyMemberCount = Math.min(party.filter(p => p.isFainted()).length, 3);
return faintedPartyMemberCount * 9;
}),
new WeightedModifierType(modifierTypes.MAX_REVIVE, (party: Pokemon[]) => {
const faintedPartyMemberCount = Math.min(party.filter(p => !p.hp).length, 3);
const faintedPartyMemberCount = Math.min(party.filter(p => p.isFainted()).length, 3);
return faintedPartyMemberCount * 3;
}),
new WeightedModifierType(modifierTypes.SACRED_ASH, (party: Pokemon[]) => {
return party.filter(p => !p.hp).length >= Math.ceil(party.length / 2) ? 1 : 0;
return party.filter(p => p.isFainted()).length >= Math.ceil(party.length / 2) ? 1 : 0;
}),
new WeightedModifierType(modifierTypes.HYPER_POTION, (party: Pokemon[]) => {
const thresholdPartyMemberCount = Math.min(party.filter(p => p.getInverseHp() >= 100 || p.getHpRatio() <= 0.625).length, 3);

View File

@ -480,7 +480,8 @@ export class TurnHealModifier extends PokemonHeldItemModifier {
if (pokemon.getHpRatio() < 1) {
const scene = pokemon.scene;
scene.unshiftPhase(new PokemonHealPhase(scene, pokemon.isPlayer(), Math.max(Math.floor(pokemon.getMaxHp() / 16) * this.stackCount, 1), getPokemonMessage(pokemon, `'s ${this.type.name}\nrestored its HP a little!`), true));
scene.unshiftPhase(new PokemonHealPhase(scene, pokemon.getBattlerIndex(),
Math.max(Math.floor(pokemon.getMaxHp() / 16) * this.stackCount, 1), getPokemonMessage(pokemon, `'s ${this.type.name}\nrestored its HP a little!`), true));
}
return true;
@ -509,7 +510,8 @@ export class HitHealModifier extends PokemonHeldItemModifier {
if (pokemon.turnData.damageDealt && pokemon.getHpRatio() < 1) {
const scene = pokemon.scene;
scene.unshiftPhase(new PokemonHealPhase(scene, pokemon.isPlayer(), Math.max(Math.floor(pokemon.turnData.damageDealt / 8) * this.stackCount, 1), getPokemonMessage(pokemon, `'s ${this.type.name}\nrestored its HP a little!`), true));
scene.unshiftPhase(new PokemonHealPhase(scene, pokemon.getBattlerIndex(),
Math.max(Math.floor(pokemon.turnData.damageDealt / 8) * this.stackCount, 1), getPokemonMessage(pokemon, `'s ${this.type.name}\nrestored its HP a little!`), true));
}
return true;
@ -1001,7 +1003,7 @@ export abstract class HeldItemTransferModifier extends PokemonHeldItemModifier {
apply(args: any[]): boolean {
const pokemon = args[0] as Pokemon;
const targetPokemon = pokemon.getOpponent();
const targetPokemon = pokemon.getOpponent(args.length > 1 ? args[1] as integer : !pokemon.scene.currentBattle.double ? 0 : Utils.randInt(2));
if (!targetPokemon)
return false;

View File

@ -1,7 +1,7 @@
import Phaser from 'phaser';
import BattleScene from './battle-scene';
import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from './ui/battle-info';
import Move, { StatChangeAttr, HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariablePowerAttr, Moves, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr } from "./data/move";
import Move, { StatChangeAttr, HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariablePowerAttr, Moves, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, AttackMove, AddBattlerTagAttr } from "./data/move";
import { pokemonLevelMoves } from './data/pokemon-level-moves';
import { default as PokemonSpecies, PokemonSpeciesForm, getPokemonSpecies } from './data/pokemon-species';
import * as Utils from './utils';
@ -21,10 +21,17 @@ import { BattlerTag, BattlerTagLapseType, BattlerTagType, TypeBoostTag, getBattl
import { Species } from './data/species';
import { WeatherType } from './data/weather';
import { TempBattleStat } from './data/temp-battle-stat';
import { ArenaTagType, GravityTag, WeakenMoveTypeTag } from './data/arena-tag';
import { ArenaTagType, WeakenMoveTypeTag } from './data/arena-tag';
import { Biome } from './data/biome';
import { Abilities, Ability, BattleStatMultiplierAbAttr, BlockCritAbAttr, PreApplyBattlerTagAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, VariableMovePowerAbAttr, abilities, applyBattleStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs } from './data/ability';
import { Abilities, Ability, BattleStatMultiplierAbAttr, BattlerTagImmunityAbAttr, BlockCritAbAttr, PreApplyBattlerTagAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, VariableMovePowerAbAttr, abilities, applyBattleStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs } from './data/ability';
import PokemonData from './system/pokemon-data';
import { BattlerIndex } from './battle';
export enum FieldPosition {
CENTER,
LEFT,
RIGHT
}
export default abstract class Pokemon extends Phaser.GameObjects.Container {
public id: integer;
@ -50,6 +57,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
public battleSummonData: PokemonBattleSummonData;
public turnData: PokemonTurnData;
public fieldPosition: FieldPosition;
public maskEnabled: boolean;
public maskSprite: Phaser.GameObjects.Sprite;
@ -139,7 +148,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.calculateStats();
scene.fieldUI.addAt(this.battleInfo, 0);
this.fieldPosition = FieldPosition.CENTER;
scene.fieldUI.add(this.battleInfo);
this.battleInfo.initInfo(this);
@ -177,8 +188,20 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
}
isFainted(checkStatus?: boolean): boolean {
return !this.hp && (!checkStatus || this.status?.effect === StatusEffect.FAINT);
}
isActive(): boolean {
return !this.isFainted() && !!this.scene;
}
abstract isPlayer(): boolean;
abstract getFieldIndex(): integer;
abstract getBattlerIndex(): BattlerIndex;
loadAssets(): Promise<void> {
return new Promise(resolve => {
const moveIds = this.getMoveset().map(m => m.getMove().id);
@ -266,6 +289,52 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.getTintSprite().play(this.getBattleSpriteKey());
}
getFieldPositionOffset(): [ number, number ] {
switch (this.fieldPosition) {
case FieldPosition.CENTER:
return [ 0, 0 ];
case FieldPosition.LEFT:
return [ -32, -8 ];
case FieldPosition.RIGHT:
return [ 32, 0 ];
}
}
setFieldPosition(fieldPosition: FieldPosition, duration?: integer): Promise<void> {
return new Promise(resolve => {
if (fieldPosition === this.fieldPosition) {
resolve();
return;
}
const initialOffset = this.getFieldPositionOffset();
this.fieldPosition = fieldPosition;
this.battleInfo.setMini(fieldPosition !== FieldPosition.CENTER);
this.battleInfo.setOffset(fieldPosition === FieldPosition.RIGHT);
const newOffset = this.getFieldPositionOffset();
let relX = newOffset[0] - initialOffset[0];
let relY = newOffset[1] - initialOffset[1];
if (duration) {
this.scene.tweens.add({
targets: this,
x: (_target, _key, value: number) => value + relX,
y: (_target, _key, value: number) => value + relY,
duration: duration,
ease: 'Sine.easeOut',
onComplete: () => resolve()
});
} else {
this.x += relX;
this.y += relY;
}
});
}
getBattleStat(stat: Stat): integer {
if (stat === Stat.HP)
return this.stats[Stat.HP];
@ -360,7 +429,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
canApplyAbility(): boolean {
return !this.getAbility().conditions.find(condition => !condition(this));
return this.hp && !this.getAbility().conditions.find(condition => !condition(this));
}
getAttackMoveEffectiveness(moveType: Type): TypeDamageMultiplier {
@ -405,7 +474,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
setMove(moveIndex: integer, moveId: Moves): void {
const move = moveId ? new PokemonMove(moveId) : null;
this.moveset[moveIndex] = move;
if (this.summonData.moveset)
if (this.summonData?.moveset)
this.summonData.moveset[moveIndex] = move;
}
@ -496,15 +565,30 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.levelExp = this.exp - getLevelTotalExp(this.level, this.getSpeciesForm().growthRate);
}
getOpponent(): Pokemon {
const ret = this.isPlayer() ? this.scene.getEnemyPokemon() : this.scene.getPlayerPokemon();
getOpponent(targetIndex: integer): Pokemon {
const ret = this.getOpponents()[targetIndex];
if (ret.summonData)
return ret;
return null;
}
apply(source: Pokemon, battlerMove: PokemonMove): MoveResult {
let result: MoveResult;
getOpponents(): Pokemon[] {
return ((this.isPlayer() ? this.scene.getEnemyField() : this.scene.getPlayerField()) as Pokemon[]).filter(p => p.isActive());
}
getOpponentDescriptor(): string {
const opponents = this.getOpponents();
if (opponents.length === 1)
return opponents[0].name;
return this.isPlayer() ? 'the opposing team' : 'your team';
}
getAlly(): Pokemon {
return (this.isPlayer() ? this.scene.getPlayerField() : this.scene.getEnemyField())[this.getFieldIndex() ? 0 : 1];
}
apply(source: Pokemon, battlerMove: PokemonMove): HitResult {
let result: HitResult;
const move = battlerMove.getMove();
const moveCategory = move.category;
let damage = 0;
@ -526,7 +610,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
applyPreDefendAbAttrs(TypeImmunityAbAttr, this, source, battlerMove, cancelled, typeMultiplier);
if (cancelled.value)
result = MoveResult.NO_EFFECT;
result = HitResult.NO_EFFECT;
else {
if (source.findTag(t => t instanceof TypeBoostTag && (t as TypeBoostTag).boostedType === move.type))
power.value *= 1.5;
@ -567,46 +651,50 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (damage && fixedDamage.value) {
damage = fixedDamage.value;
isCritical = false;
result = MoveResult.EFFECTIVE;
result = HitResult.EFFECTIVE;
}
console.log('damage', damage, move.name, move.power, sourceAtk, targetDef);
if (!result) {
if (typeMultiplier.value >= 2)
result = MoveResult.SUPER_EFFECTIVE;
result = HitResult.SUPER_EFFECTIVE;
else if (typeMultiplier.value >= 1)
result = MoveResult.EFFECTIVE;
result = HitResult.EFFECTIVE;
else if (typeMultiplier.value > 0)
result = MoveResult.NOT_VERY_EFFECTIVE;
result = HitResult.NOT_VERY_EFFECTIVE;
else
result = MoveResult.NO_EFFECT;
result = HitResult.NO_EFFECT;
}
if (damage) {
this.scene.unshiftPhase(new DamagePhase(this.scene, this.isPlayer(), result as DamageResult));
this.scene.unshiftPhase(new DamagePhase(this.scene, this.getBattlerIndex(), result as DamageResult));
if (isCritical)
this.scene.queueMessage('A critical hit!');
this.scene.setPhaseQueueSplice();
this.damage(damage);
source.turnData.damageDealt += damage;
this.turnData.attacksReceived.unshift({ move: move.id, result: result as DamageResult, damage: damage, sourceId: source.id });
}
switch (result) {
case MoveResult.SUPER_EFFECTIVE:
case HitResult.SUPER_EFFECTIVE:
this.scene.queueMessage('It\'s super effective!');
break;
case MoveResult.NOT_VERY_EFFECTIVE:
case HitResult.NOT_VERY_EFFECTIVE:
this.scene.queueMessage('It\'s not very effective!');
break;
case MoveResult.NO_EFFECT:
case HitResult.NO_EFFECT:
this.scene.queueMessage(`It doesn\'t affect ${this.name}!`);
break;
}
if (damage)
this.scene.clearPhaseQueueSplice();
}
break;
case MoveCategory.STATUS:
result = MoveResult.STATUS;
result = HitResult.STATUS;
break;
}
@ -614,7 +702,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
damage(damage: integer, preventEndure?: boolean): void {
if (!this.hp)
if (this.isFainted())
return;
if (this.hp > 1 && this.hp - damage <= 0 && !preventEndure) {
@ -625,10 +713,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
this.hp = Math.max(this.hp - damage, 0);
if (!this.hp) {
this.scene.pushPhase(new FaintPhase(this.scene, this.isPlayer()));
if (this.isFainted()) {
this.scene.unshiftPhase(new FaintPhase(this.scene, this.getBattlerIndex()));
this.resetSummonData();
this.getOpponent()?.resetBattleSummonData();
}
}
@ -946,6 +1033,14 @@ export class PlayerPokemon extends Pokemon {
return true;
}
getFieldIndex(): integer {
return this.scene.getPlayerField().indexOf(this);
}
getBattlerIndex(): BattlerIndex {
return this.getFieldIndex();
}
generateCompatibleTms(): void {
this.compatibleTms = [];
@ -1041,7 +1136,7 @@ export class EnemyPokemon extends Pokemon {
: null;
if (queuedMove) {
if (queuedMove.isUsable(this.getMoveQueue()[0].ignorePP))
return { move: queuedMove.moveId, ignorePP: this.getMoveQueue()[0].ignorePP };
return { move: queuedMove.moveId, targets: this.getMoveQueue()[0].targets, ignorePP: this.getMoveQueue()[0].ignorePP };
else {
this.getMoveQueue().shift();
return this.getNextMove();
@ -1051,52 +1146,26 @@ export class EnemyPokemon extends Pokemon {
const movePool = this.getMoveset().filter(m => m.isUsable());
if (movePool.length) {
if (movePool.length === 1)
return { move: movePool[0].moveId };
return { move: movePool[0].moveId, targets: this.getNextTargets(movePool[0].moveId) };
switch (this.aiType) {
case AiType.RANDOM:
return { move: movePool[Utils.randInt(movePool.length)].moveId };
const moveId = movePool[Utils.randInt(movePool.length)].moveId;
return { move: moveId, targets: this.getNextTargets(moveId) };
case AiType.SMART_RANDOM:
case AiType.SMART:
const target = this.scene.getPlayerPokemon();
const moveScores = movePool.map(() => 0);
const moveTargets = Object.fromEntries(movePool.map(m => [ m.moveId, this.getNextTargets(m.moveId) ]));
for (let m in movePool) {
const pokemonMove = movePool[m];
const move = pokemonMove.getMove();
let moveScore = moveScores[m];
if (move.category === MoveCategory.STATUS)
moveScore++;
else {
const effectiveness = this.getAttackMoveEffectiveness(move.type);
moveScore = Math.pow(effectiveness - 1, 2) * effectiveness < 1 ? -2 : 2;
if (moveScore) {
if (move.category === MoveCategory.PHYSICAL) {
if (this.getBattleStat(Stat.ATK) > this.getBattleStat(Stat.SPATK)) {
const statRatio = this.getBattleStat(Stat.SPATK) / this.getBattleStat(Stat.ATK);
if (statRatio <= 0.75)
moveScore *= 2;
else if (statRatio <= 0.875)
moveScore *= 1.5;
}
} else {
if (this.getBattleStat(Stat.SPATK) > this.getBattleStat(Stat.ATK)) {
const statRatio = this.getBattleStat(Stat.ATK) / this.getBattleStat(Stat.SPATK);
if (statRatio <= 0.75)
moveScore *= 2;
else if (statRatio <= 0.875)
moveScore *= 1.5;
}
for (let mt of moveTargets[move.id]) {
const target = this.scene.getField()[mt];
moveScore += move.getUserBenefitScore(this, target, move) + move.getTargetBenefitScore(this, target, move) * (mt < BattlerIndex.ENEMY === this.isPlayer() ? 1 : -1);
}
moveScore += Math.floor(move.power / 5);
}
}
const statChangeAttrs = move.getAttrs(StatChangeAttr) as StatChangeAttr[];
for (let sc of statChangeAttrs) {
moveScore += ((sc.levels >= 1) === sc.selfTarget ? -2 : 2) + sc.levels * (sc.selfTarget ? 4 : -4);
// TODO: Add awareness of current levels
}
moveScore /= moveTargets[move.id].length
// could make smarter by checking opponent def/spdef
moveScores[m] = moveScore;
@ -1116,17 +1185,48 @@ export class EnemyPokemon extends Pokemon {
r++;
}
console.log(movePool.map(m => m.getName()), moveScores, r, sortedMovePool.map(m => m.getName()));
return { move: sortedMovePool[r].moveId };
return { move: sortedMovePool[r].moveId, targets: moveTargets[sortedMovePool[r].moveId] };
}
}
return { move: Moves.STRUGGLE };
return { move: Moves.STRUGGLE, targets: this.getNextTargets(Moves.STRUGGLE) };
}
getNextTargets(moveId: Moves): BattlerIndex[] {
const moveTargets = getMoveTargets(this, moveId);
const targets = this.scene.getField().filter(p => p?.isActive() && moveTargets.targets.indexOf(p.getBattlerIndex()) > -1);
if (moveTargets.multiple)
return targets.map(p => p.getBattlerIndex());
const move = allMoves[moveId];
let benefitScores = targets
.map(p => [ p.getBattlerIndex(), move.getTargetBenefitScore(this, p, move) * (p.isPlayer() === this.isPlayer() ? 1 : -1) ]);
const sortedBenefitScores = benefitScores.slice(0);
sortedBenefitScores.sort((a, b) => {
const scoreA = a[1];
const scoreB = b[1];
return scoreA < scoreB ? 1 : scoreA > scoreB ? -1 : 0;
});
// TODO: Add some randomness
return [ sortedBenefitScores[0][0] ];
}
isPlayer() {
return false;
}
getFieldIndex(): integer {
return this.scene.getEnemyField().indexOf(this);
}
getBattlerIndex(): BattlerIndex {
return BattlerIndex.ENEMY + this.getFieldIndex();
}
addToParty() {
const party = this.scene.getParty();
let ret: PlayerPokemon = null;
@ -1143,6 +1243,7 @@ export class EnemyPokemon extends Pokemon {
export interface TurnMove {
move: Moves;
targets?: BattlerIndex[];
result: MoveResult;
virtual?: boolean;
turn?: integer;
@ -1150,6 +1251,7 @@ export interface TurnMove {
export interface QueuedMove {
move: Moves;
targets: BattlerIndex[];
ignorePP?: boolean;
}
@ -1188,17 +1290,25 @@ export enum AiType {
}
export enum MoveResult {
PENDING,
SUCCESS,
FAIL,
MISS,
OTHER
}
export enum HitResult {
EFFECTIVE = 1,
SUPER_EFFECTIVE,
NOT_VERY_EFFECTIVE,
NO_EFFECT,
STATUS,
FAILED,
MISSED,
FAIL,
MISS,
OTHER
}
export type DamageResult = MoveResult.EFFECTIVE | MoveResult.SUPER_EFFECTIVE | MoveResult.NOT_VERY_EFFECTIVE | MoveResult.OTHER;
export type DamageResult = HitResult.EFFECTIVE | HitResult.SUPER_EFFECTIVE | HitResult.NOT_VERY_EFFECTIVE | HitResult.OTHER;
export class PokemonMove {
public moveId: Moves;

View File

@ -24,7 +24,7 @@ export function initAutoPlay() {
const commandUiHandler = this.ui.handlers[Mode.COMMAND] as CommandUiHandler;
const fightUiHandler = this.ui.handlers[Mode.FIGHT] as FightUiHandler;
const partyUiHandler = this.ui.handlers[Mode.PARTY] as PartyUiHandler;
const switchCheckUiHandler = this.ui.handlers[Mode.CONFIRM] as ConfirmUiHandler;
const confirmUiHandler = this.ui.handlers[Mode.CONFIRM] as ConfirmUiHandler;
const modifierSelectUiHandler = this.ui.handlers[Mode.MODIFIER_SELECT] as ModifierSelectUiHandler;
const getBestPartyMemberIndex = () => {
@ -153,15 +153,15 @@ export function initAutoPlay() {
}
};
const originalSwitchCheckUiHandlerShow = switchCheckUiHandler.show;
switchCheckUiHandler.show = function (args: any[]) {
const originalSwitchCheckUiHandlerShow = confirmUiHandler.show;
confirmUiHandler.show = function (args: any[]) {
originalSwitchCheckUiHandlerShow.apply(this, [ args ]);
if (thisArg.auto) {
const bestPartyMemberIndex = getBestPartyMemberIndex();
thisArg.time.delayedCall(20, () => {
if (bestPartyMemberIndex)
nextPartyMemberIndex = bestPartyMemberIndex;
switchCheckUiHandler.setCursor(bestPartyMemberIndex ? 1 : 0);
confirmUiHandler.setCursor(bestPartyMemberIndex ? 1 : 0);
thisArg.time.delayedCall(20, () => this.processInput(Button.ACTION));
});
}
@ -193,7 +193,7 @@ export function initAutoPlay() {
const party = thisArg.getParty();
const modifierTypeOptions = modifierSelectUiHandler.options.map(o => o.modifierTypeOption);
const faintedPartyMemberIndex = party.findIndex(p => !p.hp);
const faintedPartyMemberIndex = party.findIndex(p => p.isFainted());
const lowHpPartyMemberIndex = party.findIndex(p => p.getHpRatio() <= 0.5);
const criticalHpPartyMemberIndex = party.findIndex(p => p.getHpRatio() <= 0.25);

View File

@ -21,7 +21,7 @@ interface SystemSaveData {
interface SessionSaveData {
party: PokemonData[];
enemyParty: PokemonData[];
enemyField: PokemonData[];
modifiers: PersistentModifierData[];
enemyModifiers: PersistentModifierData[];
arena: ArenaData;
@ -126,9 +126,9 @@ export class GameData {
saveSession(scene: BattleScene): boolean {
const sessionData = {
party: scene.getParty().map(p => new PokemonData(p)),
enemyParty: scene.getEnemyParty().map(p => new PokemonData(p)),
modifiers: scene.findModifiers(m => true).map(m => new PersistentModifierData(m, true)),
enemyModifiers: scene.findModifiers(m => true, false).map(m => new PersistentModifierData(m, false)),
enemyField: scene.getEnemyField().map(p => new PokemonData(p)),
modifiers: scene.findModifiers(() => true).map(m => new PersistentModifierData(m, true)),
enemyModifiers: scene.findModifiers(() => true, false).map(m => new PersistentModifierData(m, false)),
arena: new ArenaData(scene.arena),
pokeballCounts: scene.pokeballCounts,
waveIndex: scene.currentBattle.waveIndex,
@ -153,7 +153,7 @@ export class GameData {
try {
const sessionData = JSON.parse(atob(localStorage.getItem('sessionData')), (k: string, v: any) => {
if (k === 'party' || k === 'enemyParty') {
if (k === 'party' || k === 'enemyField') {
const ret: PokemonData[] = [];
for (let pd of v)
ret.push(new PokemonData(pd));
@ -188,16 +188,19 @@ export class GameData {
party.push(pokemon);
}
const enemyPokemon = sessionData.enemyParty[0].toPokemon(scene) as EnemyPokemon;
Object.keys(scene.pokeballCounts).forEach((key: string) => {
scene.pokeballCounts[key] = sessionData.pokeballCounts[key] || 0;
});
scene.newArena(sessionData.arena.biome, true);
scene.newBattle(sessionData.waveIndex).enemyPokemon = enemyPokemon;
scene.newArena(sessionData.arena.biome, sessionData.enemyField.length > 1);
const battle = scene.newBattle(sessionData.waveIndex, sessionData.enemyField.length > 1);
sessionData.enemyField.forEach((enemyData, e) => {
const enemyPokemon = enemyData.toPokemon(scene) as EnemyPokemon;
battle.enemyField[e] = enemyPokemon;
loadPokemonAssets.push(enemyPokemon.loadAssets());
});
scene.arena.weather = sessionData.arena.weather;
// TODO

View File

@ -37,7 +37,7 @@ export default class ModifierData {
type.generatorId = this.typeGeneratorId;
if (type instanceof ModifierTypeGenerator)
type = (type as ModifierTypeGenerator).generateType(this.player ? scene.getParty() : scene.getEnemyParty(), this.typePregenArgs);
type = (type as ModifierTypeGenerator).generateType(this.player ? scene.getParty() : scene.getEnemyField(), this.typePregenArgs);
const ret = Reflect.construct(constructor, ([ type ] as any[]).concat(this.args).concat(this.stackCount)) as PersistentModifier

View File

@ -4,6 +4,7 @@ import { TextStyle, addTextObject } from "./text";
const hiddenX = -91;
const shownX = 10;
const baseY = -116;
export default class AbilityBar extends Phaser.GameObjects.Container {
private bg: Phaser.GameObjects.Image;
@ -15,7 +16,7 @@ export default class AbilityBar extends Phaser.GameObjects.Container {
public shown: boolean;
constructor(scene: BattleScene) {
super(scene, hiddenX, (-scene.game.canvas.height / 6) + 64);
super(scene, hiddenX, baseY);
}
setup(): void {
@ -43,9 +44,12 @@ export default class AbilityBar extends Phaser.GameObjects.Container {
if (this.shown)
return;
(this.scene as BattleScene).fieldUI.bringToTop(this);
if (this.tween)
this.tween.stop();
this.y = baseY + ((this.scene as BattleScene).currentBattle.double ? 14 : 0);
this.tween = this.scene.tweens.add({
targets: this,
x: shownX,

View File

@ -60,12 +60,12 @@ export default class BallUiHandler extends UiHandler {
let success = false;
const pokeballTypeCount = Object.keys(this.scene.pokeballCounts).length;
const pokeballTypeCount = Object.keys(this.scene.currentBattle.turnPokeballCounts).length;
if (button === Button.ACTION || button === Button.CANCEL) {
success = true;
if (button === Button.ACTION && this.cursor < pokeballTypeCount) {
if (this.scene.pokeballCounts[this.cursor]) {
if (this.scene.currentBattle.turnPokeballCounts[this.cursor]) {
if ((this.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.BALL, this.cursor)) {
this.scene.ui.setMode(Mode.COMMAND);
this.scene.ui.setMode(Mode.MESSAGE);
@ -93,7 +93,7 @@ export default class BallUiHandler extends UiHandler {
}
updateCounts() {
this.countsText.setText(Object.values(this.scene.pokeballCounts).map(c => `x${c}`).join('\n'));
this.countsText.setText(Object.values(this.scene.currentBattle.turnPokeballCounts).map(c => `x${c}`).join('\n'));
}
setCursor(cursor: integer): boolean {

View File

@ -4,10 +4,12 @@ import * as Utils from '../utils';
import { addTextObject, TextStyle } from './text';
import { getGenderSymbol, getGenderColor } from '../data/gender';
import { StatusEffect } from '../data/status-effect';
import BattleScene, { maxExpLevel } from '../battle-scene';
import { maxExpLevel } from '../battle-scene';
export default class BattleInfo extends Phaser.GameObjects.Container {
private player: boolean;
private mini: boolean;
private offset: boolean;
private lastName: string;
private lastStatus: StatusEffect;
private lastHp: integer;
@ -17,9 +19,10 @@ export default class BattleInfo extends Phaser.GameObjects.Container {
private lastLevelExp: integer;
private lastLevel: integer;
private box: Phaser.GameObjects.Sprite;
private nameText: Phaser.GameObjects.Text;
private genderText: Phaser.GameObjects.Text;
private ownedIcon: Phaser.GameObjects.Image;
private ownedIcon: Phaser.GameObjects.Sprite;
private statusIndicator: Phaser.GameObjects.Sprite;
private levelContainer: Phaser.GameObjects.Container;
private hpBar: Phaser.GameObjects.Image;
@ -30,6 +33,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.mini = !player;
this.offset = false;
this.lastName = null;
this.lastStatus = StatusEffect.NONE;
this.lastHp = -1;
@ -42,9 +47,9 @@ export default class BattleInfo extends Phaser.GameObjects.Container {
// Initially invisible and shown via Pokemon.showInfo
this.setVisible(false);
const box = this.scene.add.image(0, 0, `pbinfo_${player ? 'player' : 'enemy'}`);
box.setOrigin(1, 0.5);
this.add(box);
this.box = this.scene.add.sprite(0, 0, this.getTextureName());
this.box.setOrigin(1, 0.5);
this.add(this.box);
this.nameText = addTextObject(this.scene, player ? -115 : -124, player ? -15.2 : -11.2, '', TextStyle.BATTLE_INFO);
this.nameText.setOrigin(0, 0);
@ -56,7 +61,7 @@ export default class BattleInfo extends Phaser.GameObjects.Container {
this.add(this.genderText);
if (!this.player) {
this.ownedIcon = this.scene.add.image(0, 0, 'icon_owned');
this.ownedIcon = this.scene.add.sprite(0, 0, 'icon_owned');
this.ownedIcon.setVisible(false);
this.ownedIcon.setOrigin(0, 0);
this.ownedIcon.setPositionRelative(this.nameText, 0, 11.5);
@ -115,10 +120,11 @@ export default class BattleInfo extends Phaser.GameObjects.Container {
}
this.hpBar.setScale(pokemon.getHpRatio(), 1);
this.lastHpFrame = this.hpBar.scaleX > 0.5 ? 'high' : this.hpBar.scaleX > 0.25 ? 'medium' : 'low';
this.hpBar.setFrame(this.lastHpFrame);
if (this.player)
this.setHpNumbers(pokemon.hp, pokemon.getMaxHp());
this.lastHp = pokemon.hp;
this.lastHpFrame = this.hpBar.scaleX > 0.5 ? 'high' : this.hpBar.scaleX > 0.25 ? 'medium' : 'low';
this.lastMaxHp = pokemon.getMaxHp();
this.setLevel(pokemon.level);
@ -131,6 +137,39 @@ export default class BattleInfo extends Phaser.GameObjects.Container {
}
}
getTextureName(): string {
return `pbinfo_${this.player ? 'player' : 'enemy'}${this.mini ? '_mini' : ''}`;
}
setMini(mini: boolean): void {
if (this.mini === mini)
return;
this.mini = mini;
this.box.setTexture(this.getTextureName());
if (this.player) {
this.y -= 12 * (mini ? 1 : -1);
}
const offsetElements = [ this.nameText, this.genderText, this.statusIndicator, this.levelContainer ];
offsetElements.forEach(el => el.y += 1.5 * (mini ? -1 : 1));
const toggledElements = [ this.hpNumbersContainer, this.expBar ];
toggledElements.forEach(el => el.setVisible(!mini));
}
setOffset(offset: boolean): void {
if (this.offset === offset)
return;
this.offset = offset;
this.x += 10 * (offset === this.player ? 1 : -1);
this.y += 27 * (offset ? 1 : -1);
}
updateInfo(pokemon: Pokemon, instant?: boolean): Promise<void> {
return new Promise(resolve => {
if (!this.scene) {
@ -290,4 +329,6 @@ export class EnemyBattleInfo extends BattleInfo {
constructor(scene: Phaser.Scene) {
super(scene, 140, -141, false);
}
setMini(mini: boolean): void { } // Always mini
}

View File

@ -43,7 +43,7 @@ export default class CommandUiHandler extends UiHandler {
const messageHandler = this.getUi().getMessageHandler();
messageHandler.bg.setTexture('bg_command');
messageHandler.message.setWordWrapWidth(1110);
messageHandler.showText(`What will\n${this.scene.getPlayerPokemon().name} do?`, 0);
messageHandler.showText(`What will\n${(this.scene.getCurrentPhase() as CommandPhase).getPokemon().name} do?`, 0);
this.setCursor(this.cursor);
}
@ -65,7 +65,7 @@ export default class CommandUiHandler extends UiHandler {
success = true;
break;
case 2:
ui.setMode(Mode.PARTY, PartyUiMode.SWITCH);
ui.setMode(Mode.PARTY, PartyUiMode.SWITCH, (this.scene.getCurrentPhase() as CommandPhase).getPokemon().getFieldIndex());
success = true;
break;
case 3:

View File

@ -14,6 +14,12 @@ export default class EvolutionSceneHandler extends UiHandler {
this.scene.fieldUI.add(this.evolutionContainer);
}
show(_args: any[]): void {
super.show(_args);
this.scene.fieldUI.bringToTop(this.evolutionContainer);
}
processInput(button: Button) {
this.scene.ui.getMessageHandler().processInput(button);
}

View File

@ -91,7 +91,7 @@ export default class FightUiHandler extends UiHandler {
ui.add(this.cursorObj);
}
const moveset = this.scene.getPlayerPokemon().getMoveset();
const moveset = (this.scene.getCurrentPhase() as CommandPhase).getPokemon().getMoveset();
const hasMove = cursor < moveset.length;
@ -114,7 +114,7 @@ export default class FightUiHandler extends UiHandler {
}
displayMoves() {
const moveset = this.scene.getPlayerPokemon().getMoveset();
const moveset = (this.scene.getCurrentPhase() as CommandPhase).getPokemon().getMoveset();
for (let m = 0; m < 4; m++) {
const moveText = addTextObject(this.scene, m % 2 === 0 ? 0 : 100, m < 2 ? 0 : 16, '-', TextStyle.WINDOW);
if (m < moveset.length)

View File

@ -42,7 +42,9 @@ export type PokemonMoveSelectFilter = (pokemonMove: PokemonMove) => string;
export default class PartyUiHandler extends MessageUiHandler {
private partyUiMode: PartyUiMode;
private fieldIndex: integer;
private partyBg: Phaser.GameObjects.Image;
private partyContainer: Phaser.GameObjects.Container;
private partySlotsContainer: Phaser.GameObjects.Container;
private partySlots: PartySlot[];
@ -67,7 +69,7 @@ export default class PartyUiHandler extends MessageUiHandler {
private static FilterAll = (_pokemon: PlayerPokemon) => null;
public static FilterNonFainted = (pokemon: PlayerPokemon) => {
if (!pokemon.hp)
if (pokemon.isFainted())
return `${pokemon.name} has no energy\nleft to battle!`;
return null;
};
@ -96,10 +98,10 @@ export default class PartyUiHandler extends MessageUiHandler {
this.partyContainer = partyContainer;
const partyBg = this.scene.add.image(0, 0, 'party_bg');
partyContainer.add(partyBg);
this.partyBg = this.scene.add.image(0, 0, 'party_bg');
partyContainer.add(this.partyBg);
partyBg.setOrigin(0, 1);
this.partyBg.setOrigin(0, 1);
const partySlotsContainer = this.scene.add.container(0, 0);
partyContainer.add(partySlotsContainer);
@ -143,17 +145,20 @@ export default class PartyUiHandler extends MessageUiHandler {
this.partyUiMode = args[0] as PartyUiMode;
this.fieldIndex = args.length > 1 ? args[1] as integer : -1;
this.partyContainer.setVisible(true);
this.partyBg.setTexture(`party_bg${this.scene.currentBattle.double ? '_double' : ''}`);
this.populatePartySlots();
this.setCursor(this.cursor < 6 ? this.cursor : 0);
if (args.length > 1 && args[1] instanceof Function)
this.selectCallback = args[1];
this.selectFilter = args.length > 2 && args[2] instanceof Function
? args[2] as PokemonSelectFilter
if (args.length > 2 && args[2] instanceof Function)
this.selectCallback = args[2];
this.selectFilter = args.length > 3 && args[3] instanceof Function
? args[3] as PokemonSelectFilter
: PartyUiHandler.FilterAll;
this.moveSelectFilter = args.length > 3 && args[3] instanceof Function
? args[3] as PokemonMoveSelectFilter
this.moveSelectFilter = args.length > 4 && args[4] instanceof Function
? args[4] as PokemonMoveSelectFilter
: PartyUiHandler.FilterAllMoves;
}
@ -226,7 +231,7 @@ export default class PartyUiHandler extends MessageUiHandler {
} else if (option === PartyOption.RELEASE) {
this.clearOptions();
ui.playSelect();
if (this.cursor) {
if (this.cursor >= this.scene.currentBattle.getBattlerCount()) {
this.showText(`Do you really want to release ${pokemon.name}?`, null, () => {
ui.setModeWithoutClear(Mode.CONFIRM, () => {
ui.setMode(Mode.PARTY);
@ -292,12 +297,13 @@ export default class PartyUiHandler extends MessageUiHandler {
success = this.setCursor(this.cursor < 6 ? this.cursor < slotCount - 1 ? this.cursor + 1 : 6 : 0);
break;
case Button.LEFT:
if (this.cursor && this.cursor < 6)
if (this.cursor >= this.scene.currentBattle.getBattlerCount() && this.cursor < 6)
success = this.setCursor(0);
break;
case Button.RIGHT:
if (!this.cursor)
success = this.setCursor(this.lastCursor < 6 ? this.lastCursor || 1 : 1);
const battlerCount = this.scene.currentBattle.getBattlerCount();
if (this.cursor < battlerCount)
success = this.setCursor(this.lastCursor < 6 ? this.lastCursor || battlerCount : battlerCount);
break;
}
}
@ -411,10 +417,11 @@ export default class PartyUiHandler extends MessageUiHandler {
case PartyUiMode.SWITCH:
case PartyUiMode.FAINT_SWITCH:
case PartyUiMode.POST_BATTLE_SWITCH:
if (this.cursor) {
if (this.cursor >= this.scene.currentBattle.getBattlerCount()) {
this.options.push(PartyOption.SEND_OUT);
if (this.partyUiMode !== PartyUiMode.FAINT_SWITCH
&& this.scene.findModifier(m => m instanceof SwitchEffectTransferModifier && (m as SwitchEffectTransferModifier).pokemonId === this.scene.getPlayerPokemon().id))
&& this.scene.findModifier(m => m instanceof SwitchEffectTransferModifier
&& (m as SwitchEffectTransferModifier).pokemonId === this.scene.getPlayerField()[this.fieldIndex].id))
this.options.push(PartyOption.PASS_BATON);
}
break;
@ -578,7 +585,9 @@ class PartySlot extends Phaser.GameObjects.Container {
private slotHpOverlay: Phaser.GameObjects.Sprite;
constructor(scene: BattleScene, slotIndex: integer, pokemon: PlayerPokemon) {
super(scene, slotIndex ? 230.5 : 64, slotIndex ? -184 + 28 * slotIndex : -124);
super(scene, slotIndex >= scene.currentBattle.getBattlerCount() ? 230.5 : 64,
slotIndex >= scene.currentBattle.getBattlerCount() ? -184 + (scene.currentBattle.double ? -38 : 0)
+ (28 + (scene.currentBattle.double ? 6 : 0)) * slotIndex : -124 + (scene.currentBattle.double ? -8 : 0) + slotIndex * 64);
this.slotIndex = slotIndex;
this.pokemon = pokemon;
@ -587,14 +596,16 @@ class PartySlot extends Phaser.GameObjects.Container {
}
setup() {
const slotKey = `party_slot${this.slotIndex ? '' : '_main'}`;
const battlerCount = (this.scene as BattleScene).currentBattle.getBattlerCount();
const slotKey = `party_slot${this.slotIndex >= battlerCount ? '' : '_main'}`;
const slotBg = this.scene.add.sprite(0, 0, slotKey, `${slotKey}${this.pokemon.hp ? '' : '_fnt'}`);
this.slotBg = slotBg;
this.add(slotBg);
const slotPb = this.scene.add.sprite(this.slotIndex ? -85.5 : -51, this.slotIndex ? 0 : -20.5, 'party_pb');
const slotPb = this.scene.add.sprite(this.slotIndex >= battlerCount ? -85.5 : -51, this.slotIndex >= battlerCount ? 0 : -20.5, 'party_pb');
this.slotPb = slotPb;
this.add(slotPb);
@ -609,7 +620,7 @@ class PartySlot extends Phaser.GameObjects.Container {
this.add(slotInfoContainer);
const slotName = addTextObject(this.scene, 0, 0, this.pokemon.name, TextStyle.PARTY);
slotName.setPositionRelative(slotBg, this.slotIndex ? 21 : 24, this.slotIndex ? 3 : 10);
slotName.setPositionRelative(slotBg, this.slotIndex >= battlerCount ? 21 : 24, this.slotIndex >= battlerCount ? 3 : 10);
slotName.setOrigin(0, 0);
const slotLevelLabel = this.scene.add.image(0, 0, 'party_slot_overlay_lv');
@ -621,7 +632,7 @@ class PartySlot extends Phaser.GameObjects.Container {
slotLevelText.setOrigin(0, 0.25);
const slotHpBar = this.scene.add.image(0, 0, 'party_slot_hp_bar');
slotHpBar.setPositionRelative(slotBg, this.slotIndex ? 72 : 8, this.slotIndex ? 7 : 31);
slotHpBar.setPositionRelative(slotBg, this.slotIndex >= battlerCount ? 72 : 8, this.slotIndex >= battlerCount ? 7 : 31);
slotHpBar.setOrigin(0, 0);
const hpRatio = this.pokemon.getHpRatio();
@ -669,8 +680,9 @@ class PartySlot extends Phaser.GameObjects.Container {
}
private updateSlotTexture(): void {
this.slotBg.setTexture(`party_slot${this.slotIndex ? '' : '_main'}`,
`party_slot${this.slotIndex ? '' : '_main'}${this.transfer ? '_swap' : this.pokemon.hp ? '' : '_fnt'}${this.selected ? '_sel' : ''}`);
const battlerCount = (this.scene as BattleScene).currentBattle.getBattlerCount();
this.slotBg.setTexture(`party_slot${this.slotIndex >= battlerCount ? '' : '_main'}`,
`party_slot${this.slotIndex >= battlerCount ? '' : '_main'}${this.transfer ? '_swap' : this.pokemon.hp ? '' : '_fnt'}${this.selected ? '_sel' : ''}`);
}
}

View File

@ -0,0 +1,121 @@
import { BattlerIndex } from "../battle";
import BattleScene, { Button } from "../battle-scene";
import { Moves, getMoveTargets } from "../data/move";
import { Mode } from "./ui";
import UiHandler from "./uiHandler";
import * as Utils from "../utils";
export type TargetSelectCallback = (cursor: integer) => void;
export default class TargetSelectUiHandler extends UiHandler {
private fieldIndex: integer;
private move: Moves;
private targetSelectCallback: TargetSelectCallback;
private targets: BattlerIndex[];
private targetFlashTween: Phaser.Tweens.Tween;
constructor(scene: BattleScene) {
super(scene, Mode.TARGET_SELECT);
this.cursor = -1;
}
setup(): void { }
show(args: any[]) {
if (args.length < 3)
return;
super.show(args);
this.fieldIndex = args[0] as integer;
this.move = args[1] as Moves;
this.targetSelectCallback = args[2] as TargetSelectCallback;
this.targets = getMoveTargets(this.scene.getPlayerField()[this.fieldIndex], this.move).targets;
if (!this.targets.length)
return;
this.setCursor(this.targets.indexOf(this.cursor) > -1 ? this.cursor : this.targets[0]);
}
processInput(button: Button) {
const ui = this.getUi();
let success = false;
if (button === Button.ACTION || button === Button.CANCEL) {
this.targetSelectCallback(button === Button.ACTION ? this.cursor : -1);
success = true;
} else {
switch (button) {
case Button.UP:
if (this.cursor < BattlerIndex.ENEMY && this.targets.find(t => t >= BattlerIndex.ENEMY))
success = this.setCursor(this.targets.find(t => t >= BattlerIndex.ENEMY));
break;
case Button.DOWN:
if (this.cursor >= BattlerIndex.ENEMY && this.targets.find(t => t < BattlerIndex.ENEMY))
success = this.setCursor(this.targets.find(t => t < BattlerIndex.ENEMY));
break;
case Button.LEFT:
if (this.cursor % 2 && this.targets.find(t => t === this.cursor - 1))
success = this.setCursor(this.cursor - 1);
break;
case Button.RIGHT:
if (!(this.cursor % 2) && this.targets.find(t => t === this.cursor + 1))
success = this.setCursor(this.cursor + 1);
break;
}
}
if (success)
ui.playSelect();
}
setCursor(cursor: integer): boolean {
const lastCursor = this.cursor;
const ret = super.setCursor(cursor);
if (this.targetFlashTween) {
this.targetFlashTween.stop();
const lastTarget = this.scene.getField()[lastCursor];
if (lastTarget)
lastTarget.setAlpha(1);
}
const target = this.scene.getField()[cursor];
this.targetFlashTween = this.scene.tweens.add({
targets: [ target ],
alpha: 0,
loop: -1,
duration: new Utils.FixedInt(250) as unknown as integer,
ease: 'Sine.easeIn',
yoyo: true,
onUpdate: t => {
if (target)
target.setAlpha(t.getValue());
}
});
return ret;
}
eraseCursor() {
const target = this.scene.getField()[this.cursor];
if (this.targetFlashTween) {
this.targetFlashTween.stop();
this.targetFlashTween = null;
}
if (target)
target.setAlpha(1);
}
clear() {
super.clear();
this.eraseCursor();
}
}

View File

@ -12,12 +12,14 @@ import SummaryUiHandler from './summary-ui-handler';
import StarterSelectUiHandler from './starter-select-ui-handler';
import EvolutionSceneHandler from './evolution-scene-handler';
import BiomeSelectUiHandler from './biome-select-ui-handler';
import TargetSelectUiHandler from './target-select-ui-handler';
export enum Mode {
MESSAGE,
COMMAND,
FIGHT,
BALL,
TARGET_SELECT,
MODIFIER_SELECT,
PARTY,
SUMMARY,
@ -54,6 +56,7 @@ export default class UI extends Phaser.GameObjects.Container {
new CommandUiHandler(scene),
new FightUiHandler(scene),
new BallUiHandler(scene),
new TargetSelectUiHandler(scene),
new ModifierSelectUiHandler(scene),
new PartyUiHandler(scene),
new SummaryUiHandler(scene),

View File

@ -25,6 +25,8 @@ export function padInt(value: integer, length: integer, padWith?: string): strin
export function randInt(range: integer, min?: integer): integer {
if (!min)
min = 0;
if (range === 1)
return min;
return Math.floor(Math.random() * range) + min;
}