Compare commits

...

13 Commits

Author SHA1 Message Date
NightKev 5a43a906cf
Merge 316acc1d0d into 51bb80cb66 2024-09-19 03:18:47 -07:00
NightKev 316acc1d0d
Merge branch 'beta' into nuzlocke-update 2024-09-19 03:18:44 -07:00
NightKev d381734aca Add a check for MEs on the X1 wave
This allows capture on the X2 wave instead
2024-09-19 03:18:22 -07:00
NightKev b0c9fbe56b Change in-game EN name of "No Auto Heal" challenge to "No Free Heal" 2024-09-19 02:50:30 -07:00
NightKev c7df07715d Prevent `PartyHealPhase` from reviving pokemon in hardcore challenge
Add `SelectModifierPhase` on waves 10, 20, ...
if "No Auto Heal" challenge is active
2024-09-19 02:48:14 -07:00
NightKev f292087d6c Consolidate identical variables 2024-09-19 02:08:31 -07:00
MokaStitcher 51bb80cb66
[Bug][UI] Fix scrolling UIs not resetting properly and add Scrollbars (#4312)
* [bug] fix scrollable elements not resetting properly

* [ui] add wrap around and scrolling bar to the achievements menu

* [ui] add scrollbar to the settings
2024-09-18 19:53:30 -04:00
innerthunder 605ae9e1c3
[Move] Improved damage forecasting for Shell Side Arm (#4310) 2024-09-18 19:03:01 -04:00
Madmadness65 81ea1296b3
[Miscellaneous] Add new Lake and RUins biome BGM by Lmz (#4319)
* Add new biome BGM by Firel

* Add new biome BGM by Lmz

* Update bgm-name.json
2024-09-18 19:00:06 -04:00
NightKev c6a7ac225c Uncomment code 2024-09-18 07:26:02 -07:00
NightKev 8b9b416cd0 Add English text, remove redundant code 2024-09-18 06:36:02 -07:00
NightKev 13d1dd52b2 Update tsdocs and imports 2024-09-18 03:41:35 -07:00
NightKev eadc8f046a Split Nuzlocke challenge into individual challenges 2024-09-18 03:03:04 -07:00
26 changed files with 957 additions and 417 deletions

View File

@ -56,7 +56,7 @@ Check out [Github Issues](https://github.com/pagefaultgames/pokerogue/issues) to
- Pokémon Legends: Arceus
- Pokémon Scarlet/Violet
- Firel (Custom Ice Cave, Laboratory, Metropolis, Plains, Power Plant, Seabed, Space, and Volcano biome music)
- Lmz (Custom Jungle biome music)
- Lmz (Custom Ancient Ruins, Jungle, and Lake biome music)
- Andr06 (Custom Slum and Sea biome music)
### 🎵 Sound Effects

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -3974,18 +3974,17 @@ export class StatusCategoryOnAllyAttr extends VariableMoveCategoryAttr {
export class ShellSideArmCategoryAttr extends VariableMoveCategoryAttr {
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const category = (args[0] as Utils.NumberHolder);
const atkRatio = user.getEffectiveStat(Stat.ATK, target, move) / target.getEffectiveStat(Stat.DEF, user, move);
const specialRatio = user.getEffectiveStat(Stat.SPATK, target, move) / target.getEffectiveStat(Stat.SPDEF, user, move);
// Shell Side Arm is much more complicated than it looks, this is a partial implementation to try to achieve something similar to the games
if (atkRatio > specialRatio) {
const predictedPhysDmg = target.getBaseDamage(user, move, MoveCategory.PHYSICAL, true, true);
const predictedSpecDmg = target.getBaseDamage(user, move, MoveCategory.SPECIAL, true, true);
if (predictedPhysDmg > predictedSpecDmg) {
category.value = MoveCategory.PHYSICAL;
return true;
} else if (atkRatio === specialRatio && user.randSeedInt(2) === 0) {
} else if (predictedPhysDmg === predictedSpecDmg && user.randSeedInt(2) === 0) {
category.value = MoveCategory.PHYSICAL;
return true;
}
return false;
}
}
@ -9106,7 +9105,7 @@ export function initMoves() {
new AttackMove(Moves.SHELL_SIDE_ARM, Type.POISON, MoveCategory.SPECIAL, 90, 100, 10, 20, 0, 8)
.attr(ShellSideArmCategoryAttr)
.attr(StatusEffectAttr, StatusEffect.POISON)
.partial(),
.partial(), // Physical version of the move does not make contact
new AttackMove(Moves.MISTY_EXPLOSION, Type.FAIRY, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 8)
.attr(SacrificialAttr)
.target(MoveTarget.ALL_NEAR_OTHERS)

View File

@ -5,4 +5,8 @@ export enum Challenges {
LOWER_STARTER_POINTS,
FRESH_START,
INVERSE_BATTLE,
NO_AUTO_HEAL,
HARDCORE,
NO_LEGENDS,
LIMITED_CATCH,
}

View File

@ -762,7 +762,7 @@ export class Arena {
case Biome.BEACH:
return 3.462;
case Biome.LAKE:
return 5.350;
return 7.215;
case Biome.SEABED:
return 2.600;
case Biome.MOUNTAIN:
@ -788,7 +788,7 @@ export class Arena {
case Biome.FACTORY:
return 4.985;
case Biome.RUINS:
return 2.270;
return 0.000;
case Biome.WASTELAND:
return 6.336;
case Biome.ABYSS:

View File

@ -2322,11 +2322,61 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
return accuracyMultiplier.value / evasionMultiplier.value;
}
/**
* Calculates the base damage of the given move against this Pokemon when attacked by the given source.
* Used during damage calculation and for Shell Side Arm's forecasting effect.
* @param source the attacking {@linkcode Pokemon}.
* @param move the {@linkcode Move} used in the attack.
* @param moveCategory the move's {@linkcode MoveCategory} after variable-category effects are applied.
* @param ignoreAbility if `true`, ignores this Pokemon's defensive ability effects (defaults to `false`).
* @param ignoreSourceAbility if `true`, ignore's the attacking Pokemon's ability effects (defaults to `false`).
* @param isCritical if `true`, calculates effective stats as if the hit were critical (defaults to `false`).
* @param simulated if `true`, suppresses changes to game state during calculation (defaults to `true`).
* @returns The move's base damage against this Pokemon when used by the source Pokemon.
*/
getBaseDamage(source: Pokemon, move: Move, moveCategory: MoveCategory, ignoreAbility: boolean = false, ignoreSourceAbility: boolean = false, isCritical: boolean = false, simulated: boolean = true): number {
const isPhysical = moveCategory === MoveCategory.PHYSICAL;
/** A base damage multiplier based on the source's level */
const levelMultiplier = (2 * source.level / 5 + 2);
/** The power of the move after power boosts from abilities, etc. have applied */
const power = move.calculateBattlePower(source, this, simulated);
/**
* The attacker's offensive stat for the given move's category.
* Critical hits cause negative stat stages to be ignored.
*/
const sourceAtk = new Utils.NumberHolder(source.getEffectiveStat(isPhysical ? Stat.ATK : Stat.SPATK, this, undefined, ignoreSourceAbility, ignoreAbility, isCritical, simulated));
applyMoveAttrs(VariableAtkAttr, source, this, move, sourceAtk);
/**
* This Pokemon's defensive stat for the given move's category.
* Critical hits cause positive stat stages to be ignored.
*/
const targetDef = new Utils.NumberHolder(this.getEffectiveStat(isPhysical ? Stat.DEF : Stat.SPDEF, source, move, ignoreAbility, ignoreSourceAbility, isCritical, simulated));
applyMoveAttrs(VariableDefAttr, source, this, move, targetDef);
/**
* The attack's base damage, as determined by the source's level, move power
* and Attack stat as well as this Pokemon's Defense stat
*/
const baseDamage = ((levelMultiplier * power * sourceAtk.value / targetDef.value) / 50) + 2;
/** Debug message for non-simulated calls (i.e. when damage is actually dealt) */
if (!simulated) {
console.log("base damage", baseDamage, move.name, power, sourceAtk.value, targetDef.value);
}
return baseDamage;
}
/**
* Calculates the damage of an attack made by another Pokemon against this Pokemon
* @param source {@linkcode Pokemon} the attacking Pokemon
* @param move {@linkcode Pokemon} the move used in the attack
* @param ignoreAbility If `true`, ignores this Pokemon's defensive ability effects
* @param ignoreSourceAbility If `true`, ignores the attacking Pokemon's ability effects
* @param isCritical If `true`, calculates damage for a critical hit.
* @param simulated If `true`, suppresses changes to game state during the calculation.
* @returns a {@linkcode DamageCalculationResult} object with three fields:
@ -2395,35 +2445,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
};
}
// ----- BEGIN BASE DAMAGE MULTIPLIERS -----
/** A base damage multiplier based on the source's level */
const levelMultiplier = (2 * source.level / 5 + 2);
/** The power of the move after power boosts from abilities, etc. have applied */
const power = move.calculateBattlePower(source, this, simulated);
/**
* The attacker's offensive stat for the given move's category.
* Critical hits ignore negative stat stages.
*/
const sourceAtk = new Utils.NumberHolder(source.getEffectiveStat(isPhysical ? Stat.ATK : Stat.SPATK, this, undefined, ignoreSourceAbility, ignoreAbility, isCritical, simulated));
applyMoveAttrs(VariableAtkAttr, source, this, move, sourceAtk);
/**
* This Pokemon's defensive stat for the given move's category.
* Critical hits ignore positive stat stages.
*/
const targetDef = new Utils.NumberHolder(this.getEffectiveStat(isPhysical ? Stat.DEF : Stat.SPDEF, source, move, ignoreAbility, ignoreSourceAbility, isCritical, simulated));
applyMoveAttrs(VariableDefAttr, source, this, move, targetDef);
/**
* The attack's base damage, as determined by the source's level, move power
* and Attack stat as well as this Pokemon's Defense stat
*/
const baseDamage = ((levelMultiplier * power * sourceAtk.value / targetDef.value) / 50) + 2;
// ------ END BASE DAMAGE MULTIPLIERS ------
const baseDamage = this.getBaseDamage(source, move, moveCategory, ignoreAbility, ignoreSourceAbility, isCritical, simulated);
/** 25% damage debuff on moves hitting more than one non-fainted target (regardless of immunities) */
const { targets, multiple } = getMoveTargets(source, move.id);
@ -2549,7 +2575,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
// debug message for when damage is applied (i.e. not simulated)
if (!simulated) {
console.log("damage", damage.value, move.name, power, sourceAtk, targetDef);
console.log("damage", damage.value, move.name);
}
let hitResult: HitResult;

View File

@ -112,13 +112,13 @@
"island": "PMD EoS Craggy Coast",
"jungle": "Lmz - Jungle",
"laboratory": "Firel - Laboratory",
"lake": "PMD EoS Crystal Cave",
"lake": "Lmz - Lake",
"meadow": "PMD EoS Sky Peak Forest",
"metropolis": "Firel - Metropolis",
"mountain": "PMD EoS Mt. Horn",
"plains": "Firel - Route 888",
"power_plant": "Firel - The Klink",
"ruins": "PMD EoS Deep Sealed Ruin",
"ruins": "Lmz - Ancient Ruins",
"sea": "Andr06 - Marine Mystique",
"seabed": "Firel - Seabed",
"slum": "Andr06 - Sneaky Snom",

View File

@ -33,5 +33,29 @@
"desc": "Type matchups are reversed and no type is immune to any other type.\nDisables other challenges' achievements.",
"value.0": "Off",
"value.1": "On"
},
"noAutoHeal": {
"name": "No Free Heal",
"desc": "Disables the free automatic healing which normally occurs after every 10th wave.",
"value.0": "Off",
"value.1": "On"
},
"hardcore": {
"name": "Hardcore",
"desc": "You can no longer revive Pokemon that have fainted.",
"value.0": "Off",
"value.1": "On"
},
"noLegends": {
"name": "No Starter Legends",
"desc": "You can't choose any legendary, sub-legendary or mythical Pokemon as a starter.",
"value.0": "Off",
"value.1": "On"
},
"limitedCatch": {
"name": "Limited Catch",
"desc": "You can only add the first Pokemon of a biome to your current party.",
"value.0": "Off",
"value.1": "On"
}
}

View File

@ -1,32 +1,34 @@
import * as Modifiers from "./modifier";
import { MoneyMultiplierModifier } from "./modifier";
import { allMoves, AttackMove, selfStatLowerMoves } from "../data/move";
import { getPokeballCatchMultiplier, getPokeballName, MAX_PER_TYPE_POKEBALLS, PokeballType } from "../data/pokeball";
import Pokemon, { EnemyPokemon, PlayerPokemon, PokemonMove } from "../field/pokemon";
import { EvolutionItem, pokemonEvolutions } from "../data/pokemon-evolutions";
import { tmPoolTiers, tmSpecies } from "../data/tms";
import { Type } from "../data/type";
import PartyUiHandler, { PokemonMoveSelectFilter, PokemonSelectFilter } from "../ui/party-ui-handler";
import * as Utils from "../utils";
import { getBerryEffectDescription, getBerryName } from "../data/berry";
import { Unlockables } from "../system/unlockables";
import { getStatusEffectDescriptor, StatusEffect } from "../data/status-effect";
import { SpeciesFormKey } from "../data/pokemon-species";
import BattleScene from "../battle-scene";
import { getVoucherTypeIcon, getVoucherTypeName, VoucherType } from "../system/voucher";
import { FormChangeItem, pokemonFormChanges, SpeciesFormChangeCondition, SpeciesFormChangeItemTrigger } from "../data/pokemon-forms";
import { ModifierTier } from "./modifier-tier";
import BattleScene from "#app/battle-scene";
import { getBerryEffectDescription, getBerryName } from "#app/data/berry";
import { applyChallenges, ChallengeType } from "#app/data/challenge";
import { allMoves, AttackMove, selfStatLowerMoves } from "#app/data/move";
import { getNatureName, getNatureStatMultiplier, Nature } from "#app/data/nature";
import i18next from "i18next";
import { getModifierTierTextTint } from "#app/ui/text";
import { getPokeballCatchMultiplier, getPokeballName, MAX_PER_TYPE_POKEBALLS, PokeballType } from "#app/data/pokeball";
import { EvolutionItem, pokemonEvolutions } from "#app/data/pokemon-evolutions";
import { FormChangeItem, pokemonFormChanges, SpeciesFormChangeCondition, SpeciesFormChangeItemTrigger } from "#app/data/pokemon-forms";
import { SpeciesFormKey } from "#app/data/pokemon-species";
import { getStatusEffectDescriptor, StatusEffect } from "#app/data/status-effect";
import { tmPoolTiers, tmSpecies } from "#app/data/tms";
import { Type } from "#app/data/type";
import { getStatKey, PermanentStat, Stat, TEMP_BATTLE_STATS, TempBattleStat } from "#app/enums/stat";
import Pokemon, { EnemyPokemon, PlayerPokemon, PokemonMove } from "#app/field/pokemon";
import { GameMode } from "#app/game-mode";
import { getPokemonNameWithAffix } from "#app/messages";
import Overrides from "#app/overrides";
import { Unlockables } from "#app/system/unlockables";
import { getVoucherTypeIcon, getVoucherTypeName, VoucherType } from "#app/system/voucher";
import PartyUiHandler, { PokemonMoveSelectFilter, PokemonSelectFilter } from "#app/ui/party-ui-handler";
import { getModifierTierTextTint } from "#app/ui/text";
import * as Utils from "#app/utils";
import { Abilities } from "#enums/abilities";
import { BattlerTagType } from "#enums/battler-tag-type";
import { BerryType } from "#enums/berry-type";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import { getPokemonNameWithAffix } from "#app/messages";
import { PermanentStat, TEMP_BATTLE_STATS, TempBattleStat, Stat, getStatKey } from "#app/enums/stat";
import i18next from "i18next";
import * as Modifiers from "./modifier";
import { MoneyMultiplierModifier } from "./modifier";
import { ModifierTier } from "./modifier-tier";
const outputModifierData = false;
const useMaxWeightForOutput = false;
@ -2139,8 +2141,12 @@ function getModifierTypeOptionWithRetry(existingOptions: ModifierTypeOption[], r
allowLuckUpgrades = allowLuckUpgrades ?? true;
let candidate = getNewModifierTypeOption(party, ModifierPoolType.PLAYER, tier, undefined, 0, allowLuckUpgrades);
let r = 0;
while (existingOptions.length && ++r < retryCount && existingOptions.filter(o => o.type.name === candidate?.type.name || o.type.group === candidate?.type.group).length) {
let isValidForChallenge = new Utils.BooleanHolder(true);
applyChallenges(party[0].scene.gameMode, ChallengeType.RANDOM_ITEM_BLACKLIST, candidate!, isValidForChallenge);
while (existingOptions.length && ++r < retryCount && existingOptions.filter(o => o.type.name === candidate?.type.name || o.type.group === candidate?.type.group).length || !isValidForChallenge.value) {
candidate = getNewModifierTypeOption(party, ModifierPoolType.PLAYER, candidate?.type.tier ?? tier, candidate?.upgradeCount, 0, allowLuckUpgrades);
isValidForChallenge = new Utils.BooleanHolder(true);
applyChallenges(party[0].scene.gameMode, ChallengeType.RANDOM_ITEM_BLACKLIST, candidate!, isValidForChallenge);
}
return candidate!;
}
@ -2170,7 +2176,7 @@ export function overridePlayerModifierTypeOptions(options: ModifierTypeOption[],
}
}
export function getPlayerShopModifierTypeOptionsForWave(waveIndex: integer, baseCost: integer): ModifierTypeOption[] {
export function getPlayerShopModifierTypeOptionsForWave(waveIndex: integer, baseCost: integer, gameMode: GameMode): ModifierTypeOption[] {
if (!(waveIndex % 10)) {
return [];
}
@ -2204,7 +2210,11 @@ export function getPlayerShopModifierTypeOptionsForWave(waveIndex: integer, base
new ModifierTypeOption(modifierTypes.SACRED_ASH(), 0, baseCost * 10)
]
];
return options.slice(0, Math.ceil(Math.max(waveIndex + 10, 0) / 30)).flat();
return options.slice(0, Math.ceil(Math.max(waveIndex + 10, 0) / 30)).flat().filter(x => {
const isValidForChallenge = new Utils.BooleanHolder(true);
applyChallenges(gameMode, ChallengeType.SHOP_ITEM_BLACKLIST, x, isValidForChallenge);
return isValidForChallenge.value;
});
}
export function getEnemyBuffModifierForWave(tier: ModifierTier, enemyModifiers: Modifiers.PersistentModifier[], scene: BattleScene): Modifiers.EnemyPersistentModifier {

View File

@ -1,21 +1,23 @@
import BattleScene from "#app/battle-scene";
import { BattlerIndex } from "#app/battle";
import { getPokeballCatchMultiplier, getPokeballAtlasKey, getPokeballTintColor, doPokeballBounceAnim } from "#app/data/pokeball";
import BattleScene from "#app/battle-scene";
import { SubstituteTag } from "#app/data/battler-tags";
import { ChallengeType, applyChallenges } from "#app/data/challenge";
import { doPokeballBounceAnim, getPokeballAtlasKey, getPokeballCatchMultiplier, getPokeballTintColor } from "#app/data/pokeball";
import { getStatusEffectCatchRateMultiplier } from "#app/data/status-effect";
import { PokeballType } from "#app/enums/pokeball";
import { StatusEffect } from "#app/enums/status-effect";
import { addPokeballOpenParticles, addPokeballCaptureStars } from "#app/field/anims";
import { addPokeballCaptureStars, addPokeballOpenParticles } from "#app/field/anims";
import { EnemyPokemon } from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages";
import { PokemonHeldItemModifier } from "#app/modifier/modifier";
import { achvs } from "#app/system/achv";
import { PartyUiMode, PartyOption } from "#app/ui/party-ui-handler";
import { PartyOption, PartyUiMode } from "#app/ui/party-ui-handler";
import { SummaryUiMode } from "#app/ui/summary-ui-handler";
import { Mode } from "#app/ui/ui";
import { BooleanHolder } from "#app/utils";
import i18next from "i18next";
import { PokemonPhase } from "./pokemon-phase";
import { VictoryPhase } from "./victory-phase";
import { SubstituteTag } from "#app/data/battler-tags";
export class AttemptCapturePhase extends PokemonPhase {
private pokeballType: PokeballType;
@ -249,6 +251,13 @@ export class AttemptCapturePhase extends PokemonPhase {
});
};
Promise.all([pokemon.hideInfo(), this.scene.gameData.setPokemonCaught(pokemon)]).then(() => {
const challengeCanAddToParty = new BooleanHolder(true);
applyChallenges(this.scene.gameMode, ChallengeType.ADD_POKEMON_TO_PARTY, pokemon, this.scene.currentBattle.waveIndex, challengeCanAddToParty);
if (!challengeCanAddToParty.value) {
removePokemon();
end();
return;
}
if (this.scene.getParty().length === 6) {
const promptRelease = () => {
this.scene.ui.showText(i18next.t("battle:partyFull", { pokemonName: pokemon.getNameToRender() }), null, () => {

View File

@ -1,7 +1,8 @@
import { BattleType, TurnCommand } from "#app/battle";
import BattleScene from "#app/battle-scene";
import { TurnCommand, BattleType } from "#app/battle";
import { TrappedTag, EncoreTag } from "#app/data/battler-tags";
import { MoveTargetSet, getMoveTargets } from "#app/data/move";
import { EncoreTag, TrappedTag } from "#app/data/battler-tags";
import { applyChallenges, ChallengeType } from "#app/data/challenge";
import { getMoveTargets, MoveTargetSet } from "#app/data/move";
import { speciesStarters } from "#app/data/pokemon-species";
import { Abilities } from "#app/enums/abilities";
import { BattlerTagType } from "#app/enums/battler-tag-type";
@ -12,10 +13,11 @@ import { FieldPosition, PlayerPokemon } from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages";
import { Command } from "#app/ui/command-ui-handler";
import { Mode } from "#app/ui/ui";
import { BooleanHolder } from "#app/utils";
import { MysteryEncounterMode } from "#enums/mystery-encounter-mode";
import i18next from "i18next";
import { FieldPhase } from "./field-phase";
import { SelectTargetPhase } from "./select-target-phase";
import { MysteryEncounterMode } from "#enums/mystery-encounter-mode";
export class CommandPhase extends FieldPhase {
protected fieldIndex: integer;
@ -84,6 +86,18 @@ export class CommandPhase extends FieldPhase {
switch (command) {
case Command.FIGHT:
// Check if move can be used in challenge
const isValidForChallenge = new BooleanHolder(true);
applyChallenges(this.scene.gameMode, ChallengeType.MOVE_BLACKLIST, playerPokemon.getMoveset()[cursor]!, isValidForChallenge);
if (!isValidForChallenge.value) {
const moveName = playerPokemon.getMoveset()[cursor]?.getName();
this.scene.ui.setMode(Mode.MESSAGE);
this.scene.ui.showText(i18next.t("challenges:illegalMove", { moveName: moveName }), null, () => {
this.scene.ui.clearText();
this.scene.ui.setMode(Mode.FIGHT, this.fieldIndex);
}, null, true);
break;
}
let useStruggle = false;
if (cursor === -1 ||
playerPokemon.trySelectMove(cursor, args[0] as boolean) ||

View File

@ -1,5 +1,7 @@
import BattleScene from "#app/battle-scene";
import * as Utils from "#app/utils";
import { applyChallenges, ChallengeType } from "#app/data/challenge";
import { SelectModifierPhase } from "#app/phases/select-modifier-phase";
import { BooleanHolder, fixedInt } from "#app/utils";
import { BattlePhase } from "./battle-phase";
export class PartyHealPhase extends BattlePhase {
@ -14,21 +16,36 @@ export class PartyHealPhase extends BattlePhase {
start() {
super.start();
const isHealPhaseActive = new BooleanHolder(true);
applyChallenges(this.scene.gameMode, ChallengeType.NO_HEAL_PHASE, isHealPhaseActive);
if (!isHealPhaseActive.value) {
this.scene.unshiftPhase(new SelectModifierPhase(this.scene));
this.end();
return;
}
const bgmPlaying = this.scene.isBgmPlaying();
if (bgmPlaying) {
this.scene.fadeOutBgm(1000, false);
}
const canBeRevived = new BooleanHolder(true);
this.scene.ui.fadeOut(1000).then(() => {
for (const pokemon of this.scene.getParty()) {
pokemon.hp = pokemon.getMaxHp();
pokemon.resetStatus();
for (const move of pokemon.moveset) {
move!.ppUsed = 0; // TODO: is this bang correct?
applyChallenges(this.scene.gameMode, ChallengeType.PREVENT_REVIVE, pokemon, canBeRevived);
if (canBeRevived.value || !pokemon.isFainted()) {
pokemon.hp = pokemon.getMaxHp();
pokemon.resetStatus();
for (const move of pokemon.moveset) {
if (move) {
move.ppUsed = 0;
}
}
pokemon.updateInfo(true);
}
pokemon.updateInfo(true);
}
const healSong = this.scene.playSoundWithoutBgm("heal");
this.scene.time.delayedCall(Utils.fixedInt(healSong.totalDuration * 1000), () => {
this.scene.time.delayedCall(fixedInt(healSong.totalDuration * 1000), () => {
healSong.destroy();
if (this.resumeBgm && bgmPlaying) {
this.scene.playBgm();

View File

@ -1,16 +1,15 @@
import BattleScene from "#app/battle-scene";
import { ModifierTier } from "#app/modifier/modifier-tier";
import { regenerateModifierPoolThresholds, ModifierTypeOption, ModifierType, getPlayerShopModifierTypeOptionsForWave, PokemonModifierType, FusePokemonModifierType, PokemonMoveModifierType, TmModifierType, RememberMoveModifierType, PokemonPpRestoreModifierType, PokemonPpUpModifierType, ModifierPoolType, getPlayerModifierTypeOptions } from "#app/modifier/modifier-type";
import { ExtraModifierModifier, Modifier, PokemonHeldItemModifier } from "#app/modifier/modifier";
import ModifierSelectUiHandler, { SHOP_OPTIONS_ROW_LIMIT } from "#app/ui/modifier-select-ui-handler";
import PartyUiHandler, { PartyUiMode, PartyOption } from "#app/ui/party-ui-handler";
import { Mode } from "#app/ui/ui";
import i18next from "i18next";
import * as Utils from "#app/utils";
import { BattlePhase } from "./battle-phase";
import { ModifierTier } from "#app/modifier/modifier-tier";
import { CustomModifierSettings, FusePokemonModifierType, ModifierPoolType, ModifierType, ModifierTypeOption, PokemonModifierType, PokemonMoveModifierType, PokemonPpRestoreModifierType, PokemonPpUpModifierType, RememberMoveModifierType, TmModifierType, getPlayerModifierTypeOptions, getPlayerShopModifierTypeOptionsForWave, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type";
import Overrides from "#app/overrides";
import { CustomModifierSettings } from "#app/modifier/modifier-type";
import ModifierSelectUiHandler, { SHOP_OPTIONS_ROW_LIMIT } from "#app/ui/modifier-select-ui-handler";
import PartyUiHandler, { PartyOption, PartyUiMode } from "#app/ui/party-ui-handler";
import { Mode } from "#app/ui/ui";
import * as Utils from "#app/utils";
import { isNullOrUndefined } from "#app/utils";
import i18next from "i18next";
import { BattlePhase } from "./battle-phase";
export class SelectModifierPhase extends BattlePhase {
private rerollCount: integer;
@ -128,7 +127,7 @@ export class SelectModifierPhase extends BattlePhase {
}
break;
default:
const shopOptions = getPlayerShopModifierTypeOptionsForWave(this.scene.currentBattle.waveIndex, this.scene.getWaveMoneyAmount(1));
const shopOptions = getPlayerShopModifierTypeOptionsForWave(this.scene.currentBattle.waveIndex, this.scene.getWaveMoneyAmount(1), this.scene.gameMode);
const shopOption = shopOptions[rowCursor > 2 || shopOptions.length <= SHOP_OPTIONS_ROW_LIMIT ? cursor : cursor + SHOP_OPTIONS_ROW_LIMIT];
if (shopOption.type) {
modifierType = shopOption.type;

View File

@ -0,0 +1,87 @@
import { BattlerIndex } from "#app/battle";
import { allMoves, ShellSideArmCategoryAttr } from "#app/data/move";
import { Abilities } from "#enums/abilities";
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, it, expect, vi } from "vitest";
describe("Moves - Shell Side Arm", () => {
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);
game.override
.moveset([Moves.SHELL_SIDE_ARM])
.battleType("single")
.startingLevel(100)
.enemyLevel(100)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});
it("becomes a physical attack if forecasted to deal more damage as physical", async () => {
game.override.enemySpecies(Species.SNORLAX);
await game.classicMode.startBattle([Species.MANAPHY]);
const shellSideArm = allMoves[Moves.SHELL_SIDE_ARM];
const shellSideArmAttr = shellSideArm.getAttrs(ShellSideArmCategoryAttr)[0];
vi.spyOn(shellSideArmAttr, "apply");
game.move.select(Moves.SHELL_SIDE_ARM);
await game.phaseInterceptor.to("MoveEffectPhase");
expect(shellSideArmAttr.apply).toHaveLastReturnedWith(true);
}, TIMEOUT);
it("remains a special attack if forecasted to deal more damage as special", async () => {
game.override.enemySpecies(Species.SLOWBRO);
await game.classicMode.startBattle([Species.MANAPHY]);
const shellSideArm = allMoves[Moves.SHELL_SIDE_ARM];
const shellSideArmAttr = shellSideArm.getAttrs(ShellSideArmCategoryAttr)[0];
vi.spyOn(shellSideArmAttr, "apply");
game.move.select(Moves.SHELL_SIDE_ARM);
await game.phaseInterceptor.to("MoveEffectPhase");
expect(shellSideArmAttr.apply).toHaveLastReturnedWith(false);
}, TIMEOUT);
it("respects stat stage changes when forecasting base damage", async () => {
game.override
.enemySpecies(Species.SNORLAX)
.enemyMoveset(Moves.COTTON_GUARD);
await game.classicMode.startBattle([Species.MANAPHY]);
const shellSideArm = allMoves[Moves.SHELL_SIDE_ARM];
const shellSideArmAttr = shellSideArm.getAttrs(ShellSideArmCategoryAttr)[0];
vi.spyOn(shellSideArmAttr, "apply");
game.move.select(Moves.SHELL_SIDE_ARM);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.phaseInterceptor.to("BerryPhase", false);
expect(shellSideArmAttr.apply).toHaveLastReturnedWith(false);
}, TIMEOUT);
});

View File

@ -344,6 +344,7 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler {
super.clear();
this.config = null;
this.optionSelectContainer.setVisible(false);
this.scrollCursor = 0;
this.eraseCursor();
}

View File

@ -1,12 +1,13 @@
import BattleScene from "../battle-scene";
import BattleScene from "#app/battle-scene";
import { Button } from "#enums/buttons";
import i18next from "i18next";
import { Achv, achvs, getAchievementDescription } from "../system/achv";
import { Voucher, getVoucherTypeIcon, getVoucherTypeName, vouchers } from "../system/voucher";
import MessageUiHandler from "./message-ui-handler";
import { addTextObject, TextStyle } from "./text";
import { Mode } from "./ui";
import { addWindow } from "./ui-theme";
import { Achv, achvs, getAchievementDescription } from "#app/system/achv";
import { Voucher, getVoucherTypeIcon, getVoucherTypeName, vouchers } from "#app/system/voucher";
import MessageUiHandler from "#app/ui/message-ui-handler";
import { addTextObject, TextStyle } from "#app/ui/text";
import { Mode } from "#app/ui/ui";
import { addWindow } from "#app/ui/ui-theme";
import { ScrollBar } from "#app/ui/scroll-bar";
import { PlayerGender } from "#enums/player-gender";
enum Page {
@ -49,6 +50,7 @@ export default class AchvsUiHandler extends MessageUiHandler {
private vouchersTotal: number;
private currentTotal: number;
private scrollBar: ScrollBar;
private scrollCursor: number;
private cursorObj: Phaser.GameObjects.NineSlice | null;
private currentPage: Page;
@ -91,7 +93,10 @@ export default class AchvsUiHandler extends MessageUiHandler {
this.iconsBg = addWindow(this.scene, 0, this.headerBg.height, (this.scene.game.canvas.width / 6) - 2, (this.scene.game.canvas.height / 6) - this.headerBg.height - 68);
this.iconsBg.setOrigin(0, 0);
this.iconsContainer = this.scene.add.container(6, this.headerBg.height + 6);
const yOffset = 6;
this.scrollBar = new ScrollBar(this.scene, this.iconsBg.width - 9, this.iconsBg.y + yOffset, 4, this.iconsBg.height - yOffset * 2, this.ROWS);
this.iconsContainer = this.scene.add.container(5, this.headerBg.height + 8);
this.icons = [];
@ -148,6 +153,7 @@ export default class AchvsUiHandler extends MessageUiHandler {
this.mainContainer.add(this.headerText);
this.mainContainer.add(this.headerActionText);
this.mainContainer.add(this.iconsBg);
this.mainContainer.add(this.scrollBar);
this.mainContainer.add(this.iconsContainer);
this.mainContainer.add(titleBg);
this.mainContainer.add(this.titleText);
@ -162,6 +168,7 @@ export default class AchvsUiHandler extends MessageUiHandler {
this.currentPage = Page.ACHIEVEMENTS;
this.setCursor(0);
this.setScrollCursor(0);
this.mainContainer.setVisible(false);
}
@ -175,6 +182,8 @@ export default class AchvsUiHandler extends MessageUiHandler {
this.mainContainer.setVisible(true);
this.setCursor(0);
this.setScrollCursor(0);
this.scrollBar.setTotalRows(Math.ceil(this.currentTotal / this.COLS));
this.scrollBar.setScrollCursor(0);
this.getUi().moveTo(this.mainContainer, this.getUi().length - 1);
@ -224,6 +233,8 @@ export default class AchvsUiHandler extends MessageUiHandler {
this.updateAchvIcons();
}
this.setCursor(0, true);
this.scrollBar.setTotalRows(Math.ceil(this.currentTotal / this.COLS));
this.scrollBar.setScrollCursor(0);
this.mainContainer.update();
}
if (button === Button.CANCEL) {
@ -237,32 +248,44 @@ export default class AchvsUiHandler extends MessageUiHandler {
if (this.cursor < this.COLS) {
if (this.scrollCursor) {
success = this.setScrollCursor(this.scrollCursor - 1);
} else {
// Wrap around to the last row
success = this.setScrollCursor(Math.ceil(this.currentTotal / this.COLS) - this.ROWS);
let newCursorIndex = this.cursor + (this.ROWS - 1) * this.COLS;
if (newCursorIndex > this.currentTotal - this.scrollCursor * this.COLS -1) {
newCursorIndex -= this.COLS;
}
success = success && this.setCursor(newCursorIndex);
}
} else {
success = this.setCursor(this.cursor - this.COLS);
}
break;
case Button.DOWN:
const canMoveDown = (this.cursor + itemOffset) + this.COLS < this.currentTotal;
const canMoveDown = itemOffset + 1 < this.currentTotal;
if (rowIndex >= this.ROWS - 1) {
if (this.scrollCursor < Math.ceil(this.currentTotal / this.COLS) - this.ROWS && canMoveDown) {
// scroll down one row
success = this.setScrollCursor(this.scrollCursor + 1);
} else {
// wrap back to the first row
success = this.setScrollCursor(0) && this.setCursor(this.cursor % this.COLS);
}
} else if (canMoveDown) {
success = this.setCursor(this.cursor + this.COLS);
success = this.setCursor(Math.min(this.cursor + this.COLS, this.currentTotal - itemOffset - 1));
}
break;
case Button.LEFT:
if (!this.cursor && this.scrollCursor) {
success = this.setScrollCursor(this.scrollCursor - 1) && this.setCursor(this.cursor + (this.COLS - 1));
} else if (this.cursor) {
if (this.cursor % this.COLS === 0) {
success = this.setCursor(Math.min(this.cursor + this.COLS - 1, this.currentTotal - itemOffset - 1));
} else {
success = this.setCursor(this.cursor - 1);
}
break;
case Button.RIGHT:
if (this.cursor + 1 === this.ROWS * this.COLS && this.scrollCursor < Math.ceil(this.currentTotal / this.COLS) - this.ROWS) {
success = this.setScrollCursor(this.scrollCursor + 1) && this.setCursor(this.cursor - (this.COLS - 1));
} else if (this.cursor + itemOffset < this.currentTotal - 1) {
if ((this.cursor + 1) % this.COLS === 0 || (this.cursor + itemOffset) === (this.currentTotal - 1)) {
success = this.setCursor(this.cursor - this.cursor % this.COLS);
} else {
success = this.setCursor(this.cursor + 1);
}
break;
@ -315,15 +338,22 @@ export default class AchvsUiHandler extends MessageUiHandler {
}
this.scrollCursor = scrollCursor;
this.scrollBar.setScrollCursor(this.scrollCursor);
// Cursor cannot go farther than the last element in the list
const maxCursor = Math.min(this.cursor, this.currentTotal - this.scrollCursor * this.COLS - 1);
if (maxCursor !== this.cursor) {
this.setCursor(maxCursor);
}
switch (this.currentPage) {
case Page.ACHIEVEMENTS:
this.updateAchvIcons();
this.showAchv(achvs[Object.keys(achvs)[Math.min(this.cursor + this.scrollCursor * this.COLS, Object.values(achvs).length - 1)]]);
this.showAchv(achvs[Object.keys(achvs)[this.cursor + this.scrollCursor * this.COLS]]);
break;
case Page.VOUCHERS:
this.updateVoucherIcons();
this.showVoucher(vouchers[Object.keys(vouchers)[Math.min(this.cursor + this.scrollCursor * this.COLS, Object.values(vouchers).length - 1)]]);
this.showVoucher(vouchers[Object.keys(vouchers)[this.cursor + this.scrollCursor * this.COLS]]);
break;
}
return true;
@ -411,6 +441,7 @@ export default class AchvsUiHandler extends MessageUiHandler {
super.clear();
this.currentPage = Page.ACHIEVEMENTS;
this.mainContainer.setVisible(false);
this.setScrollCursor(0);
this.eraseCursor();
}

View File

@ -1,20 +1,20 @@
import BattleScene from "../battle-scene";
import { getPlayerShopModifierTypeOptionsForWave, ModifierTypeOption, TmModifierType } from "../modifier/modifier-type";
import { getPokeballAtlasKey, PokeballType } from "../data/pokeball";
import { addTextObject, getTextStyleOptions, getModifierTierTextTint, getTextColor, TextStyle } from "./text";
import AwaitableUiHandler from "./awaitable-ui-handler";
import { Mode } from "./ui";
import { LockModifierTiersModifier, PokemonHeldItemModifier, HealShopCostModifier } from "../modifier/modifier";
import { handleTutorial, Tutorial } from "../tutorial";
import { Button } from "#enums/buttons";
import MoveInfoOverlay from "./move-info-overlay";
import { allMoves } from "../data/move";
import * as Utils from "./../utils";
import BattleScene from "#app/battle-scene";
import { applyChallenges, ChallengeType } from "#app/data/challenge";
import { allMoves } from "#app/data/move";
import { getPokeballAtlasKey, PokeballType } from "#app/data/pokeball";
import { HealShopCostModifier, LockModifierTiersModifier, PokemonHeldItemModifier } from "#app/modifier/modifier";
import { getPlayerShopModifierTypeOptionsForWave, ModifierTypeOption, TmModifierType } from "#app/modifier/modifier-type";
import Overrides from "#app/overrides";
import { handleTutorial, Tutorial } from "#app/tutorial";
import { BooleanHolder, formatMoney, NumberHolder } from "#app/utils";
import { Button } from "#enums/buttons";
import { ShopCursorTarget } from "#enums/shop-cursor-target";
import i18next from "i18next";
import { ShopCursorTarget } from "#app/enums/shop-cursor-target";
import { IntegerHolder } from "./../utils";
import Phaser from "phaser";
import AwaitableUiHandler from "./awaitable-ui-handler";
import MoveInfoOverlay from "./move-info-overlay";
import { addTextObject, getModifierTierTextTint, getTextColor, getTextStyleOptions, TextStyle } from "./text";
import { Mode } from "./ui";
export const SHOP_OPTIONS_ROW_LIMIT = 6;
@ -186,11 +186,18 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler {
const typeOptions = args[1] as ModifierTypeOption[];
const removeHealShop = this.scene.gameMode.hasNoShop;
const baseShopCost = new IntegerHolder(this.scene.getWaveMoneyAmount(1));
const baseShopCost = new NumberHolder(this.scene.getWaveMoneyAmount(1));
this.scene.applyModifier(HealShopCostModifier, true, baseShopCost);
const shopTypeOptions = !removeHealShop
? getPlayerShopModifierTypeOptionsForWave(this.scene.currentBattle.waveIndex, baseShopCost.value)
? getPlayerShopModifierTypeOptionsForWave(this.scene.currentBattle.waveIndex, baseShopCost.value, this.scene.gameMode).filter(shopItem => {
const isValidForChallenge = new BooleanHolder(true);
applyChallenges(this.scene.gameMode, ChallengeType.SHOP_ITEM_BLACKLIST, shopItem, isValidForChallenge);
return isValidForChallenge.value;
})
: [];
const optionsYOffset = shopTypeOptions.length >= SHOP_OPTIONS_ROW_LIMIT ? -8 : -24;
for (let m = 0; m < typeOptions.length; m++) {
@ -554,7 +561,7 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler {
}
const canReroll = this.scene.money >= this.rerollCost;
const formattedMoney = Utils.formatMoney(this.scene.moneyFormat, this.rerollCost);
const formattedMoney = formatMoney(this.scene.moneyFormat, this.rerollCost);
this.rerollCostText.setText(i18next.t("modifierSelectUiHandler:rerollCost", { formattedMoney }));
this.rerollCostText.setColor(this.getTextColor(canReroll ? TextStyle.MONEY : TextStyle.PARTY_RED));
@ -819,7 +826,7 @@ class ModifierOption extends Phaser.GameObjects.Container {
const cost = Overrides.WAIVE_ROLL_FEE_OVERRIDE ? 0 : this.modifierTypeOption.cost;
const textStyle = cost <= scene.money ? TextStyle.MONEY : TextStyle.PARTY_RED;
const formattedMoney = Utils.formatMoney(scene.moneyFormat, cost);
const formattedMoney = formatMoney(scene.moneyFormat, cost);
this.itemCostText.setText(i18next.t("modifierSelectUiHandler:itemCost", { formattedMoney }));
this.itemCostText.setColor(getTextColor(textStyle, false, scene.uiTheme));

View File

@ -1,36 +1,65 @@
/**
* A vertical scrollbar element that resizes dynamically based on the current scrolling
* and number of elements that can be shown on screen
*/
export class ScrollBar extends Phaser.GameObjects.Container {
private bg: Phaser.GameObjects.Image;
private bg: Phaser.GameObjects.NineSlice;
private handleBody: Phaser.GameObjects.Rectangle;
private handleBottom: Phaser.GameObjects.Image;
private pages: number;
private page: number;
private handleBottom: Phaser.GameObjects.NineSlice;
private currentRow: number;
private totalRows: number;
private maxRows: number;
constructor(scene: Phaser.Scene, x: number, y: number, pages: number) {
/**
* @param scene the current scene
* @param x the scrollbar's x position (origin: top left)
* @param y the scrollbar's y position (origin: top left)
* @param width the scrollbar's width
* @param height the scrollbar's height
* @param maxRows the maximum number of rows that can be shown at once
*/
constructor(scene: Phaser.Scene, x: number, y: number, width: number, height: number, maxRows: number) {
super(scene, x, y);
this.bg = scene.add.image(0, 0, "scroll_bar");
this.maxRows = maxRows;
const borderSize = 2;
width = Math.max(width, 4);
this.bg = scene.add.nineslice(0, 0, "scroll_bar", undefined, width, height, borderSize, borderSize, borderSize, borderSize);
this.bg.setOrigin(0, 0);
this.add(this.bg);
this.handleBody = scene.add.rectangle(1, 1, 3, 4, 0xaaaaaa);
this.handleBody = scene.add.rectangle(1, 1, width - 2, 4, 0xaaaaaa);
this.handleBody.setOrigin(0, 0);
this.add(this.handleBody);
this.handleBottom = scene.add.image(1, 1, "scroll_bar_handle");
this.handleBottom = scene.add.nineslice(1, 1, "scroll_bar_handle", undefined, width - 2, 2, 2, 0, 0, 0);
this.handleBottom.setOrigin(0, 0);
this.add(this.handleBottom);
}
setPage(page: number): void {
this.page = page;
this.handleBody.y = 1 + (this.bg.displayHeight - 1 - this.handleBottom.displayHeight) / this.pages * page;
/**
* Set the current row that is displayed
* Moves the bar handle up or down accordingly
* @param scrollCursor how many times the view was scrolled down
*/
setScrollCursor(scrollCursor: number): void {
this.currentRow = scrollCursor;
this.handleBody.y = 1 + (this.bg.displayHeight - 1 - this.handleBottom.displayHeight) / this.totalRows * this.currentRow;
this.handleBottom.y = this.handleBody.y + this.handleBody.displayHeight;
}
setPages(pages: number): void {
this.pages = pages;
this.handleBody.height = (this.bg.displayHeight - 1 - this.handleBottom.displayHeight) * 9 / this.pages;
/**
* Set the total number of rows to display
* If it's smaller than the maximum number of rows on screen the bar will get hidden
* Otherwise the scrollbar handle gets resized based on the ratio to the maximum number of rows
* @param rows how many rows of data there are in total
*/
setTotalRows(rows: number): void {
this.totalRows = rows;
this.handleBody.height = (this.bg.displayHeight - 1 - this.handleBottom.displayHeight) * this.maxRows / this.totalRows;
this.setVisible(this.pages > 9);
this.setVisible(this.totalRows > this.maxRows);
}
}

View File

@ -1,11 +1,12 @@
import UiHandler from "../ui-handler";
import BattleScene from "../../battle-scene";
import {Mode} from "../ui";
import {InterfaceConfig} from "../../inputs-controller";
import {addWindow} from "../ui-theme";
import {addTextObject, TextStyle} from "../text";
import {getIconWithSettingName} from "#app/configs/inputs/configHandler";
import NavigationMenu, {NavigationManager} from "#app/ui/settings/navigationMenu";
import UiHandler from "#app/ui/ui-handler";
import BattleScene from "#app/battle-scene";
import { Mode } from "#app/ui/ui";
import { InterfaceConfig } from "#app/inputs-controller";
import { addWindow } from "#app/ui/ui-theme";
import { addTextObject, TextStyle } from "#app/ui/text";
import { ScrollBar } from "#app/ui/scroll-bar";
import { getIconWithSettingName } from "#app/configs/inputs/configHandler";
import NavigationMenu, { NavigationManager } from "#app/ui/settings/navigationMenu";
import { Device } from "#enums/devices";
import { Button } from "#enums/buttons";
import i18next from "i18next";
@ -19,7 +20,7 @@ export interface LayoutConfig {
inputsIcons: InputsIcons;
settingLabels: Phaser.GameObjects.Text[];
optionValueLabels: Phaser.GameObjects.Text[][];
optionCursors: integer[];
optionCursors: number[];
keys: string[];
bindingSettings: Array<String>;
}
@ -31,8 +32,9 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler
protected optionsContainer: Phaser.GameObjects.Container;
protected navigationContainer: NavigationMenu;
protected scrollCursor: integer;
protected optionCursors: integer[];
protected scrollBar: ScrollBar;
protected scrollCursor: number;
protected optionCursors: number[];
protected cursorObj: Phaser.GameObjects.NineSlice | null;
protected optionsBg: Phaser.GameObjects.NineSlice;
@ -65,7 +67,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler
protected device: Device;
abstract saveSettingToLocalStorage(setting, cursor): void;
abstract setSetting(scene: BattleScene, setting, value: integer): boolean;
abstract setSetting(scene: BattleScene, setting, value: number): boolean;
/**
* Constructor for the AbstractSettingsUiHandler.
@ -241,7 +243,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler
// Calculate the total available space for placing option labels next to their setting label
// We reserve space for the setting label and then distribute the remaining space evenly
const totalSpace = (300 - labelWidth) - totalWidth / 6;
const totalSpace = (297 - labelWidth) - totalWidth / 6;
// Calculate the spacing between options based on the available space divided by the number of gaps between labels
const optionSpacing = Math.floor(totalSpace / (optionValueLabels[s].length - 1));
@ -269,6 +271,11 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler
// Add the options container to the overall settings container to be displayed in the UI.
this.settingsContainer.add(optionsContainer);
}
// Add vertical scrollbar
this.scrollBar = new ScrollBar(this.scene, this.optionsBg.width - 9, this.optionsBg.y + 5, 4, this.optionsBg.height - 11, this.rowsToDisplay);
this.settingsContainer.add(this.scrollBar);
// Add the settings container to the UI.
ui.add(this.settingsContainer);
@ -413,6 +420,8 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler
this.optionCursors = layout.optionCursors;
this.inputsIcons = layout.inputsIcons;
this.bindingSettings = layout.bindingSettings;
this.scrollBar.setTotalRows(layout.settingLabels.length);
this.scrollBar.setScrollCursor(0);
// Return true indicating the layout was successfully applied.
return true;
@ -538,7 +547,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler
* @param cursor - The cursor position to set.
* @returns `true` if the cursor was set successfully.
*/
setCursor(cursor: integer): boolean {
setCursor(cursor: number): boolean {
const ret = super.setCursor(cursor);
// If the optionsContainer is not initialized, return the result from the parent class directly.
if (!this.optionsContainer) {
@ -547,7 +556,8 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler
// Check if the cursor object exists, if not, create it.
if (!this.cursorObj) {
this.cursorObj = this.scene.add.nineslice(0, 0, "summary_moves_cursor", undefined, (this.scene.game.canvas.width / 6) - 10, 16, 1, 1, 1, 1);
const cursorWidth = (this.scene.game.canvas.width / 6) - (this.scrollBar.visible? 16 : 10);
this.cursorObj = this.scene.add.nineslice(0, 0, "summary_moves_cursor", undefined, cursorWidth, 16, 1, 1, 1, 1);
this.cursorObj.setOrigin(0, 0); // Set the origin to the top-left corner.
this.optionsContainer.add(this.cursorObj); // Add the cursor to the options container.
}
@ -564,7 +574,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler
* @param scrollCursor - The scroll cursor position to set.
* @returns `true` if the scroll cursor was set successfully.
*/
setScrollCursor(scrollCursor: integer): boolean {
setScrollCursor(scrollCursor: number): boolean {
// Check if the new scroll position is the same as the current one; if so, do not update.
if (scrollCursor === this.scrollCursor) {
return false;
@ -572,6 +582,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler
// Update the internal scroll cursor state
this.scrollCursor = scrollCursor;
this.scrollBar.setScrollCursor(this.scrollCursor);
// Apply the new scroll position to the settings UI.
this.updateSettingsScroll();
@ -590,7 +601,7 @@ export default abstract class AbstractControlSettingsUiHandler extends UiHandler
* @param save - Whether to save the setting to local storage.
* @returns `true` if the option cursor was set successfully.
*/
setOptionCursor(settingIndex: integer, cursor: integer, save?: boolean): boolean {
setOptionCursor(settingIndex: number, cursor: number, save?: boolean): boolean {
// Retrieve the specific setting using the settingIndex from the settingDevice enumeration.
const setting = this.setting[Object.keys(this.setting)[settingIndex]];

View File

@ -1,12 +1,13 @@
import BattleScene from "../../battle-scene";
import { hasTouchscreen, isMobile } from "../../touch-controls";
import { TextStyle, addTextObject } from "../text";
import { Mode } from "../ui";
import UiHandler from "../ui-handler";
import { addWindow } from "../ui-theme";
import {Button} from "#enums/buttons";
import {InputsIcons} from "#app/ui/settings/abstract-control-settings-ui-handler";
import NavigationMenu, {NavigationManager} from "#app/ui/settings/navigationMenu";
import BattleScene from "#app/battle-scene";
import { hasTouchscreen, isMobile } from "#app/touch-controls";
import { TextStyle, addTextObject } from "#app/ui/text";
import { Mode } from "#app/ui/ui";
import UiHandler from "#app/ui/ui-handler";
import { addWindow } from "#app/ui/ui-theme";
import { ScrollBar } from "#app/ui/scroll-bar";
import { Button } from "#enums/buttons";
import { InputsIcons } from "#app/ui/settings/abstract-control-settings-ui-handler";
import NavigationMenu, { NavigationManager } from "#app/ui/settings/navigationMenu";
import { Setting, SettingKeys, SettingType } from "#app/system/settings/settings";
import i18next from "i18next";
@ -19,11 +20,12 @@ export default class AbstractSettingsUiHandler extends UiHandler {
private optionsContainer: Phaser.GameObjects.Container;
private navigationContainer: NavigationMenu;
private scrollCursor: integer;
private scrollCursor: number;
private scrollBar: ScrollBar;
private optionsBg: Phaser.GameObjects.NineSlice;
private optionCursors: integer[];
private optionCursors: number[];
private settingLabels: Phaser.GameObjects.Text[];
private optionValueLabels: Phaser.GameObjects.Text[][];
@ -117,7 +119,7 @@ export default class AbstractSettingsUiHandler extends UiHandler {
const labelWidth = Math.max(78, this.settingLabels[s].displayWidth + 8);
const totalSpace = (300 - labelWidth) - totalWidth / 6;
const totalSpace = (297 - labelWidth) - totalWidth / 6;
const optionSpacing = Math.floor(totalSpace / (this.optionValueLabels[s].length - 1));
let xOffset = 0;
@ -130,7 +132,11 @@ export default class AbstractSettingsUiHandler extends UiHandler {
this.optionCursors = this.settings.map(setting => setting.default);
this.scrollBar = new ScrollBar(this.scene, this.optionsBg.width - 9, this.optionsBg.y + 5, 4, this.optionsBg.height - 11, this.rowsToDisplay);
this.scrollBar.setTotalRows(this.settings.length);
this.settingsContainer.add(this.optionsBg);
this.settingsContainer.add(this.scrollBar);
this.settingsContainer.add(this.navigationContainer);
this.settingsContainer.add(actionsBg);
this.settingsContainer.add(this.optionsContainer);
@ -186,6 +192,7 @@ export default class AbstractSettingsUiHandler extends UiHandler {
this.settingsContainer.setVisible(true);
this.setCursor(0);
this.setScrollCursor(0);
this.getUi().moveTo(this.settingsContainer, this.getUi().length - 1);
@ -301,11 +308,12 @@ export default class AbstractSettingsUiHandler extends UiHandler {
* @param cursor - The cursor position to set.
* @returns `true` if the cursor was set successfully.
*/
setCursor(cursor: integer): boolean {
setCursor(cursor: number): boolean {
const ret = super.setCursor(cursor);
if (!this.cursorObj) {
this.cursorObj = this.scene.add.nineslice(0, 0, "summary_moves_cursor", undefined, (this.scene.game.canvas.width / 6) - 10, 16, 1, 1, 1, 1);
const cursorWidth = (this.scene.game.canvas.width / 6) - (this.scrollBar.visible? 16 : 10);
this.cursorObj = this.scene.add.nineslice(0, 0, "summary_moves_cursor", undefined, cursorWidth, 16, 1, 1, 1, 1);
this.cursorObj.setOrigin(0, 0);
this.optionsContainer.add(this.cursorObj);
}
@ -323,7 +331,7 @@ export default class AbstractSettingsUiHandler extends UiHandler {
* @param save - Whether to save the setting to local storage.
* @returns `true` if the option cursor was set successfully.
*/
setOptionCursor(settingIndex: integer, cursor: integer, save?: boolean): boolean {
setOptionCursor(settingIndex: number, cursor: number, save?: boolean): boolean {
const setting = this.settings[settingIndex];
if (setting.key === SettingKeys.Touch_Controls && cursor && hasTouchscreen() && isMobile()) {
@ -359,12 +367,13 @@ export default class AbstractSettingsUiHandler extends UiHandler {
* @param scrollCursor - The scroll cursor position to set.
* @returns `true` if the scroll cursor was set successfully.
*/
setScrollCursor(scrollCursor: integer): boolean {
setScrollCursor(scrollCursor: number): boolean {
if (scrollCursor === this.scrollCursor) {
return false;
}
this.scrollCursor = scrollCursor;
this.scrollBar.setScrollCursor(this.scrollCursor);
this.updateSettingsScroll();
@ -394,6 +403,7 @@ export default class AbstractSettingsUiHandler extends UiHandler {
clear() {
super.clear();
this.settingsContainer.setVisible(false);
this.setScrollCursor(0);
this.eraseCursor();
this.getUi().bgmBar.toggleBgmBar(this.scene.showBgmBar);
if (this.reloadRequired) {

View File

@ -627,7 +627,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
const starterBoxContainer = this.scene.add.container(speciesContainerX + 6, 9); //115
this.starterSelectScrollBar = new ScrollBar(this.scene, 161, 12, 0);
this.starterSelectScrollBar = new ScrollBar(this.scene, 161, 12, 5, starterContainerWindow.height - 6, 9);
starterBoxContainer.add(this.starterSelectScrollBar);
@ -2540,8 +2540,8 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
}
});
this.starterSelectScrollBar.setPages(Math.max(Math.ceil(this.filteredStarterContainers.length / 9), 1));
this.starterSelectScrollBar.setPage(0);
this.starterSelectScrollBar.setTotalRows(Math.max(Math.ceil(this.filteredStarterContainers.length / 9), 1));
this.starterSelectScrollBar.setScrollCursor(0);
// sort
const sort = this.filterBar.getVals(DropDownColumn.SORT)[0];
@ -2576,7 +2576,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
const onScreenFirstIndex = this.scrollCursor * maxColumns;
const onScreenLastIndex = Math.min(this.filteredStarterContainers.length - 1, onScreenFirstIndex + maxRows * maxColumns -1);
this.starterSelectScrollBar.setPage(this.scrollCursor);
this.starterSelectScrollBar.setScrollCursor(this.scrollCursor);
let pokerusCursorIndex = 0;
this.filteredStarterContainers.forEach((container, i) => {