[Feature] [Same species Egg] Egg class rewrite to enable fully parameterized eggs to generate same species eggs + Egg overrides (#1833)

* Create variant-tiers enum

* Added variant tier override property to the egg class

* Added hasVariants function to pokemon species

* Implement variant override logic to egg hatching phase

* Delete src/enums/variant-tiers

* Create variant-tiers enum

* Added egg shiny and variant overrides

* fixed egg comment in overrides.ts

* Added override logic to egg hatch phase

* Added species pool filter logic when global override is set

* Added global egg tier override logic

* Added global egg tier override

* Added global gacha pull count override logic

* Added global gacha pull count override

* Renamed egg hatch override

* Renamed egg hatch override

* Added gacha pull without voucher global override

* Renamed free gacha pull global override

* Added free gacha pull override logic

* Gacha pull count override name fix

* Bugfix

* restored defaults + savegame bugfix

* eggOptions added to parameterize eggs. Added option to buy eggs of the same species.

* Small Bugfix for same species egg generation

* Removed translation from translator

* Improved the isManaphyEgg() check

* Fixed manaphy egg hatch wave count

* Added comments to IEggOptions

* Added eggOptions for hidden ability and rare egg move override

* Merge Fix: Update egg-hatch-phase.ts

* Fixed manaphy rates back to 1/256 like in PR #2182

* Renamed override, same species egg unlocks after passive is bought. Added code as comment for custom shiny, HA and rare egg move rates.

* Merge fix. Moved enums.

* quick fix for the commented out code

* Fixed that you can't buy an egg over the 99 egg limit

* Fix that you can't buy eternatus

* Use already existing randSeedShuffle instead of my own function

* Eternatus buyable again. Changed overrides to be able to set common tier/variants. Moved getGuaranteedEggTierFromPullCount().

* Changed eggOption gachaType to sourceType. Replaced eggOption overrideRareEggMove with eggMoveIndex to exatly specify an egg move. Moved egg move unlock logic into the egg class. Simplified shiny calculation. Added same species egg type descriptor. Moved custom rates for same species egg code into egg.ts.

* Added 19 unit tests for eggs

* Changed unit test description

* Added higher rates for same species eggs

* Adjusted same species egg cost for 1-3 cost starters and HA rates

* Added legacy egg loading unit test. Fixed gachaType legacy value loaded from DB and legacy tier loading

* Legacy egg loading from server DB fixed
This commit is contained in:
sirzento 2024-06-22 02:19:56 +02:00 committed by GitHub
parent 7efb0ca7fe
commit 9d090f37f9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 919 additions and 300 deletions

View File

@ -1,97 +1,513 @@
import BattleScene from "../battle-scene";
import PokemonSpecies, { getPokemonSpecies, speciesStarters } from "./pokemon-species";
import { VariantTier } from "../enums/variant-tiers";
import * as Utils from "../utils";
import * as Overrides from "../overrides";
import { pokemonPrevolutions } from "./pokemon-evolutions";
import { PlayerPokemon } from "#app/field/pokemon";
import i18next from "i18next";
import { EggTier } from "#enums/egg-type";
import { Species } from "#enums/species";
import { EggSourceType } from "#app/enums/egg-source-types.js";
export const EGG_SEED = 1073741824;
export enum GachaType {
MOVE,
LEGENDARY,
SHINY
// Rates for specific random properties in 1/x
const DEFAULT_SHINY_RATE = 128;
const GACHA_SHINY_UP_SHINY_RATE = 64;
const SAME_SPECIES_EGG_SHINY_RATE = 32;
const SAME_SPECIES_EGG_HA_RATE = 16;
const MANAPHY_EGG_MANAPHY_RATE = 8;
// 1/x for legendary eggs, 1/x*2 for epic eggs, 1/x*4 for rare eggs, and 1/x*8 for common eggs
const DEFAULT_RARE_EGGMOVE_RATE = 6;
const SAME_SPECIES_EGG_RARE_EGGMOVE_RATE = 3;
const GACHA_MOVE_UP_RARE_EGGMOVE_RATE = 3;
/** Egg options to override egg properties */
export interface IEggOptions {
/** Id. Used to check if egg type will be manaphy (id % 204 === 0) */
id?: number;
/** Timestamp when this egg got created */
timestamp?: number;
/** Defines if the egg got pulled from a gacha or not. If true, egg pity and pull statistics will be applyed.
* Egg will be automaticly added to the game data.
* NEEDS scene eggOption to work.
*/
pulled?: boolean;
/** Defines where the egg comes from. Applies specific modifiers.
* Will also define the text displayed in the egg list.
*/
sourceType?: EggSourceType;
/** Needs to be defined if eggOption pulled is defined or if no species or isShiny is degined since this will be needed to generate them. */
scene?: BattleScene;
/** Sets the tier of the egg. Only species of this tier can be hatched from this egg.
* Tier will be overriden if species eggOption is set.
*/
tier?: EggTier;
/** Sets how many waves it will take till this egg hatches. */
hatchWaves?: number;
/** Sets the exact species that will hatch from this egg.
* Needs scene eggOption if not provided.
*/
species?: Species;
/** Defines if the hatched pokemon will be a shiny. */
isShiny?: boolean;
/** Defines the variant of the pokemon that will hatch from this egg. If no variantTier is given the normal variant rates will apply. */
variantTier?: VariantTier;
/** Defines which egg move will be unlocked. 3 = rare egg move. */
eggMoveIndex?: number;
/** Defines if the egg will hatch with the hidden ability of this species.
* If no hidden ability exist, a random one will get choosen.
*/
overrideHiddenAbility?: boolean
}
export class Egg {
public id: integer;
public tier: EggTier;
public gachaType: GachaType;
public hatchWaves: integer;
public timestamp: integer;
constructor(id: integer, gachaType: GachaType, hatchWaves: integer, timestamp: integer) {
this.id = id;
this.tier = Math.floor(id / EGG_SEED);
this.gachaType = gachaType;
this.hatchWaves = hatchWaves;
this.timestamp = timestamp;
////
// #region Privat properties
////
private _id: number;
private _tier: EggTier;
private _sourceType: EggSourceType | undefined;
private _hatchWaves: number;
private _timestamp: number;
private _species: Species;
private _isShiny: boolean;
private _variantTier: VariantTier;
private _eggMoveIndex: number;
private _overrideHiddenAbility: boolean;
////
// #endregion
////
////
// #region Public facing properties
////
get id(): number {
return this._id;
}
isManaphyEgg(): boolean {
return this.tier === EggTier.COMMON && !(this.id % 204);
get tier(): EggTier {
return this._tier;
}
getKey(): string {
get sourceType(): EggSourceType | undefined {
return this._sourceType;
}
get hatchWaves(): number {
return this._hatchWaves;
}
set hatchWaves(value: number) {
this._hatchWaves = value;
}
get timestamp(): number {
return this._timestamp;
}
get species(): Species {
return this._species;
}
get isShiny(): boolean {
return this._isShiny;
}
get variantTier(): VariantTier {
return this._variantTier;
}
get eggMoveIndex(): number {
return this._eggMoveIndex;
}
get overrideHiddenAbility(): boolean {
return this._overrideHiddenAbility;
}
////
// #endregion
////
constructor(eggOptions?: IEggOptions) {
//if (eggOptions.tier && eggOptions.species) throw Error("Error egg can't have species and tier as option. only choose one of them.")
this._tier = eggOptions.tier ?? (Overrides.EGG_TIER_OVERRIDE ?? this.rollEggTier());
if (eggOptions.pulled) {
this.checkForPityTierOverrides(eggOptions.scene);
this.increasePullStatistic(eggOptions.scene);
}
this._id = eggOptions.id ?? Utils.randInt(EGG_SEED, EGG_SEED * this._tier);
this._sourceType = eggOptions.sourceType ?? undefined;
this._hatchWaves = eggOptions.hatchWaves ?? this.getEggTierDefaultHatchWaves();
this._timestamp = eggOptions.timestamp ?? new Date().getTime();
// First roll shiny and variant so we can filter if species with an variant exist
this._isShiny = eggOptions.isShiny ?? (Overrides.EGG_SHINY_OVERRIDE || this.rollShiny());
this._variantTier = eggOptions.variantTier ?? (Overrides.EGG_VARIANT_OVERRIDE ?? this.rollVariant());
this._species = eggOptions.species ?? this.rollSpecies(eggOptions.scene);
this._overrideHiddenAbility = eggOptions.overrideHiddenAbility ?? false;
this._eggMoveIndex = eggOptions.eggMoveIndex ?? this.rollEggMoveIndex();
// Override egg tier and hatchwaves if species was given
if (eggOptions.species) {
this._tier = this.getEggTierFromSpeciesStarterValue();
this._hatchWaves = eggOptions.hatchWaves ?? this.getEggTierDefaultHatchWaves();
}
if (eggOptions.pulled) {
this.addEggToGameData(eggOptions.scene);
}
}
////
// #region Public methodes
////
public isManaphyEgg(): boolean {
return (this._species === Species.PHIONE || this._species === Species.MANAPHY) ||
this._tier === EggTier.COMMON && !(this._id % 204);
}
public getKey(): string {
if (this.isManaphyEgg()) {
return "manaphy";
}
return this.tier.toString();
return this._tier.toString();
}
// Generates a PlayerPokemon from an egg
public generatePlayerPokemon(scene: BattleScene): PlayerPokemon {
// Legacy egg wants to hatch. Generate missing properties
if (!this._species) {
this._isShiny = this.rollShiny();
this._species = this.rollSpecies(scene);
}
const pokemonSpecies = getPokemonSpecies(this._species);
// Sets the hidden ability if a hidden ability exists and the override is set
// or if the same species egg hits the chance
let abilityIndex = undefined;
if (pokemonSpecies.abilityHidden && (this._overrideHiddenAbility
|| (this._sourceType === EggSourceType.SAME_SPECIES_EGG && !Utils.randSeedInt(SAME_SPECIES_EGG_HA_RATE)))) {
abilityIndex = pokemonSpecies.ability2 ? 2 : 1;
}
// This function has way to many optional parameters
const ret: PlayerPokemon = scene.addPlayerPokemon(pokemonSpecies, 1, abilityIndex, undefined, undefined, false);
ret.shiny = this._isShiny;
ret.variant = this._variantTier;
const secondaryIvs = Utils.getIvsFromId(Utils.randSeedInt(4294967295));
for (let s = 0; s < ret.ivs.length; s++) {
ret.ivs[s] = Math.max(ret.ivs[s], secondaryIvs[s]);
}
return ret;
}
// Doesn't need to be called if the egg got pulled by a gacha machiene
public addEggToGameData(scene: BattleScene): void {
scene.gameData.eggs.push(this);
}
public getEggDescriptor(): string {
if (this.isManaphyEgg()) {
return "Manaphy";
}
switch (this.tier) {
case EggTier.GREAT:
return i18next.t("egg:greatTier");
case EggTier.ULTRA:
return i18next.t("egg:ultraTier");
case EggTier.MASTER:
return i18next.t("egg:masterTier");
default:
return i18next.t("egg:defaultTier");
}
}
public getEggHatchWavesMessage(): string {
if (this.hatchWaves <= 5) {
return i18next.t("egg:hatchWavesMessageSoon");
}
if (this.hatchWaves <= 15) {
return i18next.t("egg:hatchWavesMessageClose");
}
if (this.hatchWaves <= 50) {
return i18next.t("egg:hatchWavesMessageNotClose");
}
return i18next.t("egg:hatchWavesMessageLongTime");
}
public getEggTypeDescriptor(scene: BattleScene): string {
switch (this.sourceType) {
case EggSourceType.GACHA_LEGENDARY:
return `${i18next.t("egg:gachaTypeLegendary")} (${getPokemonSpecies(getLegendaryGachaSpeciesForTimestamp(scene, this.timestamp)).getName()})`;
case EggSourceType.GACHA_MOVE:
return i18next.t("egg:gachaTypeMove");
case EggSourceType.GACHA_SHINY:
return i18next.t("egg:gachaTypeShiny");
case EggSourceType.SAME_SPECIES_EGG:
return i18next.t("egg:sameSpeciesEgg", { species: getPokemonSpecies(this._species).getName()});
}
}
////
// #endregion
////
////
// #region Private methodes
////
private rollEggMoveIndex() {
let baseChance = DEFAULT_RARE_EGGMOVE_RATE;
switch (this._sourceType) {
case EggSourceType.GACHA_MOVE:
baseChance = GACHA_MOVE_UP_RARE_EGGMOVE_RATE;
break;
case EggSourceType.SAME_SPECIES_EGG:
baseChance = SAME_SPECIES_EGG_RARE_EGGMOVE_RATE;
break;
default:
break;
}
return Utils.randSeedInt(baseChance * Math.pow(2, 3 - this.tier)) ? Utils.randSeedInt(3) : 3;
}
private getEggTierDefaultHatchWaves(eggTier?: EggTier): number {
if (this._species === Species.PHIONE || this._species === Species.MANAPHY) {
return 50;
}
switch (eggTier ?? this._tier) {
case EggTier.COMMON:
return 10;
case EggTier.GREAT:
return 25;
case EggTier.ULTRA:
return 50;
}
return 100;
}
private rollEggTier(): EggTier {
const tierValueOffset = this._sourceType === EggSourceType.GACHA_LEGENDARY ? 1 : 0;
const tierValue = Utils.randInt(256);
return tierValue >= 52 + tierValueOffset ? EggTier.COMMON : tierValue >= 8 + tierValueOffset ? EggTier.GREAT : tierValue >= 1 + tierValueOffset ? EggTier.ULTRA : EggTier.MASTER;
}
private rollSpecies(scene: BattleScene): Species {
if (!scene) {
return undefined;
}
/**
* Manaphy eggs have a 1/8 chance of being Manaphy and 7/8 chance of being Phione
* Legendary eggs pulled from the legendary gacha have a 50% of being converted into
* the species that was the legendary focus at the time
*/
if (this.isManaphyEgg()) {
const rand = Utils.randSeedInt(MANAPHY_EGG_MANAPHY_RATE);
return rand ? Species.PHIONE : Species.MANAPHY;
} else if (this.tier === EggTier.MASTER
&& this._sourceType === EggSourceType.GACHA_LEGENDARY) {
if (!Utils.randSeedInt(2)) {
return getLegendaryGachaSpeciesForTimestamp(scene, this.timestamp);
}
}
let minStarterValue: integer;
let maxStarterValue: integer;
switch (this.tier) {
case EggTier.GREAT:
minStarterValue = 4;
maxStarterValue = 5;
break;
case EggTier.ULTRA:
minStarterValue = 6;
maxStarterValue = 7;
break;
case EggTier.MASTER:
minStarterValue = 8;
maxStarterValue = 9;
break;
default:
minStarterValue = 1;
maxStarterValue = 3;
break;
}
const ignoredSpecies = [Species.PHIONE, Species.MANAPHY, Species.ETERNATUS];
let speciesPool = Object.keys(speciesStarters)
.filter(s => speciesStarters[s] >= minStarterValue && speciesStarters[s] <= maxStarterValue)
.map(s => parseInt(s) as Species)
.filter(s => !pokemonPrevolutions.hasOwnProperty(s) && getPokemonSpecies(s).isObtainable() && ignoredSpecies.indexOf(s) === -1);
// If this is the 10th egg without unlocking something new, attempt to force it.
if (scene.gameData.unlockPity[this.tier] >= 9) {
const lockedPool = speciesPool.filter(s => !scene.gameData.dexData[s].caughtAttr);
if (lockedPool.length) { // Skip this if everything is unlocked
speciesPool = lockedPool;
}
}
// If egg variant is set to RARE or EPIC, filter species pool to only include ones with variants.
if (this.variantTier && (this.variantTier === VariantTier.RARE || this.variantTier === VariantTier.EPIC)) {
speciesPool = speciesPool.filter(s => getPokemonSpecies(s).hasVariants());
}
/**
* Pokemon that are cheaper in their tier get a weight boost. Regionals get a weight penalty
* 1 cost mons get 2x
* 2 cost mons get 1.5x
* 4, 6, 8 cost mons get 1.75x
* 3, 5, 7, 9 cost mons get 1x
* Alolan, Galarian, and Paldean mons get 0.5x
* Hisui mons get 0.125x
*
* The total weight is also being calculated EACH time there is an egg hatch instead of being generated once
* and being the same each time
*/
let totalWeight = 0;
const speciesWeights = [];
for (const speciesId of speciesPool) {
let weight = Math.floor((((maxStarterValue - speciesStarters[speciesId]) / ((maxStarterValue - minStarterValue) + 1)) * 1.5 + 1) * 100);
const species = getPokemonSpecies(speciesId);
if (species.isRegional()) {
weight = Math.floor(weight / (species.isRareRegional() ? 8 : 2));
}
speciesWeights.push(totalWeight + weight);
totalWeight += weight;
}
let species: Species;
const rand = Utils.randSeedInt(totalWeight);
for (let s = 0; s < speciesWeights.length; s++) {
if (rand < speciesWeights[s]) {
species = speciesPool[s];
break;
}
}
if (!!scene.gameData.dexData[species].caughtAttr) {
scene.gameData.unlockPity[this.tier] = Math.min(scene.gameData.unlockPity[this.tier] + 1, 10);
} else {
scene.gameData.unlockPity[this.tier] = 0;
}
return species;
}
/**
* Rolls whether the egg is shiny or not.
* @returns True if the egg is shiny
**/
private rollShiny(): boolean {
let shinyChance = DEFAULT_SHINY_RATE;
switch (this._sourceType) {
case EggSourceType.GACHA_SHINY:
shinyChance = GACHA_SHINY_UP_SHINY_RATE;
break;
case EggSourceType.SAME_SPECIES_EGG:
shinyChance = SAME_SPECIES_EGG_SHINY_RATE;
break;
default:
break;
}
return !Utils.randSeedInt(shinyChance);
}
// Uses the same logic as pokemon.generateVariant(). I would like to only have this logic in one
// place but I don't want to touch the pokemon class.
private rollVariant(): VariantTier {
if (!this.isShiny) {
return VariantTier.COMMON;
}
const rand = Utils.randSeedInt(10);
if (rand >= 4) {
return VariantTier.COMMON; // 6/10
} else if (rand >= 1) {
return VariantTier.RARE; // 3/10
} else {
return VariantTier.EPIC; // 1/10
}
}
private checkForPityTierOverrides(scene: BattleScene): void {
scene.gameData.eggPity[EggTier.GREAT] += 1;
scene.gameData.eggPity[EggTier.ULTRA] += 1;
scene.gameData.eggPity[EggTier.MASTER] += 1 + this._sourceType === EggSourceType.GACHA_LEGENDARY ? 1 : 0;
// These numbers are roughly the 80% mark. That is, 80% of the time you'll get an egg before this gets triggered.
if (scene.gameData.eggPity[EggTier.MASTER] >= 412 && this._tier === EggTier.COMMON) {
this._tier = EggTier.MASTER;
} else if (scene.gameData.eggPity[EggTier.ULTRA] >= 59 && this._tier === EggTier.COMMON) {
this._tier = EggTier.ULTRA;
} else if (scene.gameData.eggPity[EggTier.GREAT] >= 9 && this._tier === EggTier.COMMON) {
this._tier = EggTier.GREAT;
}
scene.gameData.eggPity[this._tier] = 0;
}
private increasePullStatistic(scene: BattleScene): void {
scene.gameData.gameStats.eggsPulled++;
if (this.isManaphyEgg()) {
scene.gameData.gameStats.manaphyEggsPulled++;
this._hatchWaves = this.getEggTierDefaultHatchWaves(EggTier.ULTRA);
return;
}
switch (this.tier) {
case EggTier.GREAT:
scene.gameData.gameStats.rareEggsPulled++;
break;
case EggTier.ULTRA:
scene.gameData.gameStats.epicEggsPulled++;
break;
case EggTier.MASTER:
scene.gameData.gameStats.legendaryEggsPulled++;
break;
}
}
private getEggTierFromSpeciesStarterValue(): EggTier {
const speciesStartValue = speciesStarters[this.species];
if (speciesStartValue >= 1 && speciesStartValue <= 3) {
return EggTier.COMMON;
}
if (speciesStartValue >= 4 && speciesStartValue <= 5) {
return EggTier.GREAT;
}
if (speciesStartValue >= 6 && speciesStartValue <= 7) {
return EggTier.ULTRA;
}
if (speciesStartValue >= 8) {
return EggTier.MASTER;
}
}
////
// #endregion
////
}
export function getEggTierDefaultHatchWaves(tier: EggTier): integer {
switch (tier) {
case EggTier.COMMON:
return 10;
case EggTier.GREAT:
return 25;
case EggTier.ULTRA:
return 50;
}
return 100;
}
export function getEggDescriptor(egg: Egg): string {
if (egg.isManaphyEgg()) {
return "Manaphy";
}
switch (egg.tier) {
case EggTier.GREAT:
return i18next.t("egg:greatTier");
case EggTier.ULTRA:
return i18next.t("egg:ultraTier");
case EggTier.MASTER:
return i18next.t("egg:masterTier");
default:
return i18next.t("egg:defaultTier");
}
}
export function getEggHatchWavesMessage(hatchWaves: integer): string {
if (hatchWaves <= 5) {
return i18next.t("egg:hatchWavesMessageSoon");
}
if (hatchWaves <= 15) {
return i18next.t("egg:hatchWavesMessageClose");
}
if (hatchWaves <= 50) {
return i18next.t("egg:hatchWavesMessageNotClose");
}
return i18next.t("egg:hatchWavesMessageLongTime");
}
export function getEggGachaTypeDescriptor(scene: BattleScene, egg: Egg): string {
switch (egg.gachaType) {
case GachaType.LEGENDARY:
return `${i18next.t("egg:gachaTypeLegendary")} (${getPokemonSpecies(getLegendaryGachaSpeciesForTimestamp(scene, egg.timestamp)).getName()})`;
case GachaType.MOVE:
return i18next.t("egg:gachaTypeMove");
case GachaType.SHINY:
return i18next.t("egg:gachaTypeShiny");
}
}
export function getLegendaryGachaSpeciesForTimestamp(scene: BattleScene, timestamp: integer): Species {
export function getLegendaryGachaSpeciesForTimestamp(scene: BattleScene, timestamp: number): Species {
const legendarySpecies = Object.entries(speciesStarters)
.filter(s => s[1] >= 8 && s[1] <= 9)
.map(s => parseInt(s[0]))

View File

@ -819,6 +819,10 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
return super.isObtainable();
}
hasVariants() {
return variantData.hasOwnProperty(this.speciesId);
}
getFormSpriteKey(formIndex?: integer) {
if (this.forms.length && formIndex >= this.forms.length) {
console.warn(`Attempted accessing form with index ${formIndex} of species ${this.getName()} with only ${this.forms.length || 0} forms`);

View File

@ -3,17 +3,13 @@ import { Phase } from "./phase";
import BattleScene, { AnySound } from "./battle-scene";
import * as Utils from "./utils";
import { Mode } from "./ui/ui";
import { EGG_SEED, Egg, GachaType, getLegendaryGachaSpeciesForTimestamp } from "./data/egg";
import { EGG_SEED, Egg } from "./data/egg";
import EggHatchSceneHandler from "./ui/egg-hatch-scene-handler";
import { PlayerPokemon } from "./field/pokemon";
import { getPokemonSpecies, speciesStarters } from "./data/pokemon-species";
import { achvs } from "./system/achv";
import { pokemonPrevolutions } from "./data/pokemon-evolutions";
import PokemonInfoContainer from "./ui/pokemon-info-container";
import EggCounterContainer from "./ui/egg-counter-container";
import { EggCountChangedEvent } from "./events/egg";
import { EggTier } from "#enums/egg-type";
import { Species } from "#enums/species";
/**
* Class that represents egg hatching
@ -442,135 +438,10 @@ export class EggHatchPhase extends Phase {
*/
generatePokemon(): PlayerPokemon {
let ret: PlayerPokemon;
let speciesOverride: Species; // SpeciesOverride should probably be a passed in parameter for future species-eggs
this.scene.executeWithSeedOffset(() => {
/**
* Manaphy eggs have a 1/8 chance of being Manaphy and 7/8 chance of being Phione
* Legendary eggs pulled from the legendary gacha have a 50% of being converted into
* the species that was the legendary focus at the time
*/
if (this.egg.isManaphyEgg()) {
const rand = Utils.randSeedInt(8);
speciesOverride = rand ? Species.PHIONE : Species.MANAPHY;
} else if (this.egg.tier === EggTier.MASTER
&& this.egg.gachaType === GachaType.LEGENDARY) {
if (!Utils.randSeedInt(2)) {
speciesOverride = getLegendaryGachaSpeciesForTimestamp(this.scene, this.egg.timestamp);
}
}
if (speciesOverride) {
const pokemonSpecies = getPokemonSpecies(speciesOverride);
ret = this.scene.addPlayerPokemon(pokemonSpecies, 1, undefined, undefined, undefined, false);
} else {
let minStarterValue: integer;
let maxStarterValue: integer;
switch (this.egg.tier) {
case EggTier.GREAT:
minStarterValue = 4;
maxStarterValue = 5;
break;
case EggTier.ULTRA:
minStarterValue = 6;
maxStarterValue = 7;
break;
case EggTier.MASTER:
minStarterValue = 8;
maxStarterValue = 9;
break;
default:
minStarterValue = 1;
maxStarterValue = 3;
break;
}
const ignoredSpecies = [ Species.PHIONE, Species.MANAPHY, Species.ETERNATUS ];
let speciesPool = Object.keys(speciesStarters)
.filter(s => speciesStarters[s] >= minStarterValue && speciesStarters[s] <= maxStarterValue)
.map(s => parseInt(s) as Species)
.filter(s => !pokemonPrevolutions.hasOwnProperty(s) && getPokemonSpecies(s).isObtainable() && ignoredSpecies.indexOf(s) === -1);
// If this is the 10th egg without unlocking something new, attempt to force it.
if (this.scene.gameData.unlockPity[this.egg.tier] >= 9) {
const lockedPool = speciesPool.filter(s => !this.scene.gameData.dexData[s].caughtAttr);
if (lockedPool.length) { // Skip this if everything is unlocked
speciesPool = lockedPool;
}
}
/**
* Pokemon that are cheaper in their tier get a weight boost. Regionals get a weight penalty
* 1 cost mons get 2x
* 2 cost mons get 1.5x
* 4, 6, 8 cost mons get 1.75x
* 3, 5, 7, 9 cost mons get 1x
* Alolan, Galarian, and Paldean mons get 0.5x
* Hisui mons get 0.125x
*
* The total weight is also being calculated EACH time there is an egg hatch instead of being generated once
* and being the same each time
*/
let totalWeight = 0;
const speciesWeights = [];
for (const speciesId of speciesPool) {
let weight = Math.floor((((maxStarterValue - speciesStarters[speciesId]) / ((maxStarterValue - minStarterValue) + 1)) * 1.5 + 1) * 100);
const species = getPokemonSpecies(speciesId);
if (species.isRegional()) {
weight = Math.floor(weight / (species.isRareRegional() ? 8 : 2));
}
speciesWeights.push(totalWeight + weight);
totalWeight += weight;
}
let species: Species;
const rand = Utils.randSeedInt(totalWeight);
for (let s = 0; s < speciesWeights.length; s++) {
if (rand < speciesWeights[s]) {
species = speciesPool[s];
break;
}
}
if (!!this.scene.gameData.dexData[species].caughtAttr) {
this.scene.gameData.unlockPity[this.egg.tier] = Math.min(this.scene.gameData.unlockPity[this.egg.tier] + 1, 10);
} else {
this.scene.gameData.unlockPity[this.egg.tier] = 0;
}
const pokemonSpecies = getPokemonSpecies(species);
ret = this.scene.addPlayerPokemon(pokemonSpecies, 1, undefined, undefined, undefined, false);
}
/**
* Non Shiny gacha Pokemon have a 1/128 chance of being shiny
* Shiny gacha Pokemon have a 1/64 chance of being shiny
* IVs are rolled twice and the higher of each stat's IV is taken
* The egg move gacha doubles the rate of rare egg moves but the base rates are
* Common: 1/48
* Rare: 1/24
* Epic: 1/12
* Legendary: 1/6
*/
ret.trySetShiny(this.egg.gachaType === GachaType.SHINY ? 1024 : 512);
ret.variant = ret.shiny ? ret.generateVariant() : 0;
const secondaryIvs = Utils.getIvsFromId(Utils.randSeedInt(4294967295));
for (let s = 0; s < ret.ivs.length; s++) {
ret.ivs[s] = Math.max(ret.ivs[s], secondaryIvs[s]);
}
const baseChance = this.egg.gachaType === GachaType.MOVE ? 3 : 6;
this.eggMoveIndex = Utils.randSeedInt(baseChance * Math.pow(2, 3 - this.egg.tier))
? Utils.randSeedInt(3)
: 3;
ret = this.egg.generatePlayerPokemon(this.scene);
this.eggMoveIndex = this.egg.eggMoveIndex;
}, this.egg.id, EGG_SEED.toString());

View File

@ -0,0 +1,7 @@
export enum EggSourceType {
GACHA_MOVE,
GACHA_LEGENDARY,
GACHA_SHINY,
SAME_SPECIES_EGG,
EVENT
}

5
src/enums/gacha-types.ts Normal file
View File

@ -0,0 +1,5 @@
export enum GachaType {
MOVE,
LEGENDARY,
SHINY
}

View File

@ -0,0 +1,5 @@
export enum VariantTier {
COMMON,
RARE,
EPIC
}

View File

@ -1,4 +1,4 @@
import { GachaType } from "./data/egg";
import { GachaType } from "./enums/gacha-types";
import { trainerConfigs } from "./data/trainer-config";
import { getBiomeHasProps } from "./field/arena";
import CacheBustedLoaderPlugin from "./plugins/cache-busted-loader-plugin";

View File

@ -17,5 +17,6 @@ export const egg: SimpleTranslationEntries = {
"notEnoughVouchers": "Du hast nicht genug Ei-Gutscheine!",
"tooManyEggs": "Du hast schon zu viele Eier!",
"pull": "Pull",
"pulls": "Pulls"
"pulls": "Pulls",
"sameSpeciesEgg": "{{species}} wird aus dem Ei schlüpfen!",
} as const;

View File

@ -31,6 +31,7 @@ export const starterSelectUiHandler: SimpleTranslationEntries = {
"selectMoveSwapWith": "Wähle die gewünschte Attacke.",
"unlockPassive": "Passiv-Skill freischalten",
"reduceCost": "Preis reduzieren",
"sameSpeciesEgg": "Ein Ei kaufen",
"cycleShiny": ": Schillernd",
"cycleForm": ": Form",
"cycleGender": ": Geschlecht",

View File

@ -17,5 +17,6 @@ export const egg: SimpleTranslationEntries = {
"notEnoughVouchers": "You don't have enough vouchers!",
"tooManyEggs": "You have too many eggs!",
"pull": "Pull",
"pulls": "Pulls"
"pulls": "Pulls",
"sameSpeciesEgg": "{{species}} will hatch from this egg!",
} as const;

View File

@ -31,6 +31,7 @@ export const starterSelectUiHandler: SimpleTranslationEntries = {
"selectMoveSwapWith": "Select a move to swap with",
"unlockPassive": "Unlock Passive",
"reduceCost": "Reduce Cost",
"sameSpeciesEgg": "Buy an Egg",
"cycleShiny": ": Shiny",
"cycleForm": ": Form",
"cycleGender": ": Gender",

View File

@ -17,5 +17,6 @@ export const egg: SimpleTranslationEntries = {
"notEnoughVouchers": "¡No tienes suficientes vales!",
"tooManyEggs": "¡No tienes suficiente espacio!",
"pull": "Tirada",
"pulls": "Tiradas"
"pulls": "Tiradas",
"sameSpeciesEgg": "{{species}} will hatch from this egg!",
} as const;

View File

@ -31,6 +31,7 @@ export const starterSelectUiHandler: SimpleTranslationEntries = {
"selectMoveSwapWith": "Elige el movimiento que sustituirá a",
"unlockPassive": "Añadir Pasiva",
"reduceCost": "Reducir Coste",
"sameSpeciesEgg": "Buy an Egg",
"cycleShiny": ": Shiny",
"cycleForm": ": Forma",
"cycleGender": ": Género",

View File

@ -17,5 +17,6 @@ export const egg: SimpleTranslationEntries = {
"notEnoughVouchers": "Vous navez pas assez de coupons !",
"tooManyEggs": "Vous avez trop dŒufs !",
"pull": "Tirage",
"pulls": "Tirages"
"pulls": "Tirages",
"sameSpeciesEgg": "{{species}} will hatch from this egg!",
} as const;

View File

@ -31,6 +31,7 @@ export const starterSelectUiHandler: SimpleTranslationEntries = {
"selectMoveSwapWith": "Sélectionnez laquelle échanger avec",
"unlockPassive": "Débloquer Passif",
"reduceCost": "Diminuer le cout",
"sameSpeciesEgg": "Buy an Egg",
"cycleShiny": ": » Chromatiques",
"cycleForm": ": » Formes",
"cycleGender": ": » Sexes",

View File

@ -17,5 +17,6 @@ export const egg: SimpleTranslationEntries = {
"notEnoughVouchers": "Non hai abbastanza Biglietti!",
"tooManyEggs": "Hai troppe Uova!",
"pull": "Tiro",
"pulls": "Tiri"
"pulls": "Tiri",
"sameSpeciesEgg": "{{species}} will hatch from this egg!",
} as const;

View File

@ -29,6 +29,7 @@ export const starterSelectUiHandler: SimpleTranslationEntries = {
"selectNature": "Seleziona natura.",
"selectMoveSwapOut": "Seleziona una mossa da scambiare.",
"selectMoveSwapWith": "Seleziona una mossa da scambiare con",
"sameSpeciesEgg": "Buy an Egg",
"unlockPassive": "Sblocca passiva",
"reduceCost": "Riduci costo",
"cycleShiny": ": Shiny",

View File

@ -17,5 +17,6 @@ export const egg: SimpleTranslationEntries = {
"notEnoughVouchers": "바우처가 충분하지 않습니다!",
"tooManyEggs": "알을 너무 많이 갖고 있습니다!",
"pull": "뽑기",
"pulls": "뽑기"
"pulls": "뽑기",
"sameSpeciesEgg": "{{species}} will hatch from this egg!",
} as const;

View File

@ -31,6 +31,7 @@ export const starterSelectUiHandler: SimpleTranslationEntries = {
"selectMoveSwapWith": "교체될 기술을 선택해주세요. 대상:",
"unlockPassive": "패시브 해금",
"reduceCost": "코스트 줄이기",
"sameSpeciesEgg": "Buy an Egg",
"cycleShiny": ": 특별한 색",
"cycleForm": ": 폼",
"cycleGender": ": 암수",

View File

@ -17,5 +17,6 @@ export const egg: SimpleTranslationEntries = {
"notEnoughVouchers": "Você não tem vouchers suficientes!",
"tooManyEggs": "Você já tem muitos ovos!",
"pull": "Prêmio",
"pulls": "Prêmios"
"pulls": "Prêmios",
"sameSpeciesEgg": "{{species}} will hatch from this egg!",
} as const;

View File

@ -31,6 +31,7 @@ export const starterSelectUiHandler: SimpleTranslationEntries = {
"selectMoveSwapWith": "Escolha o movimento que substituirá",
"unlockPassive": "Aprender Passiva",
"reduceCost": "Reduzir Custo",
"sameSpeciesEgg": "Buy an Egg",
"cycleShiny": ": » Shiny",
"cycleForm": ": » Forma",
"cycleGender": ": » Gênero",

View File

@ -17,5 +17,6 @@ export const egg: SimpleTranslationEntries = {
"notEnoughVouchers": "你没有足够的兑换券!",
"tooManyEggs": "你的蛋太多啦!",
"pull": "次",
"pulls": "次"
"pulls": "次",
"sameSpeciesEgg": "{{species}} will hatch from this egg!",
} as const;

View File

@ -31,6 +31,7 @@ export const starterSelectUiHandler: SimpleTranslationEntries = {
"selectMoveSwapWith": "选择要替换成的招式",
"unlockPassive": "解锁被动",
"reduceCost": "降低花费",
"sameSpeciesEgg": "Buy an Egg",
"cycleShiny": ": 闪光",
"cycleForm": ": 形态",
"cycleGender": ": 性别",

View File

@ -17,5 +17,6 @@ export const egg: SimpleTranslationEntries = {
"notEnoughVouchers": "你沒有足夠的兌換券!",
"tooManyEggs": "你的蛋太多啦!",
"pull": "抽",
"pulls": "抽"
"pulls": "抽",
"sameSpeciesEgg": "{{species}} will hatch from this egg!",
} as const;

View File

@ -32,6 +32,7 @@ export const starterSelectUiHandler: SimpleTranslationEntries = {
"selectMoveSwapWith": "選擇想要替換成的招式",
"unlockPassive": "解鎖被動",
"reduceCost": "降低花費",
"sameSpeciesEgg": "Buy an Egg",
"cycleShiny": ": 閃光",
"cycleForm": ": 形態",
"cycleGender": ": 性別",

View File

@ -9,6 +9,8 @@ import { PokeballType } from "./data/pokeball";
import { Gender } from "./data/gender";
import { StatusEffect } from "./data/status-effect";
import { modifierTypes } from "./modifier/modifier-type";
import { VariantTier } from "./enums/variant-tiers";
import { EggTier } from "#enums/egg-type";
import { allSpecies } from "./data/pokemon-species"; // eslint-disable-line @typescript-eslint/no-unused-vars
import { Abilities } from "#enums/abilities";
import { BerryType } from "#enums/berry-type";
@ -36,9 +38,9 @@ export const STARTING_BIOME_OVERRIDE: Biome = Biome.TOWN;
export const ARENA_TINT_OVERRIDE: TimeOfDay = null;
// Multiplies XP gained by this value including 0. Set to null to ignore the override
export const XP_MULTIPLIER_OVERRIDE: number = null;
export const IMMEDIATE_HATCH_EGGS_OVERRIDE: boolean = false;
// default 1000
export const STARTING_MONEY_OVERRIDE: integer = 0;
export const FREE_CANDY_UPGRADE_OVERRIDE: boolean = false;
export const POKEBALL_OVERRIDE: { active: boolean, pokeballs: PokeballCounts } = {
active: false,
pokeballs: {
@ -98,6 +100,17 @@ export const OPP_SHINY_OVERRIDE: boolean = false;
export const OPP_VARIANT_OVERRIDE: Variant = 0;
export const OPP_IVS_OVERRIDE: integer | integer[] = [];
/**
* EGG OVERRIDES
*/
export const EGG_IMMEDIATE_HATCH_OVERRIDE: boolean = false;
export const EGG_TIER_OVERRIDE: EggTier = null;
export const EGG_SHINY_OVERRIDE: boolean = false;
export const EGG_VARIANT_OVERRIDE: VariantTier = null;
export const EGG_FREE_GACHA_PULLS_OVERRIDE: boolean = false;
export const EGG_GACHA_PULL_COUNT_OVERRIDE: number = 0;
/**
* MODIFIER / ITEM OVERRIDES
* if count is not provided, it will default to 1

View File

@ -5256,7 +5256,7 @@ export class EggLapsePhase extends Phase {
super.start();
const eggsToHatch: Egg[] = this.scene.gameData.eggs.filter((egg: Egg) => {
return Overrides.IMMEDIATE_HATCH_EGGS_OVERRIDE ? true : --egg.hatchWaves < 1;
return Overrides.EGG_IMMEDIATE_HATCH_OVERRIDE ? true : --egg.hatchWaves < 1;
});
let eggCount: integer = eggsToHatch.length;

View File

@ -1,20 +1,43 @@
import { Egg, GachaType } from "../data/egg";
import { EggTier } from "#enums/egg-type";
import { Species } from "#enums/species";
import { VariantTier } from "#enums/variant-tiers";
import { EGG_SEED, Egg } from "../data/egg";
import { EggSourceType } from "#app/enums/egg-source-types.js";
export default class EggData {
public id: integer;
public gachaType: GachaType;
public tier: EggTier;
public sourceType: EggSourceType;
public hatchWaves: integer;
public timestamp: integer;
public variantTier: VariantTier;
public isShiny: boolean;
public species: Species;
public eggMoveIndex: number;
public overrideHiddenAbility: boolean;
constructor(source: Egg | any) {
const sourceEgg = source instanceof Egg ? source as Egg : null;
this.id = sourceEgg ? sourceEgg.id : source.id;
this.gachaType = sourceEgg ? sourceEgg.gachaType : source.gachaType;
this.tier = sourceEgg ? sourceEgg.tier : (source.tier ?? Math.floor(this.id / EGG_SEED));
this.sourceType = sourceEgg ? sourceEgg.sourceType : (source.gachaType ?? source.sourceType);
this.hatchWaves = sourceEgg ? sourceEgg.hatchWaves : source.hatchWaves;
this.timestamp = sourceEgg ? sourceEgg.timestamp : source.timestamp;
this.variantTier = sourceEgg ? sourceEgg.variantTier : source.variantTier;
this.isShiny = sourceEgg ? sourceEgg.isShiny : source.isShiny;
this.species = sourceEgg ? sourceEgg.species : source.species;
this.eggMoveIndex = sourceEgg ? sourceEgg.eggMoveIndex : source.eggMoveIndex;
this.overrideHiddenAbility = sourceEgg ? sourceEgg.overrideHiddenAbility : source.overrideHiddenAbility;
}
toEgg(): Egg {
return new Egg(this.id, this.gachaType, this.hatchWaves, this.timestamp);
// Species will be 0 if an old legacy is loaded from DB
if (!this.species) {
return new Egg({ id: this.id, hatchWaves: this.hatchWaves, sourceType: this.sourceType, timestamp: this.timestamp, tier: Math.floor(this.id / EGG_SEED) });
} else {
return new Egg({id: this.id, tier: this.tier, sourceType: this.sourceType, hatchWaves: this.hatchWaves,
timestamp: this.timestamp, variantTier: this.variantTier, isShiny: this.isShiny, species: this.species,
eggMoveIndex: this.eggMoveIndex, overrideHiddenAbility: this.overrideHiddenAbility });
}
}
}

View File

@ -1,17 +1,33 @@
import {beforeAll, describe, expect, it} from "vitest";
import {afterEach, beforeAll, beforeEach, describe, expect, it} from "vitest";
import BattleScene from "../../battle-scene";
import { getLegendaryGachaSpeciesForTimestamp } from "#app/data/egg.js";
import { Egg, getLegendaryGachaSpeciesForTimestamp } from "#app/data/egg.js";
import { Species } from "#enums/species";
import Phaser from "phaser";
import { EggSourceType } from "#app/enums/egg-source-types.js";
import { EggTier } from "#app/enums/egg-type.js";
import { VariantTier } from "#app/enums/variant-tiers.js";
import GameManager from "../utils/gameManager";
import EggData from "#app/system/egg-data.js";
describe("getLegendaryGachaSpeciesForTimestamp", () => {
describe("Egg Generation Tests", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
new Phaser.Game({
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(async() => {
game = new GameManager(phaserGame);
await game.importData("src/test/utils/saves/everything.prsv");
});
it("should return Arceus for the 10th of June", () => {
const scene = new BattleScene();
const timestamp = new Date(2024, 5, 10, 15, 0, 0, 0).getTime();
@ -30,4 +46,184 @@ describe("getLegendaryGachaSpeciesForTimestamp", () => {
expect(result).toBe(expectedSpecies);
});
it("should hatch an Arceus. Set from legendary gacha", async() => {
const scene = game.scene;
const timestamp = new Date(2024, 6, 10, 15, 0, 0, 0).getTime();
const expectedSpecies = Species.ARCEUS;
const result = new Egg({scene, timestamp, sourceType: EggSourceType.GACHA_LEGENDARY, tier: EggTier.MASTER}).generatePlayerPokemon(scene).species.speciesId;
expect(result).toBe(expectedSpecies);
});
it("should hatch an Arceus. Set from species", () => {
const scene = game.scene;
const expectedSpecies = Species.ARCEUS;
const result = new Egg({scene,species: expectedSpecies}).generatePlayerPokemon(scene).species.speciesId;
expect(result).toBe(expectedSpecies);
});
it("should return an common tier egg", () => {
const scene = game.scene;
const expectedTier = EggTier.COMMON;
const result = new Egg({scene, tier: expectedTier}).tier;
expect(result).toBe(expectedTier);
});
it("should return an rare tier egg", () => {
const scene = game.scene;
const expectedTier = EggTier.GREAT;
const result = new Egg({scene, tier: expectedTier}).tier;
expect(result).toBe(expectedTier);
});
it("should return an epic tier egg", () => {
const scene = game.scene;
const expectedTier = EggTier.ULTRA;
const result = new Egg({scene, tier: expectedTier}).tier;
expect(result).toBe(expectedTier);
});
it("should return an legendary tier egg", () => {
const scene = game.scene;
const expectedTier = EggTier.MASTER;
const result = new Egg({scene, tier: expectedTier}).tier;
expect(result).toBe(expectedTier);
});
it("should return a manaphy egg set via species", () => {
const scene = game.scene;
const expectedResult = true;
const result = new Egg({scene, species: Species.MANAPHY}).isManaphyEgg();
expect(result).toBe(expectedResult);
});
it("should return a manaphy egg set via id", () => {
const scene = game.scene;
const expectedResult = true;
const result = new Egg({scene, tier: EggTier.COMMON, id: 204}).isManaphyEgg();
expect(result).toBe(expectedResult);
});
it("should return an egg with 1000 hatch waves", () => {
const scene = game.scene;
const expectedHatchWaves = 1000;
const result = new Egg({scene, hatchWaves: expectedHatchWaves}).hatchWaves;
expect(result).toBe(expectedHatchWaves);
});
it("should return an shiny pokemon", () => {
const scene = game.scene;
const expectedResult = true;
const result = new Egg({scene, isShiny: expectedResult, species: Species.BULBASAUR}).generatePlayerPokemon(scene).isShiny();
expect(result).toBe(expectedResult);
});
it("should return a shiny common variant", () => {
const scene = game.scene;
const expectedVariantTier = VariantTier.COMMON;
const result = new Egg({scene, isShiny: true, variantTier: expectedVariantTier, species: Species.BULBASAUR}).generatePlayerPokemon(scene).variant;
expect(result).toBe(expectedVariantTier);
});
it("should return a shiny rare variant", () => {
const scene = game.scene;
const expectedVariantTier = VariantTier.RARE;
const result = new Egg({scene, isShiny: true, variantTier: expectedVariantTier, species: Species.BULBASAUR}).generatePlayerPokemon(scene).variant;
expect(result).toBe(expectedVariantTier);
});
it("should return a shiny epic variant", () => {
const scene = game.scene;
const expectedVariantTier = VariantTier.EPIC;
const result = new Egg({scene, isShiny: true, variantTier: expectedVariantTier, species: Species.BULBASAUR}).generatePlayerPokemon(scene).variant;
expect(result).toBe(expectedVariantTier);
});
it("should return an egg with an egg move index of 0, 1, 2 or 3", () => {
const scene = game.scene;
const eggMoveIndex = new Egg({scene}).eggMoveIndex;
const result = eggMoveIndex && eggMoveIndex >= 0 && eggMoveIndex <= 3;
expect(result).toBe(true);
});
it("should return an egg with an rare egg move. Egg move index should be 3", () => {
const scene = game.scene;
const expectedEggMoveIndex = 3;
const result = new Egg({scene, eggMoveIndex: expectedEggMoveIndex}).eggMoveIndex;
expect(result).toBe(expectedEggMoveIndex);
});
it("should return a hatched pokemon with a hidden ability", () => {
const scene = game.scene;
const playerPokemon = new Egg({scene, overrideHiddenAbility: true, species: Species.BULBASAUR}).generatePlayerPokemon(scene);
const expectedAbilityIndex = playerPokemon.species.ability2 ? 2 : 1;
const result = playerPokemon.abilityIndex;
expect(result).toBe(expectedAbilityIndex);
});
it("should add the egg to the game data", () => {
const scene = game.scene;
const expectedEggCount = 1;
new Egg({scene, sourceType: EggSourceType.GACHA_LEGENDARY, pulled: true});
const result = scene.gameData.eggs.length;
expect(result).toBe(expectedEggCount);
});
it("should override the egg tier to common", () => {
const scene = game.scene;
const expectedEggTier = EggTier.COMMON;
const result = new Egg({scene, tier: EggTier.MASTER, species: Species.BULBASAUR}).tier;
expect(result).toBe(expectedEggTier);
});
it("should override the egg hatch waves", () => {
const scene = game.scene;
const expectedHatchWaves = 10;
const result = new Egg({scene, tier: EggTier.MASTER, species: Species.BULBASAUR}).hatchWaves;
expect(result).toBe(expectedHatchWaves);
});
it("should correctly load a legacy egg", () => {
const legacyEgg = {
gachaType: 1,
hatchWaves: 25,
id: 2077000788,
timestamp: 1718908955085,
isShiny: false,
overrideHiddenAbility: false,
sourceType: 0,
species: 0,
tier: 0,
variantTier: 0,
eggMoveIndex: 0,
};
const result = new EggData(legacyEgg).toEgg();
expect(result.tier).toBe(EggTier.GREAT);
expect(result.id).toBe(legacyEgg.id);
expect(result.timestamp).toBe(legacyEgg.timestamp);
expect(result.hatchWaves).toBe(legacyEgg.hatchWaves);
expect(result.sourceType).toBe(legacyEgg.gachaType);
});
});

View File

@ -3,12 +3,14 @@ import { Mode } from "./ui";
import { TextStyle, addTextObject, getEggTierTextTint } from "./text";
import MessageUiHandler from "./message-ui-handler";
import * as Utils from "../utils";
import { EGG_SEED, Egg, GachaType, getEggTierDefaultHatchWaves, getEggDescriptor, getLegendaryGachaSpeciesForTimestamp } from "../data/egg";
import { Egg, getLegendaryGachaSpeciesForTimestamp, IEggOptions } from "../data/egg";
import { VoucherType, getVoucherTypeIcon } from "../system/voucher";
import { getPokemonSpecies } from "../data/pokemon-species";
import { addWindow } from "./ui-theme";
import { Tutorial, handleTutorial } from "../tutorial";
import {Button} from "#enums/buttons";
import * as Overrides from "../overrides";
import { GachaType } from "#app/enums/gacha-types";
import i18next from "i18next";
import { EggTier } from "#enums/egg-type";
@ -285,6 +287,10 @@ export default class EggGachaUiHandler extends MessageUiHandler {
}
pull(pullCount?: integer, count?: integer, eggs?: Egg[]): void {
if (Overrides.EGG_GACHA_PULL_COUNT_OVERRIDE && !count) {
pullCount = Overrides.EGG_GACHA_PULL_COUNT_OVERRIDE;
}
this.eggGachaOptionsContainer.setVisible(false);
this.setTransitioning(true);
@ -379,56 +385,24 @@ export default class EggGachaUiHandler extends MessageUiHandler {
}
if (!eggs) {
eggs = [];
const tierValueOffset = this.gachaCursor === GachaType.LEGENDARY ? 1 : 0;
const tiers = new Array(pullCount).fill(null).map(() => {
const tierValue = Utils.randInt(256);
return tierValue >= 52 + tierValueOffset ? EggTier.COMMON : tierValue >= 8 + tierValueOffset ? EggTier.GREAT : tierValue >= 1 + tierValueOffset ? EggTier.ULTRA : EggTier.MASTER;
});
if (pullCount >= 25 && !tiers.filter(t => t >= EggTier.ULTRA).length) {
tiers[Utils.randInt(tiers.length)] = EggTier.ULTRA;
} else if (pullCount >= 10 && !tiers.filter(t => t >= EggTier.GREAT).length) {
tiers[Utils.randInt(tiers.length)] = EggTier.GREAT;
}
for (let i = 0; i < pullCount; i++) {
this.scene.gameData.eggPity[EggTier.GREAT] += 1;
this.scene.gameData.eggPity[EggTier.ULTRA] += 1;
this.scene.gameData.eggPity[EggTier.MASTER] += 1 + tierValueOffset;
// These numbers are roughly the 80% mark. That is, 80% of the time you'll get an egg before this gets triggered.
if (this.scene.gameData.eggPity[EggTier.MASTER] >= 412 && tiers[i] === EggTier.COMMON) {
tiers[i] = EggTier.MASTER;
} else if (this.scene.gameData.eggPity[EggTier.ULTRA] >= 59 && tiers[i] === EggTier.COMMON) {
tiers[i] = EggTier.ULTRA;
} else if (this.scene.gameData.eggPity[EggTier.GREAT] >= 9 && tiers[i] === EggTier.COMMON) {
tiers[i] = EggTier.GREAT;
}
this.scene.gameData.eggPity[tiers[i]] = 0;
}
for (let i = 1; i <= pullCount; i++) {
const eggOptions: IEggOptions = { scene: this.scene, pulled: true, sourceType: this.gachaCursor };
const timestamp = new Date().getTime();
for (const tier of tiers) {
const eggId = Utils.randInt(EGG_SEED, EGG_SEED * tier);
const egg = new Egg(eggId, this.gachaCursor, getEggTierDefaultHatchWaves(tier), timestamp);
if (egg.isManaphyEgg()) {
this.scene.gameData.gameStats.manaphyEggsPulled++;
egg.hatchWaves = getEggTierDefaultHatchWaves(EggTier.ULTRA);
} else {
switch (tier) {
case EggTier.GREAT:
this.scene.gameData.gameStats.rareEggsPulled++;
break;
case EggTier.ULTRA:
this.scene.gameData.gameStats.epicEggsPulled++;
break;
case EggTier.MASTER:
this.scene.gameData.gameStats.legendaryEggsPulled++;
break;
// Before creating the last egg, check if the guaranteed egg tier was already generated
// if not, override the egg tier
if (i === pullCount) {
const guaranteedEggTier = this.getGuaranteedEggTierFromPullCount(pullCount);
if (!eggs.some(egg => egg.tier >= guaranteedEggTier)) {
eggOptions.tier = guaranteedEggTier;
}
}
const egg = new Egg(eggOptions);
eggs.push(egg);
this.scene.gameData.eggs.push(egg);
this.scene.gameData.gameStats.eggsPulled++;
}
// Shuffle the eggs in case the guaranteed one got added as last egg
eggs = Utils.randSeedShuffle<Egg>(eggs);
(this.scene.currentBattle ? this.scene.gameData.saveAll(this.scene, true, true, true) : this.scene.gameData.saveSystem()).then(success => {
if (!success) {
@ -442,6 +416,17 @@ export default class EggGachaUiHandler extends MessageUiHandler {
doPull();
}
getGuaranteedEggTierFromPullCount(pullCount: number): EggTier {
switch (pullCount) {
case 10:
return EggTier.GREAT;
case 25:
return EggTier.ULTRA;
default:
return EggTier.COMMON;
}
}
showSummary(eggs: Egg[]): void {
this.transitioning = false;
this.eggGachaSummaryContainer.setVisible(true);
@ -470,7 +455,7 @@ export default class EggGachaUiHandler extends MessageUiHandler {
const eggSprite = this.scene.add.sprite(0, 0, "egg", `egg_${egg.getKey()}`);
ret.add(eggSprite);
const eggText = addTextObject(this.scene, 0, 14, getEggDescriptor(egg), TextStyle.PARTY, { align: "center" });
const eggText = addTextObject(this.scene, 0, 14, egg.getEggDescriptor(), TextStyle.PARTY, { align: "center" });
eggText.setOrigin(0.5, 0);
eggText.setTint(getEggTierTextTint(!egg.isManaphyEgg() ? egg.tier : EggTier.ULTRA));
ret.add(eggText);
@ -586,11 +571,13 @@ export default class EggGachaUiHandler extends MessageUiHandler {
case Button.ACTION:
switch (this.cursor) {
case 0:
if (!this.scene.gameData.voucherCounts[VoucherType.REGULAR]) {
if (!this.scene.gameData.voucherCounts[VoucherType.REGULAR] && !Overrides.EGG_FREE_GACHA_PULLS_OVERRIDE) {
error = true;
this.showError(i18next.t("egg:notEnoughVouchers"));
} else if (this.scene.gameData.eggs.length < 99) {
this.consumeVouchers(VoucherType.REGULAR, 1);
if (!Overrides.EGG_FREE_GACHA_PULLS_OVERRIDE) {
this.consumeVouchers(VoucherType.REGULAR, 1);
}
this.pull();
success = true;
} else {
@ -599,11 +586,13 @@ export default class EggGachaUiHandler extends MessageUiHandler {
}
break;
case 2:
if (!this.scene.gameData.voucherCounts[VoucherType.PLUS]) {
if (!this.scene.gameData.voucherCounts[VoucherType.PLUS] && !Overrides.EGG_FREE_GACHA_PULLS_OVERRIDE) {
error = true;
this.showError(i18next.t("egg:notEnoughVouchers"));
} else if (this.scene.gameData.eggs.length < 95) {
this.consumeVouchers(VoucherType.PLUS, 1);
if (!Overrides.EGG_FREE_GACHA_PULLS_OVERRIDE) {
this.consumeVouchers(VoucherType.PLUS, 1);
}
this.pull(5);
success = true;
} else {
@ -613,15 +602,19 @@ export default class EggGachaUiHandler extends MessageUiHandler {
break;
case 1:
case 3:
if ((this.cursor === 1 && this.scene.gameData.voucherCounts[VoucherType.REGULAR] < 10)
|| (this.cursor === 3 && !this.scene.gameData.voucherCounts[VoucherType.PREMIUM])) {
if ((this.cursor === 1 && this.scene.gameData.voucherCounts[VoucherType.REGULAR] < 10 && !Overrides.EGG_FREE_GACHA_PULLS_OVERRIDE)
|| (this.cursor === 3 && !this.scene.gameData.voucherCounts[VoucherType.PREMIUM] && !Overrides.EGG_FREE_GACHA_PULLS_OVERRIDE)) {
error = true;
this.showError(i18next.t("egg:notEnoughVouchers"));
} else if (this.scene.gameData.eggs.length < 90) {
if (this.cursor === 3) {
this.consumeVouchers(VoucherType.PREMIUM, 1);
if (!Overrides.EGG_FREE_GACHA_PULLS_OVERRIDE) {
this.consumeVouchers(VoucherType.PREMIUM, 1);
}
} else {
this.consumeVouchers(VoucherType.REGULAR, 10);
if (!Overrides.EGG_FREE_GACHA_PULLS_OVERRIDE) {
this.consumeVouchers(VoucherType.REGULAR, 10);
}
}
this.pull(10);
success = true;
@ -631,11 +624,13 @@ export default class EggGachaUiHandler extends MessageUiHandler {
}
break;
case 4:
if (!this.scene.gameData.voucherCounts[VoucherType.GOLDEN]) {
if (!this.scene.gameData.voucherCounts[VoucherType.GOLDEN] && !Overrides.EGG_FREE_GACHA_PULLS_OVERRIDE) {
error = true;
this.showError(i18next.t("egg:notEnoughVouchers"));
} else if (this.scene.gameData.eggs.length < 75) {
this.consumeVouchers(VoucherType.GOLDEN, 1);
if (!Overrides.EGG_FREE_GACHA_PULLS_OVERRIDE) {
this.consumeVouchers(VoucherType.GOLDEN, 1);
}
this.pull(25);
success = true;
} else {

View File

@ -3,7 +3,7 @@ import { Mode } from "./ui";
import PokemonIconAnimHandler, { PokemonIconAnimMode } from "./pokemon-icon-anim-handler";
import { TextStyle, addTextObject } from "./text";
import MessageUiHandler from "./message-ui-handler";
import { Egg, getEggGachaTypeDescriptor, getEggHatchWavesMessage, getEggDescriptor } from "../data/egg";
import { Egg } from "../data/egg";
import { addWindow } from "./ui-theme";
import {Button} from "#enums/buttons";
import i18next from "i18next";
@ -163,7 +163,7 @@ export default class EggListUiHandler extends MessageUiHandler {
setEggDetails(egg: Egg): void {
this.eggSprite.setFrame(`egg_${egg.getKey()}`);
this.eggNameText.setText(`${i18next.t("egg:egg")} (${getEggDescriptor(egg)})`);
this.eggNameText.setText(`${i18next.t("egg:egg")} (${egg.getEggDescriptor()})`);
this.eggDateText.setText(
new Date(egg.timestamp).toLocaleString(undefined, {
weekday: "short",
@ -172,8 +172,8 @@ export default class EggListUiHandler extends MessageUiHandler {
day: "numeric"
})
);
this.eggHatchWavesText.setText(getEggHatchWavesMessage(egg.hatchWaves));
this.eggGachaInfoText.setText(getEggGachaTypeDescriptor(this.scene, egg));
this.eggHatchWavesText.setText(egg.getEggHatchWavesMessage());
this.eggGachaInfoText.setText(egg.getEggTypeDescriptor(this.scene));
}
setCursor(cursor: integer): boolean {

View File

@ -27,6 +27,8 @@ import { StatsContainer } from "./stats-container";
import { TextStyle, addBBCodeTextObject, addTextObject } from "./text";
import { Mode } from "./ui";
import { addWindow } from "./ui-theme";
import { Egg } from "#app/data/egg";
import * as Overrides from "../overrides";
import {SettingKeyboard} from "#app/system/settings/settings-keyboard";
import {Passive as PassiveAttr} from "#enums/passive";
import * as Challenge from "../data/challenge";
@ -36,6 +38,7 @@ import { Device } from "#enums/devices";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import {Button} from "#enums/buttons";
import { EggSourceType } from "#app/enums/egg-source-types.js";
export type StarterSelectCallback = (starters: Starter[]) => void;
@ -98,17 +101,17 @@ const languageSettings: { [key: string]: LanguageSetting } = {
}
};
const starterCandyCosts: { passive: integer, costReduction: [integer, integer] }[] = [
{ passive: 50, costReduction: [30, 75] }, // 1
{ passive: 45, costReduction: [25, 60] }, // 2
{ passive: 40, costReduction: [20, 50] }, // 3
{ passive: 30, costReduction: [15, 40] }, // 4
{ passive: 25, costReduction: [12, 35] }, // 5
{ passive: 20, costReduction: [10, 30] }, // 6
{ passive: 15, costReduction: [8, 20] }, // 7
{ passive: 10, costReduction: [5, 15] }, // 8
{ passive: 10, costReduction: [3, 10] }, // 9
{ passive: 10, costReduction: [3, 10] }, // 10
const starterCandyCosts: { passive: integer, costReduction: [integer, integer], egg: integer }[] = [
{ passive: 50, costReduction: [30, 75], egg: 35 }, // 1
{ passive: 45, costReduction: [25, 60], egg: 35 }, // 2
{ passive: 40, costReduction: [20, 50], egg: 35 }, // 3
{ passive: 30, costReduction: [15, 40], egg: 30 }, // 4
{ passive: 25, costReduction: [12, 35], egg: 25 }, // 5
{ passive: 20, costReduction: [10, 30], egg: 20 }, // 6
{ passive: 15, costReduction: [8, 20], egg: 15 }, // 7
{ passive: 10, costReduction: [5, 15], egg: 10 }, // 8
{ passive: 10, costReduction: [3, 10], egg: 10 }, // 9
{ passive: 10, costReduction: [3, 10], egg: 10 }, // 10
];
function getPassiveCandyCount(baseValue: integer): integer {
@ -119,6 +122,10 @@ function getValueReductionCandyCounts(baseValue: integer): [integer, integer] {
return starterCandyCosts[baseValue - 1].costReduction;
}
function getSameSpeciesEggCandyCounts(baseValue: integer): integer {
return starterCandyCosts[baseValue - 1].egg;
}
/**
* Calculates the icon position for a Pokemon of a given UI index
* @param index UI index to calculate the icon position of
@ -880,6 +887,18 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
&& starterData.valueReduction < 2;
}
/**
* Determines if an same species egg can be baught for the given species ID
* @param speciesId The ID of the species to check the value reduction of
* @returns true if the user has enough candies
*/
isSameSpeciesEggAvailable(speciesId: number): boolean {
// Get this species ID's starter data
const starterData = this.scene.gameData.starterData[speciesId];
return starterData.candyCount >= getSameSpeciesEggCandyCounts(speciesStarters[speciesId]);
}
/**
* Sets a bounce animation if enabled and the Pokemon has an upgrade
* @param icon {@linkcode Phaser.GameObjects.GameObject} to animate
@ -1311,9 +1330,11 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
options.push({
label: `x${passiveCost} ${i18next.t("starterSelectUiHandler:unlockPassive")} (${allAbilities[starterPassiveAbilities[this.lastSpecies.speciesId]].name})`,
handler: () => {
if (candyCount >= passiveCost) {
if (Overrides.FREE_CANDY_UPGRADE_OVERRIDE || candyCount >= passiveCost) {
starterData.passiveAttr |= PassiveAttr.UNLOCKED | PassiveAttr.ENABLED;
starterData.candyCount -= passiveCost;
if (!Overrides.FREE_CANDY_UPGRADE_OVERRIDE) {
starterData.candyCount -= passiveCost;
}
this.pokemonCandyCountText.setText(`x${starterData.candyCount}`);
this.scene.gameData.saveSystem().then(success => {
if (!success) {
@ -1346,9 +1367,11 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
options.push({
label: `x${reductionCost} ${i18next.t("starterSelectUiHandler:reduceCost")}`,
handler: () => {
if (candyCount >= reductionCost) {
if (Overrides.FREE_CANDY_UPGRADE_OVERRIDE || candyCount >= reductionCost) {
starterData.valueReduction++;
starterData.candyCount -= reductionCost;
if (!Overrides.FREE_CANDY_UPGRADE_OVERRIDE) {
starterData.candyCount -= reductionCost;
}
this.pokemonCandyCountText.setText(`x${starterData.candyCount}`);
this.scene.gameData.saveSystem().then(success => {
if (!success) {
@ -1379,6 +1402,49 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
itemArgs: starterColors[this.lastSpecies.speciesId]
});
}
// Same species egg menu option. Only visible if passive is bought
if (passiveAttr & PassiveAttr.UNLOCKED) {
const sameSpeciesEggCost = getSameSpeciesEggCandyCounts(speciesStarters[this.lastSpecies.speciesId]);
options.push({
label: `x${sameSpeciesEggCost} ${i18next.t("starterSelectUiHandler:sameSpeciesEgg")}`,
handler: () => {
if (this.scene.gameData.eggs.length < 99 && (Overrides.FREE_CANDY_UPGRADE_OVERRIDE || candyCount >= sameSpeciesEggCost)) {
if (!Overrides.FREE_CANDY_UPGRADE_OVERRIDE) {
starterData.candyCount -= sameSpeciesEggCost;
}
this.pokemonCandyCountText.setText(`x${starterData.candyCount}`);
this.scene.gameData.saveSystem().then(success => {
if (!success) {
return this.scene.reset(true);
}
});
const egg = new Egg({scene: this.scene, species: this.lastSpecies.speciesId, sourceType: EggSourceType.SAME_SPECIES_EGG});
egg.addEggToGameData(this.scene);
ui.setMode(Mode.STARTER_SELECT);
this.scene.playSound("buy");
// If the notification setting is set to 'On', update the candy upgrade display
// if (this.scene.candyUpgradeNotification === 2) {
// if (this.isUpgradeIconEnabled() ) {
// this.setUpgradeIcon(this.cursor);
// }
// if (this.isUpgradeAnimationEnabled()) {
// const genSpecies = this.genSpecies[this.lastSpecies.generation - 1];
// this.setUpgradeAnimation(this.starterSelectGenIconContainers[this.lastSpecies.generation - 1].getAt(genSpecies.indexOf(this.lastSpecies)), this.lastSpecies, true);
// }
// }
return true;
}
return false;
},
item: "candy",
itemArgs: starterColors[this.lastSpecies.speciesId]
});
}
options.push({
label: i18next.t("menu:cancel"),
handler: () => {