From 6fbc0cb923536a23d51a50356a6572ae02eab147 Mon Sep 17 00:00:00 2001 From: Greenlamp2 <44787002+Greenlamp2@users.noreply.github.com> Date: Fri, 10 May 2024 15:25:41 +0200 Subject: [PATCH 1/8] Settings navigation, when on top to go to the bottom, and when on the bottom to go to the top (#605) * fix settings top and bottom * added some comment * TSDoc ready + less lengthy comments --- src/ui/settings-ui-handler.ts | 36 +++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/src/ui/settings-ui-handler.ts b/src/ui/settings-ui-handler.ts index 3ed83268ee6..6e40103b870 100644 --- a/src/ui/settings-ui-handler.ts +++ b/src/ui/settings-ui-handler.ts @@ -120,13 +120,25 @@ export default class SettingsUiHandler extends UiHandler { return true; } + /** + * Processes input from a specified button. + * This method handles navigation through a UI menu, including movement through menu items + * and handling special actions like cancellation. Each button press may adjust the cursor + * position or the menu scroll, and plays a sound effect if the action was successful. + * + * @param button - The button pressed by the user. + * @returns `true` if the action associated with the button was successfully processed, `false` otherwise. + */ processInput(button: Button): boolean { const ui = this.getUi(); + // Defines the maximum number of rows that can be displayed on the screen. + const rowsToDisplay = 9; let success = false; if (button === Button.CANCEL) { success = true; + // Reverts UI to its previous state on cancel. this.scene.ui.revertMode(); } else { const cursor = this.cursor + this.scrollCursor; @@ -137,27 +149,43 @@ export default class SettingsUiHandler extends UiHandler { success = this.setCursor(this.cursor - 1); else success = this.setScrollCursor(this.scrollCursor - 1); + } else { + // When at the top of the menu and pressing UP, move to the bottommost item. + // First, set the cursor to the last visible element, preparing for the scroll to the end. + const successA = this.setCursor(rowsToDisplay - 1); + // Then, adjust the scroll to display the bottommost elements of the menu. + const successB = this.setScrollCursor(this.optionValueLabels.length - rowsToDisplay); + success = successA && successB; // success is just there to play the little validation sound effect } break; case Button.DOWN: - if (cursor < this.optionValueLabels.length) { - if (this.cursor < 8) + if (cursor < this.optionValueLabels.length - 1) { + if (this.cursor < rowsToDisplay - 1) // if the visual cursor is in the frame of 0 to 8 success = this.setCursor(this.cursor + 1); - else if (this.scrollCursor < this.optionValueLabels.length - 9) + else if (this.scrollCursor < this.optionValueLabels.length - rowsToDisplay) success = this.setScrollCursor(this.scrollCursor + 1); + } else { + // When at the bottom of the menu and pressing DOWN, move to the topmost item. + // First, set the cursor to the first visible element, resetting the scroll to the top. + const successA = this.setCursor(0); + // Then, reset the scroll to start from the first element of the menu. + const successB = this.setScrollCursor(0); + success = successA && successB; // Indicates a successful cursor and scroll adjustment. } break; case Button.LEFT: - if (this.optionCursors[cursor]) + if (this.optionCursors[cursor]) // Moves the option cursor left, if possible. success = this.setOptionCursor(cursor, this.optionCursors[cursor] - 1, true); break; case Button.RIGHT: + // Moves the option cursor right, if possible. if (this.optionCursors[cursor] < this.optionValueLabels[cursor].length - 1) success = this.setOptionCursor(cursor, this.optionCursors[cursor] + 1, true); break; } } + // Plays a select sound effect if an action was successfully processed. if (success) ui.playSelect(); From 22a73642dfebacd65e8f55bfb7a2b00201ad925e Mon Sep 17 00:00:00 2001 From: Greenlamp2 <44787002+Greenlamp2@users.noreply.github.com> Date: Fri, 10 May 2024 15:26:45 +0200 Subject: [PATCH 2/8] Fix - Gamepad support setting "lost focus" behaviour (#658) * fix "lost focus behaviour" when disabling gamepad and thus repeating RIGHT key * added some comment * added full TSDoc, helped by chatGPT, refined every entry manually of course --- src/battle-scene.ts | 1 - src/inputs-controller.ts | 313 ++++++++++++++++++++++++++++++++++++--- src/system/settings.ts | 4 +- 3 files changed, 296 insertions(+), 22 deletions(-) diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 43a70e7ef2c..28a09a24dca 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -103,7 +103,6 @@ export default class BattleScene extends SceneBase { public expGainsSpeed: integer = 0; public hpBarSpeed: integer = 0; public fusionPaletteSwaps: boolean = true; - public gamepadSupport: boolean = true; public enableTouchControls: boolean = false; public enableVibration: boolean = false; public abSwapped: boolean = false; diff --git a/src/inputs-controller.ts b/src/inputs-controller.ts index 5f561535855..7de0969869d 100644 --- a/src/inputs-controller.ts +++ b/src/inputs-controller.ts @@ -23,19 +23,52 @@ export interface ActionGamepadMapping { const repeatInputDelayMillis = 250; +/** + * Manages and handles all input controls for the game, including keyboard and gamepad interactions. + * + * @remarks + * This class is designed to centralize input management across the game. It facilitates the setup, + * configuration, and handling of all game inputs, making it easier to manage various input devices + * such as keyboards and gamepads. The class provides methods for setting up input devices, handling + * their events, and responding to changes in input state (e.g., button presses, releases). + * + * The `InputsController` class also includes mechanisms to handle game focus events to ensure input + * states are correctly reset and managed when the game loses or regains focus, maintaining robust + * and responsive control handling throughout the game's lifecycle. + * + * Key responsibilities include: + * - Initializing and configuring gamepad and keyboard controls. + * - Emitting events related to specific input actions. + * - Responding to external changes such as gamepad connection/disconnection. + * - Managing game state transitions in response to input events, particularly focus loss and recovery. + * + * Usage of this class is intended to simplify input management across various parts of the game, + * providing a unified interface for all input-related interactions. + */ export class InputsController { private buttonKeys: Phaser.Input.Keyboard.Key[][]; private gamepads: Array = new Array(); private scene: Phaser.Scene; - // buttonLock ensures only a single movement key is firing repeated inputs - // (i.e. by holding down a button) at a time private buttonLock: Button; private buttonLock2: Button; private interactions: Map> = new Map(); private time: Time; private player: Map = new Map(); + private gamepadSupport: boolean = true; + + /** + * Initializes a new instance of the game control system, setting up initial state and configurations. + * + * @param scene - The Phaser scene associated with this instance. + * + * @remarks + * This constructor initializes the game control system with necessary setups for handling inputs. + * It prepares an interactions array indexed by button identifiers and configures default states for each button. + * Specific buttons like MENU and STATS are set not to repeat their actions. + * It concludes by calling the `init` method to complete the setup. + */ constructor(scene: Phaser.Scene) { this.scene = scene; this.time = this.scene.time; @@ -45,6 +78,7 @@ export class InputsController { this.interactions[b] = { pressTime: false, isPressed: false, + source: null, } } // We don't want the menu key to be repeated @@ -53,12 +87,19 @@ export class InputsController { this.init(); } + /** + * Sets up event handlers and initializes gamepad and keyboard controls. + * + * @remarks + * This method configures event listeners for both gamepad and keyboard inputs. + * It handles gamepad connections/disconnections and button press events, and ensures keyboard controls are set up. + * Additionally, it manages the game's behavior when it loses focus to prevent unwanted game actions during this state. + */ init(): void { this.events = new Phaser.Events.EventEmitter(); - // Handle the game losing focus - this.scene.game.events.on(Phaser.Core.Events.BLUR, () => { - this.loseFocus() - }) + this.scene.game.events.on(Phaser.Core.Events.BLUR, () => { + this.loseFocus() + }) if (typeof this.scene.input.gamepad !== 'undefined') { this.scene.input.gamepad.on('connected', function (thisGamepad) { @@ -83,30 +124,94 @@ export class InputsController { this.setupKeyboardControls(); } + /** + * Handles actions to take when the game loses focus, such as deactivating pressed keys. + * + * @remarks + * This method is triggered when the game or the browser tab loses focus. It ensures that any keys pressed are deactivated to prevent stuck keys affecting gameplay when the game is not active. + */ loseFocus(): void { this.deactivatePressedKey(); } + /** + * Enables or disables support for gamepad input. + * + * @param value - A boolean indicating whether gamepad support should be enabled (true) or disabled (false). + * + * @remarks + * This method toggles gamepad support. If disabled, it also ensures that all currently pressed gamepad buttons are deactivated to avoid stuck inputs. + */ + setGamepadSupport(value: boolean): void { + if (value) { + this.gamepadSupport = true; + } else { + this.gamepadSupport = false; + // if we disable the gamepad, we want to release every key pressed + this.deactivatePressedKey(); + } + } + + /** + * Updates the interaction handling by processing input states. + * This method gives priority to certain buttons by reversing the order in which they are checked. + * + * @remarks + * The method iterates over all possible buttons, checking for specific conditions such as: + * - If the button is registered in the `interactions` dictionary. + * - If the button has been held down long enough. + * - If the button is currently pressed. + * + * Special handling is applied if gamepad support is disabled but a gamepad source is still triggering inputs, + * preventing potential infinite loops by removing the last processed movement time for the button. + */ update(): void { - // reversed to let the cancel button have a kinda priority on the action button for (const b of Utils.getEnumValues(Button).reverse()) { - if (!this.interactions.hasOwnProperty(b)) continue; - if (this.repeatInputDurationJustPassed(b) && this.interactions[b].isPressed) { + if ( + this.interactions.hasOwnProperty(b) && + this.repeatInputDurationJustPassed(b) && + this.interactions[b].isPressed + ) { + // Prevents repeating button interactions when gamepad support is disabled. + if (!this.gamepadSupport && this.interactions[b].source === 'gamepad') { + // Deletes the last interaction for a button if gamepad is disabled. + this.delLastProcessedMovementTime(b); + return; + } + // Emits an event for the button press. this.events.emit('input_down', { - controller_type: 'repeated', + controller_type: this.interactions[b].source, button: b, }); - this.setLastProcessedMovementTime(b); + this.setLastProcessedMovementTime(b, this.interactions[b].source); } } } + /** + * Configures a gamepad for use based on its device ID. + * + * @param thisGamepad - The gamepad to set up. + * + * @remarks + * This method initializes a gamepad by mapping its ID to a predefined configuration. + * It updates the player's gamepad mapping based on the identified configuration, ensuring + * that the gamepad controls are correctly mapped to in-game actions. + */ setupGamepad(thisGamepad: Phaser.Input.Gamepad.Gamepad): void { let gamepadID = thisGamepad.id.toLowerCase(); const mappedPad = this.mapGamepad(gamepadID); this.player['mapping'] = mappedPad.gamepadMapping; } + /** + * Refreshes and re-indexes the list of connected gamepads. + * + * @remarks + * This method updates the list of gamepads to exclude any that are undefined. + * It corrects the index of each gamepad to account for any previously undefined entries, + * ensuring that all gamepads are properly indexed and can be accurately referenced within the game. + */ refreshGamepads(): void { // Sometimes, gamepads are undefined. For some reason. this.gamepads = this.scene.input.gamepad.gamepads.filter(function (el) { @@ -118,6 +223,17 @@ export class InputsController { } } + /** + * Retrieves the current gamepad mapping for in-game actions. + * + * @returns An object mapping gamepad buttons to in-game actions based on the player's current gamepad configuration. + * + * @remarks + * This method constructs a mapping of gamepad buttons to in-game action buttons according to the player's + * current gamepad configuration. If no configuration is available, it returns an empty mapping. + * The mapping includes directional controls, action buttons, and system commands among others, + * adjusted for any custom settings such as swapped action buttons. + */ getActionGamepadMapping(): ActionGamepadMapping { const gamepadMapping = {}; if (!this.player?.mapping) return gamepadMapping; @@ -142,8 +258,21 @@ export class InputsController { return gamepadMapping; } + /** + * Handles the 'down' event for gamepad buttons, emitting appropriate events and updating the interaction state. + * + * @param pad - The gamepad on which the button press occurred. + * @param button - The button that was pressed. + * @param value - The value associated with the button press, typically indicating pressure or degree of activation. + * + * @remarks + * This method is triggered when a gamepad button is pressed. If gamepad support is enabled, it: + * - Retrieves the current gamepad action mapping. + * - Checks if the pressed button is mapped to a game action. + * - If mapped, emits an 'input_down' event with the controller type and button action, and updates the interaction of this button. + */ gamepadButtonDown(pad: Phaser.Input.Gamepad.Gamepad, button: Phaser.Input.Gamepad.Button, value: number): void { - if (!this.scene.gamepadSupport) return; + if (!this.gamepadSupport) return; const actionMapping = this.getActionGamepadMapping(); const buttonDown = actionMapping.hasOwnProperty(button.index) && actionMapping[button.index]; if (buttonDown !== undefined) { @@ -151,12 +280,25 @@ export class InputsController { controller_type: 'gamepad', button: buttonDown, }); - this.setLastProcessedMovementTime(buttonDown); + this.setLastProcessedMovementTime(buttonDown, 'gamepad'); } } + /** + * Handles the 'up' event for gamepad buttons, emitting appropriate events and clearing the interaction state. + * + * @param pad - The gamepad on which the button release occurred. + * @param button - The button that was released. + * @param value - The value associated with the button release, typically indicating pressure or degree of deactivation. + * + * @remarks + * This method is triggered when a gamepad button is released. If gamepad support is enabled, it: + * - Retrieves the current gamepad action mapping. + * - Checks if the released button is mapped to a game action. + * - If mapped, emits an 'input_up' event with the controller type and button action, and clears the interaction for this button. + */ gamepadButtonUp(pad: Phaser.Input.Gamepad.Gamepad, button: Phaser.Input.Gamepad.Button, value: number): void { - if (!this.scene.gamepadSupport) return; + if (!this.gamepadSupport) return; const actionMapping = this.getActionGamepadMapping(); const buttonUp = actionMapping.hasOwnProperty(button.index) && actionMapping[button.index]; if (buttonUp !== undefined) { @@ -168,6 +310,24 @@ export class InputsController { } } + /** + * Configures keyboard controls for the game, mapping physical keys to game actions. + * + * @remarks + * This method sets up keyboard bindings for game controls using Phaser's `KeyCodes`. Each game action, represented + * by a button in the `Button` enum, is associated with one or more physical keys. For example, movement actions + * (up, down, left, right) are mapped to both arrow keys and WASD keys. Actions such as submit, cancel, and other + * game-specific functions are mapped to appropriate keys like Enter, Space, etc. + * + * The method does the following: + * - Defines a `keyConfig` object that associates each `Button` enum value with an array of `KeyCodes`. + * - Iterates over all values of the `Button` enum to set up these key bindings within the Phaser game scene. + * - For each button, it adds the respective keys to the game's input system and stores them in `this.buttonKeys`. + * - Additional configurations for mobile or alternative input schemes are stored in `mobileKeyConfig`. + * + * Post-setup, it initializes touch controls (if applicable) and starts listening for keyboard inputs using + * `listenInputKeyboard`, ensuring that all configured keys are actively monitored for player interactions. + */ setupKeyboardControls(): void { const keyCodes = Phaser.Input.Keyboard.KeyCodes; const keyConfig = { @@ -204,6 +364,24 @@ export class InputsController { this.listenInputKeyboard(); } + /** + * Sets up event listeners for keyboard inputs on all registered keys. + * + * @remarks + * This method iterates over an array of keyboard button rows (`this.buttonKeys`), adding 'down' and 'up' + * event listeners for each key. These listeners handle key press and release actions respectively. + * + * - **Key Down Event**: When a key is pressed down, the method emits an 'input_down' event with the button + * and the source ('keyboard'). It also records the time and state of the key press by calling + * `setLastProcessedMovementTime`. + * + * - **Key Up Event**: When a key is released, the method emits an 'input_up' event similarly, specifying the button + * and source. It then clears the recorded press time and state by calling + * `delLastProcessedMovementTime`. + * + * This setup ensures that each key on the keyboard is monitored for press and release events, + * and that these events are properly communicated within the system. + */ listenInputKeyboard(): void { this.buttonKeys.forEach((row, index) => { for (const key of row) { @@ -212,7 +390,7 @@ export class InputsController { controller_type: 'keyboard', button: index, }); - this.setLastProcessedMovementTime(index); + this.setLastProcessedMovementTime(index, 'keyboard'); }); key.on('up', () => { this.events.emit('input_up', { @@ -225,6 +403,19 @@ export class InputsController { }); } + /** + * Maps a gamepad ID to a specific gamepad configuration based on the ID's characteristics. + * + * @param id - The gamepad ID string, typically representing a unique identifier for a gamepad model or make. + * @returns A `GamepadConfig` object corresponding to the identified gamepad model. + * + * @remarks + * This function analyzes the provided gamepad ID and matches it to a predefined configuration based on known identifiers: + * - If the ID includes both '081f' and 'e401', it is identified as an unlicensed SNES gamepad. + * - If the ID contains 'xbox' and '360', it is identified as an Xbox 360 gamepad. + * - If the ID contains '054c', it is identified as a DualShock gamepad. + * If no specific identifiers are recognized, a generic gamepad configuration is returned. + */ mapGamepad(id: string): GamepadConfig { id = id.toLowerCase(); @@ -251,42 +442,124 @@ export class InputsController { } } - setLastProcessedMovementTime(button: Button): void { + /** + * This method updates the interaction state to reflect that the button is pressed. + * + * @param button - The button for which to set the interaction. + * @param source - The source of the input (defaults to 'keyboard'). This helps identify the origin of the input, especially useful in environments with multiple input devices. + * + * @remarks + * This method is responsible for updating the interaction state of a button within the `interactions` dictionary. If the button is not already registered, this method returns immediately. + * When invoked, it performs the following updates: + * - `pressTime`: Sets this to the current time, representing when the button was initially pressed. + * - `isPressed`: Marks the button as currently being pressed. + * - `source`: Identifies the source device of the input, which can vary across different hardware (e.g., keyboard, gamepad). + * + * Additionally, this method locks the button (by calling `setButtonLock`) to prevent it from being re-processed until it is released, ensuring that each press is handled distinctly. + */ + setLastProcessedMovementTime(button: Button, source: String = 'keyboard'): void { if (!this.interactions.hasOwnProperty(button)) return; this.setButtonLock(button); this.interactions[button].pressTime = this.time.now; this.interactions[button].isPressed = true; + this.interactions[button].source = source; } + /** + * Clears the last interaction for a specified button. + * + * @param button - The button for which to clear the interaction. + * + * @remarks + * This method resets the interaction details of the button, allowing it to be processed as a new input when pressed again. + * If the button is not registered in the `interactions` dictionary, this method returns immediately, otherwise: + * - `pressTime` is cleared. This was previously storing the timestamp of when the button was initially pressed. + * - `isPressed` is set to false, indicating that the button is no longer being pressed. + * - `source` is set to null, which had been indicating the device from which the button input was originating. + * + * It releases the button lock, which prevents the button from being processed repeatedly until it's explicitly released. + */ delLastProcessedMovementTime(button: Button): void { if (!this.interactions.hasOwnProperty(button)) return; this.releaseButtonLock(button); this.interactions[button].pressTime = null; this.interactions[button].isPressed = false; + this.interactions[button].source = null; } + /** + * Deactivates all currently pressed keys and resets their interaction states. + * + * @remarks + * This method is used to reset the state of all buttons within the `interactions` dictionary, + * effectively deactivating any currently pressed keys. It performs the following actions: + * + * - Releases button locks for predefined buttons (`buttonLock` and `buttonLock2`), allowing them + * to be pressed again or properly re-initialized in future interactions. + * - Iterates over all possible button values obtained via `Utils.getEnumValues(Button)`, and for + * each button: + * - Checks if the button is currently registered in the `interactions` dictionary. + * - Resets `pressTime` to null, indicating that there is no ongoing interaction. + * - Sets `isPressed` to false, marking the button as not currently active. + * - Clears the `source` field, removing the record of which device the button press came from. + * + * This method is typically called when needing to ensure that all inputs are neutralized. + */ deactivatePressedKey(): void { this.releaseButtonLock(this.buttonLock); this.releaseButtonLock(this.buttonLock2); for (const b of Utils.getEnumValues(Button)) { - if (!this.interactions.hasOwnProperty(b)) return; - this.interactions[b].pressTime = null; - this.interactions[b].isPressed = false; + if (this.interactions.hasOwnProperty(b)) { + this.interactions[b].pressTime = null; + this.interactions[b].isPressed = false; + this.interactions[b].source = null; + } } } + /** + * Checks if a specific button is currently locked. + * + * @param button - The button to check for a lock status. + * @returns `true` if the button is either of the two potentially locked buttons (`buttonLock` or `buttonLock2`), otherwise `false`. + * + * @remarks + * This method is used to determine if a given button is currently prevented from being processed due to a lock. + * It checks against two separate lock variables, allowing for up to two buttons to be locked simultaneously. + */ isButtonLocked(button: Button): boolean { return (this.buttonLock === button || this.buttonLock2 === button); } + /** + * Sets a lock on a given button if it is not already locked. + * + * @param button - The button to lock. + * + * @remarks + * This method ensures that a button is not processed multiple times inadvertently. + * It checks if the button is already locked by either of the two lock variables (`buttonLock` or `buttonLock2`). + * If not, it locks the button using the first available lock variable. + * This mechanism allows for up to two buttons to be locked at the same time. + */ setButtonLock(button: Button): void { if (this.buttonLock === button || this.buttonLock2 === button) return; if (this.buttonLock === button) this.buttonLock2 = button; else if (this.buttonLock2 === button) this.buttonLock = button; - else if(!!this.buttonLock) this.buttonLock2 = button; + else if (!!this.buttonLock) this.buttonLock2 = button; else this.buttonLock = button; } + /** + * Releases a lock on a specific button, allowing it to be processed again. + * + * @param button - The button whose lock is to be released. + * + * @remarks + * This method checks both lock variables (`buttonLock` and `buttonLock2`). + * If either lock matches the specified button, that lock is cleared. + * This action frees the button to be processed again, ensuring it can respond to new inputs. + */ releaseButtonLock(button: Button): void { if (this.buttonLock === button) this.buttonLock = null; else if (this.buttonLock2 === button) this.buttonLock2 = null; diff --git a/src/system/settings.ts b/src/system/settings.ts index ac4407c030d..680fadb1ee4 100644 --- a/src/system/settings.ts +++ b/src/system/settings.ts @@ -149,7 +149,9 @@ export function setSetting(scene: BattleScene, setting: Setting, value: integer) return false; break; case Setting.Gamepad_Support: - scene.gamepadSupport = settingOptions[setting][value] !== 'Disabled'; + // if we change the value of the gamepad support, we call a method in the inputController to + // activate or deactivate the controller listener + scene.inputController.setGamepadSupport(settingOptions[setting][value] !== 'Disabled'); break; case Setting.Swap_A_and_B: scene.abSwapped = settingOptions[setting][value] !== 'Disabled'; From b32b802ab45fe08cc5756354a17526bb26ca13c9 Mon Sep 17 00:00:00 2001 From: Greenlamp2 <44787002+Greenlamp2@users.noreply.github.com> Date: Fri, 10 May 2024 15:54:14 +0200 Subject: [PATCH 3/8] QOL - Settings to choose party Exp display at the end of fight (#488) * added settings to manage how to display party experience at the end of fight * added back stats changes support when setting is on but ignored when on skip * removed a useless parameter * added new level in the text * only Lv. UP if level is greated than 200 * cleanup * added some comment * TSDoc comment block * EXP_Party -> EXP_Party_Display, Only Up -> Level Up Notification * better duration for the level up notification * typo Co-authored-by: Samuel H --------- Co-authored-by: Samuel H --- src/battle-scene.ts | 13 +++++++++++++ src/phases.ts | 31 ++++++++++++++++++++++++------- src/system/settings.ts | 6 ++++++ src/ui/party-exp-bar.ts | 14 ++++++++++++-- 4 files changed, 55 insertions(+), 9 deletions(-) diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 28a09a24dca..9a51950a591 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -101,6 +101,19 @@ export default class BattleScene extends SceneBase { public experimentalSprites: boolean = false; public moveAnimations: boolean = true; public expGainsSpeed: integer = 0; + /** + * Defines the experience gain display mode. + * + * @remarks + * The `expParty` can have several modes: + * - `0` - Default: The normal experience gain display, nothing changed. + * - `1` - Level Up Notification: Displays the level up in the small frame instead of a message. + * - `2` - Skip: No level up frame nor message. + * + * Modes `1` and `2` are still compatible with stats display, level up, new move, etc. + * @default 0 - Uses the default normal experience gain display. + */ + public expParty: integer = 0; public hpBarSpeed: integer = 0; public fusionPaletteSwaps: boolean = true; public enableTouchControls: boolean = false; diff --git a/src/phases.ts b/src/phases.ts index e2532c1c06b..b697f7acbc2 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -3745,11 +3745,20 @@ export class ShowPartyExpBarPhase extends PlayerPartyMemberPokemonPhase { this.scene.unshiftPhase(new HidePartyExpBarPhase(this.scene)); pokemon.updateInfo(); - if (this.scene.expGainsSpeed < 3) { - this.scene.partyExpBar.showPokemonExp(pokemon, exp.value).then(() => { - if (newLevel > lastLevel) - this.end(); - else + if (this.scene.expParty === 2) { // 2 - Skip - no level up frame nor message + this.end(); + } else if (this.scene.expParty === 1) { // 1 - Only level up - we display the level up in the small frame instead of a message + if (newLevel > lastLevel) { // this means if we level up + // instead of displaying the exp gain in the small frame, we display the new level + // we use the same method for mode 0 & 1, by giving a parameter saying to display the exp or the level + this.scene.partyExpBar.showPokemonExp(pokemon, exp.value, this.scene.expParty === 1, newLevel).then(() => { + setTimeout(() => this.end(), 800 / Math.pow(2, this.scene.expGainsSpeed/2)); + }); + } else { + this.end(); + } + } else if (this.scene.expGainsSpeed < 3) { + this.scene.partyExpBar.showPokemonExp(pokemon, exp.value, this.scene.expParty === 1, newLevel).then(() => { setTimeout(() => this.end(), 500 / Math.pow(2, this.scene.expGainsSpeed)); }); } else { @@ -3780,6 +3789,7 @@ export class LevelUpPhase extends PlayerPartyMemberPokemonPhase { this.lastLevel = lastLevel; this.level = level; + this.scene = scene; } start() { @@ -3794,8 +3804,15 @@ export class LevelUpPhase extends PlayerPartyMemberPokemonPhase { const prevStats = pokemon.stats.slice(0); pokemon.calculateStats(); pokemon.updateInfo(); - this.scene.playSound('level_up_fanfare'); - this.scene.ui.showText(i18next.t('battle:levelUp', { pokemonName: this.getPokemon().name, level: this.level }), null, () => this.scene.ui.getMessageHandler().promptLevelUpStats(this.partyMemberIndex, prevStats, false).then(() => this.end()), null, true); + if (this.scene.expParty === 0) { // 0 - default - the normal exp gain display, nothing changed + this.scene.playSound('level_up_fanfare'); + this.scene.ui.showText(i18next.t('battle:levelUp', { pokemonName: this.getPokemon().name, level: this.level }), null, () => this.scene.ui.getMessageHandler().promptLevelUpStats(this.partyMemberIndex, prevStats, false).then(() => this.end()), null, true); + } else if (this.scene.expParty === 2) { // 2 - Skip - no level up frame nor message + this.end(); + } else { // 1 - Only level up - we display the level up in the small frame instead of a message + // we still want to display the stats if activated + this.scene.ui.getMessageHandler().promptLevelUpStats(this.partyMemberIndex, prevStats, false).then(() => this.end()); + } if (this.level <= 100) { const levelMoves = this.getPokemon().getLevelMoves(this.lastLevel + 1); for (let lm of levelMoves) diff --git a/src/system/settings.ts b/src/system/settings.ts index 680fadb1ee4..4b2c9eda1d1 100644 --- a/src/system/settings.ts +++ b/src/system/settings.ts @@ -21,6 +21,7 @@ export enum Setting { Move_Animations = "MOVE_ANIMATIONS", Show_Stats_on_Level_Up = "SHOW_LEVEL_UP_STATS", EXP_Gains_Speed = "EXP_GAINS_SPEED", + EXP_Party_Display = "EXP_PARTY_DISPLAY", HP_Bar_Speed = "HP_BAR_SPEED", Fusion_Palette_Swaps = "FUSION_PALETTE_SWAPS", Player_Gender = "PLAYER_GENDER", @@ -53,6 +54,7 @@ export const settingOptions: SettingOptions = { [Setting.Move_Animations]: [ 'Off', 'On' ], [Setting.Show_Stats_on_Level_Up]: [ 'Off', 'On' ], [Setting.EXP_Gains_Speed]: [ 'Normal', 'Fast', 'Faster', 'Skip' ], + [Setting.EXP_Party_Display]: [ 'Normal', 'Level Up Notification', 'Skip' ], [Setting.HP_Bar_Speed]: [ 'Normal', 'Fast', 'Faster', 'Instant' ], [Setting.Fusion_Palette_Swaps]: [ 'Off', 'On' ], [Setting.Player_Gender]: [ 'Boy', 'Girl' ], @@ -77,6 +79,7 @@ export const settingDefaults: SettingDefaults = { [Setting.Move_Animations]: 1, [Setting.Show_Stats_on_Level_Up]: 1, [Setting.EXP_Gains_Speed]: 0, + [Setting.EXP_Party_Display]: 0, [Setting.HP_Bar_Speed]: 0, [Setting.Fusion_Palette_Swaps]: 1, [Setting.Player_Gender]: 0, @@ -134,6 +137,9 @@ export function setSetting(scene: BattleScene, setting: Setting, value: integer) case Setting.EXP_Gains_Speed: scene.expGainsSpeed = value; break; + case Setting.EXP_Party_Display: + scene.expParty = value; + break; case Setting.HP_Bar_Speed: scene.hpBarSpeed = value; break; diff --git a/src/ui/party-exp-bar.ts b/src/ui/party-exp-bar.ts index a5451c5f27a..93c69ce2c1b 100644 --- a/src/ui/party-exp-bar.ts +++ b/src/ui/party-exp-bar.ts @@ -29,7 +29,7 @@ export default class PartyExpBar extends Phaser.GameObjects.Container { this.shown = false; } - showPokemonExp(pokemon: Pokemon, expValue: integer): Promise { + showPokemonExp(pokemon: Pokemon, expValue: integer, showOnlyLevelUp: boolean, newLevel: number): Promise { return new Promise(resolve => { if (this.shown) return resolve(); @@ -39,7 +39,17 @@ export default class PartyExpBar extends Phaser.GameObjects.Container { this.add(this.pokemonIcon); - this.expText.setText(`+${expValue.toString()}`); + // if we want to only display the level in the small frame + if (showOnlyLevelUp) { + if (newLevel > 200) { // if the level is greater than 200, we only display Lv. UP + this.expText.setText('Lv. UP'); + } else { // otherwise we display Lv. Up and the new level + this.expText.setText(`Lv. UP: ${newLevel.toString()}`); + } + } else { + // if we want to display the exp + this.expText.setText(`+${expValue.toString()}`); + } this.bg.width = this.expText.displayWidth + 28; From f1935a3e15569a8797e18db3b7ec3ec3cdbe60d5 Mon Sep 17 00:00:00 2001 From: Dakurei Date: Thu, 9 May 2024 23:30:24 -0400 Subject: [PATCH 4/8] Adds 'accuracy' to the move information in the fight ui + Capitalize 'power' and 'accuracy' fields --- src/locales/de/fight-ui-handler.ts | 3 ++- src/locales/en/fight-ui-handler.ts | 3 ++- src/locales/es/fight-ui-handler.ts | 3 ++- src/locales/fr/fight-ui-handler.ts | 3 ++- src/locales/it/fight-ui-handler.ts | 3 ++- src/locales/zh_CN/fight-ui-handler.ts | 3 ++- src/ui/fight-ui-handler.ts | 31 +++++++++++++++++++++------ src/ui/text.ts | 8 ++++++- 8 files changed, 44 insertions(+), 13 deletions(-) diff --git a/src/locales/de/fight-ui-handler.ts b/src/locales/de/fight-ui-handler.ts index 1ce9e0317bf..7546e9af66a 100644 --- a/src/locales/de/fight-ui-handler.ts +++ b/src/locales/de/fight-ui-handler.ts @@ -2,5 +2,6 @@ import { SimpleTranslationEntries } from "#app/plugins/i18n"; export const fightUiHandler: SimpleTranslationEntries = { "pp": "PP", - "power": "POWER", + "power": "Power", + "accuracy": "Accuracy", } as const; \ No newline at end of file diff --git a/src/locales/en/fight-ui-handler.ts b/src/locales/en/fight-ui-handler.ts index 1ce9e0317bf..7546e9af66a 100644 --- a/src/locales/en/fight-ui-handler.ts +++ b/src/locales/en/fight-ui-handler.ts @@ -2,5 +2,6 @@ import { SimpleTranslationEntries } from "#app/plugins/i18n"; export const fightUiHandler: SimpleTranslationEntries = { "pp": "PP", - "power": "POWER", + "power": "Power", + "accuracy": "Accuracy", } as const; \ No newline at end of file diff --git a/src/locales/es/fight-ui-handler.ts b/src/locales/es/fight-ui-handler.ts index b431e3b7065..5abe60abba6 100644 --- a/src/locales/es/fight-ui-handler.ts +++ b/src/locales/es/fight-ui-handler.ts @@ -2,5 +2,6 @@ import { SimpleTranslationEntries } from "#app/plugins/i18n"; export const fightUiHandler: SimpleTranslationEntries = { "pp": "PP", - "power": "POTENCIA", + "power": "Power", + "accuracy": "Accuracy", } as const; diff --git a/src/locales/fr/fight-ui-handler.ts b/src/locales/fr/fight-ui-handler.ts index 6e355f50cf6..a96e84c11f8 100644 --- a/src/locales/fr/fight-ui-handler.ts +++ b/src/locales/fr/fight-ui-handler.ts @@ -2,5 +2,6 @@ import { SimpleTranslationEntries } from "#app/plugins/i18n"; export const fightUiHandler: SimpleTranslationEntries = { "pp": "PP", - "power": "PUISSANCE", + "power": "Puissance", + "accuracy": "Précision", } as const; \ No newline at end of file diff --git a/src/locales/it/fight-ui-handler.ts b/src/locales/it/fight-ui-handler.ts index 0743e382c6b..7546e9af66a 100644 --- a/src/locales/it/fight-ui-handler.ts +++ b/src/locales/it/fight-ui-handler.ts @@ -2,5 +2,6 @@ import { SimpleTranslationEntries } from "#app/plugins/i18n"; export const fightUiHandler: SimpleTranslationEntries = { "pp": "PP", - "power": "POTENZA", + "power": "Power", + "accuracy": "Accuracy", } as const; \ No newline at end of file diff --git a/src/locales/zh_CN/fight-ui-handler.ts b/src/locales/zh_CN/fight-ui-handler.ts index 1ce9e0317bf..7546e9af66a 100644 --- a/src/locales/zh_CN/fight-ui-handler.ts +++ b/src/locales/zh_CN/fight-ui-handler.ts @@ -2,5 +2,6 @@ import { SimpleTranslationEntries } from "#app/plugins/i18n"; export const fightUiHandler: SimpleTranslationEntries = { "pp": "PP", - "power": "POWER", + "power": "Power", + "accuracy": "Accuracy", } as const; \ No newline at end of file diff --git a/src/ui/fight-ui-handler.ts b/src/ui/fight-ui-handler.ts index ee7e413db5d..084337b4086 100644 --- a/src/ui/fight-ui-handler.ts +++ b/src/ui/fight-ui-handler.ts @@ -17,6 +17,8 @@ export default class FightUiHandler extends UiHandler { private ppText: Phaser.GameObjects.Text; private powerLabel: Phaser.GameObjects.Text; private powerText: Phaser.GameObjects.Text; + private accuracyLabel: Phaser.GameObjects.Text; + private accuracyText: Phaser.GameObjects.Text; private cursorObj: Phaser.GameObjects.Image; private moveCategoryIcon: Phaser.GameObjects.Sprite; @@ -33,35 +35,46 @@ export default class FightUiHandler extends UiHandler { this.movesContainer = this.scene.add.container(18, -38.7); ui.add(this.movesContainer); - this.typeIcon = this.scene.add.sprite((this.scene.game.canvas.width / 6) - 57, -34, 'types', 'unknown'); + this.typeIcon = this.scene.add.sprite((this.scene.game.canvas.width / 6) - 57, -36, 'types', 'unknown'); this.typeIcon.setVisible(false); ui.add(this.typeIcon); - this.moveCategoryIcon = this.scene.add.sprite((this.scene.game.canvas.width / 6) - 25, -34, 'categories', 'physical'); + this.moveCategoryIcon = this.scene.add.sprite((this.scene.game.canvas.width / 6) - 25, -36, 'categories', 'physical'); this.moveCategoryIcon.setVisible(false); ui.add(this.moveCategoryIcon); - this.ppLabel = addTextObject(this.scene, (this.scene.game.canvas.width / 6) - 70, -22, 'PP', TextStyle.TOOLTIP_CONTENT); + this.ppLabel = addTextObject(this.scene, (this.scene.game.canvas.width / 6) - 70, -26, 'PP', TextStyle.MOVE_INFO_CONTENT); this.ppLabel.setOrigin(0.0, 0.5); this.ppLabel.setVisible(false); this.ppLabel.setText(i18next.t('fightUiHandler:pp')); ui.add(this.ppLabel); - this.ppText = addTextObject(this.scene, (this.scene.game.canvas.width / 6) - 12, -22, '--/--', TextStyle.TOOLTIP_CONTENT); + this.ppText = addTextObject(this.scene, (this.scene.game.canvas.width / 6) - 12, -26, '--/--', TextStyle.MOVE_INFO_CONTENT); this.ppText.setOrigin(1, 0.5); this.ppText.setVisible(false); ui.add(this.ppText); - this.powerLabel = addTextObject(this.scene, (this.scene.game.canvas.width / 6) - 70, -12, 'POWER', TextStyle.TOOLTIP_CONTENT); + this.powerLabel = addTextObject(this.scene, (this.scene.game.canvas.width / 6) - 70, -18, 'POWER', TextStyle.MOVE_INFO_CONTENT); this.powerLabel.setOrigin(0.0, 0.5); this.powerLabel.setVisible(false); this.powerLabel.setText(i18next.t('fightUiHandler:power')); ui.add(this.powerLabel); - this.powerText = addTextObject(this.scene, (this.scene.game.canvas.width / 6) - 12, -12, '---', TextStyle.TOOLTIP_CONTENT); + this.powerText = addTextObject(this.scene, (this.scene.game.canvas.width / 6) - 12, -18, '---', TextStyle.MOVE_INFO_CONTENT); this.powerText.setOrigin(1, 0.5); this.powerText.setVisible(false); ui.add(this.powerText); + + this.accuracyLabel = addTextObject(this.scene, (this.scene.game.canvas.width / 6) - 70, -10, 'ACC', TextStyle.MOVE_INFO_CONTENT); + this.accuracyLabel.setOrigin(0.0, 0.5); + this.accuracyLabel.setVisible(false); + this.accuracyLabel.setText(i18next.t('fightUiHandler:accuracy')) + ui.add(this.accuracyLabel); + + this.accuracyText = addTextObject(this.scene, (this.scene.game.canvas.width / 6) - 12, -10, '---', TextStyle.MOVE_INFO_CONTENT); + this.accuracyText.setOrigin(1, 0.5); + this.accuracyText.setVisible(false); + ui.add(this.accuracyText); } show(args: any[]): boolean { @@ -152,11 +165,13 @@ export default class FightUiHandler extends UiHandler { this.moveCategoryIcon.setTexture('categories', MoveCategory[pokemonMove.getMove().category].toLowerCase()).setScale(1.0); const power = pokemonMove.getMove().power; + const accuracy = pokemonMove.getMove().accuracy; const maxPP = pokemonMove.getMovePp(); const pp = maxPP - pokemonMove.ppUsed; this.ppText.setText(`${Utils.padInt(pp, 2, ' ')}/${Utils.padInt(maxPP, 2, ' ')}`); this.powerText.setText(`${power >= 0 ? power : '---'}`); + this.accuracyText.setText(`${accuracy >= 0 ? accuracy : '---'}`); } this.typeIcon.setVisible(hasMove); @@ -164,6 +179,8 @@ export default class FightUiHandler extends UiHandler { this.ppText.setVisible(hasMove); this.powerLabel.setVisible(hasMove); this.powerText.setVisible(hasMove); + this.accuracyLabel.setVisible(hasMove); + this.accuracyText.setVisible(hasMove); this.moveCategoryIcon.setVisible(hasMove); this.cursorObj.setPosition(13 + (cursor % 2 === 1 ? 100 : 0), -31 + (cursor >= 2 ? 15 : 0)); @@ -189,6 +206,8 @@ export default class FightUiHandler extends UiHandler { this.ppText.setVisible(false); this.powerLabel.setVisible(false); this.powerText.setVisible(false); + this.accuracyLabel.setVisible(false); + this.accuracyText.setVisible(false); this.moveCategoryIcon.setVisible(false); this.eraseCursor(); } diff --git a/src/ui/text.ts b/src/ui/text.ts index a8cce878240..3302c988aef 100644 --- a/src/ui/text.ts +++ b/src/ui/text.ts @@ -24,7 +24,8 @@ export enum TextStyle { SETTINGS_LABEL, SETTINGS_SELECTED, TOOLTIP_TITLE, - TOOLTIP_CONTENT + TOOLTIP_CONTENT, + MOVE_INFO_CONTENT }; export function addTextObject(scene: Phaser.Scene, x: number, y: number, content: string, style: TextStyle, extraStyleOptions?: Phaser.Types.GameObjects.Text.TextStyle): Phaser.GameObjects.Text { @@ -106,6 +107,10 @@ function getTextStyleOptions(style: TextStyle, uiTheme: UiTheme, extraStyleOptio styleOptions.fontSize = '64px'; shadowSize = 4; break; + case TextStyle.MOVE_INFO_CONTENT: + styleOptions.fontSize = '56px'; + shadowSize = 3; + break; } shadowColor = getTextColor(style, true, uiTheme); @@ -130,6 +135,7 @@ export function getTextColor(textStyle: TextStyle, shadow?: boolean, uiTheme: Ui case TextStyle.MESSAGE: return !shadow ? '#f8f8f8' : '#6b5a73'; case TextStyle.WINDOW: + case TextStyle.MOVE_INFO_CONTENT: case TextStyle.TOOLTIP_CONTENT: if (uiTheme) return !shadow ? '#484848' : '#d0d0c8'; From 5d62d0bb257143103e41c581ab5eca85c8aecf17 Mon Sep 17 00:00:00 2001 From: Greenlamp Date: Fri, 10 May 2024 16:32:19 +0200 Subject: [PATCH 5/8] fix speed level up notification --- src/phases.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/phases.ts b/src/phases.ts index b697f7acbc2..71de11579f7 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -3752,7 +3752,7 @@ export class ShowPartyExpBarPhase extends PlayerPartyMemberPokemonPhase { // instead of displaying the exp gain in the small frame, we display the new level // we use the same method for mode 0 & 1, by giving a parameter saying to display the exp or the level this.scene.partyExpBar.showPokemonExp(pokemon, exp.value, this.scene.expParty === 1, newLevel).then(() => { - setTimeout(() => this.end(), 800 / Math.pow(2, this.scene.expGainsSpeed/2)); + setTimeout(() => this.end(), 800 / Math.pow(2, this.scene.expGainsSpeed)); }); } else { this.end(); From 7bdb969a731f59708e1f9c8dd476b62777da174f Mon Sep 17 00:00:00 2001 From: William Burleson <72857839+Admiral-Billy@users.noreply.github.com> Date: Fri, 10 May 2024 06:42:28 -0400 Subject: [PATCH 6/8] Add rich presence support --- src/battle-scene.ts | 17 ++++++++++++++++- src/phases.ts | 2 ++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 9a51950a591..0171fb96c5e 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -53,7 +53,7 @@ import PokemonSpriteSparkleHandler from './field/pokemon-sprite-sparkle-handler' import CharSprite from './ui/char-sprite'; import DamageNumberHandler from './field/damage-number-handler'; import PokemonInfoContainer from './ui/pokemon-info-container'; -import { biomeDepths } from './data/biomes'; +import { biomeDepths, getBiomeName } from './data/biomes'; import { UiTheme } from './enums/ui-theme'; import { SceneBase } from './scene-base'; import CandyBar from './ui/candy-bar'; @@ -200,6 +200,7 @@ export default class BattleScene extends SceneBase { this.phaseQueuePrepend = []; this.phaseQueuePrependSpliceIndex = -1; this.nextCommandPhaseQueue = []; + this.updateGameInfo(); } loadPokemonAtlas(key: string, atlasPath: string, experimental?: boolean) { @@ -769,6 +770,8 @@ export default class BattleScene extends SceneBase { this.trainer.setTexture(`trainer_${this.gameData.gender === PlayerGender.FEMALE ? 'f' : 'm'}_back`); this.trainer.setPosition(406, 186); this.trainer.setVisible(true); + + this.updateGameInfo(); if (reloadI18n) { const localizable: Localizable[] = [ @@ -1963,4 +1966,16 @@ export default class BattleScene extends SceneBase { return false; } + + updateGameInfo(): void { + const gameInfo = { + gameMode: this.currentBattle ? this.gameMode.getName() : 'Title', + biome: this.currentBattle ? getBiomeName(this.arena.biomeType) : '', + wave: this.currentBattle?.waveIndex || 0, + party: this.party ? this.party.map(p => { + return { name: p.name, level: p.level }; + }) : [] + }; + (window as any).gameInfo = gameInfo; + } } \ No newline at end of file diff --git a/src/phases.ts b/src/phases.ts index 71de11579f7..6144fe47a86 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -680,6 +680,8 @@ export class EncounterPhase extends BattlePhase { start() { super.start(); + this.scene.updateGameInfo(); + this.scene.initSession(); const loadEnemyAssets = []; From 593ac38267bc25bd27408b63846ad70cec722606 Mon Sep 17 00:00:00 2001 From: Dakurei Date: Fri, 10 May 2024 17:29:22 +0200 Subject: [PATCH 7/8] Fix some errors from previous PR #711 (#716) --- src/locales/es/fight-ui-handler.ts | 2 +- src/locales/it/fight-ui-handler.ts | 2 +- src/ui/text.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/locales/es/fight-ui-handler.ts b/src/locales/es/fight-ui-handler.ts index 5abe60abba6..7a02ce66f3c 100644 --- a/src/locales/es/fight-ui-handler.ts +++ b/src/locales/es/fight-ui-handler.ts @@ -2,6 +2,6 @@ import { SimpleTranslationEntries } from "#app/plugins/i18n"; export const fightUiHandler: SimpleTranslationEntries = { "pp": "PP", - "power": "Power", + "power": "Potencia", "accuracy": "Accuracy", } as const; diff --git a/src/locales/it/fight-ui-handler.ts b/src/locales/it/fight-ui-handler.ts index 7546e9af66a..94cb41f721d 100644 --- a/src/locales/it/fight-ui-handler.ts +++ b/src/locales/it/fight-ui-handler.ts @@ -2,6 +2,6 @@ import { SimpleTranslationEntries } from "#app/plugins/i18n"; export const fightUiHandler: SimpleTranslationEntries = { "pp": "PP", - "power": "Power", + "power": "Potenza", "accuracy": "Accuracy", } as const; \ No newline at end of file diff --git a/src/ui/text.ts b/src/ui/text.ts index 3302c988aef..d7ecd3b2526 100644 --- a/src/ui/text.ts +++ b/src/ui/text.ts @@ -135,7 +135,7 @@ export function getTextColor(textStyle: TextStyle, shadow?: boolean, uiTheme: Ui case TextStyle.MESSAGE: return !shadow ? '#f8f8f8' : '#6b5a73'; case TextStyle.WINDOW: - case TextStyle.MOVE_INFO_CONTENT: + case TextStyle.MOVE_INFO_CONTENT: case TextStyle.TOOLTIP_CONTENT: if (uiTheme) return !shadow ? '#484848' : '#d0d0c8'; From 2ab335a3c5ea3cbdd1e508f7f013021d240e712b Mon Sep 17 00:00:00 2001 From: Athebyne <30442287+f-raZ0R@users.noreply.github.com> Date: Fri, 10 May 2024 11:40:21 -0400 Subject: [PATCH 8/8] Implement Snipe Shot, and Propeller Tail/Stalwart (#661) * Implemented Snipe Shot and Stalwart/Propeller Tail * Remove Testing Overrides I don't know why these got pushed, they are in the gitignore file. * Snipe Shot also has a high crit rate * Add Comment * Add TsDoc documentation to BypassRedirectAttr * Add ability pop-up for when Propeller Tail/Stalwart proc. * Fix Formatting * Tab align comment --- src/data/ability.ts | 6 ++++-- src/data/move.ts | 8 +++++++- src/phases.ts | 27 ++++++++++++++++++--------- 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index f2220c850dc..8a244b85b62 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -2377,6 +2377,8 @@ export class RedirectTypeMoveAbAttr extends RedirectMoveAbAttr { } } +export class BlockRedirectAbAttr extends AbAttr { } + export class ReduceStatusEffectDurationAbAttr extends AbAttr { private statusEffect: StatusEffect; @@ -3465,7 +3467,7 @@ export function initAbilities() { .attr(PostDefendStatChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, BattleStat.SPD, -1, false, true) .bypassFaint(), new Ability(Abilities.PROPELLER_TAIL, 8) - .unimplemented(), + .attr(BlockRedirectAbAttr), new Ability(Abilities.MIRROR_ARMOR, 8) .ignorable() .unimplemented(), @@ -3475,7 +3477,7 @@ export function initAbilities() { .attr(NoFusionAbilityAbAttr) .unimplemented(), new Ability(Abilities.STALWART, 8) - .unimplemented(), + .attr(BlockRedirectAbAttr), new Ability(Abilities.STEAM_ENGINE, 8) .attr(PostDefendStatChangeAbAttr, (target, user, move) => (move.type === Type.FIRE || move.type === Type.WATER) && move.category !== MoveCategory.STATUS, BattleStat.SPD, 6), new Ability(Abilities.PUNK_ROCK, 8) diff --git a/src/data/move.ts b/src/data/move.ts index 6e4e3f60fcb..6ac3af708f1 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -2653,6 +2653,11 @@ const crashDamageFunc = (user: Pokemon, move: Move) => { }; export class TypelessAttr extends MoveAttr { } +/** +* Attribute used for moves which ignore redirection effects, and always target their original target, i.e. Snipe Shot +* Bypasses Storm Drain, Follow Me, Ally Switch, and the like. +*/ +export class BypassRedirectAttr extends MoveAttr { } export class DisableMoveAttr extends MoveEffectAttr { constructor() { @@ -6172,7 +6177,8 @@ export function initMoves() { .attr(DiscourageFrequentUseAttr) .ignoresVirtual(), new AttackMove(Moves.SNIPE_SHOT, Type.WATER, MoveCategory.SPECIAL, 80, 100, 15, -1, 0, 8) - .partial(), + .attr(HighCritAttr) + .attr(BypassRedirectAttr), new AttackMove(Moves.JAW_LOCK, Type.DARK, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 8) .attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, false, 1) .attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, true, false, 1) diff --git a/src/phases.ts b/src/phases.ts index 6144fe47a86..20c4dc73714 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -2,7 +2,7 @@ import BattleScene, { AnySound, bypassLogin, startingWave } from "./battle-scene import { default as Pokemon, PlayerPokemon, EnemyPokemon, PokemonMove, MoveResult, DamageResult, FieldPosition, HitResult, TurnMove } from "./field/pokemon"; import * as Utils from './utils'; import { Moves } from "./data/enums/moves"; -import { allMoves, applyMoveAttrs, BypassSleepAttr, ChargeAttr, applyFilteredMoveAttrs, HitsTagAttr, MissEffectAttr, MoveAttr, MoveEffectAttr, MoveFlags, MultiHitAttr, OverrideMoveEffectAttr, VariableAccuracyAttr, MoveTarget, OneHitKOAttr, getMoveTargets, MoveTargetSet, MoveEffectTrigger, CopyMoveAttr, AttackMove, SelfStatusMove, DelayedAttackAttr, RechargeAttr, PreMoveMessageAttr, HealStatusEffectAttr, IgnoreOpponentStatChangesAttr, NoEffectAttr, FixedDamageAttr, PostVictoryStatChangeAttr, OneHitKOAccuracyAttr, ForceSwitchOutAttr, VariableTargetAttr } from "./data/move"; +import { allMoves, applyMoveAttrs, BypassSleepAttr, ChargeAttr, applyFilteredMoveAttrs, HitsTagAttr, MissEffectAttr, MoveAttr, MoveEffectAttr, MoveFlags, MultiHitAttr, OverrideMoveEffectAttr, VariableAccuracyAttr, MoveTarget, OneHitKOAttr, getMoveTargets, MoveTargetSet, MoveEffectTrigger, CopyMoveAttr, AttackMove, SelfStatusMove, DelayedAttackAttr, RechargeAttr, PreMoveMessageAttr, HealStatusEffectAttr, IgnoreOpponentStatChangesAttr, NoEffectAttr, BypassRedirectAttr ,FixedDamageAttr, PostVictoryStatChangeAttr, OneHitKOAccuracyAttr, ForceSwitchOutAttr, VariableTargetAttr } from "./data/move"; import { Mode } from './ui/ui'; import { Command } from "./ui/command-ui-handler"; import { Stat } from "./data/pokemon-stat"; @@ -30,7 +30,7 @@ import { Weather, WeatherType, getRandomWeatherType, getTerrainBlockMessage, get import { TempBattleStat } from "./data/temp-battle-stat"; import { ArenaTagSide, ArenaTrapTag, MistTag, TrickRoomTag } from "./data/arena-tag"; import { ArenaTagType } from "./data/enums/arena-tag-type"; -import { CheckTrappedAbAttr, IgnoreOpponentStatChangesAbAttr, PostAttackAbAttr, PostBattleAbAttr, PostDefendAbAttr, PostSummonAbAttr, PostTurnAbAttr, PostWeatherLapseAbAttr, PreSwitchOutAbAttr, PreWeatherDamageAbAttr, ProtectStatAbAttr, RedirectMoveAbAttr, RunSuccessAbAttr, StatChangeMultiplierAbAttr, SuppressWeatherEffectAbAttr, SyncEncounterNatureAbAttr, applyAbAttrs, applyCheckTrappedAbAttrs, applyPostAttackAbAttrs, applyPostBattleAbAttrs, applyPostDefendAbAttrs, applyPostSummonAbAttrs, applyPostTurnAbAttrs, applyPostWeatherLapseAbAttrs, applyPreStatChangeAbAttrs, applyPreSwitchOutAbAttrs, applyPreWeatherEffectAbAttrs, BattleStatMultiplierAbAttr, applyBattleStatMultiplierAbAttrs, IncrementMovePriorityAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr, applyPostBattleInitAbAttrs, PostBattleInitAbAttr, BlockNonDirectDamageAbAttr as BlockNonDirectDamageAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, PostBiomeChangeAbAttr, applyPostFaintAbAttrs, PostFaintAbAttr, IncreasePpAbAttr, PostStatChangeAbAttr, applyPostStatChangeAbAttrs, AlwaysHitAbAttr, PreventBerryUseAbAttr, StatChangeCopyAbAttr } from "./data/ability"; +import { CheckTrappedAbAttr, IgnoreOpponentStatChangesAbAttr, PostAttackAbAttr, PostBattleAbAttr, PostDefendAbAttr, PostSummonAbAttr, PostTurnAbAttr, PostWeatherLapseAbAttr, PreSwitchOutAbAttr, PreWeatherDamageAbAttr, ProtectStatAbAttr, RedirectMoveAbAttr, BlockRedirectAbAttr, RunSuccessAbAttr, StatChangeMultiplierAbAttr, SuppressWeatherEffectAbAttr, SyncEncounterNatureAbAttr, applyAbAttrs, applyCheckTrappedAbAttrs, applyPostAttackAbAttrs, applyPostBattleAbAttrs, applyPostDefendAbAttrs, applyPostSummonAbAttrs, applyPostTurnAbAttrs, applyPostWeatherLapseAbAttrs, applyPreStatChangeAbAttrs, applyPreSwitchOutAbAttrs, applyPreWeatherEffectAbAttrs, BattleStatMultiplierAbAttr, applyBattleStatMultiplierAbAttrs, IncrementMovePriorityAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr, applyPostBattleInitAbAttrs, PostBattleInitAbAttr, BlockNonDirectDamageAbAttr as BlockNonDirectDamageAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, PostBiomeChangeAbAttr, applyPostFaintAbAttrs, PostFaintAbAttr, IncreasePpAbAttr, PostStatChangeAbAttr, applyPostStatChangeAbAttrs, AlwaysHitAbAttr, PreventBerryUseAbAttr, StatChangeCopyAbAttr } from "./data/ability"; import { Unlockables, getUnlockableName } from "./system/unlockables"; import { getBiomeKey } from "./field/arena"; import { BattleType, BattlerIndex, TurnCommand } from "./battle"; @@ -2211,13 +2211,22 @@ export class MovePhase extends BattlePhase { } // Move redirection abilities (ie. Storm Drain) only support single target moves - const moveTarget = this.targets.length === 1 - ? new Utils.IntegerHolder(this.targets[0]) - : null; - if (moveTarget) { - this.scene.getField(true).filter(p => p !== this.pokemon).forEach(p => applyAbAttrs(RedirectMoveAbAttr, p, null, this.move.moveId, moveTarget)); - this.targets[0] = moveTarget.value; - } +const moveTarget = this.targets.length === 1 + ? new Utils.IntegerHolder(this.targets[0]) + : null; + if (moveTarget) { + var oldTarget = moveTarget.value; + this.scene.getField(true).filter(p => p !== this.pokemon).forEach(p => applyAbAttrs(RedirectMoveAbAttr, p, null, this.move.moveId, moveTarget)); + //Check if this move is immune to being redirected, and restore its target to the intended target if it is. + if ((this.pokemon.hasAbilityWithAttr(BlockRedirectAbAttr) || this.move.getMove().getAttrs(BypassRedirectAttr).length)) { + //If an ability prevented this move from being redirected, display its ability pop up. + if ((this.pokemon.hasAbilityWithAttr(BlockRedirectAbAttr) && !this.move.getMove().getAttrs(BypassRedirectAttr).length) && oldTarget != moveTarget.value) { + this.scene.unshiftPhase(new ShowAbilityPhase(this.scene, this.pokemon.getBattlerIndex(), this.pokemon.getPassiveAbility().hasAttr(BlockRedirectAbAttr))); + } + moveTarget.value = oldTarget; + } + this.targets[0] = moveTarget.value; +} if (this.targets.length === 1 && this.targets[0] === BattlerIndex.ATTACKER) { if (this.pokemon.turnData.attacksReceived.length) {