Merge branch 'beta' into draft

This commit is contained in:
Lylian 2024-11-08 11:20:47 +01:00
commit cc82e26cb8
9 changed files with 132 additions and 47 deletions

@ -1 +1 @@
Subproject commit 71390cba88f4103d0d2273d59a6dd8340a4fa54f Subproject commit d600913dbf1f8b47dae8dccbd8296df78f1c51b5

View File

@ -7666,7 +7666,7 @@ export function initBiomes() {
if (biome === Biome.END) { if (biome === Biome.END) {
const biomeList = Object.keys(Biome).filter(key => !isNaN(Number(key))); const biomeList = Object.keys(Biome).filter(key => !isNaN(Number(key)));
biomeList.pop(); // Removes Biome.END from the list biomeList.pop(); // Removes Biome.END from the list
const randIndex = Utils.randInt(biomeList.length, 1); // Will never be Biome.TOWN const randIndex = Utils.randSeedInt(biomeList.length, 1); // Will never be Biome.TOWN
biome = Biome[biomeList[randIndex]]; biome = Biome[biomeList[randIndex]];
} }
const linkedBiomes: (Biome | [ Biome, integer ])[] = Array.isArray(biomeLinks[biome]) const linkedBiomes: (Biome | [ Biome, integer ])[] = Array.isArray(biomeLinks[biome])

View File

@ -1005,8 +1005,8 @@ export const pokemonEvolutions: PokemonEvolutions = {
new SpeciesEvolution(Species.COSMOEM, 23, null, null) new SpeciesEvolution(Species.COSMOEM, 23, null, null)
], ],
[Species.COSMOEM]: [ [Species.COSMOEM]: [
new SpeciesEvolution(Species.SOLGALEO, 53, EvolutionItem.SUN_FLUTE, null, SpeciesWildEvolutionDelay.VERY_LONG), new SpeciesEvolution(Species.SOLGALEO, 1, EvolutionItem.SUN_FLUTE, null, SpeciesWildEvolutionDelay.VERY_LONG),
new SpeciesEvolution(Species.LUNALA, 53, EvolutionItem.MOON_FLUTE, null, SpeciesWildEvolutionDelay.VERY_LONG) new SpeciesEvolution(Species.LUNALA, 1, EvolutionItem.MOON_FLUTE, null, SpeciesWildEvolutionDelay.VERY_LONG)
], ],
[Species.MELTAN]: [ [Species.MELTAN]: [
new SpeciesEvolution(Species.MELMETAL, 48, null, null) new SpeciesEvolution(Species.MELMETAL, 48, null, null)

View File

@ -6748,7 +6748,8 @@ export class SketchAttr extends MoveEffectAttr {
return false; return false;
} }
const targetMove = target.getMoveHistory().filter(m => !m.virtual).at(-1); const targetMove = target.getLastXMoves(target.battleSummonData.turnCount)
.find(m => m.move !== Moves.NONE && m.move !== Moves.STRUGGLE && !m.virtual);
if (!targetMove) { if (!targetMove) {
return false; return false;
} }

View File

@ -1,3 +1,4 @@
import { NumberHolder } from "#app/utils";
import { PokeballType } from "#enums/pokeball"; import { PokeballType } from "#enums/pokeball";
import BattleScene from "../battle-scene"; import BattleScene from "../battle-scene";
import i18next from "i18next"; import i18next from "i18next";
@ -82,11 +83,38 @@ export function getPokeballTintColor(type: PokeballType): number {
} }
} }
export function doPokeballBounceAnim(scene: BattleScene, pokeball: Phaser.GameObjects.Sprite, y1: number, y2: number, baseBounceDuration: integer, callback: Function) { /**
* Gets the critical capture chance based on number of mons registered in Dex and modified {@link https://bulbapedia.bulbagarden.net/wiki/Catch_rate Catch rate}
* Formula from {@link https://www.dragonflycave.com/mechanics/gen-vi-vii-capturing Dragonfly Cave Gen 6 Capture Mechanics page}
* @param scene {@linkcode BattleScene} current BattleScene
* @param modifiedCatchRate the modified catch rate as calculated in {@linkcode AttemptCapturePhase}
* @returns the chance of getting a critical capture, out of 256
*/
export function getCriticalCaptureChance(scene: BattleScene, modifiedCatchRate: number): number {
if (scene.gameMode.isFreshStartChallenge()) {
return 0;
}
const dexCount = scene.gameData.getSpeciesCount(d => !!d.caughtAttr);
const catchingCharmMultiplier = new NumberHolder(1);
//scene.findModifier(m => m instanceof CriticalCatchChanceBoosterModifier)?.apply(catchingCharmMultiplier);
const dexMultiplier = scene.gameMode.isDaily || dexCount > 800 ? 2.5
: dexCount > 600 ? 2
: dexCount > 400 ? 1.5
: dexCount > 200 ? 1
: dexCount > 100 ? 0.5
: 0;
return Math.floor(catchingCharmMultiplier.value * dexMultiplier * Math.min(255, modifiedCatchRate) / 6);
}
export function doPokeballBounceAnim(scene: BattleScene, pokeball: Phaser.GameObjects.Sprite, y1: number, y2: number, baseBounceDuration: number, callback: Function, isCritical: boolean = false) {
let bouncePower = 1; let bouncePower = 1;
let bounceYOffset = y1; let bounceYOffset = y1;
let bounceY = y2; let bounceY = y2;
const yd = y2 - y1; const yd = y2 - y1;
const x0 = pokeball.x;
const x1 = x0 + 3;
const x2 = x0 - 3;
let critShakes = 4;
const doBounce = () => { const doBounce = () => {
scene.tweens.add({ scene.tweens.add({
@ -117,5 +145,40 @@ export function doPokeballBounceAnim(scene: BattleScene, pokeball: Phaser.GameOb
}); });
}; };
doBounce(); const doCritShake = () => {
scene.tweens.add({
targets: pokeball,
x: x2,
duration: 125,
ease: "Linear",
onComplete: () => {
scene.tweens.add({
targets: pokeball,
x: x1,
duration: 125,
ease: "Linear",
onComplete: () => {
critShakes--;
if (critShakes > 0) {
doCritShake();
} else {
scene.tweens.add({
targets: pokeball,
x: x0,
duration: 60,
ease: "Linear",
onComplete: () => scene.time.delayedCall(500, doBounce)
});
}
}
});
}
});
};
if (isCritical) {
scene.time.delayedCall(500, doCritShake);
} else {
doBounce();
}
} }

View File

@ -5260,26 +5260,6 @@ export class EnemyPokemon extends Pokemon {
} }
} }
heal(amount: integer): integer {
if (this.isBoss()) {
const amountRatio = amount / this.getMaxHp();
const segmentBypassCount = Math.floor(amountRatio / (1 / this.bossSegments));
const segmentSize = this.getMaxHp() / this.bossSegments;
for (let s = 1; s < this.bossSegments; s++) {
const hpThreshold = segmentSize * s;
if (this.hp <= Math.round(hpThreshold)) {
const healAmount = Math.min(amount, this.getMaxHp() - this.hp, Math.round(hpThreshold + (segmentSize * segmentBypassCount) - this.hp));
this.hp += healAmount;
return healAmount;
} else if (s >= this.bossSegmentIndex) {
return super.heal(amount);
}
}
}
return super.heal(amount);
}
getFieldIndex(): integer { getFieldIndex(): integer {
return this.scene.getEnemyField().indexOf(this); return this.scene.getEnemyField().indexOf(this);
} }

View File

@ -2,7 +2,7 @@ import { BattlerIndex } from "#app/battle";
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import { PLAYER_PARTY_MAX_SIZE } from "#app/constants"; import { PLAYER_PARTY_MAX_SIZE } from "#app/constants";
import { SubstituteTag } from "#app/data/battler-tags"; import { SubstituteTag } from "#app/data/battler-tags";
import { doPokeballBounceAnim, getPokeballAtlasKey, getPokeballCatchMultiplier, getPokeballTintColor } from "#app/data/pokeball"; import { doPokeballBounceAnim, getPokeballAtlasKey, getPokeballCatchMultiplier, getPokeballTintColor, getCriticalCaptureChance } from "#app/data/pokeball";
import { getStatusEffectCatchRateMultiplier } from "#app/data/status-effect"; import { getStatusEffectCatchRateMultiplier } from "#app/data/status-effect";
import { addPokeballCaptureStars, addPokeballOpenParticles } from "#app/field/anims"; import { addPokeballCaptureStars, addPokeballOpenParticles } from "#app/field/anims";
import { EnemyPokemon } from "#app/field/pokemon"; import { EnemyPokemon } from "#app/field/pokemon";
@ -52,8 +52,10 @@ export class AttemptCapturePhase extends PokemonPhase {
const catchRate = pokemon.species.catchRate; const catchRate = pokemon.species.catchRate;
const pokeballMultiplier = getPokeballCatchMultiplier(this.pokeballType); const pokeballMultiplier = getPokeballCatchMultiplier(this.pokeballType);
const statusMultiplier = pokemon.status ? getStatusEffectCatchRateMultiplier(pokemon.status.effect) : 1; const statusMultiplier = pokemon.status ? getStatusEffectCatchRateMultiplier(pokemon.status.effect) : 1;
const x = Math.round((((_3m - _2h) * catchRate * pokeballMultiplier) / _3m) * statusMultiplier); const modifiedCatchRate = Math.round((((_3m - _2h) * catchRate * pokeballMultiplier) / _3m) * statusMultiplier);
const y = Math.round(65536 / Math.sqrt(Math.sqrt(255 / x))); const shakeProbability = Math.round(65536 / Math.pow((255 / modifiedCatchRate), 0.1875)); // Formula taken from gen 6
const criticalCaptureChance = getCriticalCaptureChance(this.scene, modifiedCatchRate);
const isCritical = pokemon.randSeedInt(256) < criticalCaptureChance;
const fpOffset = pokemon.getFieldPositionOffset(); const fpOffset = pokemon.getFieldPositionOffset();
const pokeballAtlasKey = getPokeballAtlasKey(this.pokeballType); const pokeballAtlasKey = getPokeballAtlasKey(this.pokeballType);
@ -61,17 +63,19 @@ export class AttemptCapturePhase extends PokemonPhase {
this.pokeball.setOrigin(0.5, 0.625); this.pokeball.setOrigin(0.5, 0.625);
this.scene.field.add(this.pokeball); this.scene.field.add(this.pokeball);
this.scene.playSound("se/pb_throw"); this.scene.playSound("se/pb_throw", isCritical ? { rate: 0.2 } : undefined); // Crit catch throws are higher pitched
this.scene.time.delayedCall(300, () => { this.scene.time.delayedCall(300, () => {
this.scene.field.moveBelow(this.pokeball as Phaser.GameObjects.GameObject, pokemon); this.scene.field.moveBelow(this.pokeball as Phaser.GameObjects.GameObject, pokemon);
}); });
this.scene.tweens.add({ this.scene.tweens.add({
// Throw animation
targets: this.pokeball, targets: this.pokeball,
x: { value: 236 + fpOffset[0], ease: "Linear" }, x: { value: 236 + fpOffset[0], ease: "Linear" },
y: { value: 16 + fpOffset[1], ease: "Cubic.easeOut" }, y: { value: 16 + fpOffset[1], ease: "Cubic.easeOut" },
duration: 500, duration: 500,
onComplete: () => { onComplete: () => {
// Ball opens
this.pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`); this.pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`);
this.scene.time.delayedCall(17, () => this.pokeball.setTexture("pb", `${pokeballAtlasKey}_open`)); this.scene.time.delayedCall(17, () => this.pokeball.setTexture("pb", `${pokeballAtlasKey}_open`));
this.scene.playSound("se/pb_rel"); this.scene.playSound("se/pb_rel");
@ -80,30 +84,33 @@ export class AttemptCapturePhase extends PokemonPhase {
addPokeballOpenParticles(this.scene, this.pokeball.x, this.pokeball.y, this.pokeballType); addPokeballOpenParticles(this.scene, this.pokeball.x, this.pokeball.y, this.pokeballType);
this.scene.tweens.add({ this.scene.tweens.add({
// Mon enters ball
targets: pokemon, targets: pokemon,
duration: 500, duration: 500,
ease: "Sine.easeIn", ease: "Sine.easeIn",
scale: 0.25, scale: 0.25,
y: 20, y: 20,
onComplete: () => { onComplete: () => {
// Ball closes
this.pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`); this.pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`);
pokemon.setVisible(false); pokemon.setVisible(false);
this.scene.playSound("se/pb_catch"); this.scene.playSound("se/pb_catch");
this.scene.time.delayedCall(17, () => this.pokeball.setTexture("pb", `${pokeballAtlasKey}`)); this.scene.time.delayedCall(17, () => this.pokeball.setTexture("pb", `${pokeballAtlasKey}`));
const doShake = () => { const doShake = () => {
// After the overall catch rate check, the game does 3 shake checks before confirming the catch.
let shakeCount = 0; let shakeCount = 0;
const pbX = this.pokeball.x; const pbX = this.pokeball.x;
const shakeCounter = this.scene.tweens.addCounter({ const shakeCounter = this.scene.tweens.addCounter({
from: 0, from: 0,
to: 1, to: 1,
repeat: 4, repeat: isCritical ? 2 : 4, // Critical captures only perform 1 shake check
yoyo: true, yoyo: true,
ease: "Cubic.easeOut", ease: "Cubic.easeOut",
duration: 250, duration: 250,
repeatDelay: 500, repeatDelay: 500,
onUpdate: t => { onUpdate: t => {
if (shakeCount && shakeCount < 4) { if (shakeCount && shakeCount < (isCritical ? 2 : 4)) {
const value = t.getValue(); const value = t.getValue();
const directionMultiplier = shakeCount % 2 === 1 ? 1 : -1; const directionMultiplier = shakeCount % 2 === 1 ? 1 : -1;
this.pokeball.setX(pbX + value * 4 * directionMultiplier); this.pokeball.setX(pbX + value * 4 * directionMultiplier);
@ -114,13 +121,18 @@ export class AttemptCapturePhase extends PokemonPhase {
if (!pokemon.species.isObtainable()) { if (!pokemon.species.isObtainable()) {
shakeCounter.stop(); shakeCounter.stop();
this.failCatch(shakeCount); this.failCatch(shakeCount);
} else if (shakeCount++ < 3) { } else if (shakeCount++ < (isCritical ? 1 : 3)) {
if (pokeballMultiplier === -1 || pokemon.randSeedInt(65536) < y) { // Shake check (skip check for critical or guaranteed captures, but still play the sound)
if (pokeballMultiplier === -1 || isCritical || modifiedCatchRate >= 255 || pokemon.randSeedInt(65536) < shakeProbability) {
this.scene.playSound("se/pb_move"); this.scene.playSound("se/pb_move");
} else { } else {
shakeCounter.stop(); shakeCounter.stop();
this.failCatch(shakeCount); this.failCatch(shakeCount);
} }
} else if (isCritical && pokemon.randSeedInt(65536) >= shakeProbability) {
// Above, perform the one shake check for critical captures after the ball shakes once
shakeCounter.stop();
this.failCatch(shakeCount);
} else { } else {
this.scene.playSound("se/pb_lock"); this.scene.playSound("se/pb_lock");
addPokeballCaptureStars(this.scene, this.pokeball); addPokeballCaptureStars(this.scene, this.pokeball);
@ -153,7 +165,8 @@ export class AttemptCapturePhase extends PokemonPhase {
}); });
}; };
this.scene.time.delayedCall(250, () => doPokeballBounceAnim(this.scene, this.pokeball, 16, 72, 350, doShake)); // Ball bounces (handled in pokemon.ts)
this.scene.time.delayedCall(250, () => doPokeballBounceAnim(this.scene, this.pokeball, 16, 72, 350, doShake, isCritical));
} }
}); });
} }

View File

@ -69,7 +69,9 @@ export abstract class ApiBase {
"Content-Type": config.headers?.["Content-Type"] ?? "application/json", "Content-Type": config.headers?.["Content-Type"] ?? "application/json",
}; };
console.log(`Sending ${config.method ?? "GET"} request to: `, this.base + path, config); if (import.meta.env.DEV) {
console.log(`Sending ${config.method ?? "GET"} request to: `, this.base + path, config);
}
return await fetch(this.base + path, config); return await fetch(this.base + path, config);
} }

View File

@ -1,10 +1,12 @@
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import { MoveResult } from "#app/field/pokemon"; import { MoveResult, PokemonMove } from "#app/field/pokemon";
import GameManager from "#test/utils/gameManager"; import GameManager from "#test/utils/gameManager";
import Phaser from "phaser"; import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { StatusEffect } from "#app/enums/status-effect";
import { BattlerIndex } from "#app/battle";
describe("Moves - Sketch", () => { describe("Moves - Sketch", () => {
let phaserGame: Phaser.Game; let phaserGame: Phaser.Game;
@ -32,22 +34,46 @@ describe("Moves - Sketch", () => {
}); });
it("Sketch should not fail even if a previous Sketch failed to retrieve a valid move and ran out of PP", async () => { it("Sketch should not fail even if a previous Sketch failed to retrieve a valid move and ran out of PP", async () => {
game.override.moveset([ Moves.SKETCH, Moves.SKETCH ]);
await game.classicMode.startBattle([ Species.REGIELEKI ]); await game.classicMode.startBattle([ Species.REGIELEKI ]);
const playerPokemon = game.scene.getPlayerPokemon(); const playerPokemon = game.scene.getPlayerPokemon()!;
// can't use normal moveset override because we need to check moveset changes
playerPokemon.moveset = [ new PokemonMove(Moves.SKETCH), new PokemonMove(Moves.SKETCH) ];
game.move.select(Moves.SKETCH); game.move.select(Moves.SKETCH);
await game.phaseInterceptor.to("TurnEndPhase"); await game.phaseInterceptor.to("TurnEndPhase");
expect(playerPokemon?.getLastXMoves()[0].result).toBe(MoveResult.FAIL); expect(playerPokemon.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
const moveSlot0 = playerPokemon?.getMoveset()[0]; const moveSlot0 = playerPokemon.getMoveset()[0]!;
expect(moveSlot0?.moveId).toBe(Moves.SKETCH); expect(moveSlot0.moveId).toBe(Moves.SKETCH);
expect(moveSlot0?.getPpRatio()).toBe(0); expect(moveSlot0.getPpRatio()).toBe(0);
await game.toNextTurn(); await game.toNextTurn();
game.move.select(Moves.SKETCH); game.move.select(Moves.SKETCH);
await game.phaseInterceptor.to("TurnEndPhase"); await game.phaseInterceptor.to("TurnEndPhase");
expect(playerPokemon?.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); expect(playerPokemon.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
// Can't verify if the player Pokemon's moveset was successfully changed because of overrides. expect(playerPokemon.moveset[0]?.moveId).toBe(Moves.SPLASH);
expect(playerPokemon.moveset[1]?.moveId).toBe(Moves.SKETCH);
});
it("Sketch should retrieve the most recent valid move from its target history", async () => {
game.override.enemyStatusEffect(StatusEffect.PARALYSIS);
await game.classicMode.startBattle([ Species.REGIELEKI ]);
const playerPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
playerPokemon.moveset = [ new PokemonMove(Moves.SKETCH), new PokemonMove(Moves.GROWL) ];
game.move.select(Moves.GROWL);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
await game.move.forceStatusActivation(false);
await game.phaseInterceptor.to("TurnEndPhase");
expect(enemyPokemon.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
await game.toNextTurn();
game.move.select(Moves.SKETCH);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
await game.move.forceStatusActivation(true);
await game.phaseInterceptor.to("TurnEndPhase");
expect(playerPokemon.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
expect(playerPokemon.moveset[0]?.moveId).toBe(Moves.SPLASH);
expect(playerPokemon.moveset[1]?.moveId).toBe(Moves.GROWL);
}); });
}); });