Merge branch 'beta' into refactor-move-phase

This commit is contained in:
NightKev 2024-09-07 01:02:44 -07:00
commit b9f7f1a5b5
19 changed files with 733 additions and 328 deletions

View File

@ -15,8 +15,8 @@ on:
types: [checks_requested]
jobs:
run-tests: # Define a job named "run-tests"
name: Run tests # Human-readable name for the job
run-misc-tests: # Define a job named "run-tests"
name: Run misc tests # Human-readable name for the job
runs-on: ubuntu-latest # Specify the latest Ubuntu runner for the job
steps:
@ -31,5 +31,75 @@ jobs:
- name: Install Node.js dependencies # Step to install Node.js dependencies
run: npm ci # Use 'npm ci' to install dependencies
- name: tests # Step to run tests
run: npm run test:silent
- name: pre-test # pre-test to check overrides
run: npx vitest run --project pre
- name: test misc
run: npx vitest --project misc
run-abilities-tests:
name: Run abilities tests
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install Node.js dependencies
run: npm ci
- name: pre-test
run: npx vitest run --project pre
- name: test abilities
run: npx vitest --project abilities
run-items-tests:
name: Run items tests
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install Node.js dependencies
run: npm ci
- name: pre-test
run: npx vitest run --project pre
- name: test items
run: npx vitest --project items
run-moves-tests:
name: Run moves tests
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install Node.js dependencies
run: npm ci
- name: pre-test
run: npx vitest run --project pre
- name: test moves
run: npx vitest --project moves
run-battle-tests:
name: Run battle tests
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install Node.js dependencies
run: npm ci
- name: pre-test
run: npx vitest run --project pre
- name: test battle
run: npx vitest --project battle

View File

@ -4,7 +4,8 @@ import { fileURLToPath } from 'url';
/**
* This script creates a test boilerplate file for a move or ability.
* @param {string} type - The type of test to create. Either "move" or "ability".
* @param {string} type - The type of test to create. Either "move", "ability",
* or "item".
* @param {string} fileName - The name of the file to create.
* @example npm run create-test move tackle
*/
@ -19,7 +20,7 @@ const type = args[0]; // "move" or "ability"
let fileName = args[1]; // The file name
if (!type || !fileName) {
console.error('Please provide both a type ("move" or "ability") and a file name.');
console.error('Please provide both a type ("move", "ability", or "item") and a file name.');
process.exit(1);
}
@ -40,8 +41,11 @@ if (type === 'move') {
} else if (type === 'ability') {
dir = path.join(__dirname, 'src', 'test', 'abilities');
description = `Abilities - ${formattedName}`;
} else if (type === "item") {
dir = path.join(__dirname, 'src', 'test', 'items');
description = `Items - ${formattedName}`;
} else {
console.error('Invalid type. Please use "move" or "ability".');
console.error('Invalid type. Please use "move", "ability", or "item".');
process.exit(1);
}
@ -98,4 +102,4 @@ describe("${description}", () => {
// Write the template content to the file
fs.writeFileSync(filePath, content, 'utf8');
console.log(`File created at: ${filePath}`);
console.log(`File created at: ${filePath}`);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -47,10 +47,14 @@
"description": "Changes a Pokémon's nature to {{natureName}} and permanently unlocks the nature for the starter."
},
"DoubleBattleChanceBoosterModifierType": {
"description": "Doubles the chance of an encounter being a double battle for {{battleCount}} battles."
"description": "Quadruples the chance of an encounter being a double battle for up to {{battleCount}} battles."
},
"TempStatStageBoosterModifierType": {
"description": "Increases the {{stat}} of all party members by 1 stage for 5 battles."
"description": "Increases the {{stat}} of all party members by {{amount}} for up to 5 battles.",
"extra": {
"stage": "1 stage",
"percentage": "30%"
}
},
"AttackTypeBoosterModifierType": {
"description": "Increases the power of a Pokémon's {{moveType}}-type moves by 20%."

View File

@ -1,150 +1,150 @@
{
"music": "Music: ",
"music": "BGM: ",
"missing_entries": "{{name}}",
"battle_kanto_champion": "B2W2 Kanto Champion Battle",
"battle_johto_champion": "B2W2 Johto Champion Battle",
"battle_hoenn_champion_g5": "B2W2 Hoenn Champion Battle",
"battle_hoenn_champion_g6": "ORAS Hoenn Champion Battle",
"battle_sinnoh_champion": "B2W2 Sinnoh Champion Battle",
"battle_champion_alder": "BW Unova Champion Battle",
"battle_champion_iris": "B2W2 Unova Champion Battle",
"battle_kalos_champion": "XY Kalos Champion Battle",
"battle_alola_champion": "USUM Alola Champion Battle",
"battle_galar_champion": "SWSH Galar Champion Battle",
"battle_champion_geeta": "SV Champion Geeta Battle",
"battle_champion_nemona": "SV Champion Nemona Battle",
"battle_champion_kieran": "SV Champion Kieran Battle",
"battle_hoenn_elite": "ORAS Elite Four Battle",
"battle_unova_elite": "BW Elite Four Battle",
"battle_kalos_elite": "XY Elite Four Battle",
"battle_alola_elite": "SM Elite Four Battle",
"battle_galar_elite": "SWSH League Tournament Battle",
"battle_paldea_elite": "SV Elite Four Battle",
"battle_bb_elite": "SV BB League Elite Four Battle",
"battle_final_encounter": "PMD RTDX Rayquaza's Domain",
"battle_final": "BW Ghetsis Battle",
"battle_kanto_gym": "B2W2 Kanto Gym Battle",
"battle_johto_gym": "B2W2 Johto Gym Battle",
"battle_hoenn_gym": "B2W2 Hoenn Gym Battle",
"battle_sinnoh_gym": "B2W2 Sinnoh Gym Battle",
"battle_unova_gym": "BW Unova Gym Battle",
"battle_kalos_gym": "XY Kalos Gym Battle",
"battle_galar_gym": "SWSH Galar Gym Battle",
"battle_paldea_gym": "SV Paldea Gym Battle",
"battle_legendary_kanto": "XY Kanto Legendary Battle",
"battle_legendary_raikou": "HGSS Raikou Battle",
"battle_legendary_entei": "HGSS Entei Battle",
"battle_legendary_suicune": "HGSS Suicune Battle",
"battle_legendary_lugia": "HGSS Lugia Battle",
"battle_legendary_ho_oh": "HGSS Ho-oh Battle",
"battle_legendary_regis_g5": "B2W2 Legendary Titan Battle",
"battle_legendary_regis_g6": "ORAS Legendary Titan Battle",
"battle_legendary_gro_kyo": "ORAS Groudon & Kyogre Battle",
"battle_legendary_rayquaza": "ORAS Rayquaza Battle",
"battle_legendary_deoxys": "ORAS Deoxys Battle",
"battle_legendary_lake_trio": "ORAS Lake Guardians Battle",
"battle_legendary_sinnoh": "ORAS Sinnoh Legendary Battle",
"battle_legendary_dia_pal": "ORAS Dialga & Palkia Battle",
"battle_legendary_origin_forme": "LA Origin Dialga & Palkia Battle",
"battle_legendary_giratina": "ORAS Giratina Battle",
"battle_legendary_arceus": "HGSS Arceus Battle",
"battle_legendary_unova": "BW Unova Legendary Battle",
"battle_legendary_kyurem": "BW Kyurem Battle",
"battle_legendary_res_zek": "BW Reshiram & Zekrom Battle",
"battle_legendary_xern_yvel": "XY Xerneas & Yveltal Battle",
"battle_legendary_tapu": "SM Tapu Battle",
"battle_legendary_sol_lun": "SM Solgaleo & Lunala Battle",
"battle_legendary_ub": "SM Ultra Beast Battle",
"battle_legendary_dusk_dawn": "USUM Dusk Mane & Dawn Wings Necrozma Battle",
"battle_legendary_ultra_nec": "USUM Ultra Necrozma Battle",
"battle_legendary_zac_zam": "SWSH Zacian & Zamazenta Battle",
"battle_legendary_glas_spec": "SWSH Glastrier & Spectrier Battle",
"battle_legendary_calyrex": "SWSH Calyrex Battle",
"battle_legendary_riders": "SWSH Ice & Shadow Rider Calyrex Battle",
"battle_legendary_birds_galar": "SWSH Galarian Legendary Birds Battle",
"battle_legendary_ruinous": "SV Treasures of Ruin Battle",
"battle_legendary_kor_mir": "SV Depths of Area Zero Battle",
"battle_legendary_loyal_three": "SV Loyal Three Battle",
"battle_legendary_ogerpon": "SV Ogerpon Battle",
"battle_legendary_terapagos": "SV Terapagos Battle",
"battle_legendary_pecharunt": "SV Pecharunt Battle",
"battle_rival": "BW Rival Battle",
"battle_rival_2": "BW N Battle",
"battle_rival_3": "BW Final N Battle",
"battle_trainer": "BW Trainer Battle",
"battle_wild": "BW Wild Battle",
"battle_wild_strong": "BW Strong Wild Battle",
"end_summit": "PMD RTDX Sky Tower Summit",
"battle_rocket_grunt": "HGSS Team Rocket Battle",
"battle_aqua_magma_grunt": "ORAS Team Aqua & Magma Battle",
"battle_galactic_grunt": "BDSP Team Galactic Battle",
"battle_plasma_grunt": "BW Team Plasma Battle",
"battle_flare_grunt": "XY Team Flare Battle",
"battle_aether_grunt": "SM Aether Foundation Battle",
"battle_skull_grunt": "SM Team Skull Battle",
"battle_macro_grunt": "SWSH Trainer Battle",
"battle_galactic_admin": "BDSP Team Galactic Admin Battle",
"battle_skull_admin": "SM Team Skull Admin Battle",
"battle_oleana": "SWSH Oleana Battle",
"battle_rocket_boss": "USUM Giovanni Battle",
"battle_aqua_magma_boss": "ORAS Archie & Maxie Battle",
"battle_galactic_boss": "BDSP Cyrus Battle",
"battle_plasma_boss": "B2W2 Ghetsis Battle",
"battle_flare_boss": "XY Lysandre Battle",
"battle_aether_boss": "SM Lusamine Battle",
"battle_skull_boss": "SM Guzma Battle",
"battle_macro_boss": "SWSH Rose Battle",
"battle_kanto_champion": "B2W2 戦闘!チャンピオン(カントー)",
"battle_johto_champion": "B2W2 戦闘!チャンピオン(ジョウト)",
"battle_hoenn_champion_g5": "B2W2 戦闘!チャンピオン(ホウエン)",
"battle_hoenn_champion_g6": "ORAS 決戦!ダイゴ",
"battle_sinnoh_champion": "B2W2 戦闘!チャンピオン(シンオウ)",
"battle_champion_alder": "BW チャンピオン アデク",
"battle_champion_iris": "B2W2 戦闘!チャンピオンアイリス",
"battle_kalos_champion": "XY 戦闘!チャンピオン",
"battle_alola_champion": "USUM 頂上決戦!ハウ",
"battle_galar_champion": "SWSH 決戦!チャンピオンダンデ",
"battle_champion_geeta": "SV 戦闘!トップチャンピオン",
"battle_champion_nemona": "SV 戦闘!チャンピオンネモ",
"battle_champion_kieran": "SV 戦闘!チャンピオンスグリ",
"battle_hoenn_elite": "ORAS 戦闘!四天王",
"battle_unova_elite": "BW 戦闘!四天王",
"battle_kalos_elite": "XY 戦闘!四天王",
"battle_alola_elite": "SM 戦闘!四天王",
"battle_galar_elite": "SWSH 戦闘!ファイナルトーナメント!",
"battle_paldea_elite": "SV 戦闘!四天王",
"battle_bb_elite": "SV 戦闘!ブルベリーグ四天王",
"battle_final_encounter": "ポケダンDX レックウザ登場",
"battle_final": "BW 戦闘!ゲーチス",
"battle_kanto_gym": "B2W2 戦闘!ジムリーダー(カントー)",
"battle_johto_gym": "B2W2 戦闘!ジムリーダー(ジョウト)",
"battle_hoenn_gym": "B2W2 戦闘!ジムリーダー(ホウエン)",
"battle_sinnoh_gym": "B2W2 戦闘!ジムリーダー(シンオウ)",
"battle_unova_gym": "BW 戦闘!ジムリーダー",
"battle_kalos_gym": "XY 戦闘!ジムリーダー",
"battle_galar_gym": "SWSH 戦闘!ジムリーダー",
"battle_paldea_gym": "SV 戦闘!ジムリーダー",
"battle_legendary_kanto": "XY 戦闘!ミュウツー",
"battle_legendary_raikou": "HGSS 戦闘!ライコウ",
"battle_legendary_entei": "HGSS 戦闘!エンテイ",
"battle_legendary_suicune": "HGSS 戦闘!スイクン",
"battle_legendary_lugia": "HGSS 戦闘!ルギア",
"battle_legendary_ho_oh": "HGSS 戦闘!ホウオウ",
"battle_legendary_regis_g5": "B2W2 戦闘!レジロック・レジアイス・レジスチル",
"battle_legendary_regis_g6": "ORAS 戦闘!レジロック・レジアイス・レジスチル",
"battle_legendary_gro_kyo": "ORAS 戦闘!ゲンシカイキ",
"battle_legendary_rayquaza": "ORAS 戦闘!超古代ポケモン",
"battle_legendary_deoxys": "ORAS 戦闘!デオキシス",
"battle_legendary_lake_trio": "ORAS 戦闘!ユクシー・エムリット・アグノム",
"battle_legendary_sinnoh": "ORAS 戦闘!伝説のポケモン(シンオウ)",
"battle_legendary_dia_pal": "ORAS 戦闘!ディアルガ・パルキア",
"battle_legendary_origin_forme": "LA 戦い:ディアルガ・パルキア(オリジンフォルム)",
"battle_legendary_giratina": "ORAS 戦闘!ギラティナ",
"battle_legendary_arceus": "HGSS アルセウス",
"battle_legendary_unova": "BW 戦闘!伝説のポケモン",
"battle_legendary_kyurem": "BW 戦闘!キュレム",
"battle_legendary_res_zek": "BW 戦闘!ゼクロム・レシラム",
"battle_legendary_xern_yvel": "XY 戦闘!ゼルネアス・イベルタル・ジガルデ",
"battle_legendary_tapu": "SM 戦闘!カプ",
"battle_legendary_sol_lun": "SM 戦闘!ソルガレオ・ルナアーラ",
"battle_legendary_ub": "SM 戦闘!ウルトラビースト",
"battle_legendary_dusk_dawn": "USUM 戦闘!日食・月食ネクロズマ",
"battle_legendary_ultra_nec": "USUM 戦闘!ウルトラネクロズマ",
"battle_legendary_zac_zam": "SWSH 戦闘!ザシアン・ザマゼンタ",
"battle_legendary_glas_spec": "SWSH 戦闘!ブリザポス・レイスポス",
"battle_legendary_calyrex": "SWSH 戦闘!バドレックス",
"battle_legendary_riders": "SWSH 戦闘!豊穣の王",
"battle_legendary_birds_galar": "SWSH 戦闘!伝説のとりポケモン",
"battle_legendary_ruinous": "SV 戦闘!災厄ポケモン",
"battle_legendary_kor_mir": "SV 戦闘!エリアゼロのポケモン",
"battle_legendary_loyal_three": "SV 戦闘!ともっこ",
"battle_legendary_ogerpon": "SV 戦闘!オーガポン",
"battle_legendary_terapagos": "SV 戦闘!テラパゴス",
"battle_legendary_pecharunt": "SV 戦闘!モモワロウ",
"battle_rival": "BW 戦闘!チェレン・ベル",
"battle_rival_2": "BW 戦闘!N",
"battle_rival_3": "BW 決戦N",
"battle_trainer": "BW 戦闘!トレーナー",
"battle_wild": "BW 戦闘!野生ポケモン",
"battle_wild_strong": "BW 戦闘!強い野生ポケモン",
"end_summit": "ポケダンDX 天空の塔 最上階",
"battle_rocket_grunt": "HGSS 戦闘!ロケット団",
"battle_aqua_magma_grunt": "ORAS 戦闘!アクア団・マグマ団",
"battle_galactic_grunt": "BDSP 戦闘!ギンガ団",
"battle_plasma_grunt": "BW 戦闘!プラズマ団",
"battle_flare_grunt": "XY 戦闘!フレア団",
"battle_aether_grunt": "SM 戦闘!エーテル財団トレーナー",
"battle_skull_grunt": "SM 戦闘!スカル団",
"battle_macro_grunt": "SWSH 戦闘!トレーナー",
"battle_galactic_admin": "BDSP 戦闘!ギンガ団幹部",
"battle_skull_admin": "SM 戦闘!スカル団幹部",
"battle_oleana": "SWSH 戦闘!オリーヴ",
"battle_rocket_boss": "USUM 戦闘!レインボーロケット団ボス",
"battle_aqua_magma_boss": "ORAS 戦闘!アクア団・マグマ団のリーダー",
"battle_galactic_boss": "BDSP 戦闘!ギンガ団ボス",
"battle_plasma_boss": "B2W2 戦闘!ゲーチス",
"battle_flare_boss": "XY 戦闘!フラダリ",
"battle_aether_boss": "SM 戦闘!ルザミーネ",
"battle_skull_boss": "SM 戦闘!スカル団ボス",
"battle_macro_boss": "SWSH 戦闘!ローズ",
"abyss": "PMD EoS Dark Crater",
"badlands": "PMD EoS Barren Valley",
"beach": "PMD EoS Drenched Bluff",
"cave": "PMD EoS Sky Peak Cave",
"construction_site": "PMD EoS Boulder Quarry",
"desert": "PMD EoS Northern Desert",
"dojo": "PMD EoS Marowak Dojo",
"end": "PMD RTDX Sky Tower",
"factory": "PMD EoS Concealed Ruins",
"fairy_cave": "PMD EoS Star Cave",
"forest": "PMD EoS Dusk Forest",
"grass": "PMD EoS Apple Woods",
"graveyard": "PMD EoS Mystifying Forest",
"ice_cave": "PMD EoS Vast Ice Mountain",
"island": "PMD EoS Craggy Coast",
"jungle": "Lmz - Jungle",
"laboratory": "Firel - Laboratory",
"lake": "PMD EoS Crystal Cave",
"meadow": "PMD EoS Sky Peak Forest",
"metropolis": "Firel - Metropolis",
"mountain": "PMD EoS Mt. Horn",
"plains": "PMD EoS Sky Peak Prairie",
"power_plant": "PMD EoS Far Amp Plains",
"ruins": "PMD EoS Deep Sealed Ruin",
"sea": "Andr06 - Marine Mystique",
"seabed": "Firel - Seabed",
"slum": "Andr06 - Sneaky Snom",
"snowy_forest": "PMD EoS Sky Peak Snowfield",
"space": "Firel - Aether",
"swamp": "PMD EoS Surrounded Sea",
"tall_grass": "PMD EoS Foggy Forest",
"temple": "PMD EoS Aegis Cave",
"town": "PMD EoS Random Dungeon Theme 3",
"volcano": "PMD EoS Steam Cave",
"wasteland": "PMD EoS Hidden Highland",
"encounter_ace_trainer": "BW Trainers' Eyes Meet (Ace Trainer)",
"encounter_backpacker": "BW Trainers' Eyes Meet (Backpacker)",
"encounter_clerk": "BW Trainers' Eyes Meet (Clerk)",
"encounter_cyclist": "BW Trainers' Eyes Meet (Cyclist)",
"encounter_lass": "BW Trainers' Eyes Meet (Lass)",
"encounter_parasol_lady": "BW Trainers' Eyes Meet (Parasol Lady)",
"encounter_pokefan": "BW Trainers' Eyes Meet (Poke Fan)",
"encounter_psychic": "BW Trainers' Eyes Meet (Psychic)",
"encounter_rich": "BW Trainers' Eyes Meet (Gentleman)",
"encounter_rival": "BW Cheren",
"encounter_roughneck": "BW Trainers' Eyes Meet (Roughneck)",
"encounter_scientist": "BW Trainers' Eyes Meet (Scientist)",
"encounter_twins": "BW Trainers' Eyes Meet (Twins)",
"encounter_youngster": "BW Trainers' Eyes Meet (Youngster)",
"heal": "BW Pokémon Heal",
"menu": "PMD EoS Welcome to the World of Pokémon!",
"title": "PMD EoS Top Menu Theme"
"abyss": "ポケダン空 やみのかこう",
"badlands": "ポケダン空 こかつのたに",
"beach": "ポケダン空 しめったいわば",
"cave": "ポケダン空 そらのいただき(どうくつ)",
"construction_site": "ポケダン空 きょだいがんせきぐん",
"desert": "ポケダン空 きたのさばく",
"dojo": "ポケダン空 ガラガラどうじょう",
"end": "ポケダンDX 天空の塔",
"factory": "ポケダン空 かくされたいせき",
"fairy_cave": "ポケダン空 ほしのどうくつ",
"forest": "ポケダン空 くろのもり",
"grass": "ポケダン空 リンゴのもり",
"graveyard": "ポケダン空 しんぴのもり",
"ice_cave": "ポケダン空 だいひょうざん",
"island": "ポケダン空 えんがんのいわば",
"jungle": "Lmz - Jungle(ジャングル)",
"laboratory": "Firel - Laboratory(ラボラトリー)",
"lake": "ポケダン空 すいしょうのどうくつ",
"meadow": "ポケダン空 そらのいただき(もり)",
"metropolis": "Firel - Metropolis(大都市)",
"mountain": "ポケダン空 ツノやま",
"plains": "ポケダン空 そらのいただき(そうげん)",
"power_plant": "ポケダン空 エレキへいげん",
"ruins": "ポケダン空 ふういんのいわば",
"sea": "Andr06 - Marine Mystique(海の神秘性)",
"seabed": "Firel - Seabed(海底)",
"slum": "Andr06 - Sneaky Snom(ずるいユキハミ)",
"snowy_forest": "ポケダン空 そらのいただき(ゆきやま)",
"space": "Firel - Aether(エーテル)",
"swamp": "ポケダン空 とざされたうみ",
"tall_grass": "ポケダン空 のうむのもり",
"temple": "ポケダン空 ばんにんのどうくつ",
"town": "ポケダン空 ランダムダンジョン3",
"volcano": "ポケダン空 ねっすいのどうくつ",
"wasteland": "ポケダン空 まぼろしのだいち",
"encounter_ace_trainer": "BW 視線!エリートトレーナー",
"encounter_backpacker": "BW 視線!バックパッカー",
"encounter_clerk": "BW 視線!ビジネスマン",
"encounter_cyclist": "BW 視線!サイクリング",
"encounter_lass": "BW 視線!ミニスカート",
"encounter_parasol_lady": "BW 視線!パラソルおねえさん",
"encounter_pokefan": "BW 視線!だいすきクラブ",
"encounter_psychic": "BW 視線!サイキッカー",
"encounter_rich": "BW 視線!ジェントルマン",
"encounter_rival": "BW チェレンのテーマ",
"encounter_roughneck": "BW 視線!スキンヘッズ",
"encounter_scientist": "BW 視線!けんきゅういん",
"encounter_twins": "BW 視線!ふたごちゃん",
"encounter_youngster": "BW 視線!たんぱんこぞう",
"heal": "BW 回復",
"menu": "ポケダン空 ようこそ! ポケモンたちのせかいへ!",
"title": "ポケダン空 トップメニュー"
}

View File

@ -433,37 +433,44 @@ export class RememberMoveModifierType extends PokemonModifierType {
}
export class DoubleBattleChanceBoosterModifierType extends ModifierType {
public battleCount: integer;
private maxBattles: number;
constructor(localeKey: string, iconImage: string, battleCount: integer) {
super(localeKey, iconImage, (_type, _args) => new Modifiers.DoubleBattleChanceBoosterModifier(this, this.battleCount), "lure");
constructor(localeKey: string, iconImage: string, maxBattles: number) {
super(localeKey, iconImage, (_type, _args) => new Modifiers.DoubleBattleChanceBoosterModifier(this, maxBattles), "lure");
this.battleCount = battleCount;
this.maxBattles = maxBattles;
}
getDescription(scene: BattleScene): string {
return i18next.t("modifierType:ModifierType.DoubleBattleChanceBoosterModifierType.description", { battleCount: this.battleCount });
getDescription(_scene: BattleScene): string {
return i18next.t("modifierType:ModifierType.DoubleBattleChanceBoosterModifierType.description", {
battleCount: this.maxBattles
});
}
}
export class TempStatStageBoosterModifierType extends ModifierType implements GeneratedPersistentModifierType {
private stat: TempBattleStat;
private key: string;
private nameKey: string;
private quantityKey: string;
constructor(stat: TempBattleStat) {
const key = TempStatStageBoosterModifierTypeGenerator.items[stat];
super("", key, (_type, _args) => new Modifiers.TempStatStageBoosterModifier(this, this.stat));
const nameKey = TempStatStageBoosterModifierTypeGenerator.items[stat];
super("", nameKey, (_type, _args) => new Modifiers.TempStatStageBoosterModifier(this, this.stat, 5));
this.stat = stat;
this.key = key;
this.nameKey = nameKey;
this.quantityKey = (stat !== Stat.ACC) ? "percentage" : "stage";
}
get name(): string {
return i18next.t(`modifierType:TempStatStageBoosterItem.${this.key}`);
return i18next.t(`modifierType:TempStatStageBoosterItem.${this.nameKey}`);
}
getDescription(_scene: BattleScene): string {
return i18next.t("modifierType:ModifierType.TempStatStageBoosterModifierType.description", { stat: i18next.t(getStatKey(this.stat)) });
return i18next.t("modifierType:ModifierType.TempStatStageBoosterModifierType.description", {
stat: i18next.t(getStatKey(this.stat)),
amount: i18next.t(`modifierType:ModifierType.TempStatStageBoosterModifierType.extra.${this.quantityKey}`)
});
}
getPregenArgs(): any[] {
@ -1348,9 +1355,9 @@ export const modifierTypes = {
SUPER_REPEL: () => new DoubleBattleChanceBoosterModifierType('Super Repel', 10),
MAX_REPEL: () => new DoubleBattleChanceBoosterModifierType('Max Repel', 25),*/
LURE: () => new DoubleBattleChanceBoosterModifierType("modifierType:ModifierType.LURE", "lure", 5),
SUPER_LURE: () => new DoubleBattleChanceBoosterModifierType("modifierType:ModifierType.SUPER_LURE", "super_lure", 10),
MAX_LURE: () => new DoubleBattleChanceBoosterModifierType("modifierType:ModifierType.MAX_LURE", "max_lure", 25),
LURE: () => new DoubleBattleChanceBoosterModifierType("modifierType:ModifierType.LURE", "lure", 10),
SUPER_LURE: () => new DoubleBattleChanceBoosterModifierType("modifierType:ModifierType.SUPER_LURE", "super_lure", 15),
MAX_LURE: () => new DoubleBattleChanceBoosterModifierType("modifierType:ModifierType.MAX_LURE", "max_lure", 30),
SPECIES_STAT_BOOSTER: () => new SpeciesStatBoosterModifierTypeGenerator(),
@ -1358,9 +1365,12 @@ export const modifierTypes = {
DIRE_HIT: () => new class extends ModifierType {
getDescription(_scene: BattleScene): string {
return i18next.t("modifierType:ModifierType.TempStatStageBoosterModifierType.description", { stat: i18next.t("modifierType:ModifierType.DIRE_HIT.extra.raises") });
return i18next.t("modifierType:ModifierType.TempStatStageBoosterModifierType.description", {
stat: i18next.t("modifierType:ModifierType.DIRE_HIT.extra.raises"),
amount: i18next.t("modifierType:ModifierType.TempStatStageBoosterModifierType.extra.stage")
});
}
}("modifierType:ModifierType.DIRE_HIT", "dire_hit", (type, _args) => new Modifiers.TempCritBoosterModifier(type)),
}("modifierType:ModifierType.DIRE_HIT", "dire_hit", (type, _args) => new Modifiers.TempCritBoosterModifier(type, 5)),
BASE_STAT_BOOSTER: () => new BaseStatBoosterModifierTypeGenerator(),

View File

@ -292,70 +292,131 @@ export class AddVoucherModifier extends ConsumableModifier {
}
}
/**
* Modifier used for party-wide or passive items that start an initial
* {@linkcode battleCount} equal to {@linkcode maxBattles} that, for every
* battle, decrements. Typically, when {@linkcode battleCount} reaches 0, the
* modifier will be removed. If a modifier of the same type is to be added, it
* will reset {@linkcode battleCount} back to {@linkcode maxBattles} of the
* existing modifier instead of adding that modifier directly.
* @extends PersistentModifier
* @abstract
* @see {@linkcode add}
*/
export abstract class LapsingPersistentModifier extends PersistentModifier {
protected battlesLeft: integer;
/** The maximum amount of battles the modifier will exist for */
private maxBattles: number;
/** The current amount of battles the modifier will exist for */
private battleCount: number;
constructor(type: ModifierTypes.ModifierType, battlesLeft?: integer, stackCount?: integer) {
constructor(type: ModifierTypes.ModifierType, maxBattles: number, battleCount?: number, stackCount?: integer) {
super(type, stackCount);
this.battlesLeft = battlesLeft!; // TODO: is this bang correct?
this.maxBattles = maxBattles;
this.battleCount = battleCount ?? this.maxBattles;
}
lapse(args: any[]): boolean {
return !!--this.battlesLeft;
/**
* Goes through existing modifiers for any that match the selected modifier,
* which will then either add it to the existing modifiers if none were found
* or, if one was found, it will refresh {@linkcode battleCount}.
* @param modifiers {@linkcode PersistentModifier} array of the player's modifiers
* @param _virtual N/A
* @param _scene N/A
* @returns true if the modifier was successfully added or applied, false otherwise
*/
add(modifiers: PersistentModifier[], _virtual: boolean, scene: BattleScene): boolean {
for (const modifier of modifiers) {
if (this.match(modifier)) {
const modifierInstance = modifier as LapsingPersistentModifier;
if (modifierInstance.getBattleCount() < modifierInstance.getMaxBattles()) {
modifierInstance.resetBattleCount();
scene.playSound("se/restore");
return true;
}
// should never get here
return false;
}
}
modifiers.push(this);
return true;
}
lapse(_args: any[]): boolean {
this.battleCount--;
return this.battleCount > 0;
}
getIcon(scene: BattleScene): Phaser.GameObjects.Container {
const container = super.getIcon(scene);
const battleCountText = addTextObject(scene, 27, 0, this.battlesLeft.toString(), TextStyle.PARTY, { fontSize: "66px", color: "#f89890" });
// Linear interpolation on hue
const hue = Math.floor(120 * (this.battleCount / this.maxBattles) + 5);
// Generates the color hex code with a constant saturation and lightness but varying hue
const typeHex = Utils.hslToHex(hue, 0.50, 0.90);
const strokeHex = Utils.hslToHex(hue, 0.70, 0.30);
const battleCountText = addTextObject(scene, 27, 0, this.battleCount.toString(), TextStyle.PARTY, { fontSize: "66px", color: typeHex });
battleCountText.setShadow(0, 0);
battleCountText.setStroke("#984038", 16);
battleCountText.setStroke(strokeHex, 16);
battleCountText.setOrigin(1, 0);
container.add(battleCountText);
return container;
}
getBattlesLeft(): integer {
return this.battlesLeft;
getBattleCount(): number {
return this.battleCount;
}
getMaxStackCount(scene: BattleScene, forThreshold?: boolean): number {
return 99;
}
}
export class DoubleBattleChanceBoosterModifier extends LapsingPersistentModifier {
constructor(type: ModifierTypes.DoubleBattleChanceBoosterModifierType, battlesLeft: integer, stackCount?: integer) {
super(type, battlesLeft, stackCount);
resetBattleCount(): void {
this.battleCount = this.maxBattles;
}
match(modifier: Modifier): boolean {
if (modifier instanceof DoubleBattleChanceBoosterModifier) {
// Check type id to not match different tiers of lures
return modifier.type.id === this.type.id && modifier.battlesLeft === this.battlesLeft;
}
return false;
}
clone(): DoubleBattleChanceBoosterModifier {
return new DoubleBattleChanceBoosterModifier(this.type as ModifierTypes.DoubleBattleChanceBoosterModifierType, this.battlesLeft, this.stackCount);
getMaxBattles(): number {
return this.maxBattles;
}
getArgs(): any[] {
return [ this.battlesLeft ];
return [ this.maxBattles, this.battleCount ];
}
getMaxStackCount(_scene: BattleScene, _forThreshold?: boolean): number {
return 1;
}
}
/**
* Modifier used for passive items, specifically lures, that
* temporarily increases the chance of a double battle.
* @extends LapsingPersistentModifier
* @see {@linkcode apply}
*/
export class DoubleBattleChanceBoosterModifier extends LapsingPersistentModifier {
constructor(type: ModifierType, maxBattles:number, battleCount?: number, stackCount?: integer) {
super(type, maxBattles, battleCount, stackCount);
}
match(modifier: Modifier): boolean {
return (modifier instanceof DoubleBattleChanceBoosterModifier) && (modifier.getMaxBattles() === this.getMaxBattles());
}
clone(): DoubleBattleChanceBoosterModifier {
return new DoubleBattleChanceBoosterModifier(this.type as ModifierTypes.DoubleBattleChanceBoosterModifierType, this.getMaxBattles(), this.getBattleCount(), this.stackCount);
}
/**
* Modifies the chance of a double battle occurring
* @param args A single element array containing the double battle chance as a NumberHolder
* @returns {boolean} Returns true if the modifier was applied
* @param args [0] {@linkcode Utils.NumberHolder} for double battle chance
* @returns true if the modifier was applied
*/
apply(args: any[]): boolean {
const doubleBattleChance = args[0] as Utils.NumberHolder;
// This is divided because the chance is generated as a number from 0 to doubleBattleChance.value using Utils.randSeedInt
// A double battle will initiate if the generated number is 0
doubleBattleChance.value = Math.ceil(doubleBattleChance.value / 2);
doubleBattleChance.value = Math.ceil(doubleBattleChance.value / 4);
return true;
}
@ -369,16 +430,18 @@ export class DoubleBattleChanceBoosterModifier extends LapsingPersistentModifier
* @see {@linkcode apply}
*/
export class TempStatStageBoosterModifier extends LapsingPersistentModifier {
/** The stat whose stat stage multiplier will be temporarily increased */
private stat: TempBattleStat;
private multiplierBoost: number;
/** The amount by which the stat stage itself or its multiplier will be increased by */
private boost: number;
constructor(type: ModifierType, stat: TempBattleStat, battlesLeft?: number, stackCount?: number) {
super(type, battlesLeft ?? 5, stackCount);
constructor(type: ModifierType, stat: TempBattleStat, maxBattles: number, battleCount?: number, stackCount?: number) {
super(type, maxBattles, battleCount, stackCount);
this.stat = stat;
// Note that, because we want X Accuracy to maintain its original behavior,
// it will increment as it did previously, directly to the stat stage.
this.multiplierBoost = stat !== Stat.ACC ? 0.3 : 1;
this.boost = (stat !== Stat.ACC) ? 0.3 : 1;
}
match(modifier: Modifier): boolean {
@ -390,11 +453,11 @@ export class TempStatStageBoosterModifier extends LapsingPersistentModifier {
}
clone() {
return new TempStatStageBoosterModifier(this.type, this.stat, this.battlesLeft, this.stackCount);
return new TempStatStageBoosterModifier(this.type, this.stat, this.getMaxBattles(), this.getBattleCount(), this.stackCount);
}
getArgs(): any[] {
return [ this.stat, this.battlesLeft ];
return [ this.stat, ...super.getArgs() ];
}
/**
@ -409,44 +472,14 @@ export class TempStatStageBoosterModifier extends LapsingPersistentModifier {
}
/**
* Increases the incoming stat stage matching {@linkcode stat} by {@linkcode multiplierBoost}.
* Increases the incoming stat stage matching {@linkcode stat} by {@linkcode boost}.
* @param args [0] {@linkcode TempBattleStat} N/A
* [1] {@linkcode Utils.NumberHolder} that holds the resulting value of the stat stage multiplier
*/
apply(args: any[]): boolean {
(args[1] as Utils.NumberHolder).value += this.multiplierBoost;
(args[1] as Utils.NumberHolder).value += this.boost;
return true;
}
/**
* Goes through existing modifiers for any that match the selected modifier,
* which will then either add it to the existing modifiers if none were found
* or, if one was found, it will refresh {@linkcode battlesLeft}.
* @param modifiers {@linkcode PersistentModifier} array of the player's modifiers
* @param _virtual N/A
* @param _scene N/A
* @returns true if the modifier was successfully added or applied, false otherwise
*/
add(modifiers: PersistentModifier[], _virtual: boolean, _scene: BattleScene): boolean {
for (const modifier of modifiers) {
if (this.match(modifier)) {
const modifierInstance = modifier as TempStatStageBoosterModifier;
if (modifierInstance.getBattlesLeft() < 5) {
modifierInstance.battlesLeft = 5;
return true;
}
// should never get here
return false;
}
}
modifiers.push(this);
return true;
}
getMaxStackCount(_scene: BattleScene, _forThreshold?: boolean): number {
return 1;
}
}
/**
@ -456,12 +489,12 @@ export class TempStatStageBoosterModifier extends LapsingPersistentModifier {
* @see {@linkcode apply}
*/
export class TempCritBoosterModifier extends LapsingPersistentModifier {
constructor(type: ModifierType, battlesLeft?: integer, stackCount?: number) {
super(type, battlesLeft || 5, stackCount);
constructor(type: ModifierType, maxBattles: number, battleCount?: number, stackCount?: number) {
super(type, maxBattles, battleCount, stackCount);
}
clone() {
return new TempCritBoosterModifier(this.type, this.stackCount);
return new TempCritBoosterModifier(this.type, this.getMaxBattles(), this.getBattleCount(), this.stackCount);
}
match(modifier: Modifier): boolean {
@ -486,36 +519,6 @@ export class TempCritBoosterModifier extends LapsingPersistentModifier {
(args[0] as Utils.NumberHolder).value++;
return true;
}
/**
* Goes through existing modifiers for any that match the selected modifier,
* which will then either add it to the existing modifiers if none were found
* or, if one was found, it will refresh {@linkcode battlesLeft}.
* @param modifiers {@linkcode PersistentModifier} array of the player's modifiers
* @param _virtual N/A
* @param _scene N/A
* @returns true if the modifier was successfully added or applied, false otherwise
*/
add(modifiers: PersistentModifier[], _virtual: boolean, _scene: BattleScene): boolean {
for (const modifier of modifiers) {
if (this.match(modifier)) {
const modifierInstance = modifier as TempCritBoosterModifier;
if (modifierInstance.getBattlesLeft() < 5) {
modifierInstance.battlesLeft = 5;
return true;
}
// should never get here
return false;
}
}
modifiers.push(this);
return true;
}
getMaxStackCount(_scene: BattleScene, _forThreshold?: boolean): number {
return 1;
}
}
export class MapModifier extends PersistentModifier {

View File

@ -17,6 +17,7 @@ import { ToggleDoublePositionPhase } from "./toggle-double-position-phase";
import { GameOverPhase } from "./game-over-phase";
import { SwitchPhase } from "./switch-phase";
import { VictoryPhase } from "./victory-phase";
import { SpeciesFormChangeActiveTrigger } from "#app/data/pokemon-forms";
export class FaintPhase extends PokemonPhase {
private preventEndure: boolean;
@ -59,6 +60,7 @@ export class FaintPhase extends PokemonPhase {
}
this.scene.queueMessage(i18next.t("battle:fainted", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }), null, true);
this.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true);
if (pokemon.turnData?.attacksReceived?.length) {
const lastAttack = pokemon.turnData.attacksReceived[0];

View File

@ -49,7 +49,9 @@ export class GameOverPhase extends BattlePhase {
}
if (this.victory && this.scene.gameMode.isEndless) {
this.scene.ui.showDialogue(i18next.t("PGMmiscDialogue:ending_endless"), i18next.t("PGMmiscDialogue:ending_name"), 0, () => this.handleGameOver());
const genderIndex = this.scene.gameData.gender ?? PlayerGender.UNSET;
const genderStr = PlayerGender[genderIndex].toLowerCase();
this.scene.ui.showDialogue(i18next.t("miscDialogue:ending_endless", { context: genderStr }), i18next.t("miscDialogue:ending_name"), 0, () => this.handleGameOver());
} else if (this.victory || !this.scene.enableRetries) {
this.handleGameOver();
} else {

View File

@ -1,10 +1,11 @@
import BattleScene from "#app/battle-scene.js";
import BattleScene from "#app/battle-scene";
import { applyPreWeatherEffectAbAttrs, SuppressWeatherEffectAbAttr, PreWeatherDamageAbAttr, applyAbAttrs, BlockNonDirectDamageAbAttr, applyPostWeatherLapseAbAttrs, PostWeatherLapseAbAttr } from "#app/data/ability.js";
import { CommonAnim } from "#app/data/battle-anims.js";
import { Weather, getWeatherDamageMessage, getWeatherLapseMessage } from "#app/data/weather.js";
import { WeatherType } from "#app/enums/weather-type.js";
import Pokemon, { HitResult } from "#app/field/pokemon.js";
import * as Utils from "#app/utils.js";
import { CommonAnim } from "#app/data/battle-anims";
import { Weather, getWeatherDamageMessage, getWeatherLapseMessage } from "#app/data/weather";
import { BattlerTagType } from "#app/enums/battler-tag-type.js";
import { WeatherType } from "#app/enums/weather-type";
import Pokemon, { HitResult } from "#app/field/pokemon";
import * as Utils from "#app/utils";
import { CommonAnimPhase } from "./common-anim-phase";
export class WeatherEffectPhase extends CommonAnimPhase {
@ -39,7 +40,7 @@ export class WeatherEffectPhase extends CommonAnimPhase {
applyPreWeatherEffectAbAttrs(PreWeatherDamageAbAttr, pokemon, this.weather, cancelled);
applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled);
if (cancelled.value) {
if (cancelled.value || pokemon.getTag(BattlerTagType.UNDERGROUND) || pokemon.getTag(BattlerTagType.UNDERWATER)) {
return;
}

View File

@ -857,6 +857,14 @@ export class GameData {
const settings = JSON.parse(localStorage.getItem("settings")!); // TODO: is this bang correct?
// TODO: Remove this block after save migration is implemented
if (settings.hasOwnProperty("REROLL_TARGET") && !settings.hasOwnProperty(SettingKeys.Shop_Cursor_Target)) {
settings[SettingKeys.Shop_Cursor_Target] = settings["REROLL_TARGET"];
delete settings["REROLL_TARGET"];
localStorage.setItem("settings", JSON.stringify(settings));
}
// End of block to remove
for (const setting of Object.keys(settings)) {
setSetting(this.scene, setting, settings[setting]);
}

View File

@ -0,0 +1,62 @@
import { WeatherType } from "#app/data/weather";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { SPLASH_ONLY } from "../utils/testUtils";
import { BattlerIndex } from "#app/battle";
describe("Weather - Hail", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.weather(WeatherType.HAIL)
.battleType("single")
.moveset(SPLASH_ONLY)
.enemyMoveset(SPLASH_ONLY)
.enemySpecies(Species.MAGIKARP);
});
it("inflicts damage equal to 1/16 of Pokemon's max HP at turn end", async () => {
await game.classicMode.startBattle([Species.MAGIKARP]);
game.move.select(Moves.SPLASH);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.phaseInterceptor.to("TurnEndPhase");
game.scene.getField(true).forEach(pokemon => {
expect(pokemon.hp).toBeLessThan(pokemon.getMaxHp() - Math.floor(pokemon.getMaxHp() / 16));
});
});
it("does not inflict damage to a Pokemon that is underwater (Dive) or underground (Dig)", async () => {
game.override.moveset([Moves.DIG]);
await game.classicMode.startBattle([Species.MAGIKARP]);
game.move.select(Moves.DIG);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.phaseInterceptor.to("TurnEndPhase");
const playerPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
expect(playerPokemon.hp).toBe(playerPokemon.getMaxHp());
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp() - Math.floor(enemyPokemon.getMaxHp() / 16));
});
});

View File

@ -0,0 +1,59 @@
import { WeatherType } from "#app/data/weather";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { SPLASH_ONLY } from "../utils/testUtils";
describe("Weather - Sandstorm", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.weather(WeatherType.SANDSTORM)
.battleType("single")
.moveset(SPLASH_ONLY)
.enemyMoveset(SPLASH_ONLY)
.enemySpecies(Species.MAGIKARP);
});
it("inflicts damage equal to 1/16 of Pokemon's max HP at turn end", async () => {
await game.classicMode.startBattle([Species.MAGIKARP]);
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to("TurnEndPhase");
game.scene.getField(true).forEach(pokemon => {
expect(pokemon.hp).toBeLessThan(pokemon.getMaxHp() - Math.floor(pokemon.getMaxHp() / 16));
});
});
it("does not inflict damage to a Pokemon that is underwater (Dive) or underground (Dig)", async () => {
game.override.moveset([Moves.DIVE]);
await game.classicMode.startBattle([Species.MAGIKARP]);
game.move.select(Moves.DIVE);
await game.phaseInterceptor.to("TurnEndPhase");
const playerPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;
expect(playerPokemon.hp).toBe(playerPokemon.getMaxHp());
expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp() - Math.floor(enemyPokemon.getMaxHp() / 16));
});
});

View File

@ -72,7 +72,7 @@ describe("Items - Dire Hit", () => {
await game.phaseInterceptor.to(BattleEndPhase);
const modifier = game.scene.findModifier(m => m instanceof TempCritBoosterModifier) as TempCritBoosterModifier;
expect(modifier.getBattlesLeft()).toBe(4);
expect(modifier.getBattleCount()).toBe(4);
// Forced DIRE_HIT to spawn in the first slot with override
game.onNextPrompt("SelectModifierPhase", Mode.MODIFIER_SELECT, () => {
@ -90,7 +90,7 @@ describe("Items - Dire Hit", () => {
for (const m of game.scene.modifiers) {
if (m instanceof TempCritBoosterModifier) {
count++;
expect((m as TempCritBoosterModifier).getBattlesLeft()).toBe(5);
expect((m as TempCritBoosterModifier).getBattleCount()).toBe(5);
}
}
expect(count).toBe(1);

View File

@ -0,0 +1,105 @@
import { Moves } from "#app/enums/moves.js";
import { Species } from "#app/enums/species.js";
import { DoubleBattleChanceBoosterModifier } from "#app/modifier/modifier";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { SPLASH_ONLY } from "../utils/testUtils";
import { ShopCursorTarget } from "#app/enums/shop-cursor-target.js";
import { Mode } from "#app/ui/ui.js";
import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler.js";
import { Button } from "#app/enums/buttons.js";
describe("Items - Double Battle Chance Boosters", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
const TIMEOUT = 20 * 1000;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
});
it("should guarantee double battle with 2 unique tiers", async () => {
game.override
.startingModifier([
{ name: "LURE" },
{ name: "SUPER_LURE" }
])
.startingWave(2);
await game.classicMode.startBattle();
expect(game.scene.getEnemyField().length).toBe(2);
}, TIMEOUT);
it("should guarantee double boss battle with 3 unique tiers", async () => {
game.override
.startingModifier([
{ name: "LURE" },
{ name: "SUPER_LURE" },
{ name: "MAX_LURE" }
])
.startingWave(10);
await game.classicMode.startBattle();
const enemyField = game.scene.getEnemyField();
expect(enemyField.length).toBe(2);
expect(enemyField[0].isBoss()).toBe(true);
expect(enemyField[1].isBoss()).toBe(true);
}, TIMEOUT);
it("should renew how many battles are left of existing booster when picking up new booster of same tier", async() => {
game.override
.startingModifier([{ name: "LURE" }])
.itemRewards([{ name: "LURE" }])
.moveset(SPLASH_ONLY)
.startingLevel(200);
await game.classicMode.startBattle([
Species.PIKACHU
]);
game.move.select(Moves.SPLASH);
await game.doKillOpponents();
await game.phaseInterceptor.to("BattleEndPhase");
const modifier = game.scene.findModifier(m => m instanceof DoubleBattleChanceBoosterModifier) as DoubleBattleChanceBoosterModifier;
expect(modifier.getBattleCount()).toBe(9);
// Forced LURE to spawn in the first slot with override
game.onNextPrompt("SelectModifierPhase", Mode.MODIFIER_SELECT, () => {
const handler = game.scene.ui.getHandler() as ModifierSelectUiHandler;
// Traverse to first modifier slot
handler.setCursor(0);
handler.setRowCursor(ShopCursorTarget.REWARDS);
handler.processInput(Button.ACTION);
}, () => game.isCurrentPhase("CommandPhase") || game.isCurrentPhase("NewBattlePhase"), true);
await game.phaseInterceptor.to("TurnInitPhase");
// Making sure only one booster is in the modifier list even after picking up another
let count = 0;
for (const m of game.scene.modifiers) {
if (m instanceof DoubleBattleChanceBoosterModifier) {
count++;
const modifierInstance = m as DoubleBattleChanceBoosterModifier;
expect(modifierInstance.getBattleCount()).toBe(modifierInstance.getMaxBattles());
}
}
expect(count).toBe(1);
}, TIMEOUT);
});

View File

@ -10,12 +10,7 @@ import { Abilities } from "#app/enums/abilities";
import { TempStatStageBoosterModifier } from "#app/modifier/modifier";
import { Mode } from "#app/ui/ui";
import { Button } from "#app/enums/buttons";
import { CommandPhase } from "#app/phases/command-phase";
import { NewBattlePhase } from "#app/phases/new-battle-phase";
import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler";
import { TurnInitPhase } from "#app/phases/turn-init-phase";
import { BattleEndPhase } from "#app/phases/battle-end-phase";
import { EnemyCommandPhase } from "#app/phases/enemy-command-phase";
import { ShopCursorTarget } from "#app/enums/shop-cursor-target";
@ -46,7 +41,7 @@ describe("Items - Temporary Stat Stage Boosters", () => {
});
it("should provide a x1.3 stat stage multiplier", async() => {
await game.startBattle([
await game.classicMode.startBattle([
Species.PIKACHU
]);
@ -56,7 +51,7 @@ describe("Items - Temporary Stat Stage Boosters", () => {
game.move.select(Moves.TACKLE);
await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(TurnEndPhase);
await game.phaseInterceptor.runFrom("EnemyCommandPhase").to(TurnEndPhase);
expect(partyMember.getStatStageMultiplier).toHaveReturnedWith(1.3);
}, 20000);
@ -66,7 +61,7 @@ describe("Items - Temporary Stat Stage Boosters", () => {
.startingModifier([{ name: "TEMP_STAT_STAGE_BOOSTER", type: Stat.ACC }])
.ability(Abilities.SIMPLE);
await game.startBattle([
await game.classicMode.startBattle([
Species.PIKACHU
]);
@ -89,7 +84,7 @@ describe("Items - Temporary Stat Stage Boosters", () => {
it("should increase existing stat stage multiplier by 3/10 for the rest of the boosters", async() => {
await game.startBattle([
await game.classicMode.startBattle([
Species.PIKACHU
]);
@ -113,7 +108,7 @@ describe("Items - Temporary Stat Stage Boosters", () => {
it("should not increase past maximum stat stage multiplier", async() => {
game.override.startingModifier([{ name: "TEMP_STAT_STAGE_BOOSTER", type: Stat.ACC }, { name: "TEMP_STAT_STAGE_BOOSTER", type: Stat.ATK }]);
await game.startBattle([
await game.classicMode.startBattle([
Species.PIKACHU
]);
@ -138,7 +133,7 @@ describe("Items - Temporary Stat Stage Boosters", () => {
.startingLevel(200)
.itemRewards([{ name: "TEMP_STAT_STAGE_BOOSTER", type: Stat.ATK }]);
await game.startBattle([
await game.classicMode.startBattle([
Species.PIKACHU
]);
@ -146,10 +141,10 @@ describe("Items - Temporary Stat Stage Boosters", () => {
await game.doKillOpponents();
await game.phaseInterceptor.to(BattleEndPhase);
await game.phaseInterceptor.to("BattleEndPhase");
const modifier = game.scene.findModifier(m => m instanceof TempStatStageBoosterModifier) as TempStatStageBoosterModifier;
expect(modifier.getBattlesLeft()).toBe(4);
expect(modifier.getBattleCount()).toBe(4);
// Forced X_ATTACK to spawn in the first slot with override
game.onNextPrompt("SelectModifierPhase", Mode.MODIFIER_SELECT, () => {
@ -158,16 +153,17 @@ describe("Items - Temporary Stat Stage Boosters", () => {
handler.setCursor(0);
handler.setRowCursor(ShopCursorTarget.REWARDS);
handler.processInput(Button.ACTION);
}, () => game.isCurrentPhase(CommandPhase) || game.isCurrentPhase(NewBattlePhase), true);
}, () => game.isCurrentPhase("CommandPhase") || game.isCurrentPhase("NewBattlePhase"), true);
await game.phaseInterceptor.to(TurnInitPhase);
await game.phaseInterceptor.to("TurnInitPhase");
// Making sure only one booster is in the modifier list even after picking up another
let count = 0;
for (const m of game.scene.modifiers) {
if (m instanceof TempStatStageBoosterModifier) {
count++;
expect((m as TempStatStageBoosterModifier).getBattlesLeft()).toBe(5);
const modifierInstance = m as TempStatStageBoosterModifier;
expect(modifierInstance.getBattleCount()).toBe(modifierInstance.getMaxBattles());
}
}
expect(count).toBe(1);

View File

@ -455,6 +455,26 @@ export function rgbaToInt(rgba: integer[]): integer {
return (rgba[0] << 24) + (rgba[1] << 16) + (rgba[2] << 8) + rgba[3];
}
/**
* Provided valid HSV values, calculates and stitches together a string of that
* HSV color's corresponding hex code.
*
* Sourced from {@link https://stackoverflow.com/a/44134328}.
* @param h Hue in degrees, must be in a range of [0, 360]
* @param s Saturation percentage, must be in a range of [0, 1]
* @param l Ligthness percentage, must be in a range of [0, 1]
* @returns a string of the corresponding color hex code with a "#" prefix
*/
export function hslToHex(h: number, s: number, l: number): string {
const a = s * Math.min(l, 1 - l);
const f = (n: number) => {
const k = (n + h / 30) % 12;
const rgb = l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1));
return Math.round(rgb * 255).toString(16).padStart(2, "0");
};
return `#${f(0)}${f(8)}${f(4)}`;
}
/*This function returns true if the current lang is available for some functions
If the lang is not in the function, it usually means that lang is going to use the default english version
This function is used in:

View File

@ -1,38 +1,43 @@
import { defineProject } from 'vitest/config';
import { defineProject, UserWorkspaceConfig } from 'vitest/config';
import { defaultConfig } from './vite.config';
export const defaultProjectTestConfig: UserWorkspaceConfig["test"] = {
setupFiles: ['./src/test/vitest.setup.ts'],
server: {
deps: {
inline: ['vitest-canvas-mock'],
//@ts-ignore
optimizer: {
web: {
include: ['vitest-canvas-mock'],
}
}
}
},
environment: 'jsdom' as const,
environmentOptions: {
jsdom: {
resources: 'usable',
},
},
threads: false,
trace: true,
restoreMocks: true,
watch: false,
coverage: {
provider: 'istanbul' as const,
reportsDirectory: 'coverage' as const,
reporters: ['text-summary', 'html'],
},
}
export default defineProject(({ mode }) => ({
...defaultConfig,
test: {
...defaultProjectTestConfig,
name: "main",
include: ["./src/test/**/*.{test,spec}.ts"],
exclude: ["./src/test/pre.test.ts"],
setupFiles: ['./src/test/vitest.setup.ts'],
server: {
deps: {
inline: ['vitest-canvas-mock'],
optimizer: {
web: {
include: ['vitest-canvas-mock'],
}
}
}
},
environment: 'jsdom' as const,
environmentOptions: {
jsdom: {
resources: 'usable',
},
},
threads: false,
trace: true,
restoreMocks: true,
watch: false,
coverage: {
provider: 'istanbul' as const,
reportsDirectory: 'coverage' as const,
reporters: ['text-summary', 'html'],
},
},
esbuild: {
pure: mode === 'production' ? [ 'console.log' ] : [],

View File

@ -1,5 +1,6 @@
import { defineWorkspace } from "vitest/config";
import { defaultConfig } from "./vite.config";
import { defaultProjectTestConfig } from "./vitest.config";
export default defineWorkspace([
{
@ -10,5 +11,58 @@ export default defineWorkspace([
environment: "jsdom",
},
},
{
...defaultConfig,
test: {
...defaultProjectTestConfig,
name: "misc",
include: [
"src/test/achievements/**/*.{test,spec}.ts",
"src/test/arena/**/*.{test,spec}.ts",
"src/test/battlerTags/**/*.{test,spec}.ts",
"src/test/eggs/**/*.{test,spec}.ts",
"src/test/field/**/*.{test,spec}.ts",
"src/test/inputs/**/*.{test,spec}.ts",
"src/test/localization/**/*.{test,spec}.ts",
"src/test/phases/**/*.{test,spec}.ts",
"src/test/settingMenu/**/*.{test,spec}.ts",
"src/test/sprites/**/*.{test,spec}.ts",
"src/test/ui/**/*.{test,spec}.ts",
"src/test/*.{test,spec}.ts",
],
},
},
{
...defaultConfig,
test: {
...defaultProjectTestConfig,
name: "abilities",
include: ["src/test/abilities/**/*.{test,spec}.ts"],
},
},
{
...defaultConfig,
test: {
...defaultProjectTestConfig,
name: "battle",
include: ["src/test/battle/**/*.{test,spec}.ts"],
},
},
{
...defaultConfig,
test: {
...defaultProjectTestConfig,
name: "items",
include: ["src/test/items/**/*.{test,spec}.ts"],
},
},
{
...defaultConfig,
test: {
...defaultProjectTestConfig,
name: "moves",
include: ["src/test/moves/**/*.{test,spec}.ts"],
},
},
"./vitest.config.ts",
]);