safari encounter and sprite offset positioning updates
This commit is contained in:
parent
43ebff59a4
commit
da0aea0d1e
|
@ -4,177 +4,198 @@
|
|||
"image": "chest_blue.png",
|
||||
"format": "RGBA8888",
|
||||
"size": {
|
||||
"w": 300,
|
||||
"h": 75
|
||||
"w": 58,
|
||||
"h": 528
|
||||
},
|
||||
"scale": 1,
|
||||
"frames": [
|
||||
{
|
||||
"filename": "0001.png",
|
||||
"filename": "0000.png",
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"trimmed": true,
|
||||
"sourceSize": {
|
||||
"w": 75,
|
||||
"h": 75
|
||||
},
|
||||
"spriteSourceSize": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"x": 14,
|
||||
"y": 30,
|
||||
"w": 48,
|
||||
"h": 41
|
||||
},
|
||||
"frame": {
|
||||
"x": 1,
|
||||
"y": 1,
|
||||
"w": 48,
|
||||
"h": 41
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "0001.png",
|
||||
"rotated": false,
|
||||
"trimmed": true,
|
||||
"sourceSize": {
|
||||
"w": 75,
|
||||
"h": 75
|
||||
},
|
||||
"spriteSourceSize": {
|
||||
"x": 14,
|
||||
"y": 34,
|
||||
"w": 49,
|
||||
"h": 37
|
||||
},
|
||||
"frame": {
|
||||
"x": -15,
|
||||
"y": 0,
|
||||
"w": 75,
|
||||
"h": 75
|
||||
"x": 1,
|
||||
"y": 44,
|
||||
"w": 49,
|
||||
"h": 37
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "0002.png",
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"trimmed": true,
|
||||
"sourceSize": {
|
||||
"w": 75,
|
||||
"h": 75
|
||||
},
|
||||
"spriteSourceSize": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 75,
|
||||
"h": 75
|
||||
"x": 14,
|
||||
"y": 30,
|
||||
"w": 48,
|
||||
"h": 41
|
||||
},
|
||||
"frame": {
|
||||
"x": -15,
|
||||
"y": 0,
|
||||
"w": 75,
|
||||
"h": 75
|
||||
"x": 1,
|
||||
"y": 83,
|
||||
"w": 48,
|
||||
"h": 41
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "0003.png",
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"trimmed": true,
|
||||
"sourceSize": {
|
||||
"w": 75,
|
||||
"h": 75
|
||||
},
|
||||
"spriteSourceSize": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 75,
|
||||
"h": 75
|
||||
"x": 14,
|
||||
"y": 23,
|
||||
"w": 48,
|
||||
"h": 48
|
||||
},
|
||||
"frame": {
|
||||
"x": 57,
|
||||
"y": 0,
|
||||
"w": 75,
|
||||
"h": 75
|
||||
"x": 1,
|
||||
"y": 126,
|
||||
"w": 48,
|
||||
"h": 48
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "0004.png",
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"trimmed": true,
|
||||
"sourceSize": {
|
||||
"w": 75,
|
||||
"h": 75
|
||||
},
|
||||
"spriteSourceSize": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 75,
|
||||
"h": 75
|
||||
"x": 13,
|
||||
"y": 4,
|
||||
"w": 55,
|
||||
"h": 67
|
||||
},
|
||||
"frame": {
|
||||
"x": 57,
|
||||
"y": 0,
|
||||
"w": 75,
|
||||
"h": 75
|
||||
"x": 1,
|
||||
"y": 176,
|
||||
"w": 55,
|
||||
"h": 67
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "0005.png",
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"trimmed": true,
|
||||
"sourceSize": {
|
||||
"w": 75,
|
||||
"h": 75
|
||||
},
|
||||
"spriteSourceSize": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 75,
|
||||
"h": 75
|
||||
"x": 15,
|
||||
"y": 2,
|
||||
"w": 56,
|
||||
"h": 69
|
||||
},
|
||||
"frame": {
|
||||
"x": 129,
|
||||
"y": 0,
|
||||
"w": 75,
|
||||
"h": 75
|
||||
"x": 1,
|
||||
"y": 245,
|
||||
"w": 56,
|
||||
"h": 69
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "0006.png",
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"trimmed": true,
|
||||
"sourceSize": {
|
||||
"w": 75,
|
||||
"h": 75
|
||||
},
|
||||
"spriteSourceSize": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 75,
|
||||
"h": 75
|
||||
"x": 15,
|
||||
"y": 2,
|
||||
"w": 56,
|
||||
"h": 69
|
||||
},
|
||||
"frame": {
|
||||
"x": 129,
|
||||
"y": 0,
|
||||
"w": 75,
|
||||
"h": 75
|
||||
"x": 1,
|
||||
"y": 316,
|
||||
"w": 56,
|
||||
"h": 69
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "0007.png",
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"trimmed": true,
|
||||
"sourceSize": {
|
||||
"w": 75,
|
||||
"h": 75
|
||||
},
|
||||
"spriteSourceSize": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 75,
|
||||
"h": 75
|
||||
"x": 13,
|
||||
"y": 2,
|
||||
"w": 56,
|
||||
"h": 69
|
||||
},
|
||||
"frame": {
|
||||
"x": 201,
|
||||
"y": 0,
|
||||
"w": 75,
|
||||
"h": 75
|
||||
"x": 1,
|
||||
"y": 387,
|
||||
"w": 56,
|
||||
"h": 69
|
||||
}
|
||||
},
|
||||
{
|
||||
"filename": "0008.png",
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"trimmed": true,
|
||||
"sourceSize": {
|
||||
"w": 75,
|
||||
"h": 75
|
||||
},
|
||||
"spriteSourceSize": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 75,
|
||||
"h": 75
|
||||
"x": 13,
|
||||
"y": 2,
|
||||
"w": 56,
|
||||
"h": 69
|
||||
},
|
||||
"frame": {
|
||||
"x": 201,
|
||||
"y": 0,
|
||||
"w": 75,
|
||||
"h": 75
|
||||
"x": 1,
|
||||
"y": 458,
|
||||
"w": 56,
|
||||
"h": 69
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -183,6 +204,6 @@
|
|||
"meta": {
|
||||
"app": "https://www.codeandweb.com/texturepacker",
|
||||
"version": "3.0",
|
||||
"smartupdate": "$TexturePacker:SmartUpdate:895f0a79b89fa0fb44167f4584fd9a22:357b46953b7e17c6b2f43a62d52855d8:cc1ed0e4f90aaa9dcf1b39a0af1283b0$"
|
||||
"smartupdate": "$TexturePacker:SmartUpdate:5f36000f6160ee6f397afe5a6fd60b73:cf6f4b08e23400447813583c322eb6c7:f4f3c064e6c93b8d1290f93bee927f60$"
|
||||
}
|
||||
}
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.6 KiB |
|
@ -30,7 +30,8 @@ export const MysteriousChestEncounter: IMysteryEncounter =
|
|||
fileRoot: "mystery-encounters",
|
||||
hasShadow: true,
|
||||
x: 4,
|
||||
y: 8,
|
||||
y: 10,
|
||||
yShadowOffset: 3,
|
||||
disableAnimation: true, // Re-enabled after option select
|
||||
},
|
||||
])
|
||||
|
@ -75,10 +76,7 @@ export const MysteriousChestEncounter: IMysteryEncounter =
|
|||
],
|
||||
});
|
||||
// Display result message then proceed to rewards
|
||||
queueEncounterMessage(
|
||||
scene,
|
||||
"mysteryEncounter:mysterious_chest_option_1_normal_result"
|
||||
);
|
||||
queueEncounterMessage(scene, "mysteryEncounter:mysterious_chest_option_1_normal_result");
|
||||
leaveEncounterWithoutBattle(scene);
|
||||
} else if (roll > 40) {
|
||||
// Choose between 3 ULTRA tier items (20%)
|
||||
|
@ -90,10 +88,7 @@ export const MysteriousChestEncounter: IMysteryEncounter =
|
|||
],
|
||||
});
|
||||
// Display result message then proceed to rewards
|
||||
queueEncounterMessage(
|
||||
scene,
|
||||
"mysteryEncounter:mysterious_chest_option_1_good_result"
|
||||
);
|
||||
queueEncounterMessage(scene, "mysteryEncounter:mysterious_chest_option_1_good_result");
|
||||
leaveEncounterWithoutBattle(scene);
|
||||
} else if (roll > 36) {
|
||||
// Choose between 2 ROGUE tier items (4%)
|
||||
|
@ -101,10 +96,7 @@ export const MysteriousChestEncounter: IMysteryEncounter =
|
|||
guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ROGUE],
|
||||
});
|
||||
// Display result message then proceed to rewards
|
||||
queueEncounterMessage(
|
||||
scene,
|
||||
"mysteryEncounter:mysterious_chest_option_1_great_result"
|
||||
);
|
||||
queueEncounterMessage(scene, "mysteryEncounter:mysterious_chest_option_1_great_result");
|
||||
leaveEncounterWithoutBattle(scene);
|
||||
} else if (roll > 35) {
|
||||
// Choose 1 MASTER tier item (1%)
|
||||
|
@ -112,10 +104,7 @@ export const MysteriousChestEncounter: IMysteryEncounter =
|
|||
guaranteedModifierTiers: [ModifierTier.MASTER],
|
||||
});
|
||||
// Display result message then proceed to rewards
|
||||
queueEncounterMessage(
|
||||
scene,
|
||||
"mysteryEncounter:mysterious_chest_option_1_amazing_result"
|
||||
);
|
||||
queueEncounterMessage(scene, "mysteryEncounter:mysterious_chest_option_1_amazing_result");
|
||||
leaveEncounterWithoutBattle(scene);
|
||||
} else {
|
||||
// Your highest level unfainted Pok<6F>mon gets OHKO. Progress with no rewards (35%)
|
||||
|
@ -125,27 +114,22 @@ export const MysteriousChestEncounter: IMysteryEncounter =
|
|||
);
|
||||
koPlayerPokemon(highestLevelPokemon);
|
||||
|
||||
scene.currentBattle.mysteryEncounter.setDialogueToken(
|
||||
"pokeName",
|
||||
highestLevelPokemon.name
|
||||
);
|
||||
scene.currentBattle.mysteryEncounter.setDialogueToken("pokeName", highestLevelPokemon.name);
|
||||
// Show which Pokemon was KOed, then leave encounter with no rewards
|
||||
// Does this synchronously so that game over doesn't happen over result message
|
||||
await showEncounterText(
|
||||
scene,
|
||||
"mysteryEncounter:mysterious_chest_option_1_bad_result"
|
||||
).then(() => {
|
||||
if (
|
||||
scene.getParty().filter((p) => p.isAllowedInBattle()).length ===
|
||||
await showEncounterText(scene, "mysteryEncounter:mysterious_chest_option_1_bad_result")
|
||||
.then(() => {
|
||||
if (
|
||||
scene.getParty().filter((p) => p.isAllowedInBattle()).length ===
|
||||
0
|
||||
) {
|
||||
) {
|
||||
// All pokemon fainted, game over
|
||||
scene.clearPhaseQueue();
|
||||
scene.unshiftPhase(new GameOverPhase(scene));
|
||||
} else {
|
||||
leaveEncounterWithoutBattle(scene);
|
||||
}
|
||||
});
|
||||
scene.clearPhaseQueue();
|
||||
scene.unshiftPhase(new GameOverPhase(scene));
|
||||
} else {
|
||||
leaveEncounterWithoutBattle(scene);
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.build()
|
||||
|
|
|
@ -1,31 +1,167 @@
|
|||
import {
|
||||
getHighestLevelPlayerPokemon,
|
||||
koPlayerPokemon,
|
||||
leaveEncounterWithoutBattle,
|
||||
queueEncounterMessage,
|
||||
setEncounterRewards,
|
||||
showEncounterText,
|
||||
} from "#app/data/mystery-encounters/mystery-encounter-utils";
|
||||
import { ModifierTier } from "#app/modifier/modifier-tier";
|
||||
import { GameOverPhase } from "#app/phases";
|
||||
import { randSeedInt } from "#app/utils";
|
||||
import { getRandomSpeciesByStarterTier, initFollowupOptionSelect, leaveEncounterWithoutBattle, updatePlayerMoney, } from "#app/data/mystery-encounters/mystery-encounter-utils";
|
||||
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
|
||||
import BattleScene from "../../../battle-scene";
|
||||
import IMysteryEncounter, {
|
||||
MysteryEncounterBuilder,
|
||||
MysteryEncounterTier,
|
||||
} from "../mystery-encounter";
|
||||
import { EncounterOptionMode, MysteryEncounterOptionBuilder } from "../mystery-encounter-option";
|
||||
import IMysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier, MysteryEncounterVariant } from "../mystery-encounter";
|
||||
import MysteryEncounterOption, { EncounterOptionMode, MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option";
|
||||
import { TrainerSlot } from "#app/data/trainer-config";
|
||||
import { ScanIvsPhase, SummonPhase, VictoryPhase } from "#app/phases";
|
||||
import i18next from "i18next";
|
||||
import { HiddenAbilityRateBoosterModifier, IvScannerModifier, PokemonHeldItemModifier } from "#app/modifier/modifier";
|
||||
import { EnemyPokemon } from "#app/field/pokemon";
|
||||
import { doPokeballBounceAnim, getPokeballAtlasKey, getPokeballTintColor, PokeballType } from "#app/data/pokeball";
|
||||
import { StatusEffect } from "#app/data/status-effect";
|
||||
import { addPokeballCaptureStars, addPokeballOpenParticles } from "#app/field/anims";
|
||||
import { achvs } from "#app/system/achv";
|
||||
import { Mode } from "#app/ui/ui";
|
||||
import { PartyOption, PartyUiMode } from "#app/ui/party-ui-handler";
|
||||
import { BattlerIndex } from "#app/battle";
|
||||
import { PlayerGender } from "#enums/player-gender";
|
||||
import { IntegerHolder, randSeedInt } from "#app/utils";
|
||||
import { getPokemonSpecies } from "#app/data/pokemon-species";
|
||||
import { MoneyRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements";
|
||||
|
||||
/** the i18n namespace for the encounter */
|
||||
const namespace = "mysteryEncounter:safari_zone";
|
||||
|
||||
/**
|
||||
* SAFARI ZONE OPTIONS
|
||||
*
|
||||
* Catch and flee rate **multipliers** are calculated in the same way stat changes are (they range from -6/+6)
|
||||
* https://bulbapedia.bulbagarden.net/wiki/Catch_rate#Great_Marsh_and_Johto_Safari_Zone
|
||||
*
|
||||
* Catch Rate calculation:
|
||||
* catchRate = speciesCatchRate [1 to 255] * catchStageMultiplier [2/8 to 8/2] * ballCatchRate [1.5]
|
||||
*
|
||||
* Flee calculation:
|
||||
* The harder a species is to catch, the higher its flee rate is
|
||||
* (Caps at 50% base chance to flee for the hardest to catch Pokemon, before factoring in flee stage)
|
||||
* fleeRate = ((255^2 - speciesCatchRate^2) / 255 / 2) [0 to 127.5] * fleeStageMultiplier [2/8 to 8/2]
|
||||
* Flee chance = fleeRate / 255
|
||||
*/
|
||||
const safariZoneOptions: MysteryEncounterOption[] = [
|
||||
new MysteryEncounterOptionBuilder()
|
||||
.withOptionMode(EncounterOptionMode.DEFAULT)
|
||||
.withDialogue({
|
||||
buttonLabel: `${namespace}_pokeball_option_label`,
|
||||
buttonTooltip: `${namespace}_pokeball_option_tooltip`,
|
||||
selected: [
|
||||
{
|
||||
text: `${namespace}_pokeball_option_selected`,
|
||||
},
|
||||
],
|
||||
})
|
||||
.withOptionPhase(async (scene: BattleScene) => {
|
||||
// Throw a ball option
|
||||
const pokemon = scene.currentBattle.mysteryEncounter.misc.pokemon;
|
||||
const catchResult = await throwPokeball(scene, pokemon);
|
||||
|
||||
if (catchResult) {
|
||||
// You caught pokemon
|
||||
scene.unshiftPhase(new VictoryPhase(scene, 0));
|
||||
// Check how many safari pokemon left
|
||||
if (scene.currentBattle.mysteryEncounter.misc.safariPokemonRemaining > 0) {
|
||||
await summonSafariPokemon(scene);
|
||||
initFollowupOptionSelect(scene, { overrideOptions: safariZoneOptions, startingCursorIndex: 0, hideDescription: true });
|
||||
} else {
|
||||
// End safari mode
|
||||
leaveEncounterWithoutBattle(scene, true);
|
||||
}
|
||||
} else {
|
||||
// Pokemon failed to catch, end turn
|
||||
await doEndTurn(scene, 0);
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.build(),
|
||||
new MysteryEncounterOptionBuilder()
|
||||
.withOptionMode(EncounterOptionMode.DEFAULT)
|
||||
.withDialogue({
|
||||
buttonLabel: `${namespace}_bait_option_label`,
|
||||
buttonTooltip: `${namespace}_bait_option_tooltip`,
|
||||
selected: [
|
||||
{
|
||||
text: `${namespace}_bait_option_selected`,
|
||||
},
|
||||
],
|
||||
})
|
||||
.withOptionPhase(async (scene: BattleScene) => {
|
||||
// Throw bait option
|
||||
const pokemon = scene.currentBattle.mysteryEncounter.misc.pokemon;
|
||||
await throwBait(scene, pokemon);
|
||||
|
||||
// 100% chance to increase catch stage +2
|
||||
tryChangeCatchStage(scene, 2);
|
||||
// 80% chance to increase flee stage +1
|
||||
const fleeChangeResult = tryChangeFleeStage(scene, 1, 8);
|
||||
if (!fleeChangeResult) {
|
||||
scene.queueMessage(i18next.t(`${namespace}_pokemon_busy_eating`, { pokemonName: pokemon.name }), 0, null, 500);
|
||||
} else {
|
||||
scene.queueMessage(i18next.t(`${namespace}_pokemon_eating`, { pokemonName: pokemon.name }), 0, null, 500);
|
||||
}
|
||||
// TODO: throw bait with eat animation
|
||||
// TODO: play bug bite sfx, maybe spike cannon?
|
||||
|
||||
await doEndTurn(scene, 1);
|
||||
return true;
|
||||
})
|
||||
.build(),
|
||||
new MysteryEncounterOptionBuilder()
|
||||
.withOptionMode(EncounterOptionMode.DEFAULT)
|
||||
.withDialogue({
|
||||
buttonLabel: `${namespace}_mud_option_label`,
|
||||
buttonTooltip: `${namespace}_mud_option_tooltip`,
|
||||
selected: [
|
||||
{
|
||||
text: `${namespace}_mud_option_selected`,
|
||||
},
|
||||
],
|
||||
})
|
||||
.withOptionPhase(async (scene: BattleScene) => {
|
||||
// Throw mud option
|
||||
const pokemon = scene.currentBattle.mysteryEncounter.misc.pokemon;
|
||||
await throwMud(scene, pokemon);
|
||||
// 100% chance to decrease flee stage -2
|
||||
tryChangeFleeStage(scene, -2);
|
||||
// 80% chance to decrease catch stage -1
|
||||
const catchChangeResult = tryChangeCatchStage(scene, -1, 8);
|
||||
if (!catchChangeResult) {
|
||||
scene.queueMessage(i18next.t(`${namespace}_pokemon_beside_itself_angry`, { pokemonName: pokemon.name }), 0, null, 500);
|
||||
} else {
|
||||
scene.queueMessage(i18next.t(`${namespace}_pokemon_angry`, { pokemonName: pokemon.name }), 0, null, 500);
|
||||
}
|
||||
|
||||
await doEndTurn(scene, 2);
|
||||
return true;
|
||||
})
|
||||
.build(),
|
||||
new MysteryEncounterOptionBuilder()
|
||||
.withOptionMode(EncounterOptionMode.DEFAULT)
|
||||
.withDialogue({
|
||||
buttonLabel: `${namespace}_flee_option_label`,
|
||||
buttonTooltip: `${namespace}_flee_option_tooltip`,
|
||||
})
|
||||
.withOptionPhase(async (scene: BattleScene) => {
|
||||
// Flee option
|
||||
const pokemon = scene.currentBattle.mysteryEncounter.misc.pokemon;
|
||||
await doPlayerFlee(scene, pokemon);
|
||||
// Check how many safari pokemon left
|
||||
if (scene.currentBattle.mysteryEncounter.misc.safariPokemonRemaining > 0) {
|
||||
await summonSafariPokemon(scene);
|
||||
initFollowupOptionSelect(scene, { overrideOptions: safariZoneOptions, startingCursorIndex: 3, hideDescription: true });
|
||||
} else {
|
||||
// End safari mode
|
||||
leaveEncounterWithoutBattle(scene, true);
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.build()
|
||||
];
|
||||
|
||||
export const SafariZoneEncounter: IMysteryEncounter =
|
||||
MysteryEncounterBuilder.withEncounterType(
|
||||
MysteryEncounterType.SAFARI_ZONE
|
||||
)
|
||||
MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.SAFARI_ZONE)
|
||||
.withEncounterTier(MysteryEncounterTier.GREAT)
|
||||
.withSceneWaveRangeRequirement(10, 180) // waves 2 to 180
|
||||
.withSceneRequirement(new MoneyRequirement(0, 2.75)) // Cost equal to 1 Max Revive
|
||||
.withHideIntroVisuals(false)
|
||||
.withIntroSpriteConfigs([
|
||||
{
|
||||
|
@ -33,133 +169,57 @@ export const SafariZoneEncounter: IMysteryEncounter =
|
|||
fileRoot: "mystery-encounters",
|
||||
hasShadow: true,
|
||||
x: 4,
|
||||
y: 8,
|
||||
y: 10,
|
||||
yShadowOffset: 3,
|
||||
disableAnimation: true, // Re-enabled after option select
|
||||
},
|
||||
])
|
||||
.withIntroDialogue([
|
||||
{
|
||||
text: `mysteryEncounter:${namespace}_intro_message`,
|
||||
text: `${namespace}_intro_message`,
|
||||
},
|
||||
])
|
||||
.withTitle(`mysteryEncounter:${namespace}_title`)
|
||||
.withDescription(`mysteryEncounter:${namespace}_description`)
|
||||
.withQuery(`mysteryEncounter:${namespace}_query`)
|
||||
.withOption(
|
||||
new MysteryEncounterOptionBuilder()
|
||||
.withOptionMode(EncounterOptionMode.DEFAULT)
|
||||
.withDialogue({
|
||||
buttonLabel: "mysteryEncounter:${namespace}_option_1_label",
|
||||
buttonTooltip: "mysteryEncounter:${namespace}_option_1_tooltip",
|
||||
selected: [
|
||||
{
|
||||
text: "mysteryEncounter:${namespace}_option_1_selected_message",
|
||||
},
|
||||
],
|
||||
})
|
||||
.withPreOptionPhase(async (scene: BattleScene) => {
|
||||
// Play animation
|
||||
const introVisuals =
|
||||
scene.currentBattle.mysteryEncounter.introVisuals;
|
||||
introVisuals.spriteConfigs[0].disableAnimation = false;
|
||||
introVisuals.playAnim();
|
||||
})
|
||||
.withOptionPhase(async (scene: BattleScene) => {
|
||||
// Open the chest
|
||||
const roll = randSeedInt(100);
|
||||
if (roll > 60) {
|
||||
// Choose between 2 COMMON / 2 GREAT tier items (40%)
|
||||
setEncounterRewards(scene, {
|
||||
guaranteedModifierTiers: [
|
||||
ModifierTier.COMMON,
|
||||
ModifierTier.COMMON,
|
||||
ModifierTier.GREAT,
|
||||
ModifierTier.GREAT,
|
||||
],
|
||||
});
|
||||
// Display result message then proceed to rewards
|
||||
queueEncounterMessage(
|
||||
scene,
|
||||
"mysteryEncounter:${namespace}_option_1_normal_result"
|
||||
);
|
||||
leaveEncounterWithoutBattle(scene);
|
||||
} else if (roll > 40) {
|
||||
// Choose between 3 ULTRA tier items (20%)
|
||||
setEncounterRewards(scene, {
|
||||
guaranteedModifierTiers: [
|
||||
ModifierTier.ULTRA,
|
||||
ModifierTier.ULTRA,
|
||||
ModifierTier.ULTRA,
|
||||
],
|
||||
});
|
||||
// Display result message then proceed to rewards
|
||||
queueEncounterMessage(
|
||||
scene,
|
||||
"mysteryEncounter:${namespace}_option_1_good_result"
|
||||
);
|
||||
leaveEncounterWithoutBattle(scene);
|
||||
} else if (roll > 36) {
|
||||
// Choose between 2 ROGUE tier items (4%)
|
||||
setEncounterRewards(scene, {
|
||||
guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ROGUE],
|
||||
});
|
||||
// Display result message then proceed to rewards
|
||||
queueEncounterMessage(
|
||||
scene,
|
||||
"mysteryEncounter:${namespace}_option_1_great_result"
|
||||
);
|
||||
leaveEncounterWithoutBattle(scene);
|
||||
} else if (roll > 35) {
|
||||
// Choose 1 MASTER tier item (1%)
|
||||
setEncounterRewards(scene, {
|
||||
guaranteedModifierTiers: [ModifierTier.MASTER],
|
||||
});
|
||||
// Display result message then proceed to rewards
|
||||
queueEncounterMessage(
|
||||
scene,
|
||||
"mysteryEncounter:${namespace}_option_1_amazing_result"
|
||||
);
|
||||
leaveEncounterWithoutBattle(scene);
|
||||
} else {
|
||||
// Your highest level unfainted Pok<6F>mon gets OHKO. Progress with no rewards (35%)
|
||||
const highestLevelPokemon = getHighestLevelPlayerPokemon(
|
||||
scene,
|
||||
true
|
||||
);
|
||||
koPlayerPokemon(highestLevelPokemon);
|
||||
|
||||
scene.currentBattle.mysteryEncounter.setDialogueToken(
|
||||
"pokeName",
|
||||
highestLevelPokemon.name
|
||||
);
|
||||
// Show which Pokemon was KOed, then leave encounter with no rewards
|
||||
// Does this synchronously so that game over doesn't happen over result message
|
||||
await showEncounterText(
|
||||
scene,
|
||||
"mysteryEncounter:${namespace}_option_1_bad_result"
|
||||
).then(() => {
|
||||
if (
|
||||
scene.getParty().filter((p) => p.isAllowedInBattle()).length ===
|
||||
0
|
||||
) {
|
||||
// All pokemon fainted, game over
|
||||
scene.clearPhaseQueue();
|
||||
scene.unshiftPhase(new GameOverPhase(scene));
|
||||
} else {
|
||||
leaveEncounterWithoutBattle(scene);
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.build()
|
||||
.withTitle(`${namespace}_title`)
|
||||
.withDescription(`${namespace}_description`)
|
||||
.withQuery(`${namespace}_query`)
|
||||
.withOption(new MysteryEncounterOptionBuilder()
|
||||
.withOptionMode(EncounterOptionMode.DISABLED_OR_DEFAULT)
|
||||
// TODO: update
|
||||
.withSceneRequirement(new MoneyRequirement(0, 2.75)) // Cost equal to 1 Max Revive
|
||||
.withDialogue({
|
||||
buttonLabel: `${namespace}_option_1_label`,
|
||||
buttonTooltip: `${namespace}_option_1_tooltip`,
|
||||
selected: [
|
||||
{
|
||||
text: `${namespace}_option_1_selected_message`,
|
||||
},
|
||||
],
|
||||
})
|
||||
.withOptionPhase(async (scene: BattleScene) => {
|
||||
// Start safari encounter
|
||||
const encounter = scene.currentBattle.mysteryEncounter;
|
||||
encounter.encounterVariant = MysteryEncounterVariant.SAFARI_BATTLE;
|
||||
encounter.misc = {
|
||||
safariPokemonRemaining: 3
|
||||
};
|
||||
updatePlayerMoney(scene, -(encounter.options[0].requirements[0] as MoneyRequirement).requiredMoney);
|
||||
scene.loadSe("PRSFX- Bug Bite", "battle_anims");
|
||||
scene.loadSe("PRSFX- Sludge Bomb2", "battle_anims");
|
||||
scene.loadSe("PRSFX- Taunt2", "battle_anims");
|
||||
await hideMysteryEncounterIntroVisuals(scene);
|
||||
await summonSafariPokemon(scene);
|
||||
initFollowupOptionSelect(scene, { overrideOptions: safariZoneOptions, hideDescription: true });
|
||||
return true;
|
||||
})
|
||||
.build()
|
||||
)
|
||||
.withSimpleOption(
|
||||
{
|
||||
buttonLabel: "mysteryEncounter:${namespace}_option_2_label",
|
||||
buttonTooltip: "mysteryEncounter:${namespace}_option_2_tooltip",
|
||||
buttonLabel: `${namespace}_option_2_label`,
|
||||
buttonTooltip: `${namespace}_option_2_tooltip`,
|
||||
selected: [
|
||||
{
|
||||
text: "mysteryEncounter:${namespace}_option_2_selected_message",
|
||||
text: `${namespace}_option_2_selected_message`,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -170,3 +230,588 @@ export const SafariZoneEncounter: IMysteryEncounter =
|
|||
}
|
||||
)
|
||||
.build();
|
||||
|
||||
function hideMysteryEncounterIntroVisuals(scene: BattleScene): Promise<boolean> {
|
||||
return new Promise(resolve => {
|
||||
const introVisuals = scene.currentBattle.mysteryEncounter.introVisuals;
|
||||
if (introVisuals) {
|
||||
// Hide
|
||||
scene.tweens.add({
|
||||
targets: introVisuals,
|
||||
x: "+=16",
|
||||
y: "-=16",
|
||||
alpha: 0,
|
||||
ease: "Sine.easeInOut",
|
||||
duration: 750,
|
||||
onComplete: () => {
|
||||
scene.field.remove(introVisuals);
|
||||
introVisuals.setVisible(false);
|
||||
introVisuals.destroy();
|
||||
scene.currentBattle.mysteryEncounter.introVisuals = null;
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function summonSafariPokemon(scene: BattleScene) {
|
||||
const encounter = scene.currentBattle.mysteryEncounter;
|
||||
// Message pokemon remaining
|
||||
scene.queueMessage(i18next.t(`${namespace}_remaining_count`, { remainingCount: encounter.misc.safariPokemonRemaining}), null, true);
|
||||
|
||||
// Generate pokemon using safariPokemonRemaining so they are always the same pokemon no matter how many turns are taken
|
||||
// Safari pokemon roll twice on shiny and HA chances, but are otherwise normal
|
||||
let enemySpecies;
|
||||
let pokemon;
|
||||
scene.executeWithSeedOffset(() => {
|
||||
enemySpecies = getPokemonSpecies(getRandomSpeciesByStarterTier([0, 5]));
|
||||
enemySpecies = getPokemonSpecies(enemySpecies.getWildSpeciesForLevel(scene.currentBattle.waveIndex, true, false, scene.gameMode));
|
||||
scene.currentBattle.enemyParty = [];
|
||||
pokemon = scene.addEnemyPokemon(enemySpecies, scene.currentBattle.waveIndex, TrainerSlot.NONE, false);
|
||||
|
||||
// Roll shiny twice
|
||||
if (!pokemon.shiny) {
|
||||
pokemon.trySetShiny();
|
||||
}
|
||||
|
||||
// Roll HA twice
|
||||
if (pokemon.species.abilityHidden) {
|
||||
const hiddenIndex = pokemon.species.ability2 ? 2 : 1;
|
||||
if (pokemon.abilityIndex < hiddenIndex) {
|
||||
const hiddenAbilityChance = new IntegerHolder(256);
|
||||
scene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance);
|
||||
|
||||
const hasHiddenAbility = !randSeedInt(hiddenAbilityChance.value);
|
||||
|
||||
if (hasHiddenAbility) {
|
||||
pokemon.abilityIndex = hiddenIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pokemon.calculateStats();
|
||||
|
||||
scene.currentBattle.enemyParty[0] = pokemon;
|
||||
}, scene.currentBattle.waveIndex + encounter.misc.safariPokemonRemaining);
|
||||
|
||||
scene.gameData.setPokemonSeen(pokemon, true);
|
||||
await pokemon.loadAssets();
|
||||
|
||||
// Reset safari catch and flee rates
|
||||
encounter.misc.catchStage = 0;
|
||||
encounter.misc.fleeStage = 0;
|
||||
encounter.misc.pokemon = pokemon;
|
||||
encounter.misc.safariPokemonRemaining -= 1;
|
||||
|
||||
scene.unshiftPhase(new SummonPhase(scene, 0, false));
|
||||
|
||||
scene.ui.showText(i18next.t("battle:singleWildAppeared", { pokemonName: pokemon.name }), null, () => {
|
||||
const ivScannerModifier = scene.findModifier(m => m instanceof IvScannerModifier);
|
||||
if (ivScannerModifier) {
|
||||
scene.pushPhase(new ScanIvsPhase(scene, pokemon.getBattlerIndex(), Math.min(ivScannerModifier.getStackCount() * 2, 6)));
|
||||
}
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
async function throwPokeball(scene: BattleScene, pokemon: EnemyPokemon): Promise<boolean> {
|
||||
const pokeballType: PokeballType = PokeballType.POKEBALL;
|
||||
const originalY: number = pokemon.y;
|
||||
|
||||
const baseCatchRate = pokemon.species.catchRate;
|
||||
// Catch stage ranges from -6 to +6 (like stat boost stages)
|
||||
const safariCatchStage = scene.currentBattle.mysteryEncounter.misc.catchStage;
|
||||
// Catch modifier ranges from 2/8 (-6 stage) to 8/2 (+6)
|
||||
const safariModifier = (2 + Math.min(Math.max(safariCatchStage, 0), 6)) / (2 - Math.max(Math.min(safariCatchStage, 0), -6));
|
||||
// Catch rate same as safari ball
|
||||
const pokeballMultiplier = 1.5;
|
||||
const catchRate = Math.round(baseCatchRate * pokeballMultiplier * safariModifier);
|
||||
const ballTwitchRate = Math.round(1048560 / Math.sqrt(Math.sqrt(16711680 / catchRate)));
|
||||
const fpOffset = pokemon.getFieldPositionOffset();
|
||||
const catchSuccess = (ballTwitchRate / 65536) * (ballTwitchRate / 65536) * (ballTwitchRate / 65536);
|
||||
console.log("Catch success rate: " + catchSuccess);
|
||||
|
||||
const pokeballAtlasKey = getPokeballAtlasKey(pokeballType);
|
||||
const pokeball: Phaser.GameObjects.Sprite = scene.addFieldSprite(16 + 75, 80 + 25, "pb", pokeballAtlasKey);
|
||||
pokeball.setOrigin(0.5, 0.625);
|
||||
scene.field.add(pokeball);
|
||||
|
||||
scene.playSound("pb_throw");
|
||||
scene.time.delayedCall(300, () => {
|
||||
scene.field.moveBelow(pokeball as Phaser.GameObjects.GameObject, pokemon);
|
||||
});
|
||||
|
||||
return new Promise(resolve => {
|
||||
scene.trainer.setTexture(`trainer_${scene.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}_back_pb`);
|
||||
scene.time.delayedCall(512, () => {
|
||||
// Trainer throw frames
|
||||
scene.trainer.setFrame("2");
|
||||
scene.time.delayedCall(256, () => {
|
||||
scene.trainer.setFrame("3");
|
||||
scene.time.delayedCall(768, () => {
|
||||
scene.trainer.setTexture(`trainer_${scene.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}_back`);
|
||||
});
|
||||
});
|
||||
|
||||
// Pokeball move and catch logic
|
||||
scene.tweens.add({
|
||||
targets: pokeball,
|
||||
x: { value: 236 + fpOffset[0], ease: "Linear" },
|
||||
y: { value: 16 + fpOffset[1], ease: "Cubic.easeOut" },
|
||||
duration: 500,
|
||||
onComplete: () => {
|
||||
pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`);
|
||||
scene.time.delayedCall(17, () => pokeball.setTexture("pb", `${pokeballAtlasKey}_open`));
|
||||
scene.playSound("pb_rel");
|
||||
pokemon.tint(getPokeballTintColor(pokeballType));
|
||||
|
||||
addPokeballOpenParticles(scene, pokeball.x, pokeball.y, pokeballType);
|
||||
|
||||
scene.tweens.add({
|
||||
targets: pokemon,
|
||||
duration: 500,
|
||||
ease: "Sine.easeIn",
|
||||
scale: 0.25,
|
||||
y: 20,
|
||||
onComplete: () => {
|
||||
pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`);
|
||||
pokemon.setVisible(false);
|
||||
scene.playSound("pb_catch");
|
||||
scene.time.delayedCall(17, () => pokeball.setTexture("pb", `${pokeballAtlasKey}`));
|
||||
|
||||
const doShake = () => {
|
||||
let shakeCount = 0;
|
||||
const pbX = pokeball.x;
|
||||
const shakeCounter = scene.tweens.addCounter({
|
||||
from: 0,
|
||||
to: 1,
|
||||
repeat: 4,
|
||||
yoyo: true,
|
||||
ease: "Cubic.easeOut",
|
||||
duration: 250,
|
||||
repeatDelay: 500,
|
||||
onUpdate: t => {
|
||||
if (shakeCount && shakeCount < 4) {
|
||||
const value = t.getValue();
|
||||
const directionMultiplier = shakeCount % 2 === 1 ? 1 : -1;
|
||||
pokeball.setX(pbX + value * 4 * directionMultiplier);
|
||||
pokeball.setAngle(value * 27.5 * directionMultiplier);
|
||||
}
|
||||
},
|
||||
onRepeat: () => {
|
||||
if (!pokemon.species.isObtainable()) {
|
||||
shakeCounter.stop();
|
||||
failCatch(scene, pokemon, originalY, pokeball, pokeballType).then(() => resolve(false));
|
||||
} else if (shakeCount++ < 3) {
|
||||
if (randSeedInt(65536) < ballTwitchRate) {
|
||||
scene.playSound("pb_move");
|
||||
} else {
|
||||
shakeCounter.stop();
|
||||
failCatch(scene, pokemon, originalY, pokeball, pokeballType).then(() => resolve(false));
|
||||
}
|
||||
} else {
|
||||
scene.playSound("pb_lock");
|
||||
addPokeballCaptureStars(scene, pokeball);
|
||||
|
||||
const pbTint = scene.add.sprite(pokeball.x, pokeball.y, "pb", "pb");
|
||||
pbTint.setOrigin(pokeball.originX, pokeball.originY);
|
||||
pbTint.setTintFill(0);
|
||||
pbTint.setAlpha(0);
|
||||
scene.field.add(pbTint);
|
||||
scene.tweens.add({
|
||||
targets: pbTint,
|
||||
alpha: 0.375,
|
||||
duration: 200,
|
||||
easing: "Sine.easeOut",
|
||||
onComplete: () => {
|
||||
scene.tweens.add({
|
||||
targets: pbTint,
|
||||
alpha: 0,
|
||||
duration: 200,
|
||||
easing: "Sine.easeIn",
|
||||
onComplete: () => pbTint.destroy()
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
onComplete: () => {
|
||||
catchPokemon(scene, pokemon, pokeball, pokeballType).then(() => resolve(true));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
scene.time.delayedCall(250, () => doPokeballBounceAnim(scene, pokeball, 16, 72, 350, doShake));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function throwBait(scene: BattleScene, pokemon: EnemyPokemon): Promise<boolean> {
|
||||
// TODO: replace with bait
|
||||
const pokeballType: PokeballType = PokeballType.POKEBALL;
|
||||
const originalY: number = pokemon.y;
|
||||
|
||||
const fpOffset = pokemon.getFieldPositionOffset();
|
||||
const pokeballAtlasKey = getPokeballAtlasKey(pokeballType);
|
||||
const bait: Phaser.GameObjects.Sprite = scene.addFieldSprite(16 + 75, 80 + 25, "pb", pokeballAtlasKey);
|
||||
bait.setOrigin(0.5, 0.625);
|
||||
scene.field.add(bait);
|
||||
|
||||
scene.playSound("pb_throw");
|
||||
// scene.time.delayedCall(300, () => {
|
||||
// scene.field.moveBelow(pokemon, pokeball as Phaser.GameObjects.GameObject);
|
||||
// });
|
||||
|
||||
return new Promise(resolve => {
|
||||
scene.trainer.setTexture(`trainer_${scene.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}_back_pb`);
|
||||
scene.time.delayedCall(512, () => {
|
||||
// Trainer throw frames
|
||||
scene.trainer.setFrame("2");
|
||||
scene.time.delayedCall(256, () => {
|
||||
scene.trainer.setFrame("3");
|
||||
scene.time.delayedCall(768, () => {
|
||||
scene.trainer.setTexture(`trainer_${scene.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}_back`);
|
||||
});
|
||||
});
|
||||
|
||||
// Pokeball move and catch logic
|
||||
scene.tweens.add({
|
||||
targets: bait,
|
||||
x: { value: 210 + fpOffset[0], ease: "Linear" },
|
||||
y: { value: 55 + fpOffset[1], ease: "Cubic.easeOut" },
|
||||
duration: 500,
|
||||
onComplete: () => {
|
||||
// Bait frame 2
|
||||
bait.setTexture("pb", `${pokeballAtlasKey}_opening`);
|
||||
// Bait frame 3
|
||||
scene.time.delayedCall(17, () => bait.setTexture("pb", `${pokeballAtlasKey}_open`));
|
||||
// scene.playSound("pb_rel");
|
||||
// pokemon.tint(getPokeballTintColor(pokeballType));
|
||||
|
||||
// addPokeballOpenParticles(scene, pokeball.x, pokeball.y, pokeballType);
|
||||
scene.time.delayedCall(512, () => {
|
||||
scene.tweens.add({
|
||||
targets: pokemon,
|
||||
duration: 200,
|
||||
ease: "Cubic.easeOut",
|
||||
yoyo: true,
|
||||
y: originalY - 30,
|
||||
loop: 2,
|
||||
onStart: () => {
|
||||
scene.playSound("PRSFX- Bug Bite");
|
||||
},
|
||||
onLoop: () => {
|
||||
scene.playSound("PRSFX- Bug Bite");
|
||||
},
|
||||
onComplete: () => {
|
||||
resolve(true);
|
||||
bait.destroy();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function throwMud(scene: BattleScene, pokemon: EnemyPokemon): Promise<boolean> {
|
||||
// TODO: replace with mud
|
||||
const pokeballType: PokeballType = PokeballType.POKEBALL;
|
||||
const originalY: number = pokemon.y;
|
||||
|
||||
const fpOffset = pokemon.getFieldPositionOffset();
|
||||
const pokeballAtlasKey = getPokeballAtlasKey(pokeballType);
|
||||
const mud: Phaser.GameObjects.Sprite = scene.addFieldSprite(16 + 75, 80 + 25, "pb", pokeballAtlasKey);
|
||||
mud.setOrigin(0.5, 0.625);
|
||||
scene.field.add(mud);
|
||||
|
||||
scene.playSound("pb_throw");
|
||||
|
||||
return new Promise(resolve => {
|
||||
scene.trainer.setTexture(`trainer_${scene.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}_back_pb`);
|
||||
scene.time.delayedCall(512, () => {
|
||||
// Trainer throw frames
|
||||
scene.trainer.setFrame("2");
|
||||
scene.time.delayedCall(256, () => {
|
||||
scene.trainer.setFrame("3");
|
||||
scene.time.delayedCall(768, () => {
|
||||
scene.trainer.setTexture(`trainer_${scene.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}_back`);
|
||||
});
|
||||
});
|
||||
|
||||
// Pokeball move and catch logic
|
||||
scene.tweens.add({
|
||||
targets: mud,
|
||||
x: { value: 230 + fpOffset[0], ease: "Linear" },
|
||||
y: { value: 55 + fpOffset[1], ease: "Cubic.easeOut" },
|
||||
duration: 500,
|
||||
onComplete: () => {
|
||||
// Bait frame 2
|
||||
mud.setTexture("pb", `${pokeballAtlasKey}_opening`);
|
||||
// Bait frame 3
|
||||
scene.time.delayedCall(17, () => mud.setTexture("pb", `${pokeballAtlasKey}_open`));
|
||||
scene.playSound("PRSFX- Sludge Bomb2");
|
||||
// pokemon.tint(getPokeballTintColor(pokeballType));
|
||||
|
||||
// addPokeballOpenParticles(scene, pokeball.x, pokeball.y, pokeballType);
|
||||
scene.time.delayedCall(1536, () => {
|
||||
mud.destroy();
|
||||
scene.tweens.add({
|
||||
targets: pokemon,
|
||||
duration: 300,
|
||||
ease: "Cubic.easeOut",
|
||||
yoyo: true,
|
||||
y: originalY - 20,
|
||||
loop: 1,
|
||||
onStart: () => {
|
||||
scene.playSound("PRSFX- Taunt2");
|
||||
},
|
||||
onLoop: () => {
|
||||
scene.playSound("PRSFX- Taunt2");
|
||||
},
|
||||
onComplete: () => {
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function failCatch(scene: BattleScene, pokemon: EnemyPokemon, originalY: number, pokeball: Phaser.GameObjects.Sprite, pokeballType: PokeballType) {
|
||||
return new Promise<void>(resolve => {
|
||||
scene.playSound("pb_rel");
|
||||
pokemon.setY(originalY);
|
||||
if (pokemon.status?.effect !== StatusEffect.SLEEP) {
|
||||
pokemon.cry(pokemon.getHpRatio() > 0.25 ? undefined : { rate: 0.85 });
|
||||
}
|
||||
pokemon.tint(getPokeballTintColor(pokeballType));
|
||||
pokemon.setVisible(true);
|
||||
pokemon.untint(250, "Sine.easeOut");
|
||||
|
||||
const pokeballAtlasKey = getPokeballAtlasKey(pokeballType);
|
||||
pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`);
|
||||
scene.time.delayedCall(17, () => pokeball.setTexture("pb", `${pokeballAtlasKey}_open`));
|
||||
|
||||
scene.tweens.add({
|
||||
targets: pokemon,
|
||||
duration: 250,
|
||||
ease: "Sine.easeOut",
|
||||
scale: 1
|
||||
});
|
||||
|
||||
scene.currentBattle.lastUsedPokeball = pokeballType;
|
||||
removePb(scene, pokeball);
|
||||
|
||||
scene.ui.showText(i18next.t("battle:pokemonBrokeFree", { pokemonName: pokemon.name }), null, () => resolve(), null, true);
|
||||
});
|
||||
}
|
||||
|
||||
async function catchPokemon(scene: BattleScene, pokemon: EnemyPokemon, pokeball: Phaser.GameObjects.Sprite, pokeballType: PokeballType): Promise<void> {
|
||||
scene.unshiftPhase(new VictoryPhase(scene, BattlerIndex.ENEMY));
|
||||
|
||||
const speciesForm = !pokemon.fusionSpecies ? pokemon.getSpeciesForm() : pokemon.getFusionSpeciesForm();
|
||||
|
||||
if (speciesForm.abilityHidden && (pokemon.fusionSpecies ? pokemon.fusionAbilityIndex : pokemon.abilityIndex) === speciesForm.getAbilityCount() - 1) {
|
||||
scene.validateAchv(achvs.HIDDEN_ABILITY);
|
||||
}
|
||||
|
||||
if (pokemon.species.subLegendary) {
|
||||
scene.validateAchv(achvs.CATCH_SUB_LEGENDARY);
|
||||
}
|
||||
|
||||
if (pokemon.species.legendary) {
|
||||
scene.validateAchv(achvs.CATCH_LEGENDARY);
|
||||
}
|
||||
|
||||
if (pokemon.species.mythical) {
|
||||
scene.validateAchv(achvs.CATCH_MYTHICAL);
|
||||
}
|
||||
|
||||
scene.pokemonInfoContainer.show(pokemon, true);
|
||||
|
||||
scene.gameData.updateSpeciesDexIvs(pokemon.species.getRootSpeciesId(true), pokemon.ivs);
|
||||
|
||||
return new Promise(resolve => {
|
||||
scene.ui.showText(i18next.t("battle:pokemonCaught", { pokemonName: pokemon.name }), null, () => {
|
||||
const end = () => {
|
||||
scene.pokemonInfoContainer.hide();
|
||||
removePb(scene, pokeball);
|
||||
resolve();
|
||||
};
|
||||
const removePokemon = () => {
|
||||
scene.field.remove(pokemon, true);
|
||||
};
|
||||
const addToParty = () => {
|
||||
const newPokemon = pokemon.addToParty(pokeballType);
|
||||
const modifiers = scene.findModifiers(m => m instanceof PokemonHeldItemModifier, false);
|
||||
if (scene.getParty().filter(p => p.isShiny()).length === 6) {
|
||||
scene.validateAchv(achvs.SHINY_PARTY);
|
||||
}
|
||||
Promise.all(modifiers.map(m => scene.addModifier(m, true))).then(() => {
|
||||
scene.updateModifiers(true);
|
||||
removePokemon();
|
||||
if (newPokemon) {
|
||||
newPokemon.loadAssets().then(end);
|
||||
} else {
|
||||
end();
|
||||
}
|
||||
});
|
||||
};
|
||||
Promise.all([pokemon.hideInfo(), scene.gameData.setPokemonCaught(pokemon)]).then(() => {
|
||||
if (scene.getParty().length === 6) {
|
||||
const promptRelease = () => {
|
||||
scene.ui.showText(i18next.t("battle:partyFull", { pokemonName: pokemon.name }), null, () => {
|
||||
scene.pokemonInfoContainer.makeRoomForConfirmUi();
|
||||
scene.ui.setMode(Mode.CONFIRM, () => {
|
||||
scene.ui.setMode(Mode.PARTY, PartyUiMode.RELEASE, 0, (slotIndex: integer, _option: PartyOption) => {
|
||||
scene.ui.setMode(Mode.MESSAGE).then(() => {
|
||||
if (slotIndex < 6) {
|
||||
addToParty();
|
||||
} else {
|
||||
promptRelease();
|
||||
}
|
||||
});
|
||||
});
|
||||
}, () => {
|
||||
scene.ui.setMode(Mode.MESSAGE).then(() => {
|
||||
removePokemon();
|
||||
end();
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
promptRelease();
|
||||
} else {
|
||||
addToParty();
|
||||
}
|
||||
});
|
||||
}, 0, true);
|
||||
});
|
||||
}
|
||||
|
||||
function removePb(scene: BattleScene, pokeball: Phaser.GameObjects.Sprite) {
|
||||
scene.tweens.add({
|
||||
targets: pokeball,
|
||||
duration: 250,
|
||||
delay: 250,
|
||||
ease: "Sine.easeIn",
|
||||
alpha: 0,
|
||||
onComplete: () => pokeball.destroy()
|
||||
});
|
||||
}
|
||||
|
||||
function isPokemonFlee(pokemon: EnemyPokemon, fleeStage: number): boolean {
|
||||
const speciesCatchRate = pokemon.species.catchRate;
|
||||
const fleeModifier = (2 + Math.min(Math.max(fleeStage, 0), 6)) / (2 - Math.max(Math.min(fleeStage, 0), -6));
|
||||
const fleeRate = (255 * 255 - speciesCatchRate * speciesCatchRate) / 255 / 2 * fleeModifier;
|
||||
console.log("Flee rate: " + fleeRate);
|
||||
const roll = randSeedInt(256);
|
||||
console.log("Roll: " + roll);
|
||||
return roll < fleeRate;
|
||||
}
|
||||
|
||||
async function doPokemonFlee(scene: BattleScene, pokemon: EnemyPokemon): Promise<void> {
|
||||
const fleeAnimation = new Promise<void>(resolve => {
|
||||
// Ease pokemon out
|
||||
scene.tweens.add({
|
||||
targets: pokemon,
|
||||
x: "+=16",
|
||||
y: "-=16",
|
||||
alpha: 0,
|
||||
duration: 1000,
|
||||
ease: "Sine.easeIn",
|
||||
scale: pokemon.getSpriteScale(),
|
||||
onComplete: () => {
|
||||
pokemon.setVisible(false);
|
||||
scene.field.remove(pokemon, true);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const prompt = new Promise<void>(resolve => {
|
||||
scene.ui.showText(i18next.t("battle:pokemonFled", { pokemonName: pokemon.name }), 0, () => resolve(), 500);
|
||||
});
|
||||
|
||||
await Promise.all([fleeAnimation, prompt]);
|
||||
}
|
||||
|
||||
async function doPlayerFlee(scene: BattleScene, pokemon: EnemyPokemon): Promise<void> {
|
||||
const fleeAnimation = new Promise<void>(resolve => {
|
||||
// Ease pokemon out
|
||||
scene.tweens.add({
|
||||
targets: pokemon,
|
||||
x: "+=16",
|
||||
y: "-=16",
|
||||
alpha: 0,
|
||||
duration: 1000,
|
||||
ease: "Sine.easeIn",
|
||||
scale: pokemon.getSpriteScale(),
|
||||
onComplete: () => {
|
||||
pokemon.setVisible(false);
|
||||
scene.field.remove(pokemon, true);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const prompt = new Promise<void>(resolve => {
|
||||
scene.ui.showText(i18next.t("battle:playerFled", { pokemonName: pokemon.name }), 0, () => resolve(), 500);
|
||||
});
|
||||
|
||||
await Promise.all([fleeAnimation, prompt]);
|
||||
}
|
||||
|
||||
function tryChangeFleeStage(scene: BattleScene, change: number, chance?: number): boolean {
|
||||
if (chance && randSeedInt(10) >= chance) {
|
||||
console.log("Failed to change flee stage");
|
||||
return false;
|
||||
}
|
||||
const currentFleeStage = scene.currentBattle.mysteryEncounter.misc.fleeStage ?? 0;
|
||||
// console.log("currentFleeStage: " + currentFleeStage);
|
||||
scene.currentBattle.mysteryEncounter.misc.fleeStage = Math.min(Math.max(currentFleeStage + change, -6), 6);
|
||||
return true;
|
||||
}
|
||||
|
||||
function tryChangeCatchStage(scene: BattleScene, change: number, chance?: number): boolean {
|
||||
if (chance && randSeedInt(10) >= chance) {
|
||||
console.log("Failed to change catch stage");
|
||||
return false;
|
||||
}
|
||||
const currentCatchStage = scene.currentBattle.mysteryEncounter.misc.catchStage ?? 0;
|
||||
// console.log("currentCatchStage: " + currentCatchStage);
|
||||
scene.currentBattle.mysteryEncounter.misc.catchStage = Math.min(Math.max(currentCatchStage + change, -6), 6);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function doEndTurn(scene: BattleScene, cursorIndex: number, message?: string) {
|
||||
const pokemon = scene.currentBattle.mysteryEncounter.misc.pokemon;
|
||||
console.log("fleeStage: " + scene.currentBattle.mysteryEncounter.misc.fleeStage);
|
||||
console.log("catchStage: " + scene.currentBattle.mysteryEncounter.misc.catchStage);
|
||||
const isFlee = isPokemonFlee(pokemon, scene.currentBattle.mysteryEncounter.misc.fleeStage);
|
||||
if (isFlee) {
|
||||
// Pokemon flees!
|
||||
await doPokemonFlee(scene, pokemon);
|
||||
// Check how many safari pokemon left
|
||||
if (scene.currentBattle.mysteryEncounter.misc.safariPokemonRemaining > 0) {
|
||||
await summonSafariPokemon(scene);
|
||||
initFollowupOptionSelect(scene, { overrideOptions: safariZoneOptions, startingCursorIndex: cursorIndex, hideDescription: true });
|
||||
} else {
|
||||
// End safari mode
|
||||
leaveEncounterWithoutBattle(scene, true);
|
||||
}
|
||||
} else {
|
||||
scene.queueMessage(i18next.t(`${namespace}_pokemon_watching`, { pokemonName: pokemon.name }), 0, null, 500);
|
||||
initFollowupOptionSelect(scene, { overrideOptions: safariZoneOptions, startingCursorIndex: cursorIndex, hideDescription: true });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,15 +28,17 @@ export const ShadyVitaminDealerEncounter: IMysteryEncounter =
|
|||
fileRoot: "pokemon",
|
||||
hasShadow: true,
|
||||
repeat: true,
|
||||
x: 10,
|
||||
y: -1,
|
||||
x: 12,
|
||||
y: -5,
|
||||
yShadowOffset: -5
|
||||
},
|
||||
{
|
||||
spriteKey: "b2w2_veteran_m",
|
||||
fileRoot: "mystery-encounters",
|
||||
hasShadow: true,
|
||||
x: -10,
|
||||
y: 2,
|
||||
x: -12,
|
||||
y: 3,
|
||||
yShadowOffset: 3
|
||||
},
|
||||
])
|
||||
.withIntroDialogue([
|
||||
|
|
|
@ -10,7 +10,7 @@ import Trainer, { TrainerVariant } from "../../field/trainer";
|
|||
import { ExpBalanceModifier, ExpShareModifier, MultipleParticipantExpBonusModifier, PokemonExpBoosterModifier } from "#app/modifier/modifier";
|
||||
import { CustomModifierSettings, getModifierPoolForType, ModifierPoolType, ModifierType, ModifierTypeFunc, ModifierTypeGenerator, ModifierTypeOption, modifierTypes, PokemonHeldItemModifierType, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type";
|
||||
import { BattleEndPhase, EggLapsePhase, ExpPhase, ModifierRewardPhase, SelectModifierPhase, ShowPartyExpBarPhase, TrainerVictoryPhase } from "#app/phases";
|
||||
import { MysteryEncounterBattlePhase, MysteryEncounterRewardsPhase } from "#app/phases/mystery-encounter-phase";
|
||||
import { MysteryEncounterBattlePhase, MysteryEncounterPhase, MysteryEncounterRewardsPhase } from "#app/phases/mystery-encounter-phase";
|
||||
import * as Utils from "../../utils";
|
||||
import { isNullOrUndefined } from "#app/utils";
|
||||
import { TrainerType } from "#enums/trainer-type";
|
||||
|
@ -27,6 +27,7 @@ import { WIGHT_INCREMENT_ON_SPAWN_MISS } from "#app/data/mystery-encounters/myst
|
|||
import { getTextWithColors, TextStyle } from "#app/ui/text";
|
||||
import * as Overrides from "#app/overrides";
|
||||
import { UiTheme } from "#enums/ui-theme";
|
||||
import { MysteryEncounterUiSettings } from "#app/ui/mystery-encounter-ui-handler";
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -427,6 +428,11 @@ export function updatePlayerMoney(scene: BattleScene, changeValue: number, playS
|
|||
if (playSound) {
|
||||
scene.playSound("buy");
|
||||
}
|
||||
if (changeValue < 0) {
|
||||
scene.queueMessage(i18next.t("mysteryEncounter:paid_money", { amount: -changeValue }), null, true);
|
||||
} else {
|
||||
scene.queueMessage(i18next.t("mysteryEncounter:receive_money", { amount: changeValue }), null, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -459,7 +465,7 @@ export function generateModifierTypeOption(scene: BattleScene, modifier: () => M
|
|||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* This function is intended for use inside onPreOptionPhase() of an encounter option
|
||||
* @param scene
|
||||
* @param onPokemonSelected - Any logic that needs to be performed when Pokemon is chosen
|
||||
* If a second option needs to be selected, onPokemonSelected should return a OptionSelectItem[] object
|
||||
|
@ -674,6 +680,16 @@ export function setEncounterExp(scene: BattleScene, participantIds: integer[], b
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be used to exit an encounter without any battles or followup
|
||||
* Will skip any shops and rewards, and queue the next encounter phase as normal
|
||||
* @param scene
|
||||
* @param followupOptionSelectSettings
|
||||
*/
|
||||
export function initFollowupOptionSelect(scene: BattleScene, followupOptionSelectSettings: MysteryEncounterUiSettings) {
|
||||
scene.pushPhase(new MysteryEncounterPhase(scene, followupOptionSelectSettings));
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be used to exit an encounter without any battles or followup
|
||||
* Will skip any shops and rewards, and queue the next encounter phase as normal
|
||||
|
@ -688,7 +704,9 @@ export function leaveEncounterWithoutBattle(scene: BattleScene, addHealPhase: bo
|
|||
}
|
||||
|
||||
export function handleMysteryEncounterVictory(scene: BattleScene, addHealPhase: boolean = false) {
|
||||
if (scene.currentBattle.mysteryEncounter.encounterVariant === MysteryEncounterVariant.NO_BATTLE) {
|
||||
if (scene.currentBattle.mysteryEncounter.encounterVariant === MysteryEncounterVariant.SAFARI_BATTLE) {
|
||||
scene.pushPhase(new MysteryEncounterRewardsPhase(scene, addHealPhase));
|
||||
} else if (scene.currentBattle.mysteryEncounter.encounterVariant === MysteryEncounterVariant.NO_BATTLE) {
|
||||
scene.pushPhase(new EggLapsePhase(scene));
|
||||
scene.pushPhase(new MysteryEncounterRewardsPhase(scene, addHealPhase));
|
||||
} else if (!scene.getEnemyParty().find(p => scene.currentBattle.mysteryEncounter.encounterVariant !== MysteryEncounterVariant.TRAINER_BATTLE ? p.isOnField() : !p?.isFainted(true))) {
|
||||
|
|
|
@ -24,7 +24,8 @@ export enum MysteryEncounterVariant {
|
|||
TRAINER_BATTLE,
|
||||
WILD_BATTLE,
|
||||
BOSS_BATTLE,
|
||||
NO_BATTLE
|
||||
NO_BATTLE,
|
||||
SAFARI_BATTLE
|
||||
}
|
||||
|
||||
export enum MysteryEncounterTier {
|
||||
|
@ -118,6 +119,18 @@ export default interface IMysteryEncounter {
|
|||
*/
|
||||
expMultiplier?: number;
|
||||
|
||||
/**
|
||||
* When true, will never queue PostSummon phases from a SummonPhase
|
||||
* Defaults to false
|
||||
*/
|
||||
disableAllPostSummon?: boolean;
|
||||
|
||||
/**
|
||||
* Used for keeping RNG consistent on session resets, but increments when cycling through multiple "Encounters" on the same wave
|
||||
* You should never need to modify this
|
||||
*/
|
||||
seedOffset?: any;
|
||||
|
||||
/**
|
||||
* Generic property to set any custom data required for the encounter
|
||||
* Extremely useful for carrying state/data between onPreOptionPhase/onOptionPhase/onPostOptionPhase
|
||||
|
|
|
@ -10,6 +10,7 @@ import { MysteryEncounterType } from "#enums/mystery-encounter-type";
|
|||
import { DepartmentStoreSaleEncounter } from "./encounters/department-store-sale-encounter";
|
||||
import { ShadyVitaminDealerEncounter } from "./encounters/shady-vitamin-dealer-encounter";
|
||||
import { FieldTripEncounter } from "./encounters/field-trip-encounter";
|
||||
import { SafariZoneEncounter } from "#app/data/mystery-encounters/encounters/safari-zone-encounter";
|
||||
|
||||
// Spawn chance: (BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT + WIGHT_INCREMENT_ON_SPAWN_MISS * <number of missed spawns>) / 256
|
||||
export const BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT = 1;
|
||||
|
@ -213,6 +214,7 @@ export function initMysteryEncounters() {
|
|||
allMysteryEncounters[MysteryEncounterType.DEPARTMENT_STORE_SALE] = DepartmentStoreSaleEncounter;
|
||||
allMysteryEncounters[MysteryEncounterType.SHADY_VITAMIN_DEALER] = ShadyVitaminDealerEncounter;
|
||||
allMysteryEncounters[MysteryEncounterType.FIELD_TRIP] = FieldTripEncounter;
|
||||
allMysteryEncounters[MysteryEncounterType.SAFARI_ZONE] = SafariZoneEncounter;
|
||||
|
||||
// Add extreme encounters to biome map
|
||||
extremeBiomeEncounters.forEach(encounter => {
|
||||
|
|
|
@ -11,6 +11,7 @@ export class MysteryEncounterSpriteConfig {
|
|||
tint?: number;
|
||||
x?: number; // X offset
|
||||
y?: number; // Y offset
|
||||
yShadowOffset?: number;
|
||||
scale?: number;
|
||||
isItem?: boolean; // For item sprites, set to true
|
||||
}
|
||||
|
@ -37,10 +38,10 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con
|
|||
return;
|
||||
}
|
||||
|
||||
const getSprite = (spriteKey: string, hasShadow?: boolean) => {
|
||||
const getSprite = (spriteKey: string, hasShadow?: boolean, yShadowOffset?: number) => {
|
||||
const ret = this.scene.addFieldSprite(0, 0, spriteKey);
|
||||
ret.setOrigin(0.5, 1);
|
||||
ret.setPipeline(this.scene.spritePipeline, { tone: [0.0, 0.0, 0.0, 0.0], hasShadow: !!hasShadow });
|
||||
ret.setPipeline(this.scene.spritePipeline, { tone: [0.0, 0.0, 0.0, 0.0], hasShadow: !!hasShadow, yShadowOffset: yShadowOffset ?? 0 });
|
||||
return ret;
|
||||
};
|
||||
|
||||
|
@ -62,7 +63,7 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con
|
|||
let sprite: GameObjects.Sprite;
|
||||
let tintSprite: GameObjects.Sprite;
|
||||
if (!config.isItem) {
|
||||
sprite = getSprite(config.spriteKey, config.hasShadow);
|
||||
sprite = getSprite(config.spriteKey, config.hasShadow, config.yShadowOffset);
|
||||
tintSprite = getSprite(config.spriteKey);
|
||||
} else {
|
||||
sprite = getItemSprite(config.spriteKey);
|
||||
|
@ -83,8 +84,8 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con
|
|||
tintSprite.setPosition(origin + config.x, tintSprite.y);
|
||||
}
|
||||
if (config.y) {
|
||||
sprite.setPosition(sprite.x, config.y);
|
||||
tintSprite.setPosition(tintSprite.x, config.y);
|
||||
sprite.setPosition(sprite.x, sprite.y + config.y);
|
||||
tintSprite.setPosition(tintSprite.x, tintSprite.y + config.y);
|
||||
}
|
||||
} else {
|
||||
// Single sprite
|
||||
|
|
|
@ -16,6 +16,9 @@ export const battle: SimpleTranslationEntries = {
|
|||
"moneyWon": "You got\n₽{{moneyAmount}} for winning!",
|
||||
"moneyPickedUp": "You picked up ₽{{moneyAmount}}!",
|
||||
"pokemonCaught": "{{pokemonName}} was caught!",
|
||||
"pokemonBrokeFree": "Oh no!\nThe Pokémon broke free!",
|
||||
"pokemonFled": "The wild {{pokemonName}} fled!",
|
||||
"playerFled": "You fled from the {{pokemonName}}!",
|
||||
"addedAsAStarter": "{{pokemonName}} has been\nadded as a starter!",
|
||||
"partyFull": "Your party is full.\nRelease a Pokémon to make room for {{pokemonName}}?",
|
||||
"pokemon": "Pokémon",
|
||||
|
|
|
@ -17,6 +17,10 @@ export const mysteryEncounter: SimpleTranslationEntries = {
|
|||
// DO NOT REMOVE
|
||||
"unit_test_dialogue": "{{test}}{{test}} {{test{{test}}}} {{test1}} {{test\}} {{test\\}} {{test\\\}} {test}}",
|
||||
|
||||
// General use content
|
||||
"paid_money": "You paid ₽{{amount, number}}.",
|
||||
"receive_money": "You received ₽{{amount, number}}!",
|
||||
|
||||
// Mystery Encounters -- Common Tier
|
||||
|
||||
"mysterious_chest_intro_message": "You found...@d{32} a chest?",
|
||||
|
@ -125,7 +129,7 @@ export const mysteryEncounter: SimpleTranslationEntries = {
|
|||
"field_trip_outro_good": "Thank you so much for your kindness!\nI hope the items I had were helpful!",
|
||||
"field_trip_outro_bad": "Come along children, we'll\nfind a better demonstration elsewhere.",
|
||||
|
||||
// Mystery Encounters -- Uncommon Tier
|
||||
// Mystery Encounters -- Great Tier
|
||||
|
||||
"mysterious_challengers_intro_message": "Mysterious challengers have appeared!",
|
||||
"mysterious_challengers_title": "Mysterious Challengers",
|
||||
|
@ -140,7 +144,35 @@ export const mysteryEncounter: SimpleTranslationEntries = {
|
|||
"mysterious_challengers_option_selected_message": "The trainer steps forward...",
|
||||
"mysterious_challengers_outro_win": "The mysterious challenger was defeated!",
|
||||
|
||||
// Mystery Encounters -- Rare Tier
|
||||
"safari_zone_intro_message": "It's a safari zone!",
|
||||
"safari_zone_title": "The Safari Zone",
|
||||
"safari_zone_description": "There are all kinds of rare and special Pokémon that can be found here!\nIf you choose to enter, you'll have a time limit of 3 wild encounters where you can try to catch these special Pokémon.\nBeware, though. These Pokémon may flee before you're able to catch them!",
|
||||
"safari_zone_query": "Would you like to enter?",
|
||||
"safari_zone_option_1_label": "Enter",
|
||||
"safari_zone_option_1_tooltip": "(-) Pay {{option1Money, money}}\n@[SUMMARY_GREEN]{(?) Safari Zone}",
|
||||
"safari_zone_option_2_label": "Leave",
|
||||
"safari_zone_option_2_tooltip": "(-) No Rewards",
|
||||
"safari_zone_option_1_selected_message": "Time to test your luck.",
|
||||
"safari_zone_option_2_selected_message": "You hurry along your way,\nwith a slight feeling of regret.",
|
||||
"safari_zone_pokeball_option_label": "Throw a Pokéball",
|
||||
"safari_zone_pokeball_option_tooltip": "(+) Throw a Pokéball",
|
||||
"safari_zone_pokeball_option_selected": "You throw a Pokéball!",
|
||||
"safari_zone_bait_option_label": "Throw bait",
|
||||
"safari_zone_bait_option_tooltip": "(+) Increases Capture Rate\n(-) Chance to Increase Flee Rate",
|
||||
"safari_zone_bait_option_selected": "You throw some bait!",
|
||||
"safari_zone_mud_option_label": "Throw mud",
|
||||
"safari_zone_mud_option_tooltip": "(+) Decreases Flee Rate\n(-) Chance to Decrease Capture Rate",
|
||||
"safari_zone_mud_option_selected": "You throw some mud!",
|
||||
"safari_zone_flee_option_label": "Flee",
|
||||
"safari_zone_flee_option_tooltip": "(?) Flee from this Pokémon",
|
||||
"safari_zone_pokemon_watching": "{{pokemonName}} is watching carefully!",
|
||||
"safari_zone_pokemon_eating": "{{pokemonName}} is eating!",
|
||||
"safari_zone_pokemon_busy_eating": "{{pokemonName}} is busy eating!",
|
||||
"safari_zone_pokemon_angry": "{{pokemonName}} is angry!",
|
||||
"safari_zone_pokemon_beside_itself_angry": "{{pokemonName}} is beside itself with anger!",
|
||||
"safari_zone_remaining_count": "{{remainingCount}} Pokémon remaining!",
|
||||
|
||||
// Mystery Encounters -- Ultra Tier
|
||||
|
||||
"training_session_intro_message": "You've come across some\ntraining tools and supplies.",
|
||||
"training_session_title": "Training Session",
|
||||
|
@ -163,7 +195,7 @@ export const mysteryEncounter: SimpleTranslationEntries = {
|
|||
$Its ability was changed to {{ability}}!`,
|
||||
"training_session_outro_win": "That was a successful training session!",
|
||||
|
||||
// Mystery Encounters -- Super Rare Tier
|
||||
// Mystery Encounters -- Rogue Tier
|
||||
|
||||
"dark_deal_intro_message": "A strange man in a tattered coat\nstands in your way...",
|
||||
"dark_deal_speaker": "Shady Guy",
|
||||
|
|
|
@ -1650,7 +1650,7 @@ export class SummonPhase extends PartyMemberPokemonPhase {
|
|||
pokemon.untint(250, "Sine.easeIn");
|
||||
this.scene.updateFieldScale();
|
||||
pokemon.x += 16;
|
||||
pokemon.y -= 16;
|
||||
pokemon.y -= 20;
|
||||
pokemon.alpha = 0;
|
||||
|
||||
// Ease pokemon in
|
||||
|
@ -1680,7 +1680,9 @@ export class SummonPhase extends PartyMemberPokemonPhase {
|
|||
|
||||
pokemon.resetTurnData();
|
||||
|
||||
if (!this.loaded || this.scene.currentBattle.battleType === BattleType.TRAINER || this.scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER || (this.scene.currentBattle.waveIndex % 10) === 1) {
|
||||
const addPostSummonForEncounter = this.scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER && !this.scene.currentBattle.mysteryEncounter?.disableAllPostSummon;
|
||||
|
||||
if (!this.loaded || this.scene.currentBattle.battleType === BattleType.TRAINER || (this.scene.currentBattle.waveIndex % 10) === 1 || addPostSummonForEncounter) {
|
||||
this.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true);
|
||||
this.queuePostSummon();
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import { Tutorial, handleTutorial } from "../tutorial";
|
|||
import { IvScannerModifier } from "../modifier/modifier";
|
||||
import * as Utils from "../utils";
|
||||
import { isNullOrUndefined } from "../utils";
|
||||
import { MysteryEncounterUiSettings } from "#app/ui/mystery-encounter-ui-handler";
|
||||
|
||||
/**
|
||||
* Will handle (in order):
|
||||
|
@ -26,8 +27,11 @@ import { isNullOrUndefined } from "../utils";
|
|||
* - Queuing of the MysteryEncounterOptionSelectedPhase
|
||||
*/
|
||||
export class MysteryEncounterPhase extends Phase {
|
||||
constructor(scene: BattleScene) {
|
||||
followupOptionSelectSettings: MysteryEncounterUiSettings;
|
||||
|
||||
constructor(scene: BattleScene, followupOptionSelectSettings?: MysteryEncounterUiSettings) {
|
||||
super(scene);
|
||||
this.followupOptionSelectSettings = followupOptionSelectSettings;
|
||||
}
|
||||
|
||||
start() {
|
||||
|
@ -37,12 +41,18 @@ export class MysteryEncounterPhase extends Phase {
|
|||
this.scene.clearPhaseQueue();
|
||||
this.scene.clearPhaseQueueSplice();
|
||||
|
||||
// Sets flag that ME was encountered
|
||||
// Can be used in later MEs to check for requirements to spawn, etc.
|
||||
this.scene.mysteryEncounterData.encounteredEvents.push([this.scene.currentBattle.mysteryEncounter.encounterType, this.scene.currentBattle.mysteryEncounter.encounterTier]);
|
||||
// Generates seed offset for RNG consistency, but incremented if the same MysteryEncounter has multiple option select cycles
|
||||
const offset = this.scene.currentBattle.mysteryEncounter.seedOffset ?? this.scene.currentBattle.waveIndex * 1000;
|
||||
this.scene.currentBattle.mysteryEncounter.seedOffset = offset + 512;
|
||||
|
||||
if (!this.followupOptionSelectSettings) {
|
||||
// Sets flag that ME was encountered, only if this is not a followup option select phase
|
||||
// Can be used in later MEs to check for requirements to spawn, etc.
|
||||
this.scene.mysteryEncounterData.encounteredEvents.push([this.scene.currentBattle.mysteryEncounter.encounterType, this.scene.currentBattle.mysteryEncounter.encounterTier]);
|
||||
}
|
||||
|
||||
// Initiates encounter dialogue window and option select
|
||||
this.scene.ui.setMode(Mode.MYSTERY_ENCOUNTER);
|
||||
this.scene.ui.setMode(Mode.MYSTERY_ENCOUNTER, this.followupOptionSelectSettings);
|
||||
}
|
||||
|
||||
handleOptionSelect(option: MysteryEncounterOption, index: number): boolean {
|
||||
|
@ -61,24 +71,24 @@ export class MysteryEncounterPhase extends Phase {
|
|||
return await option.onPreOptionPhase(this.scene)
|
||||
.then((result) => {
|
||||
if (isNullOrUndefined(result) || result) {
|
||||
this.continueEncounter(index);
|
||||
this.continueEncounter();
|
||||
}
|
||||
});
|
||||
}, this.scene.currentBattle.waveIndex * 1000);
|
||||
}, this.scene.currentBattle.mysteryEncounter.seedOffset);
|
||||
} else {
|
||||
this.continueEncounter(index);
|
||||
this.continueEncounter();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
continueEncounter(optionIndex: number) {
|
||||
continueEncounter() {
|
||||
const endDialogueAndContinueEncounter = () => {
|
||||
this.scene.pushPhase(new MysteryEncounterOptionSelectedPhase(this.scene));
|
||||
this.end();
|
||||
};
|
||||
|
||||
const optionSelectDialogue = this.scene.currentBattle?.mysteryEncounter?.options?.[optionIndex]?.dialogue;
|
||||
const optionSelectDialogue = this.scene.currentBattle?.mysteryEncounter?.selectedOption?.dialogue;
|
||||
if (optionSelectDialogue?.selected?.length > 0) {
|
||||
// Handle intermediate dialogue (between player selection event and the onOptionSelect logic)
|
||||
this.scene.ui.setMode(Mode.MESSAGE);
|
||||
|
@ -139,14 +149,14 @@ export class MysteryEncounterOptionSelectedPhase extends Phase {
|
|||
this.onOptionSelect(this.scene).finally(() => {
|
||||
this.end();
|
||||
});
|
||||
}, this.scene.currentBattle.waveIndex * 1000);
|
||||
}, this.scene.currentBattle.mysteryEncounter.seedOffset);
|
||||
});
|
||||
} else {
|
||||
this.scene.executeWithSeedOffset(() => {
|
||||
this.onOptionSelect(this.scene).finally(() => {
|
||||
this.end();
|
||||
});
|
||||
}, this.scene.currentBattle.waveIndex * 1000);
|
||||
}, this.scene.currentBattle.mysteryEncounter.seedOffset);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -265,7 +275,7 @@ export class MysteryEncounterBattlePhase extends Phase {
|
|||
} else {
|
||||
const trainer = this.scene.currentBattle.trainer;
|
||||
let message: string;
|
||||
scene.executeWithSeedOffset(() => message = Utils.randSeedItem(encounterMessages), scene.currentBattle.waveIndex * 1000);
|
||||
scene.executeWithSeedOffset(() => message = Utils.randSeedItem(encounterMessages), this.scene.currentBattle.mysteryEncounter.seedOffset);
|
||||
|
||||
const showDialogueAndSummon = () => {
|
||||
scene.ui.showDialogue(message, trainer.getName(TrainerSlot.NONE, true), null, () => {
|
||||
|
@ -390,6 +400,7 @@ export class MysteryEncounterRewardsPhase extends Phase {
|
|||
this.scene.tryRemovePhase(p => p instanceof SelectModifierPhase);
|
||||
this.scene.unshiftPhase(new SelectModifierPhase(this.scene, 0, null, { fillRemaining: false, rerollMultiplier: 0 }));
|
||||
}
|
||||
// Do not use ME's seedOffset for rewards, these should always be consistent with waveIndex (once per wave)
|
||||
}, this.scene.currentBattle.waveIndex * 1000);
|
||||
|
||||
this.scene.pushPhase(new PostMysteryEncounterPhase(this.scene));
|
||||
|
@ -423,7 +434,7 @@ export class PostMysteryEncounterPhase extends Phase {
|
|||
this.continueEncounter();
|
||||
}
|
||||
});
|
||||
}, this.scene.currentBattle.waveIndex * 1000);
|
||||
}, this.scene.currentBattle.mysteryEncounter.seedOffset);
|
||||
} else {
|
||||
this.continueEncounter();
|
||||
}
|
||||
|
|
|
@ -38,6 +38,7 @@ uniform vec2 texFrameUv;
|
|||
uniform vec2 size;
|
||||
uniform vec2 texSize;
|
||||
uniform float yOffset;
|
||||
uniform float yShadowOffset;
|
||||
uniform vec4 tone;
|
||||
uniform ivec4 baseVariantColors[32];
|
||||
uniform vec4 variantColors[32];
|
||||
|
@ -252,7 +253,7 @@ void main() {
|
|||
float width = size.x - (yOffset / 2.0);
|
||||
|
||||
float spriteX = ((floor(outPosition.x / fieldScale) - relPosition.x) / width) + 0.5;
|
||||
float spriteY = ((floor(outPosition.y / fieldScale) - relPosition.y) / size.y);
|
||||
float spriteY = ((floor(outPosition.y / fieldScale) - relPosition.y - yShadowOffset) / size.y);
|
||||
|
||||
if (yCenter == 1) {
|
||||
spriteY += 0.5;
|
||||
|
@ -339,6 +340,7 @@ export default class SpritePipeline extends FieldSpritePipeline {
|
|||
this.set2f("size", 0, 0);
|
||||
this.set2f("texSize", 0, 0);
|
||||
this.set1f("yOffset", 0);
|
||||
this.set1f("yShadowOffset", 0);
|
||||
this.set4fv("tone", this._tone);
|
||||
}
|
||||
|
||||
|
@ -351,6 +353,7 @@ export default class SpritePipeline extends FieldSpritePipeline {
|
|||
const tone = data["tone"] as number[];
|
||||
const teraColor = data["teraColor"] as integer[] ?? [ 0, 0, 0 ];
|
||||
const hasShadow = data["hasShadow"] as boolean;
|
||||
const yShadowOffset = data["yShadowOffset"] as number;
|
||||
const ignoreFieldPos = data["ignoreFieldPos"] as boolean;
|
||||
const ignoreOverride = data["ignoreOverride"] as boolean;
|
||||
|
||||
|
@ -377,6 +380,7 @@ export default class SpritePipeline extends FieldSpritePipeline {
|
|||
this.set2f("size", sprite.frame.width, sprite.height);
|
||||
this.set2f("texSize", sprite.texture.source[0].width, sprite.texture.source[0].height);
|
||||
this.set1f("yOffset", sprite.height - sprite.frame.height * (isEntityObj ? sprite.parentContainer.scale : sprite.scale));
|
||||
this.set1f("yShadowOffset", yShadowOffset ?? 0);
|
||||
this.set4fv("tone", tone);
|
||||
this.bindTexture(this.game.textures.get("tera").source[0].glTexture, 1);
|
||||
|
||||
|
@ -448,6 +452,7 @@ export default class SpritePipeline extends FieldSpritePipeline {
|
|||
this.set1f("vCutoff", v1);
|
||||
|
||||
const hasShadow = sprite.pipelineData["hasShadow"] as boolean;
|
||||
const yShadowOffset = sprite.pipelineData["yShadowOffset"] as number;
|
||||
if (hasShadow) {
|
||||
const isEntityObj = sprite.parentContainer instanceof Pokemon || sprite.parentContainer instanceof Trainer || sprite.parentContainer instanceof MysteryEncounterIntroVisuals;
|
||||
const field = isEntityObj ? sprite.parentContainer.parentContainer : sprite.parentContainer;
|
||||
|
@ -455,7 +460,7 @@ export default class SpritePipeline extends FieldSpritePipeline {
|
|||
const baseY = (isEntityObj
|
||||
? sprite.parentContainer.y
|
||||
: sprite.y + sprite.height) * 6 / fieldScaleRatio;
|
||||
const bottomPadding = Math.ceil(sprite.height * 0.05) * 6 / fieldScaleRatio;
|
||||
const bottomPadding = Math.ceil(sprite.height * 0.05 + Math.max(yShadowOffset, 0)) * 6 / fieldScaleRatio;
|
||||
const yDelta = (baseY - y1) / field.scale;
|
||||
y2 = y1 = baseY + bottomPadding;
|
||||
const pixelHeight = (v1 - v0) / (sprite.frame.height * (isEntityObj ? sprite.parentContainer.scale : sprite.scale));
|
||||
|
|
|
@ -12,6 +12,16 @@ import { isNullOrUndefined } from "../utils";
|
|||
import { getPokeballAtlasKey } from "../data/pokeball";
|
||||
import { getEncounterText } from "#app/data/mystery-encounters/mystery-encounter-utils";
|
||||
|
||||
export class MysteryEncounterUiSettings {
|
||||
hideDescription?: boolean;
|
||||
slideInDescription?: boolean;
|
||||
overrideTitle?: string;
|
||||
overrideDescription?: string;
|
||||
overrideQuery?: string;
|
||||
overrideOptions?: MysteryEncounterOption[];
|
||||
startingCursorIndex?: number;
|
||||
}
|
||||
|
||||
export default class MysteryEncounterUiHandler extends UiHandler {
|
||||
private cursorContainer: Phaser.GameObjects.Container;
|
||||
private cursorObj: Phaser.GameObjects.Image;
|
||||
|
@ -27,7 +37,8 @@ export default class MysteryEncounterUiHandler extends UiHandler {
|
|||
private descriptionScrollTween: Phaser.Tweens.Tween;
|
||||
private rarityBall: Phaser.GameObjects.Sprite;
|
||||
|
||||
private filteredEncounterOptions: MysteryEncounterOption[] = [];
|
||||
private overrideSettings: MysteryEncounterUiSettings;
|
||||
private encounterOptions: MysteryEncounterOption[] = [];
|
||||
private optionsMeetsReqs: boolean[];
|
||||
|
||||
protected viewPartyIndex: integer = 0;
|
||||
|
@ -70,16 +81,21 @@ export default class MysteryEncounterUiHandler extends UiHandler {
|
|||
show(args: any[]): boolean {
|
||||
super.show(args);
|
||||
|
||||
this.overrideSettings = args[0] as MysteryEncounterUiSettings ?? {};
|
||||
const showDescriptionContainer = isNullOrUndefined(this.overrideSettings?.hideDescription) ? true : !this.overrideSettings?.hideDescription;
|
||||
const slideInDescription = isNullOrUndefined(this.overrideSettings?.slideInDescription) ? true : this.overrideSettings?.slideInDescription;
|
||||
const startingCursorIndex = this.overrideSettings?.startingCursorIndex ?? 0;
|
||||
|
||||
this.cursorContainer.setVisible(true);
|
||||
this.descriptionContainer.setVisible(true);
|
||||
this.descriptionContainer.setVisible(showDescriptionContainer);
|
||||
this.optionsContainer.setVisible(true);
|
||||
this.displayEncounterOptions(!(args[0] as boolean || false));
|
||||
this.displayEncounterOptions(slideInDescription);
|
||||
const cursor = this.getCursor();
|
||||
if (cursor === (this?.optionsContainer?.length || 0) - 1) {
|
||||
// Always resets cursor on view party button if it was last there
|
||||
this.setCursor(cursor);
|
||||
} else {
|
||||
this.setCursor(0);
|
||||
this.setCursor(startingCursorIndex);
|
||||
}
|
||||
if (this.blockInput) {
|
||||
setTimeout(() => {
|
||||
|
@ -100,12 +116,16 @@ export default class MysteryEncounterUiHandler extends UiHandler {
|
|||
|
||||
if (button === Button.CANCEL || button === Button.ACTION) {
|
||||
if (button === Button.ACTION) {
|
||||
const selected = this.filteredEncounterOptions[cursor];
|
||||
const selected = this.encounterOptions[cursor];
|
||||
if (cursor === this.viewPartyIndex) {
|
||||
// Handle view party
|
||||
success = true;
|
||||
const overrideSettings: MysteryEncounterUiSettings = {
|
||||
...this.overrideSettings,
|
||||
slideInDescription: false
|
||||
};
|
||||
this.scene.ui.setMode(Mode.PARTY, PartyUiMode.CHECK, -1, () => {
|
||||
this.scene.ui.setMode(Mode.MYSTERY_ENCOUNTER, true);
|
||||
this.scene.ui.setMode(Mode.MYSTERY_ENCOUNTER, overrideSettings);
|
||||
setTimeout(() => {
|
||||
this.setCursor(this.viewPartyIndex);
|
||||
this.unblockInput();
|
||||
|
@ -253,7 +273,7 @@ export default class MysteryEncounterUiHandler extends UiHandler {
|
|||
if (this.blockInput) {
|
||||
this.blockInput = false;
|
||||
for (let i = 0; i < this.optionsContainer.length - 1; i++) {
|
||||
const optionMode = this.filteredEncounterOptions[i].optionMode;
|
||||
const optionMode = this.encounterOptions[i].optionMode;
|
||||
if (!this.optionsMeetsReqs[i] && (optionMode === EncounterOptionMode.DISABLED_OR_DEFAULT || optionMode === EncounterOptionMode.DISABLED_OR_SPECIAL)) {
|
||||
continue;
|
||||
}
|
||||
|
@ -296,7 +316,7 @@ export default class MysteryEncounterUiHandler extends UiHandler {
|
|||
displayEncounterOptions(slideInDescription: boolean = true): void {
|
||||
this.getUi().clearText();
|
||||
const mysteryEncounter = this.scene.currentBattle.mysteryEncounter;
|
||||
this.filteredEncounterOptions = mysteryEncounter.options;
|
||||
this.encounterOptions = this.overrideSettings?.overrideOptions ?? mysteryEncounter.options;
|
||||
this.optionsMeetsReqs = [];
|
||||
|
||||
const titleText: string = getEncounterText(this.scene, mysteryEncounter.dialogue.encounterOptionsDialogue.title, TextStyle.TOOLTIP_TITLE);
|
||||
|
@ -307,11 +327,11 @@ export default class MysteryEncounterUiHandler extends UiHandler {
|
|||
this.optionsContainer.removeAll();
|
||||
|
||||
// Options Window
|
||||
for (let i = 0; i < this.filteredEncounterOptions.length; i++) {
|
||||
const option = this.filteredEncounterOptions[i];
|
||||
for (let i = 0; i < this.encounterOptions.length; i++) {
|
||||
const option = this.encounterOptions[i];
|
||||
|
||||
let optionText;
|
||||
switch (this.filteredEncounterOptions.length) {
|
||||
switch (this.encounterOptions.length) {
|
||||
case 2:
|
||||
optionText = addBBCodeTextObject(this.scene, i % 2 === 0 ? 0 : 100, 8, "-", TextStyle.WINDOW, { wordWrap: { width: 558 }, fontSize: "80px", lineSpacing: -8 });
|
||||
break;
|
||||
|
@ -424,7 +444,7 @@ export default class MysteryEncounterUiHandler extends UiHandler {
|
|||
}
|
||||
|
||||
let text: string;
|
||||
const cursorOption = this.filteredEncounterOptions[cursor];
|
||||
const cursorOption = this.encounterOptions[cursor];
|
||||
const optionDialogue = cursorOption.dialogue;
|
||||
if (!this.optionsMeetsReqs[cursor] && (cursorOption.optionMode === EncounterOptionMode.DISABLED_OR_DEFAULT || cursorOption.optionMode === EncounterOptionMode.DISABLED_OR_SPECIAL) && optionDialogue.disabledButtonTooltip) {
|
||||
text = getEncounterText(this.scene, optionDialogue.disabledButtonTooltip, TextStyle.TOOLTIP_CONTENT);
|
||||
|
@ -474,6 +494,7 @@ export default class MysteryEncounterUiHandler extends UiHandler {
|
|||
|
||||
clear(): void {
|
||||
super.clear();
|
||||
this.overrideSettings = null;
|
||||
this.optionsContainer.setVisible(false);
|
||||
this.optionsContainer.removeAll(true);
|
||||
this.descriptionContainer.setVisible(false);
|
||||
|
|
Loading…
Reference in New Issue