diff --git a/.gitignore b/.gitignore index 9d96ed04a15..e7fbe7e09f1 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,7 @@ dist-ssr *.local # Editor directories and files -.vscode/* +.vscode .idea .DS_Store *.suo diff --git a/create-test-boilerplate.js b/create-test-boilerplate.js index d47b7c4afeb..d2d672a2c45 100644 --- a/create-test-boilerplate.js +++ b/create-test-boilerplate.js @@ -72,14 +72,16 @@ async function runInteractive() { const type = typeAnswer.selectedOption.toLowerCase(); // Convert fileName from kebab-case or camelCase to snake_case + // Ex: "Cud Chew/Cud-Chew" --> "cud_chew" const fileName = fileNameAnswer.userInput .replace(/-+/g, "_") // Convert kebab-case (dashes) to underscores .replace(/([a-z])([A-Z])/g, "$1_$2") // Convert camelCase to snake_case .replace(/\s+/g, "_") // Replace spaces with underscores .toLowerCase(); // Ensure all lowercase - // Format the description for the test case + // Get proper english name for test names and data name for abilities/moves const formattedName = fileName.replace(/_/g, " ").replace(/\b\w/g, char => char.toUpperCase()); + const attrName = fileName.toUpperCase(); // Used for move/ability tests to override with ability/move being tested // Determine the directory based on the type let dir; let description; @@ -130,8 +132,8 @@ describe("${description}", () => { beforeEach(() => { game = new GameManager(phaserGame); game.override - .moveset([ Moves.SPLASH ]) - .ability(Abilities.BALL_FETCH) + .moveset(Moves.${type === "move" ? attrName : "SPLASH"}) + .ability(Abilities.${type === "ability" ? attrName : "BALL_FETCH"}) .battleType("single") .disableCrits() .enemySpecies(Species.MAGIKARP) diff --git a/docs/comments.md b/docs/comments.md index 9610052adf2..936b7fda904 100644 --- a/docs/comments.md +++ b/docs/comments.md @@ -9,14 +9,14 @@ - The best example of this is JSDoc-style comments as seen below: - When formatted this way, the comment gets shown by intellisense in VS Code or similar IDEs just by hovering over the text! - Functions also show each the comment for parameter as you type them, making keeping track of what each one does in lengthy functions much more clear -```js +```ts /** * Changes the type-based weather modifier if this move's power would be reduced by it - * @param user {@linkcode Pokemon} using this move - * @param target {@linkcode Pokemon} target of this move - * @param move {@linkcode Move} being used - * @param args [0] {@linkcode Utils.NumberHolder} for arenaAttackTypeMultiplier - * @returns true if the function succeeds + * @param user The {@linkcode Pokemon} using this move + * @param target The {@linkcode Pokemon} being targeted by this move + * @param move The {@linkcode Move} being used + * @param args `[0]` {@linkcode Utils.NumberHolder} for arenaAttackTypeMultiplier + * @returns `true` if the function succeeds */ apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { } @@ -46,7 +46,7 @@ If you're interested in going more in depth, you can find a reference guide for While other classes should be fully documented, Abilities and Moves heavily incoperate inheritance (i.e. the `extends` keyword). Because of this, much of the functionality in these classes is duplicated or only slightly changed between classes. ### With this in mind, there's a few more things to keep in mind for these: - Do not document any parameters if the function mirrors the one they extend. - - Keep this in mind for functions that are not the `apply` function as they are usually sparce and mostly reused + - Keep this in mind for functions that are not the `apply` function as they are usually sparse and mostly reused - The class itself must be documented - This must include the `@extends BaseClass` and `@see {@linkcode apply}` tags - Class member variables must be documented diff --git a/docs/enemy-ai.md b/docs/enemy-ai.md index 8edf5a3f10e..6e73243786d 100644 --- a/docs/enemy-ai.md +++ b/docs/enemy-ai.md @@ -75,7 +75,13 @@ In `getNextMove()`, the enemy Pokémon chooses a move to use in the following st As part of the move selection process, the enemy Pokémon must compute a **target score (TS)** for each legal target for each move in its move pool. The base target score for all moves is a combination of the move's **user benefit score (UBS)** and **target benefit score (TBS)**. -![equation](https://latex.codecogs.com/png.image?%5Cinline%20%5Cdpi%7B100%7D%5Cbg%7Bwhite%7D%5Ctext%7BTS%7D=%5Ctext%7BUBS%7D+%5Ctext%7BTBS%7D%5Ctimes%5Cleft%5C%7B%5Cbegin%7Bmatrix%7D-1&%5Ctext%7Bif%20target%20is%20an%20opponent%7D%5C%5C1&%5Ctext%7Botherwise%7D%5C%5C%5Cend%7Bmatrix%7D%5Cright.) +$$ +\text{TS} = \text{UBS} + \text{TBS} \times +\begin{cases} +-1 & \text{if target is an opponent} \\ +1 & \text{otherwise} +\end{cases} +$$ A move's UBS and TBS are computed with the respective functions in the `Move` class: @@ -96,16 +102,35 @@ In addition to the base score from `Move.getTargetBenefitScore()`, attack moves - The move's category (Physical/Special), and whether the user has a higher Attack or Special Attack stat. More specifically, the following steps are taken to compute the move's `attackScore`: -1. Compute a multiplier based on the move's type effectiveness: +1. Compute a multiplier based on the move's type effectiveness: - ![typeMultEqn](https://latex.codecogs.com/png.image?%5Cdpi%7B110%7D%5Cbg%7Bwhite%7D%5Ctext%7BtypeMult%7D=%5Cleft%5C%7B%5Cbegin%7Bmatrix%7D2&&%5Ctext%7Bif%20move%20is%20super%20effective(or%20better)%7D%5C%5C-2&&%5Ctext%7Botherwise%7D%5C%5C%5Cend%7Bmatrix%7D%5Cright.) + $$ + \text{typeMult} = + \begin{cases} + 2 & \text{if move is super effective (or better)} \\ + -2 & \text{otherwise} + \end{cases} + $$ 2. Compute a multiplier based on the move's category and the user's offensive stats: 1. Compute the user's offensive stat ratio: - - ![statRatioEqn](https://latex.codecogs.com/png.image?%5Cinline%20%5Cdpi%7B100%7D%5Cbg%7Bwhite%7D%5Ctext%7BstatRatio%7D=%5Cleft%5C%7B%5Cbegin%7Bmatrix%7D%5Cfrac%7B%5Ctext%7BuserSpAtk%7D%7D%7B%5Ctext%7BuserAtk%7D%7D&%5Ctext%7Bif%20move%20is%20physical%7D%5C%5C%5Cfrac%7B%5Ctext%7BuserAtk%7D%7D%7B%5Ctext%7BuserSpAtk%7D%7D&%5Ctext%7Botherwise%7D%5C%5C%5Cend%7Bmatrix%7D%5Cright.) + + $$ + \text{statRatio} = + \begin{cases} + \frac{\text{userSpAtk}}{\text{userAtk}} & \text{if move is physical} \\ + \frac{\text{userAtk}}{\text{userSpAtk}} & \text{otherwise} + \end{cases} + $$ 2. Compute the stat-based multiplier: - ![statMultEqn](https://latex.codecogs.com/png.image?%5Cinline%20%5Cdpi%7B100%7D%5Cbg%7Bwhite%7D%5Ctext%7BstatMult%7D=%5Cleft%5C%7B%5Cbegin%7Bmatrix%7D2&%5Ctext%7Bif%20statRatio%7D%5Cle%200.75%5C%5C1.5&%5Ctext%7Bif%5C;%7D0.75%5Cle%5Ctext%7BstatRatio%7D%5Cle%200.875%5C%5C1&%5Ctext%7Botherwise%7D%5C%5C%5Cend%7Bmatrix%7D%5Cright.) + $$ + \text{statMult} = + \begin{cases} + 2 & \text{if statRatio} \leq 0.75 \\ + 1.5 & \text{if } 0.75 \leq \text{statRatio} \leq 0.875 \\ + 1 & \text{otherwise} + \end{cases} + $$ 3. Calculate the move's `attackScore`: $\text{attackScore} = (\text{typeMult}\times \text{statMult})+\lfloor \frac{\text{power}}{5} \rfloor$ @@ -125,13 +150,26 @@ The final step to calculate an attack move's target score (TS) is to multiply th The enemy's target selection for single-target moves works in a very similar way to its move selection. Each potential target is given a **target selection score (TSS)** which is based on the move's [target benefit score](#calculating-move-and-target-scores) for that target: -![TSSEqn](https://latex.codecogs.com/png.image?%5Cinline%20%5Cdpi%7B100%7D%5Cbg%7Bwhite%7D%5Ctext%7BTSS%7D=%5Ctext%7BTBS%7D%5Ctimes%5Cleft%5C%7B%5Cbegin%7Bmatrix%7D-1&%5Ctext%7Bif%20target%20is%20an%20opponent%7D%5C%5C1&%5Ctext%7Botherwise%7D%5C%5C%5Cend%7Bmatrix%7D%5Cright.) +$$ +\text{TSS} = \text{TBS} \times +\begin{cases} +-1 & \text{if target is an opponent} \\ +1 & \text{otherwise} +\end{cases} +$$ Once the TSS is calculated for each target, the target is selected as follows: 1. Sort the targets (indexes) in decreasing order of their target selection scores (or weights). Let $t_i$ be the index of the *i*-th target in the sorted list, and let $w_i$ be that target's corresponding TSS. 2. Normalize the weights. Let $w_n$ be the lowest-weighted target in the sorted list, then: - - ![normWeightEqn](https://latex.codecogs.com/png.image?%5Cinline%20%5Cdpi%7B100%7D%5Cbg%7Bwhite%7DW_i=%5Cleft%5C%7B%5Cbegin%7Bmatrix%7Dw_i+%7Cw_n%7C&%5Ctext%7Bif%5C;%7Dw_n%5C;%5Ctext%7Bis%20negative%7D%5C%5Cw_i&%5Ctext%7Botherwise%7D%5C%5C%5Cend%7Bmatrix%7D%5Cright.) + + $$ + W_i = + \begin{cases} + w_i + |w_n| & \text{if } w_n \text{ is negative} \\ + w_i & \text{otherwise} + \end{cases} + $$ + 3. Remove all weights from the list such that $W_i < \frac{W_0}{2}$ 4. Generate a random integer $R=\text{rand}(0, W_{\text{total}})$ where $W_{\text{total}}$ is the sum of all the remaining weights after Step 3. 5. For each target $(t_i, W_i)$, @@ -172,13 +210,13 @@ Based on the enemy party's matchup scores, whether or not the trainer switches o Now that the enemy Pokémon with the best matchup score is on the field (assuming it survives Dachsbun's attack on the last turn), the enemy will now decide to have Excadrill use one of its moves. Assuming all of its moves are usable, we'll go through the target score calculations for each move: - **Earthquake**: In a single battle, this move is just a 100-power Ground-type physical attack with no additional effects. With no additional benefit score from attributes, the move's base target score against the player's Dachsbun is just the `attackScore` from `AttackMove.getTargetBenefitScore()`. In this case, Earthquake's `attackScore` is given by - + $\text{attackScore}=(\text{typeMult}\times \text{statMult}) + \lfloor \frac{\text{power}}{5} \rfloor = -2\times 2 + 20 = 16$ Here, `typeMult` is -2 because the move is not super effective, and `statMult` is 2 because Excadrill's Attack is significantly higher than its Sp. Atk. Accounting for STAB thanks to Excadrill's typing, the final target score for this move is **24** - **Iron Head**: This move is an 80-power Steel-type physical attack with an additional chance to cause the target to flinch. With these properties, Iron Head has a user benefit score of 0 and a target benefit score given by - + $\text{TBS}=\text{getTargetBenefitScore(FlinchAttr)}-\text{attackScore}$ Under its current implementation, the target benefit score of `FlinchAttr` is -5. Calculating the move's `attackScore`, we get: @@ -198,7 +236,7 @@ Now that the enemy Pokémon with the best matchup score is on the field (assumin where `levels` is the number of stat stages added by the attribute (in this case, +2). The final score for this move is **6** (Note: because this move is self-targeted, we don't flip the sign of TBS when computing the target score). - **Crush Claw**: This move is a 75-power Normal-type physical attack with a 50 percent chance to lower the target's Defense by one stage. The additional effect is implemented by the same `StatStageChangeAttr` as Swords Dance, so we can use the same formulas from before to compute the total TBS and base target score. - + $\text{TBS}=\text{getTargetBenefitScore(StatStageChangeAttr)}-\text{attackScore}$ $\text{TBS}=(-4 + 2)-(-2\times 2 + \lfloor \frac{75}{5} \rfloor)=-2-11=-13$ diff --git a/src/utils.ts b/src/utils.ts index ce9966c0d7f..950adf0e5a1 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -594,7 +594,7 @@ export function getLocalizedSpriteKey(baseKey: string) { * @returns `true` if number is **inclusive** between min and max */ export function isBetween(num: number, min: number, max: number): boolean { - return num >= min && num <= max; + return min <= num && num <= max; } /** diff --git a/test/testUtils/gameManager.ts b/test/testUtils/gameManager.ts index 390e71af126..65d12fd6c01 100644 --- a/test/testUtils/gameManager.ts +++ b/test/testUtils/gameManager.ts @@ -347,7 +347,10 @@ export default class GameManager { } } - /** Emulate selecting a modifier (item) */ + /** + * Emulate selecting a modifier (item) the next time the {@linkcode SelectModifierPhase} occurs. + * Does not actually take anything (in fact, it just skips grabbing an item). + */ doSelectModifier() { this.onNextPrompt( "SelectModifierPhase", @@ -421,7 +424,9 @@ export default class GameManager { await this.phaseInterceptor.to(CommandPhase); } - /** Emulate selecting a modifier (item) and transition to the next upcoming {@linkcode CommandPhase} */ + /** + * Emulate selecting a modifier (item) and transition to the next upcoming {@linkcode CommandPhase}. + */ async toNextWave() { this.doSelectModifier(); @@ -512,8 +517,9 @@ export default class GameManager { } /** - * Command an in-battle switch to another Pokemon via the main battle menu. - * @param pokemonIndex the index of the pokemon in your party to switch to + * Command an in-battle switch to another {@linkcode Pokemon} via the main battle menu. + * @param pokemonIndex the 0-indexed position of the party pokemon to switch to. + * Should generally never be called with 0 as that will just select your current active pokemon. */ doSwitchPokemon(pokemonIndex: number) { this.onNextPrompt("CommandPhase", Mode.COMMAND, () => { @@ -540,7 +546,7 @@ export default class GameManager { * of the party UI, where you just need to navigate to a party slot and press * Action twice - navigating any menus that come up after you select a party member * is not supported. - * @param slot the index of the pokemon in your party to switch to + * @param slot the 0-indexed position of the pokemon in your party to switch to * @param inPhase Which phase to expect the selection to occur in. Typically * non-command switch actions happen in SwitchPhase. */ @@ -575,7 +581,8 @@ export default class GameManager { /** * Intercepts `TurnStartPhase` and mocks {@linkcode TurnStartPhase.getSpeedOrder}'s return value. * Used to manually modify Pokemon turn order. - * Note: This *DOES NOT* account for priority, only speed. + + * Note: This *DOES NOT* account for priority. * @param {BattlerIndex[]} order The turn order to set * @example * ```ts