## File: README.md PokéRogue is a browser based Pokémon fangame heavily inspired by the roguelite genre. Battle endlessly while gathering stacking items, exploring many different biomes, fighting trainers, bosses, and more! # Contributing See [CONTRIBUTING.md](./CONTRIBUTING.md), this includes instructions on how to set up the game locally. # 📝 Credits > If this project contains assets you have produced and you do not see your name, **please** reach out, either [here on GitHub](https://github.com/pagefaultgames/pokerogue/issues/new) or via [Discord](https://discord.gg/pokerogue). Thank you to all the wonderful people that have contributed to the PokéRogue project! You can find the credits [here](./CREDITS.md). # Licensing This repository seeks to be [REUSE compliant](https://reuse.software/): copyright and/or licensing information for each file is stored either in the file itself or in an associated `REUSE.toml` file. The full licensing information for each file can be found by utilizing [REUSE's tooling](https://github.com/fsfe/reuse-tool), such as via `reuse spdx`. \ An abbreviated summary of said information is as follows: - All source code belonging to the project, unless otherwise noted, is licensed under [AGPL-v3.0-only](LICENSES/AGPL-3.0-only.txt). - All forms of documentation (both Markdown files[^1] and any comments explicitly documenting source code) are licensed under [CC-BY-NC-SA-4.0](LICENSES/CC-BY-NC-SA-4.0.txt). - Auto-generated files produced by external tools or files of insignificant originality are not copyrighted and are licensed under [CC0-1.0](LICENSES/CC0-1.0.txt). - To the extent that the assets we provide are [licensable and applicable](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.en#ref-exception-or-limitation), they are licensed under [CC-BY-NC-SA-4.0](LICENSES/CC-BY-NC-SA-4.0.txt) unless otherwise noted. Exceptions can be found in associated `REUSE.toml` files. - ⚠️ Files in `assets/` that are not explicitly licensed via `REUSE.toml` files should be considered to have _no_ licensing / copyright information. [^1]: Including this README --- ## File: docs/comments.md # Commenting code > _Any fool can write code that a computer can understand. Good programmers write code that humans can understand._ \ > \- Martin Fowler The goal of programming is to write functioning code, not create comprehensive documentation. \ However, programmers spend significantly more time _reading_ code than writing it, especially those new to a codebase. \ As such, comments and documentation are **vital** for any large codebase like this. This document is intended to serve as a guide for how to (and not to) document code while working on PokéRogue. > [!IMPORTANT] > **These are general guidelines, not "hard and fast" rules.** > When in doubt, **use common sense** and err on the side of readability. ## 📄 Table of Contents - [General Guidelines](#general-guidelines) - [TSDoc](#tsdoc) - [Example](#example) - [TSDoc Comment guidelines](#tsdoc-comment-guidelines) - [Inheritance and Documentation](#inheritance-and-documentation) ## General Guidelines **DO**: - Keep comments meaningful - Focus on explaining _why_ a line or block of code exists - a _post hoc_ understanding of the _reasons_ is infinitely more useful. - Comments should **NOT** repeat _what_ code _does_[^1] or explain concepts obvious to someone with a basic understanding of the language at hand. - Keep comments readable - A comment's verbosity should roughly scale with the complexity of its subject matter. Some people naturally write shorter or longer comments, but summarizing a 300-line function with "does a thing" is about as good as writing nothing. Conversely, writing a paragraph-level response where a basic one-liner would suffice is just as undesirable. - Long comments should be broken into multiple lines at around **100-120 characters** to avoid unnecessary scrolling in terminals and IDEs. - **Make sure comments exist on Functions, Classes, Methods, and Properties**. - These tend to be the most important things to comment. When someone goes to use a function/class/method/etc., having a comment reduces the need to flip back and forth between files to figure out what XYZ does. Peek Definition is great until you're three nesting levels deep. **DON'T**: - Leave comments for code you don't understand - Incorrect information is worse than no information. If you aren't sure how something works, don't make something up to explain it - ask for help or mark it as `TODO`. - Over-comment - Adding too many comments can risk distracting from the actual code in favor of repeating the self-evident. - Where possible, try to summarize blocks of code instead of singular lines where possible, always preferring giving a reason over stating a fact. Single line comments should call out specific oddities or features. [^1]: With exceptions for extremely long, convoluted or unintuitive methods (though a dependence on said comments is likely a symptom of poorly structured code). ## TSDoc The codebase makes extensive use of [TSDoc](https://tsdoc.org), a TypeScript-specific version of [JSDoc](https://jsdoc.app/about-getting-started) with standardized syntax and Markdown support. > [!TIP] > Most modern IDEs have functionality for showing JSDoc annotations upon hovering over attached constructs. > Some (like VS Code) also show `@param` descriptions for function parameters as you type them, helping keep track of arguments inside lengthy functions. #### TypeDoc One of TSDoc's many upsides is its standardized parser that allows other tools to read and process module documentation. \ We make use of one such tool ([TypeDoc](https://typedoc.org/)) to automatically generate [API documentation](https://pagefaultgames.github.io/pokerogue/beta/index.html) from comments on classes, interfaces and the like[^2]. [^2]: You can preview the output by running `pnpm typedoc`, though [Live Preview]() or a similar method of previewing local HTML files is recommended to make your life easier. \ Note that certain features (like the "Go to Main/Beta" navigation bar links) are disabled on local docs builds due to relying on CI-exclusive environment variables. ### Example For an example of how TSDoc comments work, here are some comments taken from `src/data/moves/move.ts`: Reference Example ```ts /** * Attribute to put in a {@link https://bulbapedia.bulbagarden.net/wiki/Substitute_(doll) | Substitute Doll} for the user. * * Used for {@linkcode MoveId.SUBSTITUTE} and {@linkcode MoveId.SHED_TAIL}. */ export class AddSubstituteAttr extends MoveEffectAttr { /** The percentage of the user's maximum HP that is required to apply this effect. */ private readonly hpCost: number; /** Whether the damage taken should be rounded up (Shed Tail rounds up). */ private readonly roundUp: boolean; constructor(hpCost: number, roundUp: boolean) { // code removed } /** * Helper function to compute the amount of HP required to create a substitute. * @param user - The {@linkcode Pokemon} using the move * @returns The amount of HP that required to create a substitute. */ private getHpCost(user: Pokemon): number { // code removed } /** * Remove a fraction of the user's maximum HP to create a 25% HP substitute doll. * @param user - The {@linkcode Pokemon} using the move * @param target - n/a * @param move - The {@linkcode Move} being used * @param args - n/a * @returns Whether the attribute successfully applied. */ public override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { // code removed } public override getUserBenefitScore(user: Pokemon, _target: Pokemon, _move: Move): number { // code removed } getCondition(): MoveConditionFunc { // code removed } public override getFailedText(user: Pokemon): string | undefined { // code removed } } ``` Looking at the example given, you may notice certain terms are annotated with `{@linkcode XYZ}` tags. This provides a clickable hyperlink to the referenced type or object in most modern IDEs[^3]. \ Also notice the dashes (` - `) between the parameter names and descriptions - these are **required** under the TSDoc spec. [^3]: For those curious, the difference between `@linkcode` and `@link` is that the former renders text in monospace, more clearly indicating a code symbol rather than a URL/hyperlink. ### TSDoc Comment guidelines With all these in mind, here are a few TSDoc-specific guidelines to ensure readability of both rendered API documentation and IDE syntax hints: - Use **proper English sentences** for descriptors Since these comments are going onto a website, annotations for properties, methods and functions should be _well-formed_, _present-tense English sentences_ where possible. - The only exceptions are single-sentence `@param`/`@typeParam` lines - these should _not_ end with periods and instead take the form of bullet-point declarations. Example: ```ts /** * Baseline arguments used to construct all {@linkcode PositionalTag}s, * the contents of which are serialized and used to construct new tags. \ * Does not contain the `tagType` parameter (which is used to select the proper class constructor during tag loading). * @privateRemarks * All {@linkcode PositionalTag}s are intended to implement a sub-interface of this containing their respective parameters, * and should refrain from adding extra serializable fields not contained in said interface. * This ensures that all tags truly "become" their respective interfaces when converted to and from JSON. */ interface PositionalTagBaseArgs { /** * The number of turns remaining until this tag's activation. \ * Decremented by 1 at the end of each turn until reaching 0, at which point it will * {@linkcode PositionalTag.trigger | trigger} the tag's effects and be removed. */ turnCount: number; /** * The {@linkcode BattlerIndex} targeted by this effect. */ readonly targetIndex: BattlerIndex; } /** * Compute the geometric mean of multiple numbers. * @param nums - The numbers whose mean will be computed * @returns The geometric mean of `nums`. * @remarks * This is equivalent to Π(nums)^(1/nums.length). * @see {@link https://en.wikipedia.org/wiki/Geometric_mean | Geometric Mean - Wikipedia} */ declare function geometricMean(nums: number[]): number; ``` - Default values **must be mentioned if present** TypeScript displays no information about default values in IDEs, so mentioning defaults inside doc comments is the easiest way to inform callers about a given property or parameter's default values. - Classes & interfaces can make use of the `@defaultValue` tag to annotate property initial values. As for function and method arguments, our codebase opts to include default value information immediately following the `@param` tag. This ensures the information is prominently visible in IDEs and similar tools. Example: ```ts class BattleScene { /** * Tracker for whether the last run attempt failed. * @defaultValue `false` */ public failedRunAway = false; } /** * Print a copiously long, procedurally generated lorem ipsum-like placeholder string. * @param charCount - (Default `1000`) The number of characters to create */ function printLorem(charCount = 1000): string {}; ``` ## Inheritance and Documentation While most class methods should be fully documented, the main exception comes with inheritance - classes and interfaces will inherit documentation comments from any other classes/interfaces they extend/implement, **provided no other comments are present on inherited symbols**. As such, _do not_ document properties or methods in sub-classes that do not substantially differ from the superclass' implementation. > [!IMPORTANT] > Any properties or methods unique to the class **must still be documented**! --- ## File: docs/enemy-ai.md # EnemyCommandPhase: How Enemy Pokémon Decide What to Do ## Step 1: Should the Enemy Pokémon Switch? When battling an enemy Trainer, the first decision the enemy needs to make is whether or not to switch an active Pokémon with another Pokémon in their party. This decision is primarily made by comparing **matchup scores** between each Pokémon in the enemy's party. ### Calculating Matchup Scores The core function for matchup score calculation can be found in `src/field/pokemon.ts`, within the `Pokemon` class: ```ts getMatchupScore(pokemon: Pokemon): number; ``` This computes the source Pokémon's matchup score against the Pokémon passed by argument using the formula $$\text{MUScore} = (\text{atkScore}+\text{defScore}) * \text{hpDiffRatio} $$ where - $\text{atkScore}$ is the combined effectiveness of the source Pokémon's types against the opposing Pokémon's defensive typing: $\prod_{\text{types}} \text{typeEffectiveness}(\text{type}, \text{oppPokemon})$. $\text{typeEffectiveness}$ is 1 when the type deals neutral damage to the opposing Pokémon's defensive typing, 2 when the type deals super effective damage, and so on. $atkScore$ is also increased by 25 percent if the source Pokémon has a higher Speed stat than the opposing Pokémon. - $\text{defScore}$ is the inverse of the opposing Pokémon's $\text{atkScore}$ against the source Pokémon's defensive typing, or $(\prod_{\text{types}} \text{typeEffectiveness}(\text{type}, \text{sourcePokemon}))^{-1}$. Unlike $\text{atkScore}$, $\text{defScore}$ is capped at a maximum score of 4. - $\text{hpDiffRatio}= \text{sourceHpRatio}-\text{oppHpRatio}+1$. This is further multiplied by 1.5 if the source Pokémon has a higher Speed stat than the opposing Pokémon; however, $\text{hpDiffRatio}$ cannot be higher than 1. The maximum possible matchup score a Pokémon could have against a single opponent is $(16+16)\times 2=64$, which occurs when - the Pokémon hits its opponent for 4x super effective damage with both of its types. - the Pokémon is immune to or resists both of the opponent's types by 4x. - the Pokémon is at max HP while the opponent's HP ratio is near zero. In most situations, though, a Pokémon's matchup score against an opponent will be at most 16, which is equivalent to having two super effective types and resisting both of the opponent's types with the same HP ratios as before. The minimum possible matchup score a Pokémon could have against a single opponent is near zero, which occurs when the Pokémon's HP ratio is near zero while the opponent is at max HP. However, a Pokémon's matchup score can also be very low when its type(s) are 4x weak to and/or resisted by its opponent's types. ### Determining Switches in EnemyCommandPhase The `EnemyCommandPhase` follows this process to determine whether or not an enemy Pokémon should switch on each turn during a Trainer battle. 1. If the Pokémon has a move already queued (e.g. they are recharging after using Hyper Beam), or they are trapped (e.g. by Bind or Arena Trap), skip to resolving a `FIGHT` command (see next section). 2. For each Pokémon in the enemy's party, [compute their matchup scores](#calculating-matchup-scores) against the active player Pokémon. If there are two active player Pokémon in the battle, add their matchup scores together. 3. Take the party member with the highest matchup score and apply a multiplier to the score that reduces the score based on how frequently the enemy trainer has switched Pokémon in the current battle. - The multiplier scales off of a counter that increments when the enemy trainer chooses to switch a Pokémon and decrements when they choose to use a move. 4. Compare the result of Step 3 with the active enemy Pokémon's matchup score. If the party member's matchup score is at least three times that of the active Pokémon, switch to that party member. - "Boss" trainers only require the party member's matchup score to be at least two times that of the active Pokémon, so they are more likely to switch than other trainers. The full list of boss trainers in the game is as follows: - All gym leaders, Elite 4 members, and Champions - All Evil Team leaders - The last three Rival Fights (on waves 95, 145, and 195) 5. If the enemy decided to switch, send a switch `turnCommand` and end this `EnemyCommandPhase`; otherwise, move on to resolving a `FIGHT` enemy command. ## Step 2: Selecting a Move At this point, the enemy (a wild or trainer Pokémon) has decided against switching and instead will use a move from its moveset. However, it still needs to figure out which move to use and, if applicable, which target to use the move against. The logic for determining an enemy's next move and target is contained within two methods: `EnemyPokemon.getNextMove()` and `EnemyPokemon.getNextTargets()` in `src/field/pokemon.ts`. ### Choosing a Move with `getNextMove()` In `getNextMove()`, the enemy Pokémon chooses a move to use in the following steps: 1. If the Pokémon has a move in its Move Queue (e.g. the second turn of a charging move), and the queued move is still usable, use that move against the given target. 2. Filter out any moves it can't use within its moveset. The remaining moves make up the enemy's **move pool** for the turn. 1. A move can be unusable if it has no PP left or it has been disabled by another move or effect. 2. If the enemy's move pool is empty, use Struggle. 3. Calculate the **move score** of each move in the enemy's move pool. 1. A move's move score is equivalent to the move's maximum **target score** among all of the move's possible targets on the field ([more on this later](#calculating-move-and-target-scores)). 2. A move's move score is set to -20 if at least one of these conditions are met: - The move is unimplemented (or, more precisely, the move's name ends with "(N)"). - Conditions for the move to succeed are not met (unless the move is Sucker Punch, Upper Hand or Thunderclap, as those moves' conditions can't be resolved until after the turn starts). - The move's target scores are 0 or `NaN` for each target. In this case, the game assumes the target score calculation for that move is unimplemented. 4. Sort the move pool in descending order of move scores. 5. From here, the enemy's move selection varies based on its `aiType`. If the enemy is a Boss Pokémon or has a Trainer, it uses the `SMART` AI type; otherwise, it uses the `SMART_RANDOM` AI type. 1. Let $m_i$ be the *i*-th move in the sorted move pool $M$: - If `aiType === SMART_RANDOM`, the enemy has a 5/8 chance of selecting $m_0$ and a 3/8 chance of advancing to the next best move $m_1$, where it then repeats this roll. This process stops when a move is selected or the last move in the move pool is reached. - If `aiType === SMART`, a similar loop is used to decide between selecting the move $m_i$ and advancing to the next iteration with the move $m_{i+1}$. However, instead of using a flat probability, the following conditions need to be met to advance from selecting $m_i$ to $m_{i+1}$: - $\text{sign}(s_i) = \text{sign}(s_{i+1})$, where $s_i$ is the move score of $m_i$. - $\text{randInt}(0, 100) < \text{round}(\frac{s_{i+1}}{s_i}\times 50)$. In other words: if the scores of $m_i$ and $m_{i+1}$ have the same sign, the chance to advance to the next iteration with $m_{i+1}$ is proportional to how close the scores are to each other. The probability to advance to the next iteration is at most 50 percent (when $s_i$ and $s_{i+1}$ are equal). 6. The enemy will use the move selected in Step 5 against the target(s) with the highest [**target selection score (TSS)**](#choosing-targets-with-getnexttargets) ### Calculating Move and Target Scores 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 is a combination of the move's **user benefit score (UBS)** and **target benefit score (TBS)**, representing how much the move helps or hinders the user and/or its target(s). $$ \text{TS} = \text{UBS} + \left( \text{TBS} \times \begin{cases} -1 & \text{if target is an opponent} \\ 1 & \text{otherwise} \end{cases} \right) $$ A move's UBS and TBS are computed with the respective functions in the `Move` class: ```ts getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): number; getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number; ``` Logically, these functions are very similar – they add up their respective benefit scores from each of the move's attributes (as determined by `attr.getUserBenefitScore`, and `attr.getTargetBenefitScore`, respectively) and return the total benefit score. However, there are two key functional differences in how the UBS and TBS of a move are handled: 1. In addition to influencing move selection, a move's TBS also influences target selection for that move, whereas UBS has no influence. 2. When evaluating the target score of a move against an opposing Pokémon, the move's TBS is multiplied by -1, whereas the move's UBS does not change. For this reason, move attributes return negative values for their TBS to reward using the move against an enemy. #### Calculating Target Benefit Score (TBS) for Attack Moves In addition to the base score from `Move.getTargetBenefitScore()`, attack moves calculate an `attackScore` which influences the move's TBS based on the following properties: - The move's power (after the move's `VariablePowerAttrs` are applied) - The move's type effectiveness against the target (note that this also accounts for type immunities from abilities such as Levitate and field effects such as Strong Winds). - 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: $$ \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: $$ \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: $$ \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$ The maximum total multiplier in `attackScore` ($\text{typeMult}\times \text{statMult}$) is 4, which occurs for attacks that are super effective against the target and are categorically aligned with the user's offensive stats (e.g. the move is physical, and the user has much higher Attack than Sp. Atk). The minimum total multiplier of -4 occurs (somewhat confusingly) for attacks that are not super effective but are categorically aligned with the user's offensive stats. The attack move's total TBS, then, is $\text{TBS}=\text{baseScore}-\text{attackScore}$, where $\text{baseScore}$ is the result of `Move.getTargetBenefitScore()`. #### Calculating Target Score (TS) for Attack Moves The final step to calculate an attack move's target score (TS) is to multiply the base target score by the move's type effectiveness and STAB (if it applies): - If the target is an enemy, the corresponding TS is multiplied by the move's type effectiveness against the enemy (e.g. 2 if the move is super effective), then by 1.5 if the move shares a type with the user. - If the target is an ally, the TS is divided by these factors instead. - If $\text{TS}=0$ after these multipliers are applied, the TS is set to -20 for the current target. ### Choosing Targets with `getNextTargets()` 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: $$ \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: $$ 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)$, 1. if $R \le \sum_{j=0}^{i} W_i$, or if $t_i$ is the last target in the list, **return** $t_i$ 2. otherwise, advance to the next target $t_{i+1}$ and repeat this check. Once the target is selected, the enemy has successfully determined its next action for the turn, and its corresponding `EnemyCommandPhase` ends. From here, the `TurnStartPhase` processes the enemy's commands alongside the player's commands and begins to resolve the turn. ## An Example in Battle Suppose you enter a single battle against an enemy trainer with the following Pokémon in their party: 1. An [Excadrill](https://bulbapedia.bulbagarden.net/wiki/Excadrill_(Pok%C3%A9mon)) with the Ability Sand Force and the following moveset 1. Earthquake 2. Iron Head 3. Crush Claw 4. Swords Dance 2. A [Heatmor](https://bulbapedia.bulbagarden.net/wiki/Heatmor_(Pok%C3%A9mon)) with the Ability Flash Fire and the following moveset 1. Fire Lash 2. Inferno 3. Hone Claws 4. Shadow Claw The enemy trainer leads with their Heatmor, and you lead with a [Dachsbun](https://bulbapedia.bulbagarden.net/wiki/Dachsbun_(Pok%C3%A9mon)) with the Ability Well-Baked Body. We'll cover the enemy's behavior over the next two turns. ### Turn 1 To determine whether the enemy should switch Pokémon, it first calculates each party member's matchup scores against the player's Dachsbun: $$\text{MUScore} = (\text{atkScore}+\text{defScore}) * \text{hpDiffRatio} $$ - Defensively, Heatmor's Fire typing resists Dachsbun's Fairy typing, so its `defScore` is 2. However, because of Dachsbun's Fire immunity granted by Well-Baked Body, Heatmor's `atkScore` against Dachsbun is 0. With both Pokémon at maximum HP, Heatmor's total matchup score is 2. - Excadrill's Steel typing also resists Fairy, so its `defScore` is also 2. In this case, though, Steel is also super effective against Fairy, so Excadrill's base `atkScore` is 2. If Excadrill outspeeds Dachsbun (possibly due to it having a +Spd nature or holding a Carbos), its `atkScore` is further increased to 2.5. Since both Pokémon are at maximum HP, Excadrill's total matchup score is 4 (or 4.5 if it outspeeds). Based on the enemy party's matchup scores, whether or not the trainer switches out Heatmor for Excadrill depends on the trainer's type. The difference in matchup scores is enough to cause a switch to Excadrill for boss trainers (e.g. gym leaders) but not for regular trainers. For this example, we'll assume the trainer is a boss and, therefore, decides to switch to Excadrill on this turn. ### Turn 2 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: $\text{attackScore}=(\text{typeMult}\times \text{statMult}) + \lfloor \frac{\text{power}}{5} \rfloor = 2\times 2 + 16 = 20$ Note that `typeMult` in this case is 2 because Iron Head is super effective (or better) against Dachsbun. With the move's UBS at 0, the base target score calculation against Dachsbun simplifies to $\text{TS}=-\text{TBS}=-(-5-20)=25$ We then need to apply a 2x multiplier for the move's type effectiveness and a 1.5x multiplier since STAB applies. After applying these multipliers, the final score for this move is **75**. - **Swords Dance**: As a non-attacking move, this move's benefit score is derived entirely from the sum of its attributes' benefit scores. Swords Dance's `StatStageChangeAttr` has a user benefit score of 0 and a target benefit score that, in this case, simplifies to $\text{TBS}=4\times \text{levels} + (-2\times \text{sign(levels)})$ 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$ $\text{TS}=-\text{TBS}=13$ This move is neutral against Dachsbun and isn't boosted by STAB from Excadrill, so we don't need to apply any extra multipliers. The final score for this move is **13**. We now have a sorted move pool in decreasing order of move scores: 1. Iron Head (**75**) 2. Earthquake (**24**) 3. Crush Claw (**13**) 4. Swords Dance (**6**) Since no other score is at least half that of Iron Head's score, the enemy AI automatically chooses to use Iron Head against Dachsbun at this point. ## Guidelines for Implementing Benefit Scores When implementing a new move attribute, it's important to override `MoveAttr`'s `getUserBenefitScore` and `getTargetBenefitScore` functions to ensure that the enemy AI can accurately determine when and how to use moves with that attribute. Here are a few basic specifications you should adhere to when implementing benefit scores for a new attribute: - A move's **user benefit score (UBS)** incentivizes (or discourages) the move's usage in general. A positive UBS gives the move more incentive to be used, while a negative UBS gives the move less incentive. - A move's **target benefit score (TBS)** incentivizes (or discourages) the move's usage on a specific target. A positive TBS indicates the move is better used on the user or its allies, while a negative TBS indicates the move is better used on enemies. - **The total benefit score (UBS + TBS) of a move should never be 0.** The move selection algorithm assumes the move's benefit score is unimplemented if the total score is 0 and penalizes the move's usage as a result. With status moves especially, it's important to have some form of implementation among the move's attributes to avoid this scenario. - **Score functions that use formulas should include comments.** If your attribute requires complex logic or formulas to calculate benefit scores, please add comments to explain how the logic works and its intended effect on the enemy's decision making. --- ## File: docs/linting.md # Linting & Formatting Writing clean, readable code is important, and linters and formatters are an integral part of ensuring code quality and readability. \ It is for this reason we are using [Biome](https://biomejs.dev), an opinionated linter/formatter (akin to Prettier) with a heavy focus on speed and performance. ### Installation You probably installed Biome already without noticing it - it's included inside `package.json` and should've been downloaded when you ran `pnpm install` after cloning the repo. If you haven't done that yet, go do that first. # Using Biome For the most part, Biome attempts to stay "out of your hair", letting you write code while enforcing a consistent formatting standard and only notifying for errors it can't automatically fix. \ On the other hand, if Biome complains about a piece of code, **there's probably a good reason why**. Disable comments should be used sparingly or when readability demands it - your first instinct should be to fix the code in question, not disable the rule. ## Editor Integration Biome has integrations with many popular code editors. See [these](https://biomejs.dev/guides/editors/first-party-extensions/) [pages](https://biomejs.dev/guides/editors/third-party-extensions/) for information about enabling Biome in your editor of choice. ## Automated Runs Generally speaking, most users shouldn't need to run Biome directly; in addition to editor integration, a [pre-commit hook](../lefthook.yml) will automatically format and lint all staged files before each commit. > [!WARNING] > You will **not** be able to commit code if any staged files contain `error`-level linting problems. \ > If you, for whatever reason, _absolutely need_ to bypass Lefthook for a given commit, > pass the `--no-verify` flag to `git commit`. We also have a [GitHub Actions workflow](../.github/workflows/linting.yml) to verify code quality each time a PR is updated, preventing bad code from inadvertently making its way upstream. \ These are effectively the same commands as run by Lefthook, merely on a project-wide scale. ## Running Biome via CLI To run Biome on your files manually, you have 2 main options: 1. Run the scripts included in `package.json` (`pnpm biome` and `pnpm biome:all`). \ These have sensible defaults for command-line options, but do not allow altering certain flags (as some cannot be specified twice in the same command) 2. Execute the Biome executable manually from the command line like so: ```sh pnpm exec biome check --[flags] ``` This allows customizing non-overridable flags like `--diagnostic-level` on a more granular level, but requires slightly more verbosity and specifying more options. A full list of flags and options can be found on [their website](https://biomejs.dev/reference/cli/), but here's a few useful ones to keep in mind: - `--write` will cause Biome to write all "safe" fixes and formatting changes directly to your files (rather than just complaining and erroring out). - `--changed` and `--staged` will limit checking to all changed or staged files respectively. Biome sources this info from the relevant version control system (in this case `git`). Great for quick checks before committing or pushing. - `diagnostic-level=XXX` will only show diagnostics with at least the given severity level (`info/warn/error`). Useful to only focus on errors causing a failed workflow run or similar. - `only=XXX` will only check for the given rule(s). This can be used in combination with `diagnostic-level` to only show errors for a specific rule, for instance. ## Linting Rules We primarily use Biome's [recommended ruleset](https://biomejs.dev/linter/rules/) for linting JS/TS files, with some customizations to better suit our project's needs. \ A complete list of rules can be found in the [`biome.jsonc`](../biome.jsonc) file in the project root. Most rules are accompanied by comments explaining the reasons for their inclusion/exclusion. > [!IMPORTANT] > Certain lint rules may be marked as `info` or `warn` to allow for gradual refactoring without blocking development. > **Do not write new code that triggers these rules!** Any questions about linting rules can be brought up in the `#pokerogue-dev` channel in the community Discord. --- ## File: docs/localization.md # Localization 101 PokéRogue's localization team puts immense effort into making the game accessible around the world, supporting over 12 different languages at the time of writing this document. \ As a developer, it's important to help maintain global accessibility by effectively coordinating with the Translation Team on any new features or enhancements. This document aims to cover everything you need to know to help keep the integration process for localization smooth and simple. # Prerequisites Before you continue, this document assumes: - You have already forked the repository and set up a development environment according to [CONTRIBUTING.md](../CONTRIBUTING.md). - You have a basic level of familiarity with Git commands and GitHub repositories. - You have joined the [community Discord](https://discord.gg/pokerogue) and have access to `#pokerogue-dev` and related channels via **[#select-roles](https://discord.com/channels/1125469663833370665/1194825607738052621)**. This is the easiest way to keep in touch with both the Translation Team and other like-minded contributors! # About the `pokerogue-locales` submodule PokéRogue's translations are managed under a separate dedicated repository, [`pokerogue-locales`](https://github.com/pagefaultgames/pokerogue-locales/). This repository is integrated into the main one as a [git submodule](https://git-scm.com/book/en/v2/Git-Tools-Submodules) within the `locales` folder. ## What Is a Submodule? In essence, a submodule is a way for one repository (i.e. `pokerogue`) to use another repository (i.e. `pokerogue-locales`) internally. The parent repo (the "superproject") houses a cloned version of the 2nd repository (the "submodule") inside it, making locales effectively a "repository within a repository", so to speak. > [!TIP] > Many popular IDEs have integrated `git` support with special handling around submodules: > > > > ## Fetching Changes from Submodules The following command will initialize your branch's `assets` and `locales` repository and update its HEAD: ```bash pnpm update-locales ``` > [!TIP] > This command is run _automatically_ after cloning, merging or changing branches, so you should rarely have to run it manually. > [!IMPORTANT] > If you EVER run into issues with the `locales` submodule, try deleting the `.git/modules/locales` and `locales` folders before re-initializing it again. ## How Are Translations Integrated? This project uses the [i18next library](https://www.i18next.com/) to integrate translations from `locales` into the source code. The basic process for fetching translated text goes roughly as follows: 1. The source code fetches text by a given key. ```ts globalScene.phaseManager.queueMessage( i18next.t("fileName:keyName", { arg1: "Hello", arg2: "an example", ... }) ); ``` 2. The game looks up the key in the corresponding JSON file for the user's language. ```jsonc // from "en/file-name.json"... { "keyName": "{{arg1}}! This is {{arg2}} of translated text!" } ``` If the key doesn't exist for the given language, the game will default to an appropriate fallback (usually the corresponding English key). 3. The game shows the translated text to the user. ```ts "Hello! This is an example of translated text!" ``` # Submitting Locales Changes If you have a feature or enhancement that requires additions or changes to in-game text, you will need to make a fork of the `pokerogue-locales` repo and submit your text changes as a pull request _in addition_ to your pull request to the main project. \ Since these two PRs aren't _technically_ linked, it's important to coordinate with the Translation Team to ensure that both PRs are integrated safely into the project. > [!CAUTION] > **DO NOT HARDCODE PLAYER-FACING TEXT INTO THE CODE!** ## Making Changes One perk of submodules is you don't actually _need_ to clone the locales repository to start contributing - `git` already does that for you on initialization. Given `pokerogue-locales` is a full-fledged `git` repository _inside_ `pokerogue`, making changes is roughly the same as normal, merely using `locales` as your root directory. > [!WARNING] > Make sure to checkout or rebase onto `upstream/main` (`pnpm update-locales:remote`) **BEFORE** creating a locales PR! > The checked-out commit is based on the superproject's SHA-1 by default, so hastily making changes may see you basing your commits on last week's `HEAD`. ## Requirements for Adding Translated Text When a new feature or enhancement requires adding a new locales key **without changing text in existing keys**, we have the following workflow with regards to localization: 1. You (the developer) make a pull request to the main repository for your new feature. If this feature requires new text, the text should be integrated into the code with a new `i18next` key pointing to where you plan to add it into the locales repository. 2. You then make another pull request — this time to the `pokerogue-locales` repository — adding a new entry with text for each key you added to your main PR. - You must add the corresponding **English keys** while making the PR; the Translation Team can take care of the rest[^2]. - For any feature pulled from the mainline Pokémon games (e.g. a Move or Ability implementation), it's best practice to include a source link for any added text. \ [Poké Corpus](https://abcboy101.github.io/poke-corpus/) is a great resource for finding text from the mainline games; otherwise, a video/picture showing the text being displayed should suffice. - You should also [notify the current Head of Translation](#notifying-translation) to ensure a fast response. 3. At this point, you may begin [testing locales integration in your main PR](#documenting-locales-changes). 4. The Translation Team will approve the locales PR (after corrections, if necessary), then merge it into `pokerogue-locales`. 5. The Dev Team will approve your main PR for your feature, then merge it into PokéRogue's beta environment. [^2]: For those wondering, the reason for choosing English specifically is due to it being the master language set in Pontoon (the program used by the Translation Team to perform locale updates). If a key is present in any language _except_ the master language, it won't appear anywhere else in the translation tool, rendering missing English keys quite a hassle. > [!IMPORTANT] > The Dev and Translation teams have strict requirements for ensuring consistency of newly added locales entries. > PRs failing these requirements **will not be mergeable into `locales`**! > - File names should be in `kebab-case`. Example: `trainer-names.json` > - Key names should be in `camelCase`. Example: `aceTrainer` > - Keys making use of i18next's inbuilt [context support](https://www.i18next.com/translation-function/context) must use `snake_case` for the context extension[^3]. Example: `aceTrainer_male` [^3]: If your PR introduces a new context extension not already used in the codebase, the validation workflow will be unable to detect it and flag it as invalid. \ To fix this, update the [`i18nextKeyExtensions`](https://github.com/pagefaultgames/pokerogue-locales/blob/main/.github/scripts/locales-format-checker/constants.js#L30) array with the new entries. ### Requirements for Modifying Translated Text PRs that modify existing text have different risks with respect to coordination between development and translation, so their requirements are slightly different: - As above, you set up 2 PRs: one for the feature itself in the main repo, and another for the associated locales changes in the locale repo. - Now, however, you need to have your main PR be approved by the Dev Team **before** your corresponding locale changes are merged in. - After your main PR is approved, you may update the submodule and post video evidence of integration into the **locales PR**. - A Lead or Senior Translator from the Translation Team will then approve your main PR (if all is well), clearing your feature for merging into `beta`. ## Documenting Locales Changes After making a PR involving any outwards-facing behavior (but _especially_ locales-related ones), it's generally considered good practice to attach proof of those changes working in-game. The basic procedure is roughly as follows: 1. Update your locales submodule to point to **the branch you used to make the locales PR**. \ Many IDEs with `git` integration support doing this from the GUI, \ or you can simply do it via command-line: ```bash cd locales git checkout your-branch-name-here ``` 2. Set some of the [in-game overrides](../CONTRIBUTING.md#1---manual-testing) inside `overrides.ts` to values corresponding to the interactions being tested. 3. Start a local dev server (`pnpm start:dev`) and open localhost in your browser. 4. Take screenshots or record a video of the locales changes being displayed in-game using the software of your choice[^4]. [^4]: For those lacking a dedicated screen capture software, [OBS Studio](https://obsproject.com) is a popular open-source option, available on all major OSes. > [!NOTE] > For those aiming to film their changes, bear in mind that GitHub has a hard **10mB limit** on uploaded media content. > If your video is too large, consider making it shorter or downscaling the quality. ## Notifying Translation Put simply, stating that a PR exists makes it much easier to review and merge. The easiest way to do this is by **pinging the current Head of Translation** in the [community Discord](https://discord.gg/pokerogue) (ideally in `#pokerogue-dev` or similar). > [!IMPORTANT] > The current Head of Translation is: \ > `@lugiadrien` (@Adri1 on GitHub) # Closing Remarks If you have any questions about the developer process for localization, don't hesitate to ask! Feel free to contact us on Discord - the Dev Team and Translation Team will be happy to answer any questions. --- ## File: docs/podman.md # Using Podman ## Requirements * `podman >=5.x` ## Steps 1. `podman build -t pokerogue -f Dockerfile .` 2. `podman create --name temp-pokerogue localhost/pokerogue` 3. `podman cp temp-pokerogue:/app/node_modules ./` 4. `podman cp temp-pokerogue:/app/assets ./assets/` 5. `podman cp temp-pokerogue:/app/locales ./locales/` 6. `podman rm temp-pokerogue` 7. `podman run --rm -p 8000:8000 -v $(pwd):/app:Z --userns=keep-id -u $(id -u):$(id -g) localhost/pokerogue` 8. Visit `http://localhost:8000/` Note: 1. Steps 2-5 are required because mounting working directory without installed `node_modules/` and assets/locales locally will be empty, this way we prevent it by copying them from the container itself to local directory 2. `podman run` may take a couple of minutes to mount the working directory ### Running tests inside container `podman run --rm -p 8000:8000 -v $(pwd):/app:Z --userns=keep-id -u $(id -u):$(id -g) localhost/pokerogue pnpm test:silent `