This commit is contained in:
Wlowscha 2025-01-30 00:58:46 +00:00 committed by GitHub
commit 80c971439b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 5607 additions and 120 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@ -161,6 +161,7 @@ export default class BattleScene extends SceneBase {
public reroll: boolean = false;
public shopCursorTarget: number = ShopCursorTarget.REWARDS;
public commandCursorMemory: boolean = false;
public dexForDevs: boolean = false;
public showMovesetFlyout: boolean = true;
public showArenaFlyout: boolean = true;
public showTimeOfDayWidget: boolean = true;

View File

@ -102,6 +102,18 @@ export interface BiomePokemonPools {
[key: integer]: BiomeTierPokemonPools
}
export interface BiomeTierTod {
biome: Biome,
tier: BiomePoolTier,
tod: TimeOfDay[]
}
export interface CatchableSpecies{
[key: integer]: BiomeTierTod[]
}
export const catchableSpecies: CatchableSpecies = {};
export interface BiomeTierTrainerPools {
[key: integer]: TrainerType[]
}
@ -7716,6 +7728,10 @@ export function initBiomes() {
uncatchableSpecies.push(speciesId);
}
// prepares new array in catchableSpecies to host available biomes
//TODO: this must be improved to only make arrays for starters
catchableSpecies[speciesId] = [];
for (const b of biomeEntries) {
const biome = b[0];
const tier = b[1];
@ -7725,6 +7741,12 @@ export function initBiomes() {
: [ b[2] ]
: [ TimeOfDay.ALL ];
catchableSpecies[speciesId].push({
biome: biome as Biome,
tier: tier as BiomePoolTier,
tod: timesOfDay as TimeOfDay[]
});
for (const tod of timesOfDay) {
if (!biomePokemonPools.hasOwnProperty(biome) || !biomePokemonPools[biome].hasOwnProperty(tier) || !biomePokemonPools[biome][tier].hasOwnProperty(tod)) {
continue;

View File

@ -92,6 +92,7 @@ export class SpeciesFormEvolution {
public item: EvolutionItem | null;
public condition: SpeciesEvolutionCondition | null;
public wildDelay: SpeciesWildEvolutionDelay;
public description: string = "";
constructor(speciesId: Species, preFormKey: string | null, evoFormKey: string | null, level: integer, item: EvolutionItem | null, condition: SpeciesEvolutionCondition | null, wildDelay?: SpeciesWildEvolutionDelay) {
this.speciesId = speciesId;
@ -101,6 +102,23 @@ export class SpeciesFormEvolution {
this.item = item || EvolutionItem.NONE;
this.condition = condition;
this.wildDelay = wildDelay ?? SpeciesWildEvolutionDelay.NONE;
const strings: string[] = [];
if (this.level > 1) {
strings.push(i18next.t("pokemonEvolutions:level") + ` ${this.level}`);
}
if (this.item) {
const itemDescription = i18next.t(`modifierType:EvolutionItem.${EvolutionItem[this.item].toUpperCase()}`);
const rarity = this.item > 50 ? i18next.t("pokemonEvolutions:ULTRA") : i18next.t("pokemonEvolutions:GREAT");
strings.push(i18next.t("pokemonEvolutions:using") + itemDescription + ` (${rarity})`);
}
if (this.condition) {
strings.push(this.condition.description);
}
this.description = strings
.filter(str => str !== "")
.map((str, index) => index > 0 ? str[0].toLowerCase() + str.slice(1) : str)
.join(i18next.t("pokemonEvolutions:connector"));
}
}
@ -237,6 +255,7 @@ class WeatherEvolutionCondition extends SpeciesEvolutionCondition {
constructor(weatherTypes: WeatherType[]) {
super(() => weatherTypes.indexOf(globalScene.arena.weather?.weatherType || WeatherType.NONE) > -1);
this.weatherTypes = weatherTypes;
this.description = i18next.t("pokemonEvolutions:weather");
}
}
@ -1377,7 +1396,7 @@ export const pokemonEvolutions: PokemonEvolutions = {
],
[Species.TANDEMAUS]: [
new SpeciesFormEvolution(Species.MAUSHOLD, "", "three", 25, null, new TandemausEvolutionCondition()),
new SpeciesEvolution(Species.MAUSHOLD, 25, null, null)
new SpeciesFormEvolution(Species.MAUSHOLD, "", "four", 25, null, null)
],
[Species.FIDOUGH]: [
new SpeciesEvolution(Species.DACHSBUN, 26, null, null)
@ -1540,7 +1559,7 @@ export const pokemonEvolutions: PokemonEvolutions = {
],
[Species.DUNSPARCE]: [
new SpeciesFormEvolution(Species.DUDUNSPARCE, "", "three-segment", 32, null, new DunsparceEvolutionCondition(), SpeciesWildEvolutionDelay.LONG),
new SpeciesEvolution(Species.DUDUNSPARCE, 32, null, new MoveEvolutionCondition(Moves.HYPER_DRILL), SpeciesWildEvolutionDelay.LONG)
new SpeciesFormEvolution(Species.DUDUNSPARCE, "", "two-segment", 32, null, new MoveEvolutionCondition(Moves.HYPER_DRILL), SpeciesWildEvolutionDelay.LONG)
],
[Species.GLIGAR]: [
new SpeciesEvolution(Species.GLISCOR, 1, EvolutionItem.RAZOR_FANG, new TimeOfDayEvolutionCondition("night") /* Razor fang at night*/, SpeciesWildEvolutionDelay.VERY_LONG)

View File

@ -68433,6 +68433,31 @@ export const tmSpecies: TmSpecies = {
],
};
interface SpeciesTmMoves {
[key: integer]: Moves[]
}
function flipTmSpecies(tmSpecies: TmSpecies): SpeciesTmMoves {
const flipped: SpeciesTmMoves = {};
for (const move in tmSpecies) {
const moveKey = Number(move);
const speciesList = tmSpecies[move];
for (const species of speciesList) {
const speciesKey = Number(species);
if (!flipped[speciesKey]) {
flipped[speciesKey] = [];
}
flipped[speciesKey].push(moveKey);
}
}
return flipped;
}
export const speciesTmMoves: SpeciesTmMoves = flipTmSpecies(tmSpecies);
interface TmPoolTiers {
[key: integer]: ModifierTier
}

View File

@ -328,7 +328,8 @@ export class SpeciesFormChangeMoveLearnedTrigger extends SpeciesFormChangeTrigge
this.move = move;
this.known = known;
const moveKey = Moves[this.move].split("_").filter(f => f).map((f, i) => i ? `${f[0]}${f.slice(1).toLowerCase()}` : f.toLowerCase()).join("") as unknown as string;
this.description = i18next.t("pokemonEvolutions:Forms.moveLearned", { move: i18next.t(`move:${moveKey}.name`) });
this.description = known ? i18next.t("pokemonEvolutions:Forms.moveLearned", { move: i18next.t(`move:${moveKey}.name`) }) :
i18next.t("pokemonEvolutions:Forms.moveForgotten", { move: i18next.t(`move:${moveKey}.name`) });
}
canChange(pokemon: Pokemon): boolean {

View File

@ -298,8 +298,8 @@ export abstract class PokemonSpeciesForm {
return ret;
}
getSpriteAtlasPath(female: boolean, formIndex?: number, shiny?: boolean, variant?: number): string {
const spriteId = this.getSpriteId(female, formIndex, shiny, variant).replace(/\_{2}/g, "/");
getSpriteAtlasPath(female: boolean, formIndex?: number, shiny?: boolean, variant?: number, back?: boolean): string {
const spriteId = this.getSpriteId(female, formIndex, shiny, variant, back).replace(/\_{2}/g, "/");
return `${/_[1-3]$/.test(spriteId) ? "variant/" : ""}${spriteId}`;
}
@ -320,8 +320,8 @@ export abstract class PokemonSpeciesForm {
return `${back ? "back__" : ""}${shiny && (!variantSet || (!variant && !variantSet[variant || 0])) ? "shiny__" : ""}${baseSpriteKey}${shiny && variantSet && variantSet[variant] === 2 ? `_${variant + 1}` : ""}`;
}
getSpriteKey(female: boolean, formIndex?: number, shiny?: boolean, variant?: number): string {
return `pkmn__${this.getSpriteId(female, formIndex, shiny, variant)}`;
getSpriteKey(female: boolean, formIndex?: number, shiny?: boolean, variant?: number, back?: boolean): string {
return `pkmn__${this.getSpriteId(female, formIndex, shiny, variant, back)}`;
}
abstract getFormSpriteKey(formIndex?: number): string;
@ -494,10 +494,10 @@ export abstract class PokemonSpeciesForm {
return true;
}
loadAssets(female: boolean, formIndex?: number, shiny?: boolean, variant?: Variant, startLoad?: boolean): Promise<void> {
loadAssets(female: boolean, formIndex?: number, shiny?: boolean, variant?: Variant, startLoad?: boolean, back?: boolean): Promise<void> {
return new Promise(resolve => {
const spriteKey = this.getSpriteKey(female, formIndex, shiny, variant);
globalScene.loadPokemonAtlas(spriteKey, this.getSpriteAtlasPath(female, formIndex, shiny, variant));
const spriteKey = this.getSpriteKey(female, formIndex, shiny, variant, back);
globalScene.loadPokemonAtlas(spriteKey, this.getSpriteAtlasPath(female, formIndex, shiny, variant, back));
globalScene.load.audio(`${this.getCryKey(formIndex)}`, `audio/${this.getCryKey(formIndex)}.m4a`);
globalScene.load.once(Phaser.Loader.Events.COMPLETE, () => {
const originalWarn = console.warn;
@ -507,7 +507,7 @@ export abstract class PokemonSpeciesForm {
console.warn = originalWarn;
if (!(globalScene.anims.exists(spriteKey))) {
globalScene.anims.create({
key: this.getSpriteKey(female, formIndex, shiny, variant),
key: this.getSpriteKey(female, formIndex, shiny, variant, back),
frames: frameNames,
frameRate: 10,
repeat: -1
@ -515,7 +515,7 @@ export abstract class PokemonSpeciesForm {
} else {
globalScene.anims.get(spriteKey).frameRate = 10;
}
const spritePath = this.getSpriteAtlasPath(female, formIndex, shiny, variant).replace("variant/", "").replace(/_[1-3]$/, "");
const spritePath = this.getSpriteAtlasPath(female, formIndex, shiny, variant, back).replace("variant/", "").replace(/_[1-3]$/, "");
globalScene.loadPokemonVariantAssets(spriteKey, spritePath, variant).then(() => resolve());
});
if (startLoad) {
@ -2637,18 +2637,10 @@ export function initSpecies() {
new PokemonSpecies(Species.ROARING_MOON, 9, false, false, false, "Paradox Pokémon", Type.DRAGON, Type.DARK, 2, 380, Abilities.PROTOSYNTHESIS, Abilities.NONE, Abilities.NONE, 590, 105, 139, 71, 55, 101, 119, 10, 0, 295, GrowthRate.SLOW, null, false),
new PokemonSpecies(Species.IRON_VALIANT, 9, false, false, false, "Paradox Pokémon", Type.FAIRY, Type.FIGHTING, 1.4, 35, Abilities.QUARK_DRIVE, Abilities.NONE, Abilities.NONE, 590, 74, 130, 90, 120, 60, 116, 10, 0, 295, GrowthRate.SLOW, null, false),
new PokemonSpecies(Species.KORAIDON, 9, false, true, false, "Paradox Pokémon", Type.FIGHTING, Type.DRAGON, 2.5, 303, Abilities.ORICHALCUM_PULSE, Abilities.NONE, Abilities.NONE, 670, 100, 135, 115, 85, 100, 135, 3, 0, 335, GrowthRate.SLOW, null, false, false,
new PokemonForm("Apex Build", "apex-build", Type.FIGHTING, Type.DRAGON, 2.5, 303, Abilities.ORICHALCUM_PULSE, Abilities.NONE, Abilities.NONE, 670, 100, 135, 115, 85, 100, 135, 3, 0, 335, false, null, true),
new PokemonForm("Limited Build", "limited-build", Type.FIGHTING, Type.DRAGON, 3.5, 303, Abilities.ORICHALCUM_PULSE, Abilities.NONE, Abilities.NONE, 670, 100, 135, 115, 85, 100, 135, 3, 0, 335, false, null, true),
new PokemonForm("Sprinting Build", "sprinting-build", Type.FIGHTING, Type.DRAGON, 3.5, 303, Abilities.ORICHALCUM_PULSE, Abilities.NONE, Abilities.NONE, 670, 100, 135, 115, 85, 100, 135, 3, 0, 335, false, null, true),
new PokemonForm("Swimming Build", "swimming-build", Type.FIGHTING, Type.DRAGON, 3.5, 303, Abilities.ORICHALCUM_PULSE, Abilities.NONE, Abilities.NONE, 670, 100, 135, 115, 85, 100, 135, 3, 0, 335, false, null, true),
new PokemonForm("Gliding Build", "gliding-build", Type.FIGHTING, Type.DRAGON, 3.5, 303, Abilities.ORICHALCUM_PULSE, Abilities.NONE, Abilities.NONE, 670, 100, 135, 115, 85, 100, 135, 3, 0, 335, false, null, true),
new PokemonForm("Apex Build", "apex-build", Type.FIGHTING, Type.DRAGON, 2.5, 303, Abilities.ORICHALCUM_PULSE, Abilities.NONE, Abilities.NONE, 670, 100, 135, 115, 85, 100, 135, 3, 0, 335, false, null, true)
),
new PokemonSpecies(Species.MIRAIDON, 9, false, true, false, "Paradox Pokémon", Type.ELECTRIC, Type.DRAGON, 3.5, 240, Abilities.HADRON_ENGINE, Abilities.NONE, Abilities.NONE, 670, 100, 85, 100, 135, 115, 135, 3, 0, 335, GrowthRate.SLOW, null, false, false,
new PokemonForm("Ultimate Mode", "ultimate-mode", Type.ELECTRIC, Type.DRAGON, 3.5, 240, Abilities.HADRON_ENGINE, Abilities.NONE, Abilities.NONE, 670, 100, 85, 100, 135, 115, 135, 3, 0, 335, false, null, true),
new PokemonForm("Low-Power Mode", "low-power-mode", Type.ELECTRIC, Type.DRAGON, 2.8, 240, Abilities.HADRON_ENGINE, Abilities.NONE, Abilities.NONE, 670, 100, 85, 100, 135, 115, 135, 3, 0, 335, false, null, true),
new PokemonForm("Drive Mode", "drive-mode", Type.ELECTRIC, Type.DRAGON, 2.8, 240, Abilities.HADRON_ENGINE, Abilities.NONE, Abilities.NONE, 670, 100, 85, 100, 135, 115, 135, 3, 0, 335, false, null, true),
new PokemonForm("Aquatic Mode", "aquatic-mode", Type.ELECTRIC, Type.DRAGON, 2.8, 240, Abilities.HADRON_ENGINE, Abilities.NONE, Abilities.NONE, 670, 100, 85, 100, 135, 115, 135, 3, 0, 335, false, null, true),
new PokemonForm("Glide Mode", "glide-mode", Type.ELECTRIC, Type.DRAGON, 2.8, 240, Abilities.HADRON_ENGINE, Abilities.NONE, Abilities.NONE, 670, 100, 85, 100, 135, 115, 135, 3, 0, 335, false, null, true),
new PokemonForm("Ultimate Mode", "ultimate-mode", Type.ELECTRIC, Type.DRAGON, 3.5, 240, Abilities.HADRON_ENGINE, Abilities.NONE, Abilities.NONE, 670, 100, 85, 100, 135, 115, 135, 3, 0, 335, false, null, true)
),
new PokemonSpecies(Species.WALKING_WAKE, 9, false, false, false, "Paradox Pokémon", Type.WATER, Type.DRAGON, 3.5, 280, Abilities.PROTOSYNTHESIS, Abilities.NONE, Abilities.NONE, 590, 99, 83, 91, 125, 83, 109, 10, 0, 295, GrowthRate.SLOW, null, false), //Custom Catchrate, matching Gouging Fire and Raging Bolt
new PokemonSpecies(Species.IRON_LEAVES, 9, false, false, false, "Paradox Pokémon", Type.GRASS, Type.PSYCHIC, 1.5, 125, Abilities.QUARK_DRIVE, Abilities.NONE, Abilities.NONE, 590, 90, 130, 88, 70, 108, 104, 10, 0, 295, GrowthRate.SLOW, null, false), //Custom Catchrate, matching Iron Boulder and Iron Crown

View File

@ -5,7 +5,7 @@ import { SceneBase } from "#app/scene-base";
import { WindowVariant, getWindowVariantSuffix } from "#app/ui/ui-theme";
import { isMobile } from "#app/touch-controls";
import * as Utils from "#app/utils";
import { initPokemonPrevolutions } from "#app/data/balance/pokemon-evolutions";
import { initPokemonPrevolutions, initPokemonStarters } from "#app/data/balance/pokemon-evolutions";
import { initBiomes } from "#app/data/balance/biomes";
import { initEggMoves } from "#app/data/balance/egg-moves";
import { initPokemonForms } from "#app/data/pokemon-forms";
@ -103,6 +103,8 @@ export class LoadingScene extends SceneBase {
this.loadImage("icon_tera", "ui");
this.loadImage("type_tera", "ui");
this.loadAtlas("type_bgs", "ui");
this.loadImage("mystery_egg", "ui");
this.loadImage("normal_memory", "ui");
this.loadImage("dawn_icon_fg", "ui");
this.loadImage("dawn_icon_mg", "ui");
@ -154,6 +156,7 @@ export class LoadingScene extends SceneBase {
this.loadImage("scroll_bar_handle", "ui");
this.loadImage("starter_container_bg", "ui");
this.loadImage("starter_select_bg", "ui");
this.loadImage("pokedex_summary_bg", "ui");
this.loadImage("select_cursor", "ui");
this.loadImage("select_cursor_highlight", "ui");
this.loadImage("select_cursor_highlight_thick", "ui");
@ -354,6 +357,7 @@ export class LoadingScene extends SceneBase {
initVouchers();
initStatsKeys();
initPokemonPrevolutions();
initPokemonStarters();
initBiomes();
initEggMoves();
initPokemonForms();

View File

@ -193,6 +193,7 @@ export async function initI18n(): Promise<void> {
"egg",
"fightUiHandler",
"filterBar",
"filterText",
"gameMode",
"gameStatsUiHandler",
"growth",
@ -203,6 +204,7 @@ export async function initI18n(): Promise<void> {
"move",
"nature",
"pokeball",
"pokedexUiHandler",
"pokemon",
"pokemonEvolutions",
"pokemonForm",

View File

@ -9,6 +9,7 @@ import { EaseType } from "#enums/ease-type";
import { MoneyFormat } from "#enums/money-format";
import { PlayerGender } from "#enums/player-gender";
import { ShopCursorTarget } from "#enums/shop-cursor-target";
import * as Utils from "../../utils";
const VOLUME_OPTIONS: SettingOption[] = new Array(11).fill(null).map((_, i) => i ? {
value: (i * 10).toString(),
@ -150,6 +151,7 @@ export const SettingKeys = {
Show_Stats_on_Level_Up: "SHOW_LEVEL_UP_STATS",
Shop_Cursor_Target: "SHOP_CURSOR_TARGET",
Command_Cursor_Memory: "COMMAND_CURSOR_MEMORY",
Dex_For_Devs: "DEX_FOR_DEVS",
Candy_Upgrade_Notification: "CANDY_UPGRADE_NOTIFICATION",
Candy_Upgrade_Display: "CANDY_UPGRADE_DISPLAY",
Move_Info: "MOVE_INFO",
@ -691,6 +693,16 @@ export const Setting: Array<Setting> = [
}
];
if (Utils.isLocal) {
Setting.push({
key: SettingKeys.Dex_For_Devs,
label: i18next.t("settings:dexForDevs"),
options: OFF_ON,
default: 0,
type: SettingType.GENERAL
});
}
/**
* Return the index of a Setting
* @param key SettingKey
@ -828,6 +840,9 @@ export function setSetting(setting: string, value: integer): boolean {
case SettingKeys.Command_Cursor_Memory:
globalScene.commandCursorMemory = Setting[index].options[value].value === "On";
break;
case SettingKeys.Dex_For_Devs:
globalScene.dexForDevs = Setting[index].options[value].value === "On";
break;
case SettingKeys.EXP_Gains_Speed:
globalScene.expGainsSpeed = value;
break;

View File

@ -174,7 +174,7 @@ describe("Evolution", () => {
for (let f = 1; f < 4; f++) {
vi.spyOn(Utils, "randSeedInt").mockReturnValue(f); // setting the random generator to 1, 2 and 3 to force 4 family mausholds
const fourForm = playerPokemon.getEvolution()!;
expect(fourForm.evoFormKey).toBe(null); // meanwhile, according to the pokemon-forms, the evoFormKey for a 4 family maushold is null
expect(fourForm.evoFormKey).toBe("four"); // meanwhile, according to the pokemon-forms, the evoFormKey for a 4 family maushold is "four"
}
});
});

View File

@ -10,6 +10,7 @@ export enum Tutorial {
Access_Menu = "ACCESS_MENU",
Menu = "MENU",
Starter_Select = "STARTER_SELECT",
Pokedex = "POKEDEX",
Pokerus = "POKERUS",
Stat_Change = "STAT_CHANGE",
Select_Item = "SELECT_ITEM",

View File

@ -12,6 +12,8 @@ import { globalScene } from "#app/global-scene";
import SettingsDisplayUiHandler from "./ui/settings/settings-display-ui-handler";
import SettingsAudioUiHandler from "./ui/settings/settings-audio-ui-handler";
import RunInfoUiHandler from "./ui/run-info-ui-handler";
import PokedexUiHandler from "./ui/pokedex-ui-handler";
import PokedexPageUiHandler from "./ui/pokedex-page-ui-handler";
type ActionKeys = Record<Button, () => void>;
@ -140,7 +142,7 @@ export class UiInputs {
}
buttonGoToFilter(button: Button): void {
const whitelist = [ StarterSelectUiHandler ];
const whitelist = [ StarterSelectUiHandler, PokedexUiHandler, PokedexPageUiHandler ];
const uiHandler = globalScene.ui?.getHandler();
if (whitelist.some(handler => uiHandler instanceof handler)) {
globalScene.ui.processInput(button);
@ -178,6 +180,7 @@ export class UiInputs {
globalScene.ui.setOverlayMode(Mode.MENU);
break;
case Mode.STARTER_SELECT:
case Mode.POKEDEX_PAGE:
this.buttonTouch();
break;
case Mode.MENU:
@ -190,7 +193,7 @@ export class UiInputs {
}
buttonCycleOption(button: Button): void {
const whitelist = [ StarterSelectUiHandler, SettingsUiHandler, RunInfoUiHandler, SettingsDisplayUiHandler, SettingsAudioUiHandler, SettingsGamepadUiHandler, SettingsKeyboardUiHandler ];
const whitelist = [ StarterSelectUiHandler, PokedexUiHandler, PokedexPageUiHandler, SettingsUiHandler, RunInfoUiHandler, SettingsDisplayUiHandler, SettingsAudioUiHandler, SettingsGamepadUiHandler, SettingsKeyboardUiHandler ];
const uiHandler = globalScene.ui?.getHandler();
if (whitelist.some(handler => uiHandler instanceof handler)) {
globalScene.ui.processInput(button);

View File

@ -1,11 +1,12 @@
import { globalScene } from "#app/global-scene";
import { TextStyle, addTextObject, getTextStyleOptions } from "./text";
import { TextStyle, addBBCodeTextObject, getTextColor, getTextStyleOptions } from "./text";
import { Mode } from "./ui";
import UiHandler from "./ui-handler";
import { addWindow } from "./ui-theme";
import * as Utils from "../utils";
import { argbFromRgba } from "@material/material-color-utilities";
import { Button } from "#enums/buttons";
import BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext";
export interface OptionSelectConfig {
xOffset?: number;
@ -21,8 +22,10 @@ export interface OptionSelectItem {
label: string;
handler: () => boolean;
onHover?: () => void;
skip?: boolean;
keepOpen?: boolean;
overrideSound?: boolean;
style?: TextStyle;
item?: string;
itemArgs?: any[];
}
@ -33,7 +36,7 @@ const scrollDownLabel = "↓";
export default abstract class AbstractOptionSelectUiHandler extends UiHandler {
protected optionSelectContainer: Phaser.GameObjects.Container;
protected optionSelectBg: Phaser.GameObjects.NineSlice;
protected optionSelectText: Phaser.GameObjects.Text;
protected optionSelectText: BBCodeText;
protected optionSelectIcons: Phaser.GameObjects.Sprite[];
protected config: OptionSelectConfig | null;
@ -41,11 +44,17 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler {
protected blockInput: boolean;
protected scrollCursor: integer = 0;
protected fullCursor: integer = 0;
protected scale: number = 0.1666666667;
private cursorObj: Phaser.GameObjects.Image | null;
protected unskippedIndices: number[] = [];
protected defaultTextStyle: TextStyle = TextStyle.WINDOW;
constructor(mode: Mode | null) {
super(mode);
}
@ -79,44 +88,54 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler {
protected setupOptions() {
const configOptions = this.config?.options ?? [];
let options: OptionSelectItem[];
const options: OptionSelectItem[] = configOptions;
// for performance reasons, this limits how many options we can see at once. Without this, it would try to make text options for every single options
// which makes the performance take a hit. If there's not enough options to do this (set to 10 at the moment) and the ui mode !== Mode.AUTO_COMPLETE,
// this is ignored and the original code is untouched, with the options array being all the options from the config
if (configOptions.length >= 10 && globalScene.ui.getMode() === Mode.AUTO_COMPLETE) {
const optionsScrollTotal = configOptions.length;
const optionStartIndex = this.scrollCursor;
const optionEndIndex = Math.min(optionsScrollTotal, optionStartIndex + (!optionStartIndex || this.scrollCursor + (this.config?.maxOptions! - 1) >= optionsScrollTotal ? this.config?.maxOptions! - 1 : this.config?.maxOptions! - 2));
options = configOptions.slice(optionStartIndex, optionEndIndex + 2);
} else {
options = configOptions;
}
this.unskippedIndices = this.getUnskippedIndices(configOptions);
if (this.optionSelectText) {
if (this.optionSelectText instanceof BBCodeText) {
try {
this.optionSelectText.destroy();
} catch (error) {
console.error("Error while destroying optionSelectText:", error);
}
} else {
console.warn("optionSelectText is not an instance of BBCodeText.");
}
}
if (this.optionSelectIcons?.length) {
this.optionSelectIcons.map(i => i.destroy());
this.optionSelectIcons.splice(0, this.optionSelectIcons.length);
}
this.optionSelectText = addTextObject(0, 0, options.map(o => o.item ? ` ${o.label}` : o.label).join("\n"), TextStyle.WINDOW, { maxLines: options.length });
this.optionSelectText.setLineSpacing(this.scale * 72);
const optionsWithScroll = (this.config?.options && this.config?.options.length > (this.config?.maxOptions!)) ? this.getOptionsWithScroll() : options;
// Setting the initial text to establish the width of the select object. We consider all options, even ones that are not displayed,
// Except in the case of autocomplete, where we don't want to set up a text element with potentially hundreds of lines.
const optionsForWidth = globalScene.ui.getMode() === Mode.AUTO_COMPLETE ? optionsWithScroll : options;
this.optionSelectText = addBBCodeTextObject(
0, 0, optionsForWidth.map(o => o.item
? `[shadow=${getTextColor(o.style ?? this.defaultTextStyle, true, globalScene.uiTheme)}][color=${getTextColor(o.style ?? TextStyle.WINDOW, false, globalScene.uiTheme)}] ${o.label}[/color][/shadow]`
: `[shadow=${getTextColor(o.style ?? this.defaultTextStyle, true, globalScene.uiTheme)}][color=${getTextColor(o.style ?? TextStyle.WINDOW, false, globalScene.uiTheme)}]${o.label}[/color][/shadow]`
).join("\n"),
TextStyle.WINDOW, { maxLines: options.length, lineSpacing: 12 }
);
this.optionSelectText.setOrigin(0, 0);
this.optionSelectText.setName("text-option-select");
this.optionSelectText.setLineSpacing(12);
this.optionSelectContainer.add(this.optionSelectText);
this.optionSelectContainer.setPosition((globalScene.game.canvas.width / 6) - 1 - (this.config?.xOffset || 0), -48 + (this.config?.yOffset || 0));
this.optionSelectBg.width = Math.max(this.optionSelectText.displayWidth + 24, this.getWindowWidth());
if (this.config?.options && this.config?.options.length > (this.config?.maxOptions!)) { // TODO: is this bang correct?
this.optionSelectText.setText(this.getOptionsWithScroll().map(o => o.label).join("\n"));
}
this.optionSelectBg.height = this.getWindowHeight();
this.optionSelectText.setPosition(this.optionSelectBg.x - this.optionSelectBg.width + 12 + 24 * this.scale, this.optionSelectBg.y - this.optionSelectBg.height + 2 + 42 * this.scale);
this.optionSelectText.setPositionRelative(this.optionSelectBg, 12 + 24 * this.scale, 2 + 42 * this.scale);
// Now that the container and background widths are established, we can set up the proper text restricted to visible options
this.optionSelectText.setText(optionsWithScroll.map(o => o.item
? `[shadow=${getTextColor(o.style ?? this.defaultTextStyle, true, globalScene.uiTheme)}][color=${getTextColor(o.style ?? TextStyle.WINDOW, false, globalScene.uiTheme)}] ${o.label}[/color][/shadow]`
: `[shadow=${getTextColor(o.style ?? this.defaultTextStyle, true, globalScene.uiTheme)}][color=${getTextColor(o.style ?? TextStyle.WINDOW, false, globalScene.uiTheme)}]${o.label}[/color][/shadow]`
).join("\n")
);
options.forEach((option: OptionSelectItem, i: integer) => {
if (option.item) {
@ -160,6 +179,7 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler {
this.optionSelectContainer.setVisible(true);
this.scrollCursor = 0;
this.fullCursor = 0;
this.setCursor(0);
if (this.config.delay) {
@ -169,6 +189,11 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler {
globalScene.time.delayedCall(Utils.fixedInt(this.config.delay), () => this.unblockInput());
}
if (this.config?.supportHover) {
// handle hover code if the element supports hover-handlers and the option has the optional hover-handler set.
this.config?.options[this.unskippedIndices[this.fullCursor]]?.onHover?.();
}
return true;
}
@ -177,8 +202,6 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler {
let success = false;
const options = this.getOptionsWithScroll();
let playSound = true;
if (button === Button.ACTION || button === Button.CANCEL) {
@ -190,15 +213,14 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler {
success = true;
if (button === Button.CANCEL) {
if (this.config?.maxOptions && this.config.options.length > this.config.maxOptions) {
this.scrollCursor = (this.config.options.length - this.config.maxOptions) + 1;
this.cursor = options.length - 1;
this.setCursor(this.unskippedIndices.length - 1);
} else if (!this.config?.noCancel) {
this.setCursor(options.length - 1);
this.setCursor(this.unskippedIndices.length - 1);
} else {
return false;
}
}
const option = this.config?.options[this.cursor + (this.scrollCursor - (this.scrollCursor ? 1 : 0))];
const option = this.config?.options[this.unskippedIndices[this.fullCursor]];
if (option?.handler()) {
if (!option.keepOpen) {
this.clear();
@ -211,7 +233,7 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler {
// this is here to differentiate between a Button.SUBMIT vs Button.ACTION within the autocomplete handler
// this is here because Button.ACTION is picked up as z on the keyboard, meaning if you're typing and hit z, it'll select the option you've chosen
success = true;
const option = this.config?.options[this.cursor + (this.scrollCursor - (this.scrollCursor ? 1 : 0))];
const option = this.config?.options[this.unskippedIndices[this.fullCursor]];
if (option?.handler()) {
if (!option.keepOpen) {
this.clear();
@ -223,15 +245,15 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler {
} else {
switch (button) {
case Button.UP:
if (this.cursor) {
success = this.setCursor(this.cursor - 1);
} else if (this.cursor === 0) {
success = this.setCursor(options.length - 1);
if (this.fullCursor === 0) {
success = this.setCursor(this.unskippedIndices.length - 1);
} else if (this.fullCursor) {
success = this.setCursor(this.fullCursor - 1);
}
break;
case Button.DOWN:
if (this.cursor < options.length - 1) {
success = this.setCursor(this.cursor + 1);
if (this.fullCursor < this.unskippedIndices.length - 1) {
success = this.setCursor(this.fullCursor + 1);
} else {
success = this.setCursor(0);
}
@ -239,7 +261,7 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler {
}
if (this.config?.supportHover) {
// handle hover code if the element supports hover-handlers and the option has the optional hover-handler set.
this.config?.options[this.cursor + (this.scrollCursor - (this.scrollCursor ? 1 : 0))]?.onHover?.();
this.config?.options[this.unskippedIndices[this.fullCursor]]?.onHover?.();
}
}
@ -273,7 +295,9 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler {
const optionsScrollTotal = options.length;
const optionStartIndex = this.scrollCursor;
const optionEndIndex = Math.min(optionsScrollTotal, optionStartIndex + (!optionStartIndex || this.scrollCursor + (this.config.maxOptions - 1) >= optionsScrollTotal ? this.config.maxOptions - 1 : this.config.maxOptions - 2));
const optionEndIndex = Math.min(optionsScrollTotal, optionStartIndex +
(!optionStartIndex || this.scrollCursor + (this.config.maxOptions - 1) >= optionsScrollTotal ? this.config.maxOptions - 1 : this.config.maxOptions - 2)
);
if (this.config?.maxOptions && options.length > this.config.maxOptions) {
options.splice(optionEndIndex, optionsScrollTotal);
@ -281,13 +305,15 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler {
if (optionStartIndex) {
options.unshift({
label: scrollUpLabel,
handler: () => true
handler: () => true,
style: this.defaultTextStyle
});
}
if (optionEndIndex < optionsScrollTotal) {
options.push({
label: scrollDownLabel,
handler: () => true
handler: () => true,
style: this.defaultTextStyle
});
}
}
@ -295,41 +321,63 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler {
return options;
}
setCursor(cursor: integer): boolean {
const changed = this.cursor !== cursor;
getUnskippedIndices(options: OptionSelectItem[]) {
const unskippedIndices = options
.map((option, index) => (option.skip ? null : index)) // Map to index or null if skipped
.filter(index => index !== null) as number[];
return unskippedIndices;
}
setCursor(fullCursor: integer): boolean {
const changed = this.fullCursor !== fullCursor;
let isScroll = false;
const options = this.getOptionsWithScroll();
if (changed && this.config?.maxOptions && this.config.options.length > this.config.maxOptions) {
if (Math.abs(cursor - this.cursor) === options.length - 1) {
// Wrap around the list
const optionsScrollTotal = this.config.options.length;
this.scrollCursor = cursor ? optionsScrollTotal - (this.config.maxOptions - 1) : 0;
this.setupOptions();
// If the fullCursor is the last possible value, we go to the bottom
if (fullCursor === this.unskippedIndices.length - 1) {
this.fullCursor = fullCursor;
this.cursor = this.config.maxOptions - (this.config.options.length - this.unskippedIndices[fullCursor]);
this.scrollCursor = this.config.options.length - this.config.maxOptions + 1;
// If the fullCursor is the first possible value, we go to the top
} else if (fullCursor === 0) {
this.fullCursor = fullCursor;
this.cursor = this.unskippedIndices[fullCursor];
this.scrollCursor = 0;
} else {
// Move the cursor up or down by 1
const isDown = cursor && cursor > this.cursor;
const isDown = fullCursor && fullCursor > this.fullCursor;
if (isDown) {
if (options[cursor].label === scrollDownLabel) {
isScroll = true;
this.scrollCursor++;
// If there are skipped options under the next selection, we show them
const jumpFromCurrent = this.unskippedIndices[fullCursor] - this.unskippedIndices[this.fullCursor];
const skipsFromNext = this.unskippedIndices[fullCursor + 1] - this.unskippedIndices[fullCursor] - 1;
if (this.cursor + jumpFromCurrent + skipsFromNext >= this.config.maxOptions - 1) {
this.fullCursor = fullCursor;
this.cursor = this.config.maxOptions - 2 - skipsFromNext;
this.scrollCursor = this.unskippedIndices[this.fullCursor] - this.cursor + 1;
} else {
this.fullCursor = fullCursor;
this.cursor = this.unskippedIndices[fullCursor] - this.scrollCursor + (this.scrollCursor ? 1 : 0);
}
} else {
if (!cursor && this.scrollCursor) {
isScroll = true;
this.scrollCursor--;
}
}
if (isScroll && this.scrollCursor === 1) {
this.scrollCursor += isDown ? 1 : -1;
const jumpFromPrevious = this.unskippedIndices[fullCursor] - this.unskippedIndices[fullCursor - 1];
if (this.cursor - jumpFromPrevious < 1) {
this.fullCursor = fullCursor;
this.cursor = 1;
this.scrollCursor = this.unskippedIndices[this.fullCursor] - this.cursor + 1;
} else {
this.fullCursor = fullCursor;
this.cursor = this.unskippedIndices[fullCursor] - this.scrollCursor + (this.scrollCursor ? 1 : 0);
}
}
}
if (isScroll) {
} else {
this.fullCursor = fullCursor;
this.cursor = this.unskippedIndices[fullCursor];
}
this.setupOptions();
} else {
this.cursor = cursor;
}
if (!this.cursorObj) {
this.cursorObj = globalScene.add.image(0, 0, "cursor");
@ -346,6 +394,7 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler {
super.clear();
this.config = null;
this.optionSelectContainer.setVisible(false);
this.fullCursor = 0;
this.scrollCursor = 0;
this.eraseCursor();
}

View File

@ -0,0 +1,124 @@
import type { InfoToggle } from "../battle-scene";
import { TextStyle, addTextObject } from "./text";
import { addWindow } from "./ui-theme";
import * as Utils from "../utils";
import i18next from "i18next";
import { globalScene } from "#app/global-scene";
export interface BaseStatsOverlaySettings {
scale?:number; // scale the box? A scale of 0.5 is recommended
x?: number;
y?: number;
/** Default is always half the screen, regardless of scale */
width?: number;
}
const HEIGHT = 120;
const BORDER = 8;
const GLOBAL_SCALE = 6;
const shortStats = [ "HP", "ATK", "DEF", "SPATK", "SPDEF", "SPD" ];
export default class BaseStatsOverlay extends Phaser.GameObjects.Container implements InfoToggle {
public active: boolean = false;
private statsLabels: Phaser.GameObjects.Text[] = [];
private statsRectangles: Phaser.GameObjects.Rectangle[] = [];
private statsShadows: Phaser.GameObjects.Rectangle[] = [];
private statsTotalLabel: Phaser.GameObjects.Text;
private statsBg: Phaser.GameObjects.NineSlice;
private options: BaseStatsOverlaySettings;
public scale: number;
public width: number;
constructor(options?: BaseStatsOverlaySettings) {
super(globalScene, options?.x, options?.y);
this.scale = options?.scale || 1; // set up the scale
this.setScale(this.scale);
this.options = options || {};
// prepare the description box
this.width = (options?.width || BaseStatsOverlay.getWidth(this.scale)) / this.scale; // divide by scale as we always want this to be half a window wide
this.statsBg = addWindow(0, 0, this.width, HEIGHT);
this.statsBg.setOrigin(0, 0);
this.add(this.statsBg);
for (let i = 0; i < 6; i++) {
const shadow = globalScene.add.rectangle(this.width - BORDER + 1, BORDER + 3 + i * 15, 100, 5, 0x006860);
shadow.setOrigin(1, 0);
this.statsShadows.push(shadow);
this.add(shadow);
const rectangle = globalScene.add.rectangle(this.width - BORDER, BORDER + 2 + i * 15, 100, 5, 0x66aa99);
rectangle.setOrigin(1, 0);
this.statsRectangles.push(rectangle);
this.add(rectangle);
const label = addTextObject(BORDER, BORDER - 2 + i * 15, "A", TextStyle.BATTLE_INFO);
this.statsLabels.push(label);
this.add(label);
}
this.statsTotalLabel = addTextObject(BORDER, BORDER + 6 * 15, "A", TextStyle.MONEY_WINDOW);
this.add(this.statsTotalLabel);
// hide this component for now
this.setVisible(false);
}
// show this component with infos for the specific move
show(values: number[], total: number):boolean {
for (let i = 0; i < 6; i++) {
this.statsLabels[i].setText(i18next.t(`pokemonInfo:Stat.${shortStats[i]}shortened`) + ": " + `${values[i]}`);
// This accounts for base stats up to 200, might not be enough.
// TODO: change color based on value.
this.statsShadows[i].setSize(values[i] / 2, 5);
this.statsRectangles[i].setSize(values[i] / 2, 5);
}
this.statsTotalLabel.setText(i18next.t("pokedexUiHandler:baseTotal") + ": " + `${total}`);
this.setVisible(true);
this.active = true;
return true;
}
clear() {
this.setVisible(false);
this.active = false;
}
toggleInfo(visible: boolean): void {
if (visible) {
this.setVisible(true);
}
globalScene.tweens.add({
targets: this.statsLabels,
duration: Utils.fixedInt(125),
ease: "Sine.easeInOut",
alpha: visible ? 1 : 0
});
if (!visible) {
this.setVisible(false);
}
}
isActive(): boolean {
return this.active;
}
// width of this element
static getWidth(scale:number):number {
return globalScene.game.canvas.width / GLOBAL_SCALE / 2;
}
// height of this element
static getHeight(scale:number, onSide?: boolean):number {
return HEIGHT * scale;
}
}

View File

@ -1,6 +1,7 @@
import { globalScene } from "#app/global-scene";
import { addTextObject, TextStyle } from "./text";
import { addWindow, WindowVariant } from "./ui-theme";
import { ScrollBar } from "#app/ui/scroll-bar";
import i18next from "i18next";
export enum DropDownState {
@ -293,21 +294,37 @@ export class DropDown extends Phaser.GameObjects.Container {
private onChange: () => void;
private lastDir: SortDirection = SortDirection.ASC;
private defaultSettings: any[];
private dropDownScrollBar: ScrollBar;
private totalOptions: number = 0;
private maxOptions: number = 0;
private shownOptions: number = 0;
private tooManyOptions: Boolean = false;
private firstShown: number = 0;
private optionHeight: number = 0;
private optionSpacing: number = 0;
private optionPaddingX: number = 4;
private optionPaddingY: number = 6;
private optionWidth: number = 100;
private cursorOffset: number = 0;
constructor(x: number, y: number, options: DropDownOption[], onChange: () => void, type: DropDownType = DropDownType.MULTI, optionSpacing: number = 2) {
const windowPadding = 5;
const optionHeight = 7;
const optionPaddingX = 4;
const optionPaddingY = 6;
const cursorOffset = 7;
const optionWidth = 100;
super(globalScene, x - cursorOffset - windowPadding, y);
this.optionWidth = 100;
this.optionHeight = 7;
this.optionSpacing = optionSpacing;
this.optionPaddingX = 4;
this.optionPaddingY = 6;
this.cursorOffset = cursorOffset;
this.options = options;
this.dropDownType = type;
this.onChange = onChange;
this.cursorObj = globalScene.add.image(optionPaddingX + 3, 0, "cursor");
this.cursorObj = globalScene.add.image(this.optionPaddingX + 3, 0, "cursor");
this.cursorObj.setScale(0.5);
this.cursorObj.setOrigin(0, 0.5);
this.cursorObj.setVisible(false);
@ -317,31 +334,51 @@ export class DropDown extends Phaser.GameObjects.Container {
this.options.unshift(new DropDownOption("ALL", new DropDownLabel(i18next.t("filterBar:all"), undefined, this.checkForAllOn() ? DropDownState.ON : DropDownState.OFF)));
}
this.maxOptions = 19;
this.totalOptions = this.options.length;
this.tooManyOptions = this.totalOptions > this.maxOptions;
this.shownOptions = this.tooManyOptions ? this.maxOptions : this.totalOptions;
this.defaultSettings = this.getSettings();
// Place ui elements in the correct spot
options.forEach((option, index) => {
const toggleVisibility = type !== DropDownType.SINGLE || option.state === DropDownState.ON;
option.setupToggleIcon(type, toggleVisibility);
option.width = optionWidth;
option.y = index * optionHeight + index * optionSpacing + optionPaddingY;
option.width = this.optionWidth;
option.y = index * this.optionHeight + index * optionSpacing + this.optionPaddingY;
const baseX = cursorOffset + optionPaddingX + 3;
const baseY = optionHeight / 2;
const baseX = cursorOffset + this.optionPaddingX + 3;
const baseY = this.optionHeight / 2;
option.setLabelPosition(baseX + 8, baseY);
if (type === DropDownType.SINGLE) {
option.setTogglePosition(baseX + 3, baseY + 1);
} else {
option.setTogglePosition(baseX, baseY);
}
if (index >= this.shownOptions) {
option.visible = false;
}
this.firstShown = 0;
});
this.window = addWindow(0, 0, optionWidth, options[options.length - 1].y + optionHeight + optionPaddingY, false, false, undefined, undefined, WindowVariant.XTHIN);
this.window = addWindow(0, 0, this.optionWidth, options[this.shownOptions - 1].y + this.optionHeight + this.optionPaddingY, false, false, undefined, undefined, WindowVariant.XTHIN);
this.add(this.window);
this.add(options);
this.add(this.cursorObj);
this.setVisible(false);
if (this.tooManyOptions) {
// Setting the last parameter to 1 turns out to be optimal in all cases.
this.dropDownScrollBar = new ScrollBar(this.window.width - 3, 5, 5, this.window.height - 10, 1);
this.add(this.dropDownScrollBar);
this.dropDownScrollBar.setTotalRows(this.totalOptions);
this.dropDownScrollBar.setScrollCursor(0);
}
}
getWidth(): number {
@ -371,6 +408,11 @@ export class DropDown extends Phaser.GameObjects.Container {
}
setCursor(cursor: integer): boolean {
if (this.tooManyOptions) {
this.setLabels(cursor);
}
this.cursor = cursor;
if (cursor < 0) {
cursor = 0;
@ -393,6 +435,41 @@ export class DropDown extends Phaser.GameObjects.Container {
return true;
}
setLabels(cursor: integer) {
if ((cursor === 0) && (this.lastCursor === this.totalOptions - 1)) {
this.firstShown = 0;
} else if ((cursor === this.totalOptions - 1) && (this.lastCursor === 0)) {
this.firstShown = this.totalOptions - this.shownOptions;
} else if ((cursor - this.firstShown >= this.shownOptions) && (cursor > this.lastCursor)) {
this.firstShown += 1;
} else if ((cursor < this.firstShown) && (cursor < this.lastCursor)) {
this.firstShown -= 1;
}
this.options.forEach((option, index) => {
option.y = (index - this.firstShown) * (this.optionHeight + this.optionSpacing) + this.optionPaddingY;
const baseX = this.cursorOffset + this.optionPaddingX + 3;
const baseY = this.optionHeight / 2;
option.setLabelPosition(baseX + 8, baseY);
if (this.dropDownType === DropDownType.SINGLE) {
option.setTogglePosition(baseX + 3, baseY + 1);
} else {
option.setTogglePosition(baseX, baseY);
}
if ((index < this.firstShown) || ( index >= this.firstShown + this.shownOptions)) {
option.visible = false;
} else {
option.visible = true;
}
});
this.dropDownScrollBar.setScrollCursor(cursor);
}
/**
* Switch the option at the provided index to its next state and update visuals
* Update accordingly the other options if needed:
@ -597,7 +674,12 @@ export class DropDown extends Phaser.GameObjects.Container {
x = this.options[i].getCurrentLabelX() ?? 0;
}
}
this.window.width = maxWidth + x - this.window.x + 6;
this.window.width = maxWidth + x - this.window.x + 9;
if (this.tooManyOptions) {
this.window.width += 6;
this.dropDownScrollBar.x = this.window.width - 9;
}
if (this.x + this.window.width > this.parentContainer.width) {
this.x = this.parentContainer.width - this.window.width;

View File

@ -9,6 +9,7 @@ import { globalScene } from "#app/global-scene";
export enum DropDownColumn {
GEN,
TYPES,
BIOME,
CAUGHT,
UNLOCKS,
MISC,
@ -25,13 +26,20 @@ export class FilterBar extends Phaser.GameObjects.Container {
public openDropDown: boolean = false;
private lastCursor: number = -1;
private uiTheme: UiTheme;
private leftPaddingX: number;
private rightPaddingX: number;
private cursorOffset: number;
constructor(x: number, y: number, width: number, height: number) {
constructor(x: number, y: number, width: number, height: number, leftPaddingX: number = 6, rightPaddingX: number = 6, cursorOffset: number = 8) {
super(globalScene, x, y);
this.width = width;
this.height = height;
this.leftPaddingX = leftPaddingX;
this.rightPaddingX = rightPaddingX;
this.cursorOffset = cursorOffset;
this.window = addWindow(0, 0, width, height, false, false, undefined, undefined, WindowVariant.THIN);
this.add(this.window);
@ -40,8 +48,6 @@ export class FilterBar extends Phaser.GameObjects.Container {
this.cursorObj.setVisible(false);
this.cursorObj.setOrigin(0, 0);
this.add(this.cursorObj);
this.uiTheme = globalScene.uiTheme;
}
/**
@ -86,9 +92,9 @@ export class FilterBar extends Phaser.GameObjects.Container {
updateFilterLabels(): void {
for (let i = 0; i < this.numFilters; i++) {
if (this.dropDowns[i].hasDefaultValues()) {
this.labels[i].setColor(getTextColor(TextStyle.TOOLTIP_CONTENT, false, this.uiTheme));
this.labels[i].setColor(getTextColor(TextStyle.TOOLTIP_CONTENT, false, globalScene.uiTheme));
} else {
this.labels[i].setColor(getTextColor(TextStyle.STATS_LABEL, false, this.uiTheme));
this.labels[i].setColor(getTextColor(TextStyle.STATS_LABEL, false, globalScene.uiTheme));
}
}
}
@ -97,23 +103,21 @@ export class FilterBar extends Phaser.GameObjects.Container {
* Position the filter dropdowns evenly across the width of the container
*/
private calcFilterPositions(): void {
const paddingX = 6;
const cursorOffset = 8;
let totalWidth = paddingX * 2 + cursorOffset;
let totalWidth = this.leftPaddingX + this.rightPaddingX + this.cursorOffset;
this.labels.forEach(label => {
totalWidth += label.displayWidth + cursorOffset;
totalWidth += label.displayWidth + this.cursorOffset;
});
const spacing = (this.width - totalWidth) / (this.labels.length - 1);
for (let i = 0; i < this.labels.length; i++) {
if (i === 0) {
this.labels[i].x = paddingX + cursorOffset;
this.labels[i].x = this.leftPaddingX + this.cursorOffset;
} else {
const lastRight = this.labels[i - 1].x + this.labels[i - 1].displayWidth;
this.labels[i].x = lastRight + spacing + cursorOffset;
this.labels[i].x = lastRight + spacing + this.cursorOffset;
}
this.dropDowns[i].x = this.labels[i].x - cursorOffset - paddingX;
this.dropDowns[i].x = this.labels[i].x - this.cursorOffset - this.leftPaddingX;
this.dropDowns[i].y = this.height;
}
}
@ -140,8 +144,7 @@ export class FilterBar extends Phaser.GameObjects.Container {
}
}
const cursorOffset = 8;
this.cursorObj.setPosition(this.labels[cursor].x - cursorOffset + 2, 6);
this.cursorObj.setPosition(this.labels[cursor].x - this.cursorOffset + 2, 6);
this.lastCursor = cursor;
}

230
src/ui/filter-text.ts Normal file
View File

@ -0,0 +1,230 @@
import type { StarterContainer } from "./starter-container";
import { addTextObject, getTextColor, TextStyle } from "./text";
import type { UiTheme } from "#enums/ui-theme";
import { addWindow, WindowVariant } from "./ui-theme";
import i18next from "i18next";
import type AwaitableUiHandler from "./awaitable-ui-handler";
import type UI from "./ui";
import { Mode } from "./ui";
import { globalScene } from "#app/global-scene";
export enum FilterTextRow{
NAME,
MOVE_1,
MOVE_2,
ABILITY_1,
ABILITY_2,
}
export class FilterText extends Phaser.GameObjects.Container {
private window: Phaser.GameObjects.NineSlice;
private labels: Phaser.GameObjects.Text[] = [];
private selections: Phaser.GameObjects.Text[] = [];
private selectionStrings: string[] = [];
// private dropDowns: DropDown[] = [];
private rows: FilterTextRow[] = [];
public cursorObj: Phaser.GameObjects.Image;
public numFilters: number = 0;
// public openDropDown: boolean = false;
private lastCursor: number = -1;
private uiTheme: UiTheme;
private menuMessageBoxContainer: Phaser.GameObjects.Container;
private dialogueMessageBox: Phaser.GameObjects.NineSlice;
message: any;
private readonly textPadding = 8;
private readonly defaultWordWrapWidth = 1224;
private onChange: () => void;
public defaultText: string = "---";
constructor(x: number, y: number, width: number, height: number, onChange: () => void,) {
super(globalScene, x, y);
this.onChange = onChange;
this.width = width;
this.height = height;
this.window = addWindow(0, 0, width, height, false, false, undefined, undefined, WindowVariant.THIN);
this.add(this.window);
this.cursorObj = globalScene.add.image(1, 1, "cursor");
this.cursorObj.setScale(0.5);
this.cursorObj.setVisible(false);
this.cursorObj.setOrigin(0, 0);
this.add(this.cursorObj);
this.menuMessageBoxContainer = globalScene.add.container(0, 130);
this.menuMessageBoxContainer.setName("menu-message-box");
this.menuMessageBoxContainer.setVisible(false);
// Full-width window used for testing dialog messages in debug mode
this.dialogueMessageBox = addWindow(-this.textPadding, 0, globalScene.game.canvas.width / 6 + this.textPadding * 2, 49, false, false, 0, 0, WindowVariant.THIN);
this.dialogueMessageBox.setOrigin(0, 0);
this.menuMessageBoxContainer.add(this.dialogueMessageBox);
const menuMessageText = addTextObject(this.textPadding, this.textPadding, "", TextStyle.WINDOW, { maxLines: 2 });
menuMessageText.setName("menu-message");
menuMessageText.setOrigin(0, 0);
this.menuMessageBoxContainer.add(menuMessageText);
// this.initTutorialOverlay(this.menuContainer);
// this.initPromptSprite(this.menuMessageBoxContainer);
this.message = menuMessageText;
}
/**
* Add a new filter to the FilterBar, as long that a unique DropDownColumn is provided
* @param column the DropDownColumn that will be used to access the filter values
* @param title the string that will get displayed in the filter bar
* @param dropDown the DropDown with all options for this filter
* @returns true if successful, false if the provided column was already in use for another filter
*/
addFilter(row: FilterTextRow, title: string): boolean {
// The column should be unique to each filter,
const paddingX = 6;
const cursorOffset = 8;
const extraSpaceX = 40;
if (this.rows.includes(row)) {
return false;
}
this.rows.push(row);
const filterTypesLabel = addTextObject(paddingX + cursorOffset, 3, title, TextStyle.TOOLTIP_CONTENT);
this.labels.push(filterTypesLabel);
this.add(filterTypesLabel);
const filterTypesSelection = addTextObject(paddingX + cursorOffset + extraSpaceX, 3, this.defaultText, TextStyle.TOOLTIP_CONTENT);
this.selections.push(filterTypesSelection);
this.add(filterTypesSelection);
this.selectionStrings.push("");
this.calcFilterPositions();
this.numFilters++;
return true;
}
resetSelection(index: number): void {
this.selections[index].setText(this.defaultText);
this.selectionStrings[index] = "";
this.onChange();
}
setValsToDefault(): void {
for (let i = 0; i < this.numFilters; i++) {
this.resetSelection(i);
}
}
startSearch(index: number, ui: UI): void {
ui.playSelect();
const prefilledText = "";
const buttonAction: any = {};
buttonAction["buttonActions"] = [
(sanitizedName: string) => {
// ui.revertMode();
ui.playSelect();
const dialogueTestName = sanitizedName;
//TODO: Is it really necessary to encode and decode?
const dialogueName = decodeURIComponent(escape(atob(dialogueTestName)));
const handler = ui.getHandler() as AwaitableUiHandler;
handler.tutorialActive = true;
// Switch to the dialog test window
this.selections[index].setText(String(i18next.t(dialogueName)));
ui.revertMode();
this.onChange();
},
() => {
ui.revertMode();
this.onChange;
}
];
ui.setOverlayMode(Mode.POKEDEX_SCAN, buttonAction, prefilledText, index);
}
setCursor(cursor: number): void {
const cursorOffset = 8;
this.cursorObj.setPosition(cursorOffset, this.labels[cursor].y + 3);
this.lastCursor = cursor;
}
/////////////////From here down changes must be made
/**
* Highlight the labels of the FilterBar if the filters are different from their default values
*/
updateFilterLabels(): void {
for (let i = 0; i < this.numFilters; i++) {
if (this.selections[i].text === this.defaultText) {
this.labels[i].setColor(getTextColor(TextStyle.TOOLTIP_CONTENT, false, globalScene.uiTheme));
} else {
this.labels[i].setColor(getTextColor(TextStyle.STATS_LABEL, false, globalScene.uiTheme));
}
}
}
/**
* Position the filter dropdowns evenly across the width of the container
*/
private calcFilterPositions(): void {
const paddingY = 8;
let totalHeight = paddingY * 2;
this.labels.forEach(label => {
totalHeight += label.displayHeight;
});
const spacing = (this.height - totalHeight) / (this.labels.length - 1);
for (let i = 0; i < this.labels.length; i++) {
if (i === 0) {
this.labels[i].y = paddingY;
this.selections[i].y = paddingY;
} else {
const lastBottom = this.labels[i - 1].y + this.labels[i - 1].displayHeight;
this.labels[i].y = lastBottom + spacing;
this.selections[i].y = lastBottom + spacing;
}
// Uncomment and adjust if necessary to position dropdowns vertically
// this.dropDowns[i].y = this.labels[i].y + this.labels[i].displayHeight + paddingY;
// this.dropDowns[i].x = this.width;
}
}
getValue(row: number): string {
return this.selections[row].getWrappedText()[0];
}
/**
* Find the nearest filter to the provided container on the y-axis
* @param container the StarterContainer to compare position against
* @returns the index of the closest filter
*/
getNearestFilter(container: StarterContainer): number {
const midy = container.y + container.icon.displayHeight / 2;
let nearest = 0;
let nearestDist = 1000;
for (let i = 0; i < this.labels.length; i++) {
const dist = Math.abs(midy - (this.labels[i].y + this.labels[i].displayHeight / 3));
if (dist < nearestDist) {
nearest = i;
nearestDist = dist;
}
}
return nearest;
}
}

View File

@ -24,6 +24,7 @@ enum MenuOptions {
RUN_HISTORY,
EGG_LIST,
EGG_GACHA,
POKEDEX,
MANAGE_DATA,
COMMUNITY,
SAVE_AND_QUIT,
@ -527,6 +528,11 @@ export default class MenuUiHandler extends MessageUiHandler {
ui.setOverlayMode(Mode.EGG_GACHA);
success = true;
break;
case MenuOptions.POKEDEX:
ui.revertMode();
ui.setOverlayMode(Mode.POKEDEX);
success = true;
break;
case MenuOptions.MANAGE_DATA:
if (!bypassLogin && !this.manageDataConfig.options.some(o => o.label === i18next.t("menuUiHandler:linkDiscord") || o.label === i18next.t("menuUiHandler:unlinkDiscord"))) {
this.manageDataConfig.options.splice(this.manageDataConfig.options.length - 1, 0,

View File

@ -8,7 +8,7 @@ import { Mode } from "#app/ui/ui";
import * as Utils from "#app/utils";
import { PokemonFormChangeItemModifier, PokemonHeldItemModifier, SwitchEffectTransferModifier } from "#app/modifier/modifier";
import { allMoves, ForceSwitchOutAttr } from "#app/data/move";
import { getGenderColor, getGenderSymbol } from "#app/data/gender";
import { Gender, getGenderColor, getGenderSymbol } from "#app/data/gender";
import { StatusEffect } from "#enums/status-effect";
import PokemonIconAnimHandler, { PokemonIconAnimMode } from "#app/ui/pokemon-icon-anim-handler";
import { pokemonEvolutions } from "#app/data/balance/pokemon-evolutions";
@ -109,6 +109,7 @@ export enum PartyOption {
TEACH,
TRANSFER,
SUMMARY,
POKEDEX,
UNPAUSE_EVOLUTION,
SPLICE,
UNSPLICE,
@ -218,7 +219,7 @@ export default class PartyUiHandler extends MessageUiHandler {
public static NoEffectMessage = i18next.t("partyUiHandler:anyEffect");
private localizedOptions = [ PartyOption.SEND_OUT, PartyOption.SUMMARY, PartyOption.CANCEL, PartyOption.APPLY, PartyOption.RELEASE, PartyOption.TEACH, PartyOption.SPLICE, PartyOption.UNSPLICE, PartyOption.REVIVE, PartyOption.TRANSFER, PartyOption.UNPAUSE_EVOLUTION, PartyOption.PASS_BATON, PartyOption.RENAME, PartyOption.SELECT ];
private localizedOptions = [ PartyOption.SEND_OUT, PartyOption.SUMMARY, PartyOption.POKEDEX, PartyOption.CANCEL, PartyOption.APPLY, PartyOption.RELEASE, PartyOption.TEACH, PartyOption.SPLICE, PartyOption.UNSPLICE, PartyOption.REVIVE, PartyOption.TRANSFER, PartyOption.UNPAUSE_EVOLUTION, PartyOption.PASS_BATON, PartyOption.RENAME, PartyOption.SELECT ];
constructor() {
super(Mode.PARTY);
@ -397,7 +398,7 @@ export default class PartyUiHandler extends MessageUiHandler {
}
ui.playSelect();
return true;
} else if ((option !== PartyOption.SUMMARY && option !== PartyOption.UNPAUSE_EVOLUTION && option !== PartyOption.UNSPLICE && option !== PartyOption.RELEASE && option !== PartyOption.CANCEL && option !== PartyOption.RENAME)
} else if ((option !== PartyOption.SUMMARY && option !== PartyOption.POKEDEX && option !== PartyOption.UNPAUSE_EVOLUTION && option !== PartyOption.UNSPLICE && option !== PartyOption.RELEASE && option !== PartyOption.CANCEL && option !== PartyOption.RENAME)
|| (option === PartyOption.RELEASE && this.partyUiMode === PartyUiMode.RELEASE)) {
let filterResult: string | null;
const getTransferrableItemsFromPokemon = (pokemon: PlayerPokemon) =>
@ -466,6 +467,16 @@ export default class PartyUiHandler extends MessageUiHandler {
ui.playSelect();
ui.setModeWithoutClear(Mode.SUMMARY, pokemon).then(() => this.clearOptions());
return true;
} else if (option === PartyOption.POKEDEX) {
ui.playSelect();
const attributes = {
shiny: pokemon.shiny,
variant: pokemon.variant,
form: pokemon.formIndex,
female: pokemon.gender === Gender.FEMALE ? true : false
};
ui.setOverlayMode(Mode.POKEDEX_PAGE, pokemon.species, pokemon.formIndex, attributes).then(() => this.clearOptions());
return true;
} else if (option === PartyOption.UNPAUSE_EVOLUTION) {
this.clearOptions();
ui.playSelect();
@ -892,6 +903,7 @@ export default class PartyUiHandler extends MessageUiHandler {
}
this.options.push(PartyOption.SUMMARY);
this.options.push(PartyOption.POKEDEX);
this.options.push(PartyOption.RENAME);
if ((pokemonEvolutions.hasOwnProperty(pokemon.species.speciesId) || (pokemon.isFusion() && pokemon.fusionSpecies && pokemonEvolutions.hasOwnProperty(pokemon.fusionSpecies.speciesId)))) {

View File

@ -0,0 +1,174 @@
import type { InfoToggle } from "../battle-scene";
import { TextStyle, addTextObject } from "./text";
import { addWindow } from "./ui-theme";
import * as Utils from "../utils";
import i18next from "i18next";
import { globalScene } from "#app/global-scene";
export interface PokedexInfoOverlaySettings {
delayVisibility?: boolean; // if true, showing the overlay will only set it to active and populate the fields and the handler using this field has to manually call setVisible later.
scale?:number; // scale the box? A scale of 0.5 is recommended
//location and width of the component; unaffected by scaling
x?: number;
y?: number;
/** Default is always half the screen, regardless of scale */
width?: number;
/** Determines whether to display the small secondary box */
hideEffectBox?: boolean;
hideBg?: boolean;
}
const DESC_HEIGHT = 48;
const BORDER = 8;
const GLOBAL_SCALE = 6;
export default class PokedexInfoOverlay extends Phaser.GameObjects.Container implements InfoToggle {
public active: boolean = false;
private desc: Phaser.GameObjects.Text;
private descScroll : Phaser.Tweens.Tween | null = null;
private descBg: Phaser.GameObjects.NineSlice;
private options: PokedexInfoOverlaySettings;
private textMaskRect: Phaser.GameObjects.Graphics;
private maskPointOriginX: number;
private maskPointOriginY: number;
public scale: number;
public width: number;
constructor(options?: PokedexInfoOverlaySettings) {
super(globalScene, options?.x, options?.y);
this.scale = options?.scale || 1; // set up the scale
this.setScale(this.scale);
this.options = options || {};
// prepare the description box
this.width = (options?.width || PokedexInfoOverlay.getWidth(this.scale)) / this.scale; // divide by scale as we always want this to be half a window wide
this.descBg = addWindow(0, 0, this.width, DESC_HEIGHT);
this.descBg.setOrigin(0, 0);
this.add(this.descBg);
// set up the description; wordWrap uses true pixels, unaffected by any scaling, while other values are affected
this.desc = addTextObject(BORDER, BORDER - 2, "", TextStyle.BATTLE_INFO, { wordWrap: { width: (this.width - (BORDER - 2) * 2) * GLOBAL_SCALE }});
this.desc.setLineSpacing(i18next.resolvedLanguage === "ja" ? 25 : 5);
// limit the text rendering, required for scrolling later on
this.maskPointOriginX = options?.x || 0;
this.maskPointOriginY = options?.y || 0;
if (this.maskPointOriginX < 0) {
this.maskPointOriginX += globalScene.game.canvas.width / GLOBAL_SCALE;
}
if (this.maskPointOriginY < 0) {
this.maskPointOriginY += globalScene.game.canvas.height / GLOBAL_SCALE;
}
this.textMaskRect = globalScene.make.graphics();
this.textMaskRect.fillStyle(0xFF0000);
this.textMaskRect.fillRect(
this.maskPointOriginX + BORDER * this.scale, this.maskPointOriginY + (BORDER - 2) * this.scale,
this.width - (BORDER * 2) * this.scale, (DESC_HEIGHT - (BORDER - 2) * 2) * this.scale);
this.textMaskRect.setScale(6);
const textMask = this.createGeometryMask(this.textMaskRect);
this.add(this.desc);
this.desc.setMask(textMask);
if (options?.hideBg) {
this.descBg.setVisible(false);
}
// hide this component for now
this.setVisible(false);
}
// show this component with infos for the specific move
show(text: string):boolean {
if (!globalScene.enableMoveInfo) {
return false; // move infos have been disabled // TODO:: is `false` correct? i used to be `undeefined`
}
this.desc.setText(text ?? "");
// stop previous scrolling effects and reset y position
if (this.descScroll) {
this.descScroll.remove();
this.descScroll = null;
this.desc.y = BORDER - 2;
}
// determine if we need to add new scrolling effects
const lineCount = Math.floor(this.desc.displayHeight * (96 / 72) / 14.83);
const newHeight = lineCount >= 3 ? 48 : (lineCount === 2 ? 36 : 24);
this.textMaskRect.clear();
this.textMaskRect.fillStyle(0xFF0000);
this.textMaskRect.fillRect(
this.maskPointOriginX + BORDER * this.scale,
this.maskPointOriginY + (BORDER - 2) * this.scale + (48 - newHeight),
this.width - (BORDER * 2) * this.scale,
(newHeight - (BORDER - 2) * 2) * this.scale
);
const updatedMask = this.createGeometryMask(this.textMaskRect);
this.desc.setMask(updatedMask);
this.descBg.setSize(this.descBg.width, newHeight);
this.descBg.setY(48 - newHeight);
this.desc.setY(BORDER - 2 + (48 - newHeight));
if (lineCount > 3) {
// generate scrolling effects
this.descScroll = globalScene.tweens.add({
targets: this.desc,
delay: Utils.fixedInt(2000),
loop: -1,
hold: Utils.fixedInt(2000),
duration: Utils.fixedInt((lineCount - 3) * 2000),
y: `-=${14.83 * (72 / 96) * (lineCount - 3)}`
});
}
if (!this.options.delayVisibility) {
this.setVisible(true);
}
this.active = true;
return true;
}
clear() {
this.setVisible(false);
this.active = false;
}
toggleInfo(visible: boolean): void {
if (visible) {
this.setVisible(true);
}
globalScene.tweens.add({
targets: this.desc,
duration: Utils.fixedInt(125),
ease: "Sine.easeInOut",
alpha: visible ? 1 : 0
});
if (!visible) {
this.setVisible(false);
}
}
isActive(): boolean {
return this.active;
}
// width of this element
static getWidth(scale:number):number {
return globalScene.game.canvas.width / GLOBAL_SCALE / 2;
}
// height of this element
static getHeight(scale:number, onSide?: boolean):number {
return DESC_HEIGHT * scale;
}
}

View File

@ -0,0 +1,164 @@
import { globalScene } from "#app/global-scene";
import type PokemonSpecies from "../data/pokemon-species";
import { addTextObject, TextStyle } from "./text";
export class PokedexMonContainer extends Phaser.GameObjects.Container {
public species: PokemonSpecies;
public icon: Phaser.GameObjects.Sprite;
public shinyIcons: Phaser.GameObjects.Image[] = [];
public label: Phaser.GameObjects.Text;
public starterPassiveBgs: Phaser.GameObjects.Image;
public hiddenAbilityIcon: Phaser.GameObjects.Image;
public favoriteIcon: Phaser.GameObjects.Image;
public classicWinIcon: Phaser.GameObjects.Image;
public candyUpgradeIcon: Phaser.GameObjects.Image;
public candyUpgradeOverlayIcon: Phaser.GameObjects.Image;
public eggMove1Icon: Phaser.GameObjects.Image;
public tmMove1Icon: Phaser.GameObjects.Image;
public eggMove2Icon: Phaser.GameObjects.Image;
public tmMove2Icon: Phaser.GameObjects.Image;
public passive1Icon: Phaser.GameObjects.Image;
public passive2Icon: Phaser.GameObjects.Image;
public cost: number = 0;
constructor(species: PokemonSpecies) {
super(globalScene, 0, 0);
this.species = species;
const defaultDexAttr = globalScene.gameData.getSpeciesDefaultDexAttr(species, false, true);
const defaultProps = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr);
// starter passive bg
const starterPassiveBg = globalScene.add.image(2, 5, "passive_bg");
starterPassiveBg.setOrigin(0, 0);
starterPassiveBg.setScale(0.75);
starterPassiveBg.setVisible(false);
this.add(starterPassiveBg);
this.starterPassiveBgs = starterPassiveBg;
// icon
this.icon = globalScene.add.sprite(-2, 2, species.getIconAtlasKey(defaultProps.formIndex, defaultProps.shiny, defaultProps.variant));
this.icon.setScale(0.5);
this.icon.setOrigin(0, 0);
this.icon.setFrame(species.getIconId(defaultProps.female, defaultProps.formIndex, defaultProps.shiny, defaultProps.variant));
this.checkIconId(defaultProps.female, defaultProps.formIndex, defaultProps.shiny, defaultProps.variant);
this.icon.setTint(0);
this.add(this.icon);
// shiny icons
for (let i = 0; i < 3; i++) {
const shinyIcon = globalScene.add.image(i * -3 + 12, 2, "shiny_star_small");
shinyIcon.setScale(0.5);
shinyIcon.setOrigin(0, 0);
shinyIcon.setVisible(false);
this.shinyIcons.push(shinyIcon);
}
this.add(this.shinyIcons);
// value label
const label = addTextObject(1, 2, "0", TextStyle.WINDOW, { fontSize: "32px" });
label.setShadowOffset(2, 2);
label.setOrigin(0, 0);
label.setVisible(false);
this.add(label);
this.label = label;
// hidden ability icon
const abilityIcon = globalScene.add.image(12, 7, "ha_capsule");
abilityIcon.setOrigin(0, 0);
abilityIcon.setScale(0.5);
abilityIcon.setVisible(false);
this.add(abilityIcon);
this.hiddenAbilityIcon = abilityIcon;
// favorite icon
const favoriteIcon = globalScene.add.image(0, 7, "favorite");
favoriteIcon.setOrigin(0, 0);
favoriteIcon.setScale(0.5);
favoriteIcon.setVisible(false);
this.add(favoriteIcon);
this.favoriteIcon = favoriteIcon;
// classic win icon
const classicWinIcon = globalScene.add.image(0, 12, "champion_ribbon");
classicWinIcon.setOrigin(0, 0);
classicWinIcon.setScale(0.5);
classicWinIcon.setVisible(false);
this.add(classicWinIcon);
this.classicWinIcon = classicWinIcon;
// candy upgrade icon
const candyUpgradeIcon = globalScene.add.image(12, 12, "candy");
candyUpgradeIcon.setOrigin(0, 0);
candyUpgradeIcon.setScale(0.25);
candyUpgradeIcon.setVisible(false);
this.add(candyUpgradeIcon);
this.candyUpgradeIcon = candyUpgradeIcon;
// candy upgrade overlay icon
const candyUpgradeOverlayIcon = globalScene.add.image(12, 12, "candy_overlay");
candyUpgradeOverlayIcon.setOrigin(0, 0);
candyUpgradeOverlayIcon.setScale(0.25);
candyUpgradeOverlayIcon.setVisible(false);
this.add(candyUpgradeOverlayIcon);
this.candyUpgradeOverlayIcon = candyUpgradeOverlayIcon;
// move icons
const eggMove1Icon = globalScene.add.image(0, 12, "mystery_egg");
eggMove1Icon.setOrigin(0, 0);
eggMove1Icon.setScale(0.25);
eggMove1Icon.setVisible(false);
this.add(eggMove1Icon);
this.eggMove1Icon = eggMove1Icon;
// move icons
const tmMove1Icon = globalScene.add.image(0, 12, "normal_memory");
tmMove1Icon.setOrigin(0, 0);
tmMove1Icon.setScale(0.25);
tmMove1Icon.setVisible(false);
this.add(tmMove1Icon);
this.tmMove1Icon = tmMove1Icon;
// move icons
const eggMove2Icon = globalScene.add.image(7, 12, "mystery_egg");
eggMove2Icon.setOrigin(0, 0);
eggMove2Icon.setScale(0.25);
eggMove2Icon.setVisible(false);
this.add(eggMove2Icon);
this.eggMove2Icon = eggMove2Icon;
// move icons
const tmMove2Icon = globalScene.add.image(7, 12, "normal_memory");
tmMove2Icon.setOrigin(0, 0);
tmMove2Icon.setScale(0.25);
tmMove2Icon.setVisible(false);
this.add(tmMove2Icon);
this.tmMove2Icon = tmMove2Icon;
// move icons
const passive1Icon = globalScene.add.image(3, 3, "candy");
passive1Icon.setOrigin(0, 0);
passive1Icon.setScale(0.25);
passive1Icon.setVisible(false);
this.add(passive1Icon);
this.passive1Icon = passive1Icon;
// move icons
const passive2Icon = globalScene.add.image(12, 3, "candy");
passive2Icon.setOrigin(0, 0);
passive2Icon.setScale(0.25);
passive2Icon.setVisible(false);
this.add(passive2Icon);
this.passive2Icon = passive2Icon;
}
checkIconId(female, formIndex, shiny, variant) {
if (this.icon.frame.name !== this.species.getIconId(female, formIndex, shiny, variant)) {
console.log(`${this.species.name}'s variant icon does not exist. Replacing with default.`);
this.icon.setTexture(this.species.getIconAtlasKey(formIndex, false, variant));
this.icon.setFrame(this.species.getIconId(female, formIndex, false, variant));
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,191 @@
import type { InputFieldConfig } from "./form-modal-ui-handler";
import { FormModalUiHandler } from "./form-modal-ui-handler";
import type { ModalConfig } from "./modal-ui-handler";
import type { PlayerPokemon } from "#app/field/pokemon";
import type { OptionSelectItem } from "./abstact-option-select-ui-handler";
import { isNullOrUndefined } from "#app/utils";
import { Mode } from "./ui";
import { FilterTextRow } from "./filter-text";
import { allAbilities } from "#app/data/ability";
import { allMoves } from "#app/data/move";
import { allSpecies } from "#app/data/pokemon-species";
import i18next from "i18next";
export default class PokedexScanUiHandler extends FormModalUiHandler {
keys: string[];
reducedKeys: string[];
parallelKeys: string[];
nameKeys: string[];
moveKeys: string[];
abilityKeys: string[];
row: number;
constructor(mode) {
super(mode);
}
setup() {
super.setup();
this.nameKeys = allSpecies.map(a => a.name).filter((value, index, self) => self.indexOf(value) === index);
this.moveKeys = allMoves.map(a => a.name);
this.abilityKeys = allAbilities.map(a => a.name);
}
getModalTitle(config?: ModalConfig): string {
return i18next.t("pokedexUiHandler:scanChooseOption");
}
getWidth(config?: ModalConfig): number {
return 300;
}
getMargin(config?: ModalConfig): [number, number, number, number] {
return [ 0, 0, 48, 0 ];
}
getButtonLabels(config?: ModalConfig): string[] {
return [ i18next.t("pokedexUiHandler:scanSelect"), i18next.t("pokedexUiHandler:scanCancel") ];
}
getReadableErrorMessage(error: string): string {
const colonIndex = error?.indexOf(":");
if (colonIndex > 0) {
error = error.slice(0, colonIndex);
}
return super.getReadableErrorMessage(error);
}
override getInputFieldConfigs(): InputFieldConfig[] {
switch (this.row) {
case FilterTextRow.NAME: {
return [{ label: i18next.t("pokedexUiHandler:scanLabelName") }];
}
case FilterTextRow.MOVE_1:
case FilterTextRow.MOVE_2: {
return [{ label: i18next.t("pokedexUiHandler:scanLabelMove") }];
}
case FilterTextRow.ABILITY_1:{
return [{ label: i18next.t("pokedexUiHandler:scanLabelAbility") }];
}
case FilterTextRow.ABILITY_2: {
return [{ label: i18next.t("pokedexUiHandler:scanLabelPassive") }];
}
default: {
return [{ label: "" }];
}
}
}
reduceKeys(): void {
switch (this.row) {
case FilterTextRow.NAME: {
this.reducedKeys = this.nameKeys;
break;
}
case FilterTextRow.MOVE_1:
case FilterTextRow.MOVE_2: {
this.reducedKeys = this.moveKeys;
break;
}
case FilterTextRow.ABILITY_1:
case FilterTextRow.ABILITY_2: {
this.reducedKeys = this.abilityKeys;
break;
}
default: {
this.reducedKeys = this.keys;
}
}
}
// args[2] is an index of FilterTextRow
show(args: any[]): boolean {
this.row = args[2];
const ui = this.getUi();
const hasTitle = !!this.getModalTitle();
this.updateFields(this.getInputFieldConfigs(), hasTitle);
this.updateContainer(args[0] as ModalConfig);
const input = this.inputs[0];
input.setMaxLength(255);
this.reduceKeys();
input.on("keydown", (inputObject, evt: KeyboardEvent) => {
if ([ "escape", "space" ].some((v) => v === evt.key.toLowerCase() || v === evt.code.toLowerCase()) && ui.getMode() === Mode.AUTO_COMPLETE) {
// Delete autocomplete list and recovery focus.
inputObject.on("blur", () => inputObject.node.focus(), { once: true });
ui.revertMode();
}
});
input.on("textchange", (inputObject, evt: InputEvent) => {
// Delete autocomplete.
if (ui.getMode() === Mode.AUTO_COMPLETE) {
ui.revertMode();
}
let options: OptionSelectItem[] = [];
const filteredKeys = this.reducedKeys.filter((command) => command.toLowerCase().includes(inputObject.text.toLowerCase()));
if (inputObject.text !== "" && filteredKeys.length > 0) {
options = filteredKeys.slice(0).map((value) => {
return {
label: value,
handler: () => {
if (!isNullOrUndefined(evt.data) || evt.inputType?.toLowerCase() === "deletecontentbackward") {
inputObject.setText(value);
}
ui.revertMode();
return true;
}
};
});
}
if (options.length > 0) {
const modalOpts = {
options: options,
maxOptions: 5,
modalContainer: this.modalContainer
};
ui.setOverlayMode(Mode.AUTO_COMPLETE, modalOpts);
}
});
if (super.show(args)) {
const config = args[0] as ModalConfig;
this.inputs[0].resize(1150, 116);
this.inputContainers[0].list[0].width = 200;
if (args[1] && typeof (args[1] as PlayerPokemon).getNameToRender === "function") {
this.inputs[0].text = (args[1] as PlayerPokemon).getNameToRender();
} else {
this.inputs[0].text = args[1];
}
this.submitAction = (_) => {
if (ui.getMode() === Mode.POKEDEX_SCAN) {
this.sanitizeInputs();
const sanitizedName = btoa(unescape(encodeURIComponent(this.inputs[0].text)));
config.buttonActions[0](sanitizedName);
return true;
}
return false;
};
return true;
}
return false;
}
clear(): void {
super.clear();
// Clearing the labels so they don't appear again and overlap
this.formLabels.forEach(label => {
label.destroy();
});
}
}

1907
src/ui/pokedex-ui-handler.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1981,6 +1981,21 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
}
});
}
options.push({
label: i18next.t("menuUiHandler:POKEDEX"),
handler: () => {
ui.setMode(Mode.STARTER_SELECT).then(() => {
const attributes = {
shiny: starterAttributes.shiny,
variant: starterAttributes.variant,
form: starterAttributes.form,
female: starterAttributes.female
};
ui.setOverlayMode(Mode.POKEDEX_PAGE, this.lastSpecies, starterAttributes.form, attributes);
return true;
});
}
});
options.push({
label: i18next.t("menu:cancel"),
handler: () => {

View File

@ -42,6 +42,7 @@ export enum TextStyle {
PERFECT_IV,
ME_OPTION_DEFAULT, // Default style for choices in ME
ME_OPTION_SPECIAL, // Style for choices with special requirements in ME
SHADOW_TEXT // To obscure unavailable options
}
export interface TextStyleOptions {
@ -359,6 +360,12 @@ export function getTextColor(textStyle: TextStyle, shadow?: boolean, uiTheme: Ui
return !shadow ? "#f8b050" : "#c07800"; // Gold
}
return !shadow ? "#78c850" : "#306850"; // Green
// Leaving the logic in place, in case someone wants to pick an even darker hue for the shadow down the line
case TextStyle.SHADOW_TEXT:
if (isLegacyTheme) {
return !shadow ? "#d0d0c8" : "#d0d0c8";
}
return !shadow ? "#6b5a73" : "#6b5a73";
}
}

View File

@ -23,6 +23,7 @@ import OptionSelectUiHandler from "./settings/option-select-ui-handler";
import EggHatchSceneHandler from "./egg-hatch-scene-handler";
import EggListUiHandler from "./egg-list-ui-handler";
import EggGachaUiHandler from "./egg-gacha-ui-handler";
import PokedexUiHandler from "./pokedex-ui-handler";
import { addWindow } from "./ui-theme";
import LoginFormUiHandler from "./login-form-ui-handler";
import RegistrationFormUiHandler from "./registration-form-ui-handler";
@ -53,6 +54,8 @@ import TestDialogueUiHandler from "#app/ui/test-dialogue-ui-handler";
import AutoCompleteUiHandler from "./autocomplete-ui-handler";
import { Device } from "#enums/devices";
import MysteryEncounterUiHandler from "./mystery-encounter-ui-handler";
import PokedexScanUiHandler from "./pokedex-scan-ui-handler";
import PokedexPageUiHandler from "./pokedex-page-ui-handler";
import { NavigationManager } from "./settings/navigationMenu";
export enum Mode {
@ -85,6 +88,9 @@ export enum Mode {
GAME_STATS,
EGG_LIST,
EGG_GACHA,
POKEDEX,
POKEDEX_SCAN,
POKEDEX_PAGE,
LOGIN_FORM,
REGISTRATION_FORM,
LOADING,
@ -109,6 +115,8 @@ const transitionModes = [
Mode.EGG_HATCH_SCENE,
Mode.EGG_LIST,
Mode.EGG_GACHA,
Mode.POKEDEX,
Mode.POKEDEX_PAGE,
Mode.CHALLENGE_SELECT,
Mode.RUN_HISTORY,
];
@ -128,6 +136,7 @@ const noTransitionModes = [
Mode.SETTINGS_KEYBOARD,
Mode.ACHIEVEMENTS,
Mode.GAME_STATS,
Mode.POKEDEX_SCAN,
Mode.LOGIN_FORM,
Mode.REGISTRATION_FORM,
Mode.LOADING,
@ -193,6 +202,9 @@ export default class UI extends Phaser.GameObjects.Container {
new GameStatsUiHandler(),
new EggListUiHandler(),
new EggGachaUiHandler(),
new PokedexUiHandler(),
new PokedexScanUiHandler(Mode.TEST_DIALOGUE),
new PokedexPageUiHandler(),
new LoginFormUiHandler(),
new RegistrationFormUiHandler(),
new LoadingModalUiHandler(),
@ -558,6 +570,7 @@ export default class UI extends Phaser.GameObjects.Container {
revertMode(): Promise<boolean> {
return new Promise<boolean>(resolve => {
if (!this?.modeChain?.length) {
return resolve(false);
}