diff --git a/.github/workflows/close.yml b/.github/workflows/close.yml deleted file mode 100644 index 45eabef0b..000000000 --- a/.github/workflows/close.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: Issue Closed -on: - issues: - types: ["closed"] -jobs: - close: - runs-on: ["ubuntu-latest"] - steps: - - name: Checkout - uses: actions/checkout@v2.4.0 - - name: Create Project Card Action - uses: technote-space/create-project-card-action@v1.0.3 - with: - GITHUB_TOKEN: ${{ secrets.METRICS_TOKEN }} - PROJECT: Issue Management - COLUMN: Closed - CHECK_ORG_PROJECT: true diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index a2813863b..000000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Main Workflow -on: - workflow_dispatch: - push: - branches: ["main", "master"] - schedule: - - cron: '*/30 * * * *' -jobs: -# build: -# runs-on: ["windows-latest"] -# steps: -# - name: Checkout -# uses: actions/checkout@v2.4.0 -# - name: Build with py2exe -# run: | -# pip install py2exe -# cd app -# python build_to_exe.py py2exe - publish: - runs-on: ["ubuntu-latest"] - steps: - - name: Publish - uses: mikeal/publish-to-github-action@master - env: - GITHUB_TOKEN: ${{ secrets.METRICS_TOKEN }} - github-metrics: - runs-on: ubuntu-latest - steps: - - uses: lowlighter/metrics@latest - with: - token: ${{ secrets.METRICS_TOKEN }} - user: Pokemon-PythonRed - template: classic - plugin_introduction: yes - plugin_introduction_title: no - plugin_languages: yes - plugin_languages_sections: most-used - plugin_languages_indepth: no - plugin_languages_categories: data, programming - plugin_traffic: yes diff --git a/.github/workflows/open.yml b/.github/workflows/open.yml deleted file mode 100644 index 4c9131be1..000000000 --- a/.github/workflows/open.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: Issue Opened -on: - issues: - types: ["opened", "reopened"] -jobs: - open: - runs-on: ["ubuntu-latest"] - steps: - - name: Checkout - uses: actions/checkout@v2.4.0 - - name: Create Project Card Action - uses: technote-space/create-project-card-action@v1.0.3 - with: - GITHUB_TOKEN: ${{ secrets.METRICS_TOKEN }} - PROJECT: Issue Management - COLUMN: Open - CHECK_ORG_PROJECT: true diff --git a/.gitignore b/.gitignore index b26aca252..fa1a72de6 100644 --- a/.gitignore +++ b/.gitignore @@ -147,8 +147,5 @@ dmypy.json # Ignore code-workspaces *.code-workspace -# Pokémon PythonRed save files +# PPR save files */.ppr-save - -# Windows dummy files to appease Pylance -app/getch.py diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 48149b5cc..000000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Python: Current File", - "type": "python", - "request": "launch", - "program": "${file}", - "console": "integratedTerminal", - "justMyCode": true - } - ] -} diff --git a/CONVENTIONS.md b/CONVENTIONS.md index 674315a95..7a41b5044 100644 --- a/CONVENTIONS.md +++ b/CONVENTIONS.md @@ -4,14 +4,17 @@ When contributing code to *Pokémon PythonRed*, please follow these conventions. ## File system -- Use `snake_case` for file names. +- Use `snake_case` for file names, except for the following: + - `app/locations/.py` uses `kebab-case` - Files should be placed in the appropriate directory according to the following chart: | Directory | Purpose | | :-- | :-: | -| `app/data/base/` | JSON files not accessed by the program
and
Python files used to build JSON files for `app/data/` | -| `app/data/` | JSON files accessed directly by `app/main.py` | | `app/` | Application code
and
The player's save file (`app/.ppr-save`) | +| `app/data/` | JSON files accessed directly by `app/main.py` | +| `app/data/base/` | JSON files not accessed by the program
and
Python files used to build JSON files for `app/data/` | +| `app/events/` | Python files to run when an event in triggered | +| `app/locations/` | Python files to run when a location is reached | ## Game mechanics @@ -23,14 +26,14 @@ When contributing code to *Pokémon PythonRed*, please follow these conventions. - Indent with tab characters, not spaces. - Use `snake_case` for variable names and `PascalCase` for class names. - Use single quotes for strings, and three single quotes for docstrings. Textual apostrophes should be backslash-escaped. -- Use the `#` character for comments. "Todo" comments should be formatted as `# TODO: `. +- Use the `#` character for comments. "Todo" comments should be formatted as `# \TODO: ` without the backslash. Example code: ```python def return_a_string(): '''This is a docstring.''' - return 'Apostrophes (\') should be escaped.' # TODO: This is a todo comment. + return 'Apostrophes (\') should be escaped.' # \TODO: This is a todo comment. ``` ## JSON diff --git a/README.md b/README.md index e385aa65c..1cf2c8770 100644 --- a/README.md +++ b/README.md @@ -28,10 +28,6 @@

---- - -
Project Status Updates
- --- @@ -43,7 +39,7 @@ - This project ([@Pokemon-PythonRed](https://github.com/Pokemon-PythonRed "Pokémon PythonRed Homepage") and anything found within) is not endorsed by Nintendo, GAME FREAK, Creatures Inc., or The Pokémon Company. This is an independent, fan-made game. - This game's plot is a work of fiction! Any references to real people or events are completely coincidental. - Pokémon PythonRed is a work-in-progress and is intended to be a long-term project. The owners feel no rush to meet community deadlines or expectations. Please be considerate. -- Any problems with the game can be reported in [issues](https://github.com/Pokemon-PythonRed/Pokemon-PythonRed/issues "Pokémon PythonRed Issues"). +- Any problems with the game can be reported as [issues](https://github.com/Pokemon-PythonRed/Pokemon-PythonRed/issues "Pokémon PythonRed Issues"). --- @@ -62,12 +58,9 @@ For more information on various topics regarding this project, please visit the - [Code Of Conduct](https://github.com/Pokemon-PythonRed/Pokemon-PythonRed/blob/master/CODE_OF_CONDUCT.md "Pokémon PythonRed Code Of Conduct") - [Contributing Guidelines](https://github.com/Pokemon-PythonRed/Pokemon-PythonRed/blob/master/CONTRIBUTING.md "Pokémon PythonRed Contributing Guidelines") - [Contributors List](https://github.com/Pokemon-PythonRed/Pokemon-PythonRed#contributors "Pokémon PythonRed Contributors") -- ~~[Current Tasks](https://github.com/orgs/Pokemon-PythonRed/projects/1 "Pokémon PythonRed Current Tasks")~~ (closed) - [Discussions](https://github.com/orgs/Pokemon-PythonRed/discussions "Pokémon PythonRed Discussions") -- [Installation Guide](https://github.com/Pokemon-PythonRed/Pokemon-PythonRed#installation "Pokémon PythonRed Installation") -- [Issue Management](https://github.com/orgs/Pokemon-PythonRed/projects/2 "Pokémon PythonRed Issue Management") -- [License](https://github.com/Pokemon-PythonRed/Pokemon-PythonRed/blob/master/LICENSE "Pokémon PythonRed License") -- [Project Status Updates](https://github.com/Pokemon-PythonRed/Pokemon-PythonRed/discussions/59) +- [Installation Guide](https://github.com/Pokemon-PythonRed/Pokemon-PythonRed#installation "Pokémon PythonRed Installation Guide") +- [License (`MIT`)](https://github.com/Pokemon-PythonRed/Pokemon-PythonRed/blob/master/LICENSE "Pokémon PythonRed License (MIT)") - [Security Policy](https://github.com/Pokemon-PythonRed/Pokemon-PythonRed/blob/master/SECURITY.md "Pokémon PythonRed Security Policy") --- @@ -137,21 +130,20 @@ Bot command template: - Windows or Linux OS - Python 3 (aka CPython 3.x) - Follow [this link](https://github.com/Pokemon-PythonRed/Pokemon-PythonRed/archive/refs/heads/master.zip) to download a `.zip` file of this repository. Once it downloads, unzip and open it. -- You will need to open a command line in the folder, then run the following command to install dependencies: +- From the folder, run these commands: ```sh -python -m pip install -r requirements.txt +$ python -m pip install -r requirements.txt +$ python app/main.py ``` -- Finally, open `main.py` in the `app` folder. - - If you encounter any problems when opening the game, feel free to [create an issue](https://github.com/Pokemon-PythonRed/Pokemon-PythonRed/issues/new "Pokémon PythonRed Issues"). - If you want to contribute to the project, please see [the Contributing Guidelines](https://github.com/Pokemon-PythonRed/Pokemon-PythonRed/blob/master/CONTRIBUTING.md). ### Controls -- When `>` is shown, press ⏎ Enter to continue. You may need to input text first. +- When `>` is shown, press a key to continue. - When text bullets are shown, you need to enter the character next to the option you choose. - In the overworld, navigation bullets are WASD for directional movement, then lettered for interactions. Other commands might also be available. - Enter the M command from the overworld to open the menu. @@ -160,15 +152,13 @@ python -m pip install -r requirements.txt #### Examples -No text needed, just press ⏎ Enter: +Press any key: ```console -Press Enter to continue. - ->_ +Press any key to continue._ ``` -Type text from a bullet point (e.g. 1 or 2), then press ⏎ Enter: +Press a key corresponding to a bullet point (e.g. 1 or 2): ```console Choose an option. @@ -179,7 +169,7 @@ Choose an option. >_ ``` -Type any variant of `yes`, `y`, `no`, or `n`, then press ⏎ Enter: +Press Y or N: ```console Would you like to save? Y/N @@ -187,129 +177,4 @@ Would you like to save? Y/N >_ ``` -Press any key: - -```console -OAK: Hello there! Welcome to the world of POKéMON! -_ -``` - ---- - -## Extras - -Here's some extra information that isn't required to play the game, but might still be interesting. - -
Technicalities - ---- - -### Technicalities - -Since this is a Python game, some elements will have to be changed from the original version. Here are some examples of challenging changes. - -#### Save data and saving - -The game has to keep track of the many variables that make up a Save File. These include: - -- Trainer info -- Party / Box info -- If a place has been visited, for Fly locations (each city separately) -- If cutscenes have been triggered -- Event flags -- Pokédex -- Game mechanic settings for accessibility - -These are stored in a Python `dictionary` variable, which is then saved to the external file `.ppr-save` via Python's `json` module. - -#### Game data - -Not to be confused with save data, game data is composed of the numbers and calculations that the game uses. These include: - -- Pokémon info -- Trainer battle info -- Save file templates - -Large sets of data may be stored as `.json` files in the project folder. This is done to keep the program files concise. These files may be added, removed, or changed at any time. - -Save file templates contain all the things the game must keep track of between sessions. On each save or load, the player's file is automatically updated with the latest data, in case an update was performed. This means that when the game is updated, the player can copy their `.ppr-save` file into the new version, and the save file will automatically be updated with the latest data. - -#### Menus - -Pokémon Red is full of menus that look like the following: - -``` -Would you like to save your progress? -> Save - Don't Save -``` - -This has been changed to be more suitable for a text-adventure game: - -```console -Would you like to save your progress? Y/N - ->_ -``` - -This is done with code similar to the following: - -```python -import json -save = { - # Save Data -} -print('Would you like to save your progress? Y/N') - saveOption = ' ' - while saveOption.lower()[0] not in ['y', 'n']: - saveOption = input('>') + ' ' - if saveOption.lower()[0] in ['y']: - open('.ppr-save', 'w').write(json.dumps(save)) - print('Game saved successfully!') -``` - -#### Save management - -The user can use multiple save files by moving or backing up their `.ppr-save` file to a different directory on their device. This has been implemented in the interest of user-friendliness and safety. Save files can easily be backed up, reset, or shared. - ---- - -
- -
Mystery Gifts - ---- - -### Mystery Gifts - -Pokémon games use Mystery Gifts to bring communities together and incentivise players to take part in events, and Pokémon PythonRed is no different. Codes can be given out in planned giveaway events to specific people, left online to be found by anyone, or even given in-person to specific people. Most of them are online, so you should try looking in places [@TurnipGuy30](https://github.com/TurnipGuy30) has been. - -#### Technical information - -As of the time of writing, Mystery Gifts have not yet been implemented into the game. The base game will have to be completed first. For now, keep track of any codes you find. - -#### Formatting - -Pokémon PythonRed Mystery Gift codes are easy to identify because they will always be given in the following format: - -``` -Pokémon PythonRed Mystery Gift #20: "POKEMONPYTHONRED" -``` - -(Yes, this is a valid code. Consider it a free trial. You're welcome.) - -#### Possible rewards - -Redeemed codes will reward a player with in-game items or Pokémon. - -#### Recipient responsibilities - -Any person or group who finds or receives a code has no responsibility to keep it to themselves unless otherwise stated by the giver of the code. - -#### Summary - -Mystery Gifts are meant to unite the community and provide a fun way to interact with the game. - --- - -
diff --git a/app/abort_early.py b/app/abort_early.py new file mode 100644 index 000000000..e44c49d8d --- /dev/null +++ b/app/abort_early.py @@ -0,0 +1,6 @@ +from sys import exit + +# abort function to be used before functions that require libraries +def abort_early() -> None: + input('\nIt appears that you are using an unsupported operating system. Please use Windows or Linux.\n\nPress Enter to exit.') + exit() diff --git a/app/data/README.md b/app/data/README.md new file mode 100644 index 000000000..b8043960a --- /dev/null +++ b/app/data/README.md @@ -0,0 +1,3 @@ +# `app/data/` + +TODO: write this file diff --git a/app/data/base/README.md b/app/data/base/README.md new file mode 100644 index 000000000..f97b5bdac --- /dev/null +++ b/app/data/base/README.md @@ -0,0 +1,5 @@ +# `app/data/base/` + +The formatting of these files isn't moderated the same way as everything else because they're taken directly from online sources and compiled into files the game can use. + +In other words, this folder is not accessed by anything outside of it and may be removed at a later point. diff --git a/app/data/item.json b/app/data/items.json similarity index 100% rename from app/data/item.json rename to app/data/items.json diff --git a/app/data/locations.json b/app/data/locations.json new file mode 100644 index 000000000..01f48d3ff --- /dev/null +++ b/app/data/locations.json @@ -0,0 +1,22 @@ +{ + "pallet": { + "directions": { + "w": "route1s", + "a": "playerhouseup", + "s": "route21n", + "d": "oaklab" + }, + "has_center": false, + "title": "Pallet Town", + "title_full": "Pallet Town - \"Shades of your journey await!\"" + }, + "playerhouse_up": { + "arrival": "", + "directions": { + "s": "playerhousedown", + "1": [null, "playerhouseup_computer"] + }, + "title": "{save['name']}'s Room", + "title_full": "{save['name']}'s Room (Upstairs)" + } +} diff --git a/app/data/save_template.json b/app/data/save_template.json index 5c3432e7e..422b54396 100644 --- a/app/data/save_template.json +++ b/app/data/save_template.json @@ -3,7 +3,7 @@ "bag": {}, "box": [], "dex": {}, - "flag": { + "flags": { "been_to_route_1": false, "chosen_starter": false, "delivered_package": false, @@ -35,6 +35,6 @@ "textSpeed": null }, "party": [], - "recent_center": "playerHouseUp", + "recent_center": "playerhouse-up", "user": [] } diff --git a/app/data_opener.py b/app/data_opener.py new file mode 100644 index 000000000..fad27141b --- /dev/null +++ b/app/data_opener.py @@ -0,0 +1,156 @@ +from json import load +from os import path +from sys import path as syspath + +from handling import abort + +# import from files +try: + + # dex + dex = load( + open( + path.join( + syspath[0], 'data', 'dex.json' + ), encoding='utf8' + ) + ) + open( + path.join( + syspath[0], 'data', 'dex.json' + ) + ).close() + + # items + items = load( + open( + path.join( + syspath[0], 'data', 'items.json' + ), encoding='utf8' + ) + ) + + # locations + locations = load( + open( + path.join( + syspath[0], 'data', 'locations.json' + ), encoding='utf8' + ) + ) + + # moves + moves = load( + open( + path.join( + syspath[0], 'data', 'moves.json' + ), encoding='utf8' + ) + ) + open( + path.join( + syspath[0], 'data', 'moves.json' + ) + ).close() + + # rates + rates = load( + open( + path.join( + syspath[0], 'data', 'map.json' + ), encoding='utf8' + ) + ) + open( + path.join( + syspath[0], 'data', 'map.json' + ) + ).close() + + # save_template + save_template = load( + open( + path.join( + syspath[0], 'data', 'save_template.json' + ), encoding='utf8' + ) + ) + open( + path.join( + syspath[0], 'data', 'save_template.json' + ) + ).close() + + # trainer_types + trainer_types = load( + open( + path.join( + syspath[0], 'data', 'trainer_types.json' + ), encoding='utf8' + ) + ) + open( + path.join( + syspath[0], 'data', 'trainer_types.json' + ) + ).close() + + # types + types = load( + open( + path.join( + syspath[0], 'data', 'types.json' + ), encoding='utf8' + ) + ) + open( + path.join( + syspath[0], 'data', 'types.json' + ) + ).close() + + # xp + xp = load( + open( + path.join( + syspath[0], 'data', 'level.json' + ), encoding='utf8' + ) + ) + open( + path.join( + syspath[0], 'data', 'level.json' + ) + ).close() + + # pokemart + pokemart = load( + open( + path.join( + syspath[0], 'data', 'pokemart.json' + ), encoding='utf8' + ) + ) + open( + path.join( + syspath[0], 'data', 'pokemart.json' + ) + ).close() + + # trainers + trainers = load( + open( + path.join( + syspath[0], 'data', 'trainers.json' + ), encoding='utf8' + ) + ) + open( + path.join( + syspath[0], 'data', 'trainers.json' + ) + ).close() + +# handle file error +except Exception: + abort('Failed to load a file!') diff --git a/app/event_opener.py b/app/event_opener.py new file mode 100644 index 000000000..f4f1c3e49 --- /dev/null +++ b/app/event_opener.py @@ -0,0 +1,21 @@ +from importlib import import_module +from os import path +from sys import path as syspath +from typing import Any, Optional + +from saving import SaveFile + +# event class +class Event: + + # set internals + def __init__(self, id: str) -> None: + self.id = id + self.module_name = f'events.{self.id}' + self.module = import_module(self.module_name) + + def __str__(self) -> str: + return f'{self.id}' + + def execute(self, save: Optional[SaveFile]=None) -> SaveFile: + return self.module.execute(save) diff --git a/app/events/README.md b/app/events/README.md new file mode 100644 index 000000000..9cea069a6 --- /dev/null +++ b/app/events/README.md @@ -0,0 +1,24 @@ +# `app/events/` + +Each file in this folder corresponds to an in-game event. Names are formatted as follows: + +* Event names: `_` +* Filenames: `app/events/.py` + +When writing an event file, start by importing basic in/out functions, as well as the `SaveFile` class. Write the file as a function. In most cases, it should take a parameter called `save` of type `SaveFile`, and return `save`. + +Example function: + +```python +from input import get +from output import sp +from saving import SaveFile + +def example(save: SaveFile) -> SaveFile: + sp([ + ('Event reached!', False), + ('Press any key to continue.', True) + ]) + save['flag']['example'] = True + return save +``` diff --git a/app/events/intro.py b/app/events/intro.py new file mode 100644 index 000000000..d66e320a8 --- /dev/null +++ b/app/events/intro.py @@ -0,0 +1,38 @@ +from input import get +from output import sp +from saving import SaveFile + +def intro(save: SaveFile) -> SaveFile: + sp([ + ('(Intro Start!)\n', False), + ('OAK: Hello there! Welcome to the world of Pokémon!', True), + ('My name is OAK! People call me the Pokémon Professor!', True), + ('This world is inhabited by creatures called Pokémon!', True), + ('For some people, Pokémon are pets. Others use them for fights. Myself...', True), + ('I study Pokémon as a profession.', True), + ('\nFirst, what is your name?\n\n[1] - PYTHON\n[2] - New Name\n', False) + ]) + intro_answer = get(['1', '2']) + if intro_answer == '1': + player_name = 'PYTHON' + else: + sp([('\n(Caps, 15 chars. max)\n', False)]) + player_name = get([], char=False, max_len=15) + player_name = player_name.upper() + sp([ + (f'\nRight! So your name is {player_name}!', True), + ('\nNow, since you\'re so raring to go, I\'ve prepared a rival for you.', True), + ('He will go on an adventure just like yours, and battle you along the way.', True), + ('\n...Erm, what is his name again?\n', False) + ]) + get([], char=False) + sp([ + ('\n...', True), + ('Hoho, just kidding! His name is JOHNNY! You\'ll meet him soon!\n', True), + (f'{player_name}! Your very own Pokémon legend is about to unfold! A world of dreams and adventures with Pokémon awaits! Let\'s go!', True) + ]) + save['name'] = player_name + save['location'] = 'playerhouse-up' + save['flag']['intro_complete'] = True + sp([('\n(Intro Complete!)', False)]) + return save diff --git a/app/events/menu.py b/app/events/menu.py new file mode 100644 index 000000000..a7785e3f7 --- /dev/null +++ b/app/events/menu.py @@ -0,0 +1,56 @@ +from data_opener import dex, xp +from input import get, y, yn +from output import sp +from pokemon import badges +from saving import SaveFile + +def menu(save: SaveFile) -> SaveFile: # sourcery skip: low-code-quality + global exit_status, menu_open, option, options_open + option = '' + menu = f'Menu\n[d] - Pokédex\n[p] - Pokémon\n[i] - Item\n[t] - {save["name"]}\n[s] - Save Game\n[o] - Options\n[e] - Exit Menu\n[q] - Quit Game\n' if 'Pokedex' in save['bag'] else f'Menu\n[p] - Pokémon\n[i] - Item\n[t] - {save["name"]}\n[s] - Save Game\n[o] - Options\n[e] - Exit Menu\n[q] - Quit Game\n' + while menu_open: + sp([(menu, False)]) + while not option: + option = get(['d', 'e', 'i', 'o', 'p', 'q', 's', 't']) + if option not in ['e', 'o']: + sp([('', False)]) + if option == 'd' and 'Pokedex' in save['bag']: + option = '' + dex_string = '' + for i in dex.keys(): + if i in save['dex'].keys(): + dex_string += f'\n{dex[i]["index"]} - {i}: Seen' if save['dex'][i]['seen'] else '' + if save['dex'][i]['caught']: + dex_string += ', Caught' + sp([(f'{save["name"]}\'s Pokédex{dex_string}' if dex_string else '\nYou have no Pokémon in your Pokédex!', False)]) + elif option == 'p': + if save['party']: + sp([('\n'.join(f'{i.name} (`{i.type}`-type)\nLevel {i.level} ({i.current_xp}/{str(xp["next"][i.level_type][str(i.level)])} XP to next level)\n{i.stats["chp"]}/{i.stats["hp"]} HP' for i in save['party']), False)]) + else: + sp([('Your party is empty!', False)]) + elif option == 'i': + if save['bag']: + for i in save['bag']: + sp([(f'{i}: {save["bag"][i]}', False)]) + else: + sp([('Your bag is empty!', False)]) + elif option == 't': + sp([ + (f'Name: {save["name"]}', False), + (f'Money: ¥{"{:,}".format(save["money"])}', False), + (f'''Badges: {''.join(f"[{'-' if save['badges'][i] else ' '}]" for i in badges)}''', False) + ]) + elif option == 's': + save.backup() + elif option == 'o': + options_open = True + elif option == 'e': + menu_open = False + elif option == 'q': + sp([('Are you sure you want to quit? Any unsaved progress will be lost. (Y/N)\n', False)]) + option = '' + while option not in yn: + option = get(yn) + if option in y: + exit_status = True + return save diff --git a/app/events/title.py b/app/events/title.py new file mode 100644 index 000000000..668e2e37a --- /dev/null +++ b/app/events/title.py @@ -0,0 +1,67 @@ +from json import load +from os import path +from sys import path as syspath +from time import sleep +from webbrowser import open as webopen + +from input import get, getch +from output import cls +from saving import SaveFile + +def execute(save: SaveFile) -> SaveFile: + + # display title screen + cls() + sleep(1) + print(r''' + ,'\ + _.----. ____ ,' _\ ___ ___ ____ +_,-' `. | | /`. \,-' | \ / | | \ |`. +\ __ \ '-. | / `. ___ | \/ | '-. \ | | + \. \ \ | __ | |/ ,','_ `. | | __ | \| | + \ \/ /,' _`.| ,' / / / / | ,' _`.| | | + \ ,-'/ / \ ,' | \/ / ,`.| / / \ | | + \ \ | \_/ | `-. \ `' /| | || \_/ | |\ | + \ \ \ / `-.`.___,-' | |\ /| \ / | | | + \ \ `.__,'| |`-._ `| |__| \/ | `.__,'| | | | + \_.-' |__| `-._ | '-.| '-.| | | + `' '-._| +''') + sleep(2.65) + print(' PythonRed Version') + sleep(1.85) + print(' Press any key to begin!') + getch() + print('\n\n[1] - Continue Game\n[2] - New Game\n[3] - GitHub Repository') + start_option = '' + while start_option != '2': + start_option = get(['1', '2', '3'], char=True, max_len=1) + + # continue from save file + if start_option == '1': + try: + has_saved = load( + open(path.join(syspath[0], '.ppr-save')))['flag']['has_saved'] + if path.isfile(path.join(syspath[0], '.ppr-save')) and has_saved: + break + except KeyError: + print('Your save file is outdated and the game cannot load it. Please back up your save file and contact us with option [3].') + except ValueError: + print('Your save file is empty and cannot be loaded!') + else: + print('No previous save file found!') + + # new game + elif start_option == '2': + break + + # open github link + elif start_option == '3': + try: + webopen('https://github.com/Pokemon-PythonRed/Pokemon-PythonRed', new=2, autoraise=True) + except Exception: + print('Failed to open website, here\'s the link:\n[https://github.com/Pokemon-PythonRed/Pokemon-PythonRed]\n') + else: + print('Repository page opened successfully!') + + return save diff --git a/app/fix_dex.py b/app/fix_dex.py deleted file mode 100644 index dc758d239..000000000 --- a/app/fix_dex.py +++ /dev/null @@ -1,14 +0,0 @@ -from json import dumps, load -from os import path -from sys import path as syspath -with open(path.join(syspath[0], 'data/dex.json')) as f: - old_dex = load(f) -new_dex = {} -for i in range(len(old_dex)): - for key, dex_entry in old_dex.items(): - if dex_entry['index'] == i: - new_dex[key] = dex_entry - print(f'Added {dex_entry["name"]}') - break -with open(path.join(syspath[0], 'data/dex.json'), 'w') as f: - f.write(dumps(new_dex, indent=4).replace(' ', '\t')) diff --git a/app/handling.py b/app/handling.py new file mode 100644 index 000000000..f07b88e28 --- /dev/null +++ b/app/handling.py @@ -0,0 +1,14 @@ +from input import is_debug +from output import colours, link, sp + +# error message +def abort(message) -> None: + print(f'\n{colours["FIRE"]}- - - INTERNAL ERROR - - -{colours["RESET"]}\n\nERROR MESSAGE: {message}\n\nIf you have not edited any files, feel free to create an issue on the repository by going to the link below.\n\nNote: your save file will be preserved in the program folder. Any unsaved progress will be lost (sorry).\n\n[{link["issue"]}]\n\nPress Enter to exit.') + input('\n> ') + global exit + exit = True + +# debug statements +def debug(text) -> None: + if is_debug: + sp(f'{colours["GROUND"]}Debug: {text}{colours["RESET"]}') diff --git a/app/input.py b/app/input.py new file mode 100644 index 000000000..044904535 --- /dev/null +++ b/app/input.py @@ -0,0 +1,48 @@ +from sys import stdout +from typing import Union + +from abort_early import abort_early + +# menu variables +exit_status = is_debug = menu_open = options_open = False +y, n, yn = ['y'], ['n'], ['y', 'n'] + +# import getch according to system +try: + from msvcrt import getch, getche # type: ignore +except ImportError: + try: + from getch import getch, getche # type: ignore + except ImportError: + abort_early() + +# input function: +def get(valid: Union[list[str], None]=[], char: bool=False, max_len: int=255, prompt: str='»') -> str: + # sourcery skip: default-mutable-arg + if valid is None: + valid = [] + user_input = '' + stdout.write(prompt) + if char: + print() + if valid: + while user_input not in valid: + user_input = bytes.decode(getch()) + else: + user_input = bytes.decode(getch()) + elif valid: + while not all([ + bool(user_input), + len(user_input) > max_len, + user_input in valid + ]): + user_input = input(prompt) + else: + while not all([ + bool(user_input), + len(user_input) > max_len + ]): + user_input = input(prompt) + stdout.write(user_input) + stdout.flush() + return user_input diff --git a/app/location_opener.py b/app/location_opener.py new file mode 100644 index 000000000..5897ac526 --- /dev/null +++ b/app/location_opener.py @@ -0,0 +1,31 @@ +from keyboard import KeyboardEvent +from os import path +from sys import path as syspath +from typing import Optional, Union + +from data_opener import locations, rates +from event_opener import Event +from saving import SaveFile + +# location class +class Location: + + # set internals + def __init__(self, id: str) -> None: + self.id = id + self.title = locations[self.id]['title'] + self.subtitle = locations[self.id]['subtitle'] + self.titled = locations[self.id]['titled'] + self.directions = locations[self.id]['directions'] + if self.title in rates.keys(): + self.rates = rates[self.title] + self.is_encounter_zone = True + else: + self.rates = None + self.is_encounter_zone = False + + # display options + def execute(self) -> SaveFile: # TODO: execute location, save param, return SaveFile + print(f'\n{self.title}') + for key, val in self.directions.items(): + print(f'[{key}] - {locations[val]["title"]}') diff --git a/app/main.py b/app/main.py index e38fc81a2..c61843565 100644 --- a/app/main.py +++ b/app/main.py @@ -1,1633 +1,486 @@ -''' -Project Page - [https://Pokemon-PythonRed.github.io] -Repository - [https://github.com/Pokemon-PythonRed/Pokemon-PythonRed] -License - MIT -''' - # import system modules +from contextlib import suppress +from copy import deepcopy from datetime import datetime from getpass import getuser +from importlib import import_module from json import dumps, loads from math import ceil, floor, sqrt from os import path, system, remove from platform import system as platform from random import choice, choices, randint -# TODO: from string import ... +from string import Formatter from sys import exit as sysexit, path as syspath, stdout from time import sleep, time from typing import Optional, Union from webbrowser import open as webopen +# import folderspace files +try: + from abort_early import abort_early + from data_opener import dex, items, locations, moves, rates, save_template, trainer_types, types, xp, pokemart, trainers + from event_opener import Event + from handling import abort, debug + from input import exit_status, get, getch, getche, menu_open, options_open, y, yn + from location_opener import Location + from output import cls, link, reset_sp, sp, text, text_speed + from pokemon import badges, battle, display_pokemart, display_trainers, find_moves, get_encounter, heal, Pokemon, trainer_interaction + from saving import SaveFile +except ImportError as e: + input(f'\n{e}.\n\nPlease see [https://github.com/Pokemon-PythonRed/Pokemon-PythonRed#installation] for more information.\n\nPress Enter to exit.\n') + sysexit() + # import installed modules from jsons import dump, load -# TODO: from pygame import ... - -# abort function to be used before functions that require libraries -def abort_early() -> None: - input('It appears that you are using an unsupported operating system. Please use Windows or Linux.\n\nPress Enter to exit.') - system.exit() - -# import getch according to system -if platform() == 'Windows': - from msvcrt import getch # type: ignore -elif platform() == 'Linux': - from getch import getch # type: ignore -else: - abort_early() +from pygame.mixer import music -# type colours -colours = { - 'NORMAL': '\x1b[0;0m', - 'FIRE': '\x1b[38;5;196m', - 'WATER': '\x1b[38;5;027m', - 'GRASS': '\x1b[38;5;082m', - 'ELECTRIC': '\x1b[38;5;184m', - 'ICE': '\x1b[38;5;159m', - 'FIGHTING': '\x1b[38;5;167m', - 'POISON': '\x1b[38;5;135m', - 'GROUND': '\x1b[38;5;215m', - 'FLYING': '\x1b[38;5;183m', - 'PSYCHIC': '\x1b[38;5;198m', - 'BUG': '\x1b[38;5;028m', - 'ROCK': '\x1b[38;5;179m', - 'GHOST': '\x1b[38;5;126m', - 'DRAGON': '\x1b[38;5;057m', - 'DARK': '\x1b[38;5;095m', - 'STEEL': '\x1b[38;5;250m', - 'FAIRY': '\x1b[38;5;212m', - 'RESET': '\x1b[00;0;000m' -} - -# declare timed text output -text = { - 'slow': 0.03, - 'normal': 0.02, - 'fast': 0.01, - 'ultra': 0.005, - 'debug': 0.0 -} -text_speed = 'normal' -def reset_sp(speed) -> None: - global sp, sg - def sp(text, g=False) -> None: - for key in colours.keys(): - text = text.replace(f'`{key}`', f'`{colours[key]}{key}{colours["RESET"]}`') - coloured = False - colour_char = False - i = 0 - for char in f'{text}\n': - if char == '`': - if not coloured: - colour_char = True - coloured = not coloured - continue - elif coloured and char == '[': - colour_char = True - elif not colour_char: - sleep(speed) - elif i>=10: - i = 0 - colour_char = False - sleep(speed) - else: - i+=1 - stdout.write(char) - stdout.flush() - if g: - getch() - - def sg(text) -> None: - sp(text, g=True) +# set default text speed reset_sp(speed=text[text_speed]) # load screen -sp('Loading...') +sp([('Loading...', False)]) -# input function: -def get() -> str: - return input('> ') - -# store links -link = { - 'repository': 'https://github.com/Pokemon-PythonRed/Pokemon-PythonRed', - 'installation': 'https://github.com/Pokemon-PythonRed/Pokemon-PythonRed#installation', - 'issue': 'https://github.com/Pokemon-PythonRed/Pokemon-PythonRed/issues/new/choose' -} - -# check for required files -if not (path.isfile(path.join(syspath[0], i)) for i in [ - 'data/dex.json', - 'data/level.json', - 'data/trainer_types.json', - 'data/types.json', - 'data/moves.json', - 'data/map.json', - 'data/pokemart.json', - 'data/trainers.json' - 'build_to_exe.py' -]): - sp(f'\nOne or more required files are not found.\n\nPlease see\n[{link["installation"]}]\nfor more information.\n\nPress Enter to exit.\n') - get() - sysexit() - -# declare clear -platforms = [ - ['darwin', 'clear'], - ['java', 'System.out.print("\\033[H\\033[2J");System.out.flush();'], - ['linux', 'clear'], - ['windows', 'cls'] -] -for i in range(len(platforms)): - if platform().lower() == (platforms[i][0]): - cls_command = platforms[i][1] - def cls(command=cls_command) -> int: return system(command) - break +# clear screen try: - cls() # type: ignore + cls() except NameError: abort_early() -# menu variables -exit = is_debug = menu_open = options_open = False -y, n, yn = ['y'], ['n'], ['y', 'n'] -types = ['NORMAL', 'FIRE', 'WATER', 'GRASS', 'ELECTRIC', 'ICE', 'FIGHTING', 'POISON', 'GROUND', 'FLYING', 'PSYCHIC', 'BUG', 'ROCK', 'GHOST', 'DARK', 'DRAGON', 'STEEL', 'FAIRY'] -badges = ['Boulder', 'Cascade', 'Thunder', 'Rainbow', 'Soul', 'Marsh', 'Volcano', 'Earth'] - -# battle screen variables -name_length = 15 -bars_length = 20 - # enables ANSI escape codes in Windows system('') -# error message -def abort(message) -> None: - print(f'\n{colours["FIRE"]}- - - INTERNAL ERROR - - -{colours["RESET"]}\n\nERROR MESSAGE: {message}\n\nIf you have not edited any files, feel free to create an issue on the repository by going to the link below.\n\nNote: your save file will be preserved in the program folder. Any unsaved progress will be lost (sorry).\n\n[{link["issue"]}]\n\nPress Enter to exit.') - input('\n> ') - global exit - exit = True - -# save from pause menu or pokemon centre -def backup(pokemon_centre = False) -> None: - if not pokemon_centre: - sp('Would you like to save your progress? (Y/N)\n') - save_option = ' ' - while save_option.lower()[0] not in yn: - save_option = f'{get()} ' - if save_option.lower()[0] in y: - save_data_to_file() - else: - save_data_to_file() - -# save data to file -def save_data_to_file(): - save['flag']['has_saved'] = True - save_temp = {**save, 'party': [dump(i) for i in save['party']], 'box': [dump(i) for i in save['box']], 'last_played': None} # TODO: save time last played - open(path.join(syspath[0], '.ppr-save'), 'w').write(f'{dumps(save_temp, indent=4, sort_keys=True)}\n') - sp('\nGame saved successfully!') - -# decide if damage is critical -def critical() -> bool: - return randint(0, 255) <= 17 - -# pokemon class -class Pokemon: - - # set internals - def __init__(self, species, level, ivs, moves=None, chp=None, current_xp=0, fainted=False, player_pokemon = False) -> None: - self.species = species - self.index = dex[self.species]['index'] # type: ignore - self.name = dex[self.species]['name'] # type: ignore - self.type = dex[self.species]['type'] # type: ignore - self.level = level - self.ivs = ivs if ivs != 'random' else {i: randint(0, 31) for i in ['hp', 'atk', 'def', 'spa', 'spd', 'spe']} - self.level_type = dex[self.species]['xp'] # type: ignore - self.total_xp = xp['total'][self.level_type][str(self.level)] # type: ignore - self.current_xp = current_xp - self.moves = moves or find_moves(self.species, self.level) - self.status = { - 'burn': False, - 'confusion': False, - 'freeze': False, - 'paralysis': False, - 'poison': False, - 'sleep': False - } - - # update pokedex - if self.species not in save['dex']: - save['dex'].update({self.species: {'seen': True, 'caught': False}}) - else: - if 'seen' not in save['dex'][self.species]: - save['dex'][self.species]['seen'] = True - if 'caught' not in save['dex'][self.species]: - save['dex'][self.species]['caught'] = False - if self.type not in save['flag']['type']: - save['flag']['type'].update({self.type: {'seen': True, 'caught': False}}) - else: - if 'seen' not in save['flag']['type'][self.type]: - save['flag']['type'][self.species]['seen'] = True - if 'caught' not in save['flag']['type'][self.type]: - save['flag']['type'][self.species]['caught'] = False - - # initialise stats - self.reset_stats(chp, fainted, player_pokemon) +# TODO: test title screen event +save = Event('title').execute(SaveFile(save_exists=False)) - # reset stats - def reset_stats(self, chp=None, fainted=None, player_pokemon = False) -> None: - self.stats = { - 'hp': floor(((dex[self.species]['hp'] + self.ivs['hp']) * 2 + floor(ceil(sqrt(self.ivs['hp'])) / 4) * self.level) / 100) + self.level + 10, # type: ignore - 'atk': floor(((dex[self.species]['atk'] + self.ivs['atk']) * 2 + floor(ceil(sqrt(self.ivs['atk'])) / 4) * self.level) / 100) + 5, # type: ignore - 'def': floor(((dex[self.species]['def'] + self.ivs['def']) * 2 + floor(ceil(sqrt(self.ivs['def'])) / 4) * self.level) / 100) + 5, # type: ignore - 'spa': floor(((dex[self.species]['spa'] + self.ivs['spa']) * 2 + floor(ceil(sqrt(self.ivs['spa'])) / 4) * self.level) / 100) + 5, # type: ignore - 'spd': floor(((dex[self.species]['spd'] + self.ivs['spd']) * 2 + floor(ceil(sqrt(self.ivs['spd'])) / 4) * self.level) / 100) + 5, # type: ignore - 'spe': floor(((dex[self.species]['spe'] + self.ivs['spe']) * 2 + floor(ceil(sqrt(self.ivs['spe'])) / 4) * self.level) / 100) + 5 # type: ignore - } - if player_pokemon == False: - for move in self.moves: - move['pp'] = list(filter(lambda m, move=move: m['name'] == move['name'], moves))[0]['pp'] # type: ignore - self.stats['chp'] = chp or self.stats['hp'] - self.fainted = fainted or self.stats['chp'] <= 0 - if self.fainted: - self.stats['chp'] = 0 - - def check_level_up(self) -> None: - while self.current_xp >= xp['next'][self.level_type][str(self.level)]: # type: ignore - self.current_xp -= xp['next'][self.level_type][str(self.level)] # type: ignore - self.level_up(self) - if self.level == 100: - sp(f'\nCongratulations, {self.name} has reached level 100!') - break - - # check if pokemon is fainted - def check_fainted(self) -> bool: - if self.stats['chp'] <= 0: - self.stats['chp'] = 0 - self.fainted = True - return True - return False - - # lower chp when pokemon is attacked - def deal_damage(self, attacker, move) -> Optional[int]: - move_entry = list(filter(lambda m: m['name'] == move['name'], moves))[0] # type: ignore - sp(f'\n{attacker.name} used {move["name"].upper()}!') - if move_entry['damage_class'] == 'status': - # TODO: Implement status conditions - sp(f'(Note: {move["name"].upper()} is a status move)') - else: - if randint(1,100) <= move_entry["accuracy"]: - return self.damage_calc(move_entry, attacker) - sp(f'{attacker.name} missed!') - return 0 - - # TODO Rename this here and in `deal_damage` - def damage_calc(self, move_entry, attacker): - is_critical = critical() - attack_defense = ('atk', 'def') if move_entry['damage_class'] == 'physical' else ('spa', 'spd') - result = floor((((((2 * attacker.level * (2 if is_critical else 1) / 5) + 2) * move_entry['power'] * attacker.stats[attack_defense[0]] / self.stats[attack_defense[1]]) / 50) + 2) * (1.5 if move_entry['type'] == attacker.type else 1) * randint(217, 255) / 255 * (type_effectiveness(move_entry, self) if save['flag']['been_to_route_1'] else 1)) - - self.stats['chp'] -= result - if result > 0: - sp(f'\n{attacker.name} dealt {result} damage to {self.name}!') - if is_critical: - sp('A critical hit!') - for i in [ - (0, 'It had no effect!'), - (0.5, 'It\'s super effective!'), - (2, 'It\'s not very effective!') - ]: - if types[self.type][move_entry['type'].upper()] == i[0]: - sp(f'{i[1]}') - self.check_fainted() - if self.fainted: - sp(f'\n{self.name} fainted!') - return result - - def deal_struggle_damage(self, damage): - sp(f'{self.name} is hit with recoil!') - self.stats['chp'] -= floor(damage / 2) - self.check_fainted() - if self.fainted: - sp(f'\n{self.name} fainted!') - - # calculate xp rewarded after battle - def calculate_xp(self, battle_type='wild') -> int: - return ceil((self.total_xp * self.level * (1 if battle_type == 'wild' else 1.5)) / 7) # type: ignore - - # give xp from opponent pokemon to party in battle - def give_xp(self, participating_pokemon, type="wild"): - total_xp = self.calculate_xp(type) # type: ignore - debug(f'total xp: {total_xp}') - if 'EXP. ALL' in save['bag']: - for p in participating_pokemon: - save['party'][p].current_xp += floor(total_xp / (len(participating_pokemon) + 1)) - sg(f'{save["party"][p].name} gained {floor(total_xp / (len(participating_pokemon) + 1))} XP!') - save['party'][p].check_level_up() - - other_pokemon = [pokemon for pokemon in save['party'] if pokemon not in participating_pokemon] - for o in other_pokemon: - save['party'][o].current_xp += floor((total_xp / (len(participating_pokemon) + 1)) / len(other_pokemon)) - sg(f'{save["party"][o].name} gained {floor((total_xp / (len(participating_pokemon) + 1)) / len(other_pokemon))} XP!') - save['party'][o].check_level_up() - - else: - for p in participating_pokemon: - save['party'][p].current_xp += floor(total_xp / len(participating_pokemon)) - sg(f'{save["party"][p].name} gained {floor(total_xp / len(participating_pokemon))} XP!') - save['party'][p].check_level_up() - sp("") - sleep(0.5) - - # evolve pokemon - def evolve(self): - sp(f'\nWhat? {self.name} is evolving!') - input_cancel = getch() - # for _ in range(3): - if input_cancel in ['e','b']: - sg(f'{self.name} didn\'t evolve') - return - - sleep(0.5) - print("...") - sleep(2) - - self.index += 1 - old_name = self.name - for p in dex.keys(): # type: ignore - if dex[p]['index'] == self.index: # type: ignore - self.species = p - self.name = dex[self.species]['name'] # type: ignore - self.reset_stats() - sg(f'\n{old_name} evolved into {self.species}!') # type: ignore - - save['dex'][self.species] = {'seen': True, 'caught': True} - save['flag']['type'][self.type] = {'seen': True, 'caught': True} - for move in dex[self.species]['moves']: # type: ignore - # TODO: Possibly keep track of moves that were forgotten too and not reprompt to learn as well? - if move['level'] <= self.level and move['name'] not in (m['name'] for m in self.moves): - self.learn_move(move) - - def learn_move(self, move): - if len(self.moves) == 4: - sg(f'{self.name} wants to learn {move["name"].upper()}!') - sg(f'But {self.name} already knows 4 moves') - all_moves = [*self.moves, move] - move_forgotten = False - while not move_forgotten: - sp(f'Which move should {self.name} forget?\n') - for i in range(5): - print(f'[{i+1}] - {all_moves[i]["name"].upper().replace("-", " ")}') - forget_move = '' - while not forget_move: - forget_move = get() - if forget_move not in ['1', '2', '3', '4', '5']: - forget_move = '' - else: - if forget_move == '5': - sp(f'\nAre you sure you want {self.name} to not learn {move["name"].upper()}? (Y/N)') - else: - sp(f'\nAre you sure you want {self.name} to forget {all_moves[int(forget_move)-1]["name"].upper()}? (Y/N)') - option = '' - while option not in ['y','n']: - option = get() - if option in ['y']: - if forget_move == '5': - sp(f'\n{self.name} didn\'t learn {move["name"].upper()}') - else: - sp(f'\n{self.name} forgot {all_moves[int(forget_move)-1]["name"].upper()}\n') - sp(f'\n{self.name} learned {move["name"].upper()}!') - self.moves = [move for move in self.moves if move['name'] != all_moves[int(forget_move) - 1]['name']] - self.moves.append({"name": move['name'], "pp": list(filter(lambda mv: mv['name'] == move['name'], moves))[0]['pp']}) # type: ignore - move_forgotten = True - else: - sg(f'{self.name} learned {move["name"].upper()}') - self.moves.append({"name": move['name'], "pp": list(filter(lambda mv, move=move: mv['name'] == move['name'], moves))[0]['pp']}) # type: ignore +# reset getch according to options +reset_sp(text[save['options']['text_speed']]) - # raw level up - def level_up(self, pokemon): - pokemon.level += 1 - pokemon.reset_stats() - sg(f'{pokemon.name} grew to level {pokemon.level}!') - if ('evolution' in dex[pokemon.species] and pokemon.level >= dex[pokemon.species]['evolution']): # type: ignore - pokemon.evolve() - for m in dex[pokemon.species]['moves']: # type: ignore - if m['level'] == pokemon.level: - pokemon.learn_move(m) +# intro +if save['flag']['intro_complete'] == False: + save = Event('intro').execute(save) - # catch Pokemon - def catch(self, ball: str) -> bool: - global save - if max(bool(self.status[i]) for i in ['freeze', 'sleep']): - status = 25 - elif max(bool(self.status[i]) for i in ['burn', 'poison', 'paralysis']): - status = 12 - else: - status = 0 +# main loop +while not exit_status: + save['location'] = Location(save['location']).execute() - # find Poke Ball type - if ball == "Great Ball": - ball_modifier = 201 - elif ball == "Master Ball": - pass # guaranteed catch - elif ball == "Poke Ball": - ball_modifier = 256 - elif ball == "Ultra Ball": - ball_modifier = 151 - else: - abort(f'Invalid ball: {ball}') +# TODO: translate everything below this point into `data/locations.json` and `events/*.py` + if False: - # decide whether caught - C = dex[self.species]['catch'] # type: ignore - if ball == "Master Ball": - catch = True - elif self.stats['hp'] / (2 if ball == "Great Ball" else 3) >= self.stats['chp'] and (status + C + 1) / ball_modifier >= 1: # type: ignore - catch = True - else: - X = randint(0, ball_modifier-1) # type: ignore - if X < status: - catch = True - elif X > status + C: - catch = False + # options menu + if options_open == True: + sp('Options Menu\n[1] - Text Speed\n[2] - EMPTY\n[3] - EMPTY\n[4] - Back\n') + while not option: + option = get() + if option == '1': + option = '' + while option != '5': + sp('\nText Speed\n[1] - Slow\n[2] - Normal\n[3] - Fast\n[4] - Ultra\n[5] - Back\n') + option = '' + while (not option) and option not in ['1', '2', '3', '4', '5']: + option = get() + if option != '5': + sp('') + if option == '1': + save['options']['text_speed'] = 'slow' + sp('Text Speed set to Slow!') + elif option == '2': + save['options']['text_speed'] = 'normal' + sp('Text Speed set to Normal!') + elif option == '3': + save['options']['text_speed'] = 'fast' + sp('Text Speed set to Fast!') + elif option == '4': + save['options']['text_speed'] = 'ultra' + sp('Text Speed set to Ultra!') + reset_sp(text[save['options']['text_speed']]) + elif option in ['2', '3']: + sp('Coming Soon!') + elif option == '4': + options_open = False else: - catch = min( - 255, - self.stats['hp'] * 255 // (8 if ball == "Great Ball" else 12) // max(1, floor(self.stats['chp'] / 4)) - ) >= randint(0, 255) + sp('\nInvalid answer!') - if catch: - return self.add_caught_pokemon(save) - wobble_chance = ((C * 100) // ball_modifier * min(255, self.stats['hp'] * 255 // (8 if ball == "Great Ball" else 12) // max(1, floor(self.stats['chp'] / 4)))) // 255 + status # type: ignore - debug(wobble_chance) - - if wobble_chance >= 0 and wobble_chance < 10: # No wobbles - sp('The ball missed the Pokémon!') - elif wobble_chance >= 10 and wobble_chance < 30: # 1 wobble - sp('Darn! The Pokémon broke free!') - elif wobble_chance >= 30 and wobble_chance < 70: # 2 wobbles - sp('Aww! It appeared to be caught!') - elif wobble_chance >= 70 and wobble_chance <= 100: # 3 wobbles - sp('Shoot! It was so close too!') - return False - - # once pokemon is caught, add to party or box - def add_caught_pokemon(self, save): - location = 'party' if len(save['party']) < 6 else 'box' - save[location].append(self) - save['dex'][self.species] = {'seen': True, 'caught': True} - save['flag']['type'][self.type] = {'seen': True, 'caught': True} - sg(f'\nYou caught {self.name}!') - sg(f'\n{self.name} (`{self.type}`-type) was added to your {location}.') - return True - -# check if party is alive -def is_alive(self) -> bool: - return any(not i.fainted for i in self) - -# use item from bag -def use_item(battle=False) -> str: # type: ignore - global save - item_used = False - sp('\nPlease choose an item to use.') - if battle: - sp('\n'.join(f'{key}: {save["bag"][key]}' for key in save['bag'] if items[key]['battle'])) # type: ignore - else: - sp('\n'.join(f'{key}: {save["bag"][key]}' for key in save['bag'])) - sp('[e] - Back\n') - while not item_used: - item = '' - while not item: - item = get() - if item == "e": - return "exit" - if item in save['bag']: - if save['bag'][item] > 0: - save['bag'][item] -= 1 - # exec(items[item]['command']) # type: ignore - return item + # player house - upstairs + elif save['location'] == 'playerhouse-up': + sp(f'Current Location: {save["name"]}\'s Room (Upstairs)\n\n[s] - Go Downstairs\n[1] - Computer\n[2] - Notebook\n') + while option == '': + option = get() + if option == '1': + sg('\n...') + sg('Looks like you can\'t use it yet.') + elif option == '2': + sg('\nThe notebook is open to a page that says:\n\n"Use the [m] command in the overworld to open the menu.\nFrom the menu, you can save your progress, check your Pokémon, and more!"') + elif option == 's': + save['location'] = 'playerhouse-down' + elif option == 'm': + menu_open = True else: - sp('You have none of that item!') - -# randomise escape -def escape(pokemon, opponent, escape_attempts) -> bool: - return floor((pokemon.stats['spe'] * 32) / (floor(opponent.stats['spe'] / 4) % 256)) + 30 * escape_attempts > 255 or floor(opponent.stats['spe'] / 4) % 256 == 0 - -# calculate type effectiveness -def type_effectiveness(move, defender) -> float: - return types[move['type'].upper()][defender.type] # type: ignore - -# calculate prize money -def prize_money(party=None, type='Pokémon Trainer') -> int: - return floor(trainer_types[type] * max(i.level for i in (save['party'] if party is None else party))) # type: ignore - -# find moves of a wild pokemon -def find_moves(name, level) -> list: - learned_moves = [{**move, "pp": list(filter(lambda m, move=move: m['name'] == move['name'], moves))[0]['pp']} for move in dex[name]['moves'] if move['level'] <= level] # type: ignore - - learned_moves = sorted(learned_moves, key=lambda m: m['level'], reverse=True) - if len(learned_moves) >= 4: - return list(map(lambda m: {"name": m['name'], "pp": m["pp"]}, learned_moves[:4])) - else: - return list(map(lambda m: {"name": m['name'], "pp": m["pp"]}, learned_moves)) - -# switch pokemon in battle -def switch_pokemon(party_length: int) -> Union[int, str]: - sp(f'''\nWhich Pokémon should you switch to?\n\n{ - chr(10).join(f'{f"[{i+1}]" if not save["party"][i].check_fainted() else "FAINTED"} - {save["party"][i].name} ({save["party"][i].stats["chp"]}/{save["party"][i].stats["hp"]}) - Level {save["party"][i].level} ({colours[save["party"][i].type.upper()]}{save["party"][i].type}{colours["NORMAL"]})' for i in range(party_length)) - }''') - sp('[e] - Back\n') - switch_choice = '' - while not switch_choice: - while switch_choice == '': - switch_choice = get() - if switch_choice == 'e': - return 'exit' - try: - if switch_choice not in [str(i+1) for i in range(party_length)]: - switch_choice = '' - sp('\nInvalid choice.') - elif save['party'][int(switch_choice)-1].check_fainted(): - switch_choice = '' - sp('That Pokémon is fainted!') - except (TypeError, ValueError): - switch_choice = '' - sp('\nInvalid choice.') - return int(switch_choice) - -# create battle process -def battle(opponent_party=None, battle_type='wild', name=None, title=None, start_diagloue=None, end_dialouge=None, earn_xp=True) -> None: - global save - debug('Entered battle!') - debug(f'Party: {[i.name for i in save["party"]]}') - party_length = len(save['party']) - current = '' - opponent_current = 0 - for i in range(party_length): - if not save['party'][i].check_fainted(): - debug(f'{save["party"][i].name} is the first alive Pokemon in the party.') - current = i - break - - # battle intro - if battle_type == 'trainer': - sg(f'\n{name if name else title}: {start_diagloue}') - sg(f'\n{title} {name+" " if name else ""}wants to fight!') - elif battle_type == 'wild': - sp(f'\nA wild {opponent_party[opponent_current].name} appeared!') # type: ignore - else: - abort('\nInvalid battle type: neither trainer nor wild.') - sp(f'\nGo, {save["party"][current].name}!') - sleep(0.5) - if battle_type == 'trainer': - sp(f'\n{name if name else title} sent out {opponent_party[opponent_current].name}!') # type: ignore - - # battle variables - escaped_from_battle = False - escape_attempts = 0 - caught = False - catch_attempt = False - switched = False - participating_pokemon = [current] - - # check if parties are alive - debug(f'\nPlayer party alive: {is_alive(save["party"])}\nOpponent party alive: {is_alive(opponent_party)}') - - # battle loop - while is_alive(save['party']) and is_alive(opponent_party): - - # player turn - debug('Turn start!') - player_attacked_this_turn = False - opponent_attacked_this_turn = False - catch_attempt = False - switched = False + sp('\nInvalid answer!') - # calculate health bars according to ratio (chp:hp) - bars = ceil((save['party'][current].stats['chp']/(save['party'][current].stats['hp']))*bars_length) - opponent_bars = ceil((opponent_party[opponent_current].stats['chp']/(opponent_party[opponent_current].stats['hp']))*bars_length) # type: ignore - debug(f'Player bars: {bars}\nOpponent bars: {opponent_bars}') - debug(f'Player level: {save["party"][current].level}\nOpponent level: {opponent_party[opponent_current].level}') # type: ignore - sp(f'''\n{save["party"][current].name}{' '*(name_length-len(save['party'][current].name))}[{'='*bars}{' '*(bars_length-bars)}] {str(save['party'][current].stats['chp'])}/{save['party'][current].stats['hp']} (`{save["party"][current].type}`) Lv. {save["party"][current].level}\n{opponent_party[opponent_current].name}{' '*(name_length-len(opponent_party[opponent_current].name))}[{'='*opponent_bars}{' '*(bars_length-opponent_bars)}] {opponent_party[opponent_current].stats['chp']}/{opponent_party[opponent_current].stats['hp']} (`{opponent_party[opponent_current].type}`) Lv. {opponent_party[opponent_current].level}''') # type: ignore - sp(f'\nWhat should {save["party"][current].name} do?\n\n[1] - Attack\n[2] - Switch\n[3] - Item\n[4] - Run\n') - - valid_choice = False - while not valid_choice: - user_choice = get() - if user_choice == '2' and len(save['party']) == 1: - sp('You can\'t switch out your only Pokémon!') - elif user_choice == '3' and len(save['bag']) == 0: - sp('You have no items!') - elif user_choice == '4' and battle_type == 'trainer': - sp('You can\'t run from a trainer battle!') - elif user_choice in ['1', '2', '3', '4']: - valid_choice = True - - # choose attack - if user_choice == '1': # type: ignore - struggle = True - for move_iter in save['party'][current].moves: - if move_iter['pp'] > 0: - struggle = False - if struggle: - sp(f'{save["party"][current].name} has no moves left!') - chosen_move = {'name': 'struggle'} + # player house - downstairs + elif save['location'] == 'playerhouse-down': + sp(f'Current Location: {save["name"]}\'s House (Downstairs)\n\n[w] - Go Upstairs\n[d] - Go Outside\n') + while option == '': + option = get() + if option == 'd': + save['location'] = 'pallet' + elif option == 'w': + save['location'] = 'playerhouse-up' + elif option == 'm': + menu_open = True else: - options = [] - sp('') - move_names = [] - type_names = [] - for i in save['party'][current].moves: - move_names.append(i['name']) - type_names.append(list(filter(lambda m, i=i: m['name'] == i['name'], moves))[0]['type']) # type: ignore - longest_move_name_length = len(max(move_names, key=len)) - longest_type_name_length = len(max(type_names, key=len)) - - for i in range(len(save['party'][current].moves)): - move_entry = list(filter(lambda m, i=i: m['name'] == save['party'][current].moves[i]['name'], moves))[0] # type: ignore - sp(f'[{i+1}] - {save["party"][current].moves[i]["name"].upper().replace("-"," ")}{" "*(longest_move_name_length-len(save["party"][current].moves[i]["name"].upper().replace("-"," ")))} | `{move_entry["type"].upper()}`{" "*(longest_type_name_length-len(move_entry["type"].upper()))} - {save["party"][current].moves[i]["pp"]}/{move_entry["pp"]}') - options.append(str(i+1)) - sp(f'[e] - Back\n') - valid_choice = False - while not valid_choice: - move_choice = get() - if move_choice in options: - if save['party'][current].moves[int(move_choice)-1]['pp'] == 0: - sp(f'{save["party"][current].name} cannot use {save["party"][current].moves[int(move_choice)-1]["name"]}') - else: valid_choice = True - elif move_choice == "e": - valid_choice = True - if move_choice == "e": # type: ignore - continue + sp('\nInvalid answer!') - chosen_move = save["party"][current].moves[int(move_choice)-1] # type: ignore - - if save['party'][current].stats['spe'] >= opponent_party[opponent_current].stats['spe']: # type: ignore - damage = opponent_party[opponent_current].deal_damage(save['party'][current], chosen_move) # type: ignore - if chosen_move["name"] == "struggle": - save['party'][current].deal_struggle_damage(damage) + # pallet town + elif save['location'] == 'pallet': + sp(f'Current Location: Pallet Town - "Shades of your journey await!"\n\n[w] - Go to Route 1\n[a] - Go to {save["name"]}\'s House\n[s] - Go to Sea-Route 21\n[d] - Go to OAK\'s LAB\n') + while option == '': + option = get() + if option == 'w': + if save['flag']['chosen_starter']: + save['location'] = 'route1-s' + encounter = get_encounter('route1-s', 'tall-grass') + battle([Pokemon(encounter['pokemon'], encounter['level'], {})]) else: - save["party"][current].moves[int(move_choice)-1]['pp'] -= 1 # type: ignore - - player_attacked_this_turn = True - - # choose switch - elif user_choice == '2': # type: ignore - switch_choice = switch_pokemon(party_length) - - if switch_choice == "exit": - continue - - if int(switch_choice)-1 == current: - continue - - current = int(switch_choice)-1 - switched = True - if int(switch_choice)-1 not in participating_pokemon: - participating_pokemon.append(current) - - # choose item - elif user_choice == '3': # type: ignore - item = use_item(battle=True) - if item == "exit": - continue - if (item == 'Poke Ball' or item == 'Great Ball' or item == 'Ultra Ball' or item == 'Master Ball') and battle_type == 'trainer': - sp("You can't catch another trainer's Pokémon!") - elif item == 'Poke Ball': - if opponent_party[opponent_current].catch("Poke Ball"): # type: ignore - caught = True - break - else: catch_attempt = True - elif item == 'Great Ball': - if opponent_party[opponent_current].catch("Great Ball"): # type: ignore - caught = True - break - else: catch_attempt = True - elif item == 'Ultra Ball': - if opponent_party[opponent_current].catch("Ultra Ball"): # type: ignore - caught = True - break - else: catch_attempt = True - elif item == 'Master Ball': - if opponent_party[opponent_current].catch("Master Ball"): # type: ignore - caught = True - break - else: catch_attempt = True - - # choose run - elif user_choice == '4': # type: ignore - if escape(save['party'][current], opponent_party[opponent_current], escape_attempts): # type: ignore - escaped_from_battle = True - break - else: - escape_attempts += 1 - - # reset consecutive escape attempts - if user_choice != '4': # type: ignore - escape_attempts = 0 - - # opponent attack - if not save['party'][current].check_fainted() and not opponent_party[opponent_current].check_fainted(): # type: ignore - save['party'][current].deal_damage(opponent_party[opponent_current], choice(opponent_party[opponent_current].moves)) # type: ignore - opponent_attacked_this_turn = True - - # player attack if player speed is lower - if save['party'][current].check_fainted() and opponent_party[opponent_current].check_fainted() and not player_attacked_this_turn and escape_attempts == 0 and not catch_attempt and not switched: # type: ignore - damage = opponent_party[opponent_current].deal_damage(save['party'][current], chosen_move) # type: ignore - if chosen_move["name"] == "struggle": # type: ignore - save['party'][current].deal_struggle_damage(damage) + sg('\nYou take a step into the tall grass north of Pallet Town.') + sg('...') + sg('Suddenly, you hear a voice shouting from behind you.') + sg('\nOAK: Hey! Wait! Don\'t go out!') + sg('\nProfessor OAK runs up to you from behind.') + sg('\nOAK: It\'s unsafe! Wild Pokémon live in tall grass! You need your own Pokémon for protection. Come with me!') + sg('\nProfessor OAK leads you to his laboratory. He walks up to a table with three Poké Balls on it.') + sg(f'\nOAK: Here, {save["name"]}! There are three Pokémon here, reserved for new trainers.') + while not save['flag']['chosen_starter']: + sp('Go ahead and choose one!\n\n[1] - Bulbasaur\n[2] - Charmander\n[3] - Squirtle\n') + option = '' + while not option and option not in ['1', '2', '3']: + option = get() + if option in ['1', '2', '3']: + sp(f'\nDo you want the `{["GRASS", "FIRE", "WATER"][int(option)-1]}`-type Pokémon, {["Bulbasaur", "Charmander", "Squirtle"][int(option)-1]}? (Y/N)\n') + confirm = '' + while confirm not in yn: + confirm = get() + if confirm in y: + save['flag']['chosen_starter'] = True + save['starter'] = ['BULBASAUR', 'CHARMANDER', 'SQUIRTLE'][int(option)-1] + save['dex'] = {save['starter']: {'seen': True, 'caught': True}} + save['flag']['type'] = {dex[save['starter']]['type']: {'seen': True, 'caught': True}} + for i in [('BULBASAUR', 'CHARMANDER'), ('CHARMANDER', 'SQUIRTLE'), ('SQUIRTLE', 'BULBASAUR')]: + if save['starter'] == i[0]: + save['rivalStarter'] = i[1] + save['party'].append(Pokemon(save['starter'], 5, { + 'hp': 31, + 'atk': 31, + 'def': 31, + 'spa': 31, + 'spd': 31, + 'spe': 31 + }, find_moves(save['starter'], 5) )) + else: + sp('') + sg(f'\nOAK: {save["starter"]} looks really energetic!') + sg('\nJust as you turn to leave, another young trainer enters the lab.') + sg(f'\nOAK: Ah, JOHNNY! Perfect timing! {save["name"]} here has just chosen a Pokémon! Why don\'t you choose one too?') + sg(f'\nJOHNNY walks up to the table and thinks for a few seconds, before picking up a Poké Ball containing {save["rivalStarter"].upper()}.') + sg(f'\nOAK: {save["starter"]} and {save["rivalStarter"]} are both brilliant choices!') + sg('\nJOHNNY nods to you, then turns to walk away. But, before he does, Professor OAK calls him back.') + sg(f'\nOAK: JOHNNY! Why don\'t you battle {save["name"]} before you go?') + sg('\nJOHNNY stops and looks at you over his shoulder, as if he doesn\'t understand.') + sg('\n...Suddenly, he gives a smile and tosses his Poké Ball into the air!') + battle([Pokemon(save['rivalStarter'], 5, {}, find_moves(save['rivalStarter'], 5))], battle_type='trainer', name='JOHNNY', start_diagloue='...', title='Pokémon Trainer', end_dialouge='...') + sg(f'\nOAK: A marvellous battle! Congratulations, {save["name"] if save["flag"]["won_first_battle"] else "JOHNNY"}!') + sg('Let me heal your Pokémon for you.') + heal(save) + sg('\nOAK: You can make your Pokémon stronger by training on Route 1.') + sg('\nJOHNNY tips his hat to you before taking his leave of the Lab.') + sg('You notice that he\'s heading North.') + save['location'] = 'oak-lab' + elif option == 'a': + save['location'] = 'playerhouse-down' + elif option == 's': + sg('\nThe water is a deep, clear blue.') + if save['hms']['surf']: + sp('\n...Would you like to use Surf? (Y/N)') + option = '' + while option not in yn: + option = input('\n> ') + if option in y: + save['location'] = 'seaRoute21' + else: + sg('\nYou decided not to use Surf.') + elif option == 'd': + if save['flag']['chosen_starter']: + save['location'] = 'oak-lab' + else: + sg('\n...') + sg('It appears to be locked.') + elif option == 'm': + menu_open = True else: - save["party"][current].moves[int(move_choice)-1]['pp'] -= 1 # type: ignore - player_attacked_this_turn = True - - # give XP when opponent faints - if opponent_party[opponent_current].check_fainted() and earn_xp == True: # type: ignore - opponent_party[opponent_current].give_xp(participating_pokemon, battle_type) # type: ignore - if battle_type == "trainer": - if is_alive(opponent_party): - opponent_current += 1 - sp(f'\n{name if name else title} sent out {opponent_party[opponent_current].name}!') # type: ignore - - # end battle if player wins or loses - if is_alive(save['party']) and not is_alive(opponent_party) or not is_alive(save['party']): - break + sp('\nInvalid answer!') - if save['party'][current].check_fainted(): - participating_pokemon = list(filter(lambda p, current=current: save['party'][p].name != save['party'][current].name, participating_pokemon)) - switch_choice = switch_pokemon(party_length) - current = int(switch_choice)-1 - switched = True - if int(switch_choice)-1 not in participating_pokemon: - participating_pokemon.append(current) - - # display turn details - debug(f'Higher Speed: {"Player" if save["party"][current].stats["spe"] > opponent_party[opponent_current].stats["spe"] else "Opponent"}\nPlayer Attacked: {player_attacked_this_turn}\nOpponent Attacked: {opponent_attacked_this_turn}\n') # type: ignore - - # upon escaping - if escaped_from_battle: - sp('You escaped!') - - # upon catching - elif caught: - # TODO: earn xp - pass # type: ignore - - # upon winning - elif is_alive(save['party']) and not is_alive(opponent_party): - if save['flag']['been_to_route_1']: - if battle_type == 'trainer': - sg(f'\n{save["name"]} won the battle!') - save['money'] += prize_money(opponent_party, title) # type: ignore - sg(f'You recieved ¥{prize_money(opponent_party, title)} as prize money.') # type: ignore - sg(f'\n{name if name else title}: {end_dialouge}') - else: - save['flag']['won_first_battle'] = True - - # upon losing - elif is_alive(opponent_party) and (not is_alive(save['party'])): - if battle_type == 'trainer': - if save['flag']['been_to_route_1']: - sg('You lost the battle!') - sg(f'You gave ¥{round(save["money"] / 2)} as prize money.') + # oak's lab + elif save['location'] == 'oak-lab': + sp('Current Location: OAK\'s Lab\n\n[a] - Go to Pallet Town\n[1] - Lab Assistant (Left)\n[2] - Lab Assistant (Right)\n[3] - Professor OAK\n[4] - OAK\'s Computer\n') + while option == '': + option = get() + if option == 'a': + save['location'] = 'pallet' + elif option == '1': + sg('\nASSISTANT: I study Pokémon as Professor OAK\'s aide.') + elif option == '2': + sg('\nASSISTANT: Professor OAK is an authority on Pokémon!') + sg('Many Pokémon trainers hold him in high regard!') + elif option == '3': + if 'Oak\'s Parcel' in save['bag']: + sg(f'\nOAK: Oh, {save["name"]}! How is my old Pokémon? Well, it seems to like you a lot.') + sg('You must be talented as a Pokémon trainer!') + sg('\n(You hold the parcel out to Professor OAK.)') + sg('\nOAK: What? You have something for me?') + sg(f'\n{save["name"]} delivered Oak\'s Parcel.') + save['flag']['delivered_package'] = True + save['bag'].pop('Oak\'s Parcel') + sg('OAK: Ah! This is the custom POKE BALL I ordered! Thank you!') + sg('\nJust at that moment, JOHNNY enters the building. OAK notices and calls him over.') + sg('\nOAK: JOHNNY! You\'re just in time!') + sg('I have a request of you two.') + sg('On the desk there is my invention, POKEDEX!') + sg('It automatically records data on Pokémon you\'ve seen or caught, like a hi-tech encyclopedia!') + sg(f'\n{save["name"]} and JOHNNY! Take these with you!') + sg(f'({save["name"]} obtained the POKEDEX!)') + save['bag']['Pokedex'] = 1 + sg('\nOAK: To make a complete guide on all the Pokémon in the world...') + sg('That was my dream! But, I\'m too old! I can\'t do it!') + sg('So, I want you two to fulfill my dream for me!') + sg('Get moving, you two! This is a great undertaking in Pokémon history!') + sg('\nJOHNNY nods and takes his leave.') + sg(f'\nOAK: Pokémon around the world wait for you, {save["name"]}!') + else: + sg('\nOAK: You\'ve caught a total of...') + sg(f'\n{sum(1 if save["dex"][i]["caught"] else 0 for i in save["dex"])} Pokémon!') + elif option == '4': + sg('\nThere\' an email message here:') + sg('"Calling all Pokémon trainers!\nThe elite trainers of Pokémon League are ready to take on all comers! Bring your best Pokémon and see how you rate as a trainer!\nPokémon LEAGUE HQ INDIGO PLATEAU\nPS: Professor OAK, please visit us!"') + elif option == 'm': + menu_open = True else: - save['flag']['won_first_battle'] = False - sg('...') - sg(f'{save["name"]} blacked out!') - save['money'] = round(save['money'] / 2) - save['location'] = save['recent_center'] - heal() - - # if battle is neither won nor lost - else: - abort('\nInvalid battle state; neither won, lost, caught, nor escaped. Could not load player turn.') - -# pokemon center heal -def heal(pokemon=None, party=None, type='party') -> None: - global save - if type == 'party': - party = party or save['party'] - elif type == 'single': - pokemon = pokemon or save['party'][0] - party = [pokemon] - else: - abort('Invalid heal type: neither party nor single.') - sp('') - for i in party: # type: ignore - i.reset_stats() - sp(f'{i.name} was healed to max health.') - - if type == 'party': - backup(pokemon_centre=True) - -def get_encounter(loc, type) -> dict: - pokemon = [] - weights = [] - for chance in rates[loc][type]: # type: ignore - for i in range(len(rates[loc][type][chance])): # type: ignore - pokemon.append(rates[loc][type][chance][i]) # type: ignore - weights.append(int(chance)/255) - return choices(pokemon, weights)[0] - -def display_pokemart(loc) -> None: # sourcery skip: low-code-quality - choice = '' - action_choice = '' - pokemart_exit = False - while not pokemart_exit: - while not action_choice: - while not action_choice: - sp("\n[b] - Buy\n[s] - Sell\n[e] - Back\n") - action_choice = get() - if action_choice not in ['b', 's', 'e']: - action_choice = '' - if action_choice == 'e': - pokemart_exit = True - elif action_choice == 's': - sp(f'\nMoney: ¥{"{:,}".format(save["money"])}') - while not choice: - options = ['e'] - max_name_length = (len(max(pokemart[loc], key=len))) # type: ignore - for i, item in enumerate(pokemart[loc], start=1): # type: ignore - options.append(str(i)) - price_len = len("{:,}".format(items[item]["price"])) # type: ignore - sp(f'[{i}] - {item}{" "*(max_name_length-len(item))}{" "*(8-price_len)}¥{"{:,}".format(items[item]["sell_price"])}') # type: ignore - sp(f'[e] - Back\n') - while not choice: - choice = get() - if choice not in options: - choice = '' - if choice == "e": - action_choice = '' - choice = '' + sp('\nInvalid answer!') + + # route 1 - south + elif save['location'] == 'route1-s': + if not save['flag']['been_to_route_1']: save['flag']['been_to_route_1'] = True + sp('Current Location: Route 1 (South)\n\n[w] - Go to Route 1 (North)\n[s] - Go to Pallet Town\n') + trainer_options = display_trainers(save['location']) + while option == '': + option = get() + if option == 'w': + save['location'] = 'route1-n' + encounter = get_encounter('route1-n', 'tall-grass') + battle([Pokemon(encounter['pokemon'], encounter['level'], {})]) + elif option == 's': + save['location'] = 'pallet' + elif option == 'm': + menu_open = True + elif option in trainer_options: + trainer_interaction(save['location'], option) else: - amount = 0 - try: - in_bag = save['bag'][pokemart[loc][int(choice)-1]] # type: ignore - except KeyError: - in_bag = 0 - sp(f'\n{pokemart[loc][int(choice)-1]}: ¥{"{:,}".format(items[pokemart[loc][int(choice)-1]]["sell_price"])} (in bag: {in_bag})') # type: ignore - sp(items[pokemart[loc][int(choice)-1]]["description"]) # type: ignore - sp("How many would you like to sell(1-99)? (press 'e' to go back)\n") - while not amount: - while not amount: - amount = get() - if amount == 'e': - break - if (not amount.isnumeric()) or int(amount) > 99 or int(amount) < 1: - amount = '' - if amount == 'e': - choice = '' - amount = '' - elif in_bag < int(amount): - sp(f'\nYou do not have enough items (you need {int(amount)-in_bag} more)') - amount = '' - else: - save['bag'][pokemart[loc][int(choice)-1]] -= int(amount) # type: ignore - save['money'] += items[pokemart[loc][int(choice)-1]]["sell_price"]*int(amount) # type: ignore - debug(f'Sold {amount} {pokemart[loc][int(choice)-1]}s for ¥{items[pokemart[loc][int(choice)-1]]["sell_price"]*int(amount)}') # type: ignore - choice = '' + sp('\nInvalid answer!') - elif action_choice == 'b': - sp(f'\nMoney: ¥{"{:,}".format(save["money"])}') - while not choice: - options = ['e'] - max_name_length = (len(max(pokemart[loc], key=len))) # type: ignore - for i, item in enumerate(pokemart[loc], start=1): # type: ignore - options.append(str(i)) - price_len = len("{:,}".format(items[item]["price"])) # type: ignore - sp(f'[{i}] - {item}{" "*(max_name_length-len(item))}{" "*(8-price_len)}¥{"{:,}".format(items[item]["price"])}') # type: ignore - sp(f'[e] - Back\n') - while not choice: - choice = get() - if choice not in options: - choice = '' - if choice == "e": - action_choice = '' - choice = '' + # route 1 - north + elif save['location'] == 'route1-n': + sp('Current Location: Route 1 (North)\n\n[w] - Go to Viridian City\n[s] - Go to Route 1 (South)\n') + trainer_options = display_trainers(save['location']) + while option == '': + option = get() + if option == 'w': + save['location'] = 'viridian' + elif option == 's': + save['location'] = 'route1-s' + encounter = get_encounter('route1-s', 'tall-grass') + battle([Pokemon(encounter['pokemon'], encounter['level'], {})]) + elif option == 'm': + menu_open = True + elif option in trainer_options: + trainer_interaction(save['location'], option) else: - amount = 0 - try: - in_bag = save['bag'][pokemart[loc][int(choice)-1]] # type: ignore - except KeyError: - in_bag = 0 + sp('\nInvalid answer!') - sp(f'\n{pokemart[loc][int(choice)-1]}: ¥{"{:,}".format(items[pokemart[loc][int(choice)-1]]["price"])} (in bag: {in_bag})') # type: ignore - sp(items[pokemart[loc][int(choice)-1]]["description"]) # type: ignore - sp("How many would you like to buy(1-99)? (press 'e' to go back)\n") - while not amount: - while not amount: - amount = get() - if amount == 'e': - break - if (not amount.isnumeric()) or int(amount) > 99 or int(amount) < 1: - amount = '' - if amount == 'e': - choice = '' - amount = '' + # viridian city + elif save['location'] == 'viridian': + sp('Current Location: Viridian City\n\n[w] - Go to Route 2 (South)\n[a] - Go to Route 22 (East)\n[s] - Go to Route 1 (North)\n[1] - Viridian Pokémon Centre\n[2] - Viridian Pokémart\n[3] - Viridian Pokemon Gym\n') + while option == '': + option = get() + if option == 'w': + if save['flag']['delivered_package']: + save['location'] = 'route2-s' + encounter = get_encounter('route2-s', 'tall-grass') + battle([Pokemon(encounter['pokemon'], encounter['level'], {})]) else: - required_money = items[pokemart[loc][int(choice)-1]]["price"]*int(amount) # type: ignore - if required_money > save['money']: - sp(f'\nYou do not have enough money (you need ¥{required_money-save["money"]} more)') - amount = '' - else: - if pokemart[loc][int(choice)-1] not in save['bag']: # type: ignore - save['bag'][pokemart[loc][int(choice)-1]] = int(amount) # type: ignore - else: - save['bag'][pokemart[loc][int(choice)-1]] += int(amount) # type: ignore - save['money'] -= required_money - sp(f'\n{save["name"]} obtained {amount} {pokemart[loc][int(choice)-1]}(s)') # type: ignore - choice = '' - -def display_trainers(loc) -> list: - if loc not in trainers: return [] # type: ignore - - possible_trainers = [] - for trainer in trainers[loc]: # type: ignore - if not (trainer in save['flag']['characters_spoken'] and trainer['leave_after_speaking']): # type: ignore - possible_trainers.append(trainer) - - if possible_trainers == []: return [] - - valid_options = [i+1 for i in range(len(possible_trainers))] # type: ignore - for i in valid_options: - trainer = possible_trainers[i-1] # type: ignore - if trainer['type'] == 'trainer': - sp(f'[{i}] - Speak to {trainer["trainer_class"]}') # TODO: change to name - elif trainer['type'] == 'character' and not (trainer in save['flag']['characters_spoken'] and trainer['leave_after_speaking']): # type: ignore - sp(f'[{i}] - Speak to {trainer["name"]}') - sp("") - return [str(i) for i in valid_options] - -def trainer_interaction(loc, option) -> None: - possible_trainers = [] - for trainer in trainers[loc]: # type: ignore - if not (trainer in save['flag']['characters_spoken'] and trainer['leave_after_speaking']): # type: ignore - possible_trainers.append(trainer) - - trainer = possible_trainers[int(option)-1] - - if trainer['type'] == "trainer": - if trainer in save['flag']['trainer_fought']: - sg(f'\n{trainer["trainer_class"]}: {trainer["after_battling_dialouge"]}') # TODO: change to trainer["name"] instead of trainer["trainer_class"] - return - - battle( - opponent_party=[Pokemon(pokemon['species'], pokemon['level'], ivs={ 'atk': 9, 'hp': 8, 'def': 8, 'spa': 8, 'spd': 8, 'spe': 8 }) for pokemon in trainer['pokemon']], - battle_type="trainer", title=trainer['trainer_class'], start_diagloue=trainer['before_dialouge'], end_dialouge=trainer['win_dialouge'] - ) # TODO: add names to battle - - save['flag']['trainer_fought'].append(trainer) - - elif trainer['type'] == 'character': - - if trainer in save['flag']['characters_spoken']: - sp("") - for line in trainer['after_text']: - if line.startswith('`') and line.endswith('`'): - item = line[1:-1].split(':')[0] - amount = int(line[1:-1].split(':')[1]) - if item in save['bag']: - save['bag'][item] += amount - else: - save['bag'][item] = amount - sg(f'{save["name"]} recieved {amount} {item}(s)') - continue + sg('\nAn old man is blocking the way, accompanied by an apologetic young lady.') + sg('\nMAN: Hey you, get off my property!') + sg('\nGIRL: Oh, grandpa! Don\'t be so mean!') + sg('\nIt looks like you won\'t be able to pass until later.') + elif option == 'a': + sg('\nComing soon!') + elif option == 's': + save['location'] = 'route1-n' + encounter = get_encounter('route1-n', 'tall-grass') + battle([Pokemon(encounter['pokemon'], encounter['level'], {})]) + elif option == '1': + heal(save) + save['recent_center'] = 'viridian' + elif option == '2': + if save['flag']['delivered_package']: + display_pokemart('viridian') + elif 'Oak\'s Parcel' in save['bag']: + sg('\nCLERK: Please deliver Oak\'s Parcel!') else: - sg(line) - - else: - sp("") - for line in trainer['text']: - if line.startswith('`') and line.endswith('`'): - item = line[1:-1].split(':')[0] - amount = int(line[1:-1].split(':')[1]) - if item in save['bag']: - save['bag'][item] += amount - else: - save['bag'][item] = amount - sg(f'{save["name"]} recieved {amount} {item}(s)') - continue + sg('\nCLERK: Hey! You came from PALLET TOWN? You know Professor OAK, right?') + sg('His order came in. Will you take it to him?') + save['bag']['Oak\'s Parcel'] = 1 + sg(f'\n({save["name"]} recieved Oak\'s Parcel!)\n') + sg('\nCLERK: Okay! Say hi to the Professor for me!') + elif option == '3': + if save['badges']['Boulder'] and save['badges']['Cascade'] and save['badges']['Volcano'] and save['badges']['Marsh'] and save['badges']['Rainbow'] and save['badges']['Soul'] and save['badges']['Thunder']: + save['location'] = 'viridian-gym' else: - sg(line) - - save['flag']['characters_spoken'].append(trainer) - -# display title screen -cls() # type: ignore -title = ['''\n ,'\\\n _.----. ____ ,' _\ ___ ___ ____\n_,-' `. | | /`. \,-' | \ / | | \ |`.\n\ __ \ '-. | / `. ___ | \/ | '-. \ | |\n \. \ \ | __ | |/ ,','_ `. | | __ | \| |\n \ \/ /,' _`.| ,' / / / / | ,' _`.| | |\n \ ,-'/ / \ ,' | \/ / ,`.| / / \ | |\n \ \ | \_/ | `-. \ `' /| | || \_/ | |\ |\n \ \ \ / `-.`.___,-' | |\ /| \ / | | |\n \ \ `.__,'| |`-._ `| |__| \/ | `.__,'| | | |\n \_.-' |__| `-._ | '-.| '-.| | |\n `' '-._|\n''', ' PythonRed Version\n', ' Press any key to begin!'] # type: ignore -title.append(f'{title[0]}\n{title[1]}\n{title[2]}\n\n') -sleep(1) -print(title[0]) -sleep(2.65) -print(title[1]) -sleep(1.85) -print(title[2]) -getch() # type: ignore -cls() # type: ignore -print(f'{title[3]}Please choose an option.\n\n[1] - Continue Game\n[2] - New Game\n[3] - GitHub Repository\n') -start_option = '' -while start_option != '2': - start_option = get() - cls() # type: ignore - - # continue from save file - if start_option == '1': - try: - has_saved = loads(open(path.join(syspath[0], '.ppr-save')).read())['flag']['has_saved'] - if path.isfile(path.join(syspath[0], '.ppr-save')) and has_saved: - cls() # type: ignore - print(f'{title[3]}Loading save file!\n\n[1] - Continue Game\n[2] - New Game\n[3] - GitHub Repository\n\n> 1\n') - break - except KeyError: - print(f'{title[3]}Your save file is outdated and the game cannot load it. Please back up your save file and contact us with option [3].\n\n[1] - Continue Game\n[2] - New Game\n[3] - GitHub Repository\n') - except ValueError: - print(f'{title[3]}Your save file is empty and cannot be loaded!\n\n[1] - Continue Game\n[2] - New Game\n[3] - GitHub Repository\n') - else: - print(f'{title[3]}No previous save file found!\n\n[1] - Continue Game\n[2] - New Game\n[3] - GitHub Repository\n') - - # new game - elif start_option == '2': - cls() # type: ignore - print(f'{title[3]}Starting game!\n\n[1] - Continue Game\n[2] - New Game\n[3] - GitHub Repository\n') - - # open github link - elif start_option == '3': - try: - webopen(link['repository'], new=2, autoraise=True) - except Exception: - print(f'{title[3]}Failed to open website, here\'s the link:\n[{link["repository"]}]\n') - else: - print(f'{title[3]}Repository page opened successfully!') - finally: - print('\n[1] - Continue Game\n[2] - New Game\n[3] - GitHub Repository\n') - - # handle invalid input - else: - print(f'{title[3]}Invalid input!\n\n[1] - Continue Game\n[2] - New Game\n[3] - GitHub Repository\n') - -# load data from files -for i in [ - ['dex', 'dex.json'], - ['items', 'item.json'], - ['moves', 'moves.json'], - ['rates', 'map.json'], - ['save_template', 'save_template.json'], - ['trainer_types', 'trainer_types.json'], - ['types', 'types.json'], - ['xp', 'level.json'], - ['pokemart', 'pokemart.json'], - ['trainers', 'trainers.json'] -]: - try: - exec(f'{i[0]} = loads(open(path.join(syspath[0], "data", "{i[1]}"), encoding="utf8").read())\nopen(path.join(syspath[0], "data", "{i[1]}")).close()') - except Exception: - abort(f'Failed to load {i[1]}!') - -# debug statements -def debug(text) -> None: - if is_debug: - sp(f'{colours["GROUND"]}Debug: {text}{colours["RESET"]}') - -# load save file -if start_option == '1': - save_temp = {**save_template, **loads(open(path.join(syspath[0], '.ppr-save'), 'r').read())} # type: ignore - open(path.join(syspath[0], '.ppr-save')).close() - save = save_temp - for pokemon_location in ['party', 'box']: - save[pokemon_location] = [Pokemon(species=i['species'], level=i['level'], ivs=i['ivs'], moves=i['moves'], chp=i['stats']['chp'], current_xp=i['current_xp'], fainted=i['fainted'], player_pokemon=True) for i in save_temp[pokemon_location]] -else: - save = save_template # type: ignore - save['badges'] = {i: False for i in badges} - save['options']['text_speed'] = 'normal' -if getuser() not in save['user']: - save['user'].append(getuser()) -is_debug = save['options']['debug'] - -# test party status (debug) -if start_option == '1': - for i in range(len(save['party'])): - debug(f'{save["party"][i].name} is type {type(save["party"][i])}') - -# check for illegal save data -try: - if max([ - len(save['name']) > 15, - len(save['party']) > 6, - save['flag']['been_to_route_1'] and len(save['party']) == 0, - save['flag']['chosen_starter'] and not save['flag']['intro_complete'], - save['flag']['chosen_starter'] and save['location'] == '', - save['name'] != '' and not save['flag']['intro_complete'], - save['name'] != save['name'].upper(), - save['name'] == '' and save['flag']['intro_complete'] - ]): - abort('Illegal or outdated save data detected!') -except KeyError: - abort('Illegal or outdated save data detected!') - -# reset getch according to options -reset_sp(text[save['options']['text_speed']]) - -# main loop -while not exit: - option = dex_string = '' - - # intro - if save['flag']['intro_complete'] == False: - sp('(Intro Start!)\n') - sg('OAK: Hello there! Welcome to the world of Pokémon!') - sg('My name is OAK! People call me the Pokémon Professor!') - sg('This world is inhabited by creatures called Pokémon!') - sg('For some people, Pokémon are pets. Others use them for fights. Myself...') - sg('I study Pokémon as a profession.') - sp('\nFirst, what is your name?\n\n[1] - PYTHON\n[2] - New Name\n') - introAnswer = '' - while introAnswer not in ['1', '2']: - introAnswer = get() - if introAnswer == '1': - playerName = 'PYTHON' - elif introAnswer == '2': - sp('\n(Caps, 15 chars. max)\n') - playerName = get() - while len(playerName) > 15 or playerName == '': - playerName = get() - else: - sp('\nInvalid answer!') - playerName = playerName.upper() # type: ignore - sg(f'\nRight! So your name is {playerName}!') - sg('\nNow, since you\'re so raring to go, I\'ve prepared a rival for you.') - sg('He will go on an adventure just like yours, and battle you along the way.') - sp('\n...Erm, what is his name again?\n') - get() - sg('\n...') - sg('Hoho, just kidding! His name is JOHNNY! You\'ll meet him soon!\n') - sg(f'{playerName}! Your very own Pokémon legend is about to unfold! A world of dreams and adventures with Pokémon awaits! Let\'s go!') - save['name'] = playerName - save['location'] = 'playerHouseUp' - save['flag']['intro_complete'] = True - sp('\n(Intro Complete!)') - - # options menu - elif options_open == True: - sp('Options Menu\n[1] - Text Speed\n[2] - EMPTY\n[3] - EMPTY\n[4] - Back\n') - while not option: - option = get() - if option == '1': - option = '' - while option != '5': - sp('\nText Speed\n[1] - Slow\n[2] - Normal\n[3] - Fast\n[4] - Ultra\n[5] - Back\n') - option = '' - while (not option) and option not in ['1', '2', '3', '4', '5']: - option = get() - if option != '5': - sp('') - if option == '1': - save['options']['text_speed'] = 'slow' - sp('Text Speed set to Slow!') - elif option == '2': - save['options']['text_speed'] = 'normal' - sp('Text Speed set to Normal!') - elif option == '3': - save['options']['text_speed'] = 'fast' - sp('Text Speed set to Fast!') - elif option == '4': - save['options']['text_speed'] = 'ultra' - sp('Text Speed set to Ultra!') - reset_sp(text[save['options']['text_speed']]) - elif option in ['2', '3']: - sp('Coming Soon!') - elif option == '4': - options_open = False - else: - sp('\nInvalid answer!') - - # pause menu - elif menu_open == True: - menu = f'Menu\n[d] - Pokédex\n[p] - Pokémon\n[i] - Item\n[t] - {save["name"]}\n[s] - Save Game\n[o] - Options\n[e] - Exit Menu\n[q] - Quit Game\n' if 'Pokedex' in save['bag'] else f'Menu\n[p] - Pokémon\n[i] - Item\n[t] - {save["name"]}\n[s] - Save Game\n[o] - Options\n[e] - Exit Menu\n[q] - Quit Game\n' - sp(menu) - while not option: - option = get() - if option not in ['e', 'o']: - sp('') - if option == 'd' and 'Pokedex' in save['bag']: - option = '' - dex_string = '' - for i in dex.keys(): # type: ignore - if i in save['dex'].keys(): - dex_string += f'\n{dex[i]["index"]} - {i}: Seen' if save['dex'][i]['seen'] else '' # type: ignore - if save['dex'][i]['caught']: - dex_string += ', Caught' - sp(f'{save["name"]}\'s Pokédex{dex_string}' if dex_string else '\nYou have no Pokémon in your Pokédex!') - elif option == 'p': - if save['party']: - sp('\n'.join(f'{i.name} (`{i.type}`-type)\nLevel {i.level} ({i.current_xp}/{str(xp["next"][i.level_type][str(i.level)])} XP to next level)\n{i.stats["chp"]}/{i.stats["hp"]} HP' for i in save['party'])) # type: ignore - else: - sp('Your party is empty!') - elif option == 'i': - if save['bag']: - for i in save['bag']: - sp(f'{i}: {save["bag"][i]}') - else: - sp('Your bag is empty!') - elif option == 't': - sp(f'Name: {save["name"]}') - sp(f'Money: ¥{"{:,}".format(save["money"])}') - sp(f'''Badges: {''.join(f"[{'x' if save['badges'][i] else ' '}]" for i in badges)}''') - elif option == 's': - backup() - elif option == 'o': - options_open = True - elif option == 'e': - menu_open = False - elif option == 'q': - sp('Are you sure you want to quit? Any unsaved progress will be lost. (Y/N)\n') - option = '' - while option not in yn: + sg("\nThe gym is closed") + sg('\nYou won\'t be able to enter until later.') + elif option == 'm': + menu_open = True + + elif save['location'] == 'route2-s': + sp('Current Location: Route 2 (South)\n\n[w] - Go to Viridian Forest (South)\n[s] - Go to Viridian City\n[d] - Go to Route 2 (North)\n') + trainer_options = display_trainers(save['location']) + while option == '': option = get() - if option in y: - exit = True - else: - sp('\nInvalid answer!') - if option not in ['e', 'i', 'o', 'p', 's']: - sp('') - - # player house - upstairs - elif save['location'] == 'playerHouseUp': - sp(f'Current Location: {save["name"]}\'s Room (Upstairs)\n\n[s] - Go Downstairs\n[1] - Computer\n[2] - Notebook\n') - while option == '': - option = get() - if option == '1': - sg('\n...') - sg('Looks like you can\'t use it yet.') - elif option == '2': - sg('\nThe notebook is open to a page that says:\n\n"Use the [m] command in the overworld to open the menu.\nFrom the menu, you can save your progress, check your Pokémon, and more!"') - elif option == 's': - save['location'] = 'playerHouseDown' - elif option == 'm': - menu_open = True - else: - sp('\nInvalid answer!') - - # player house - downstairs - elif save['location'] == 'playerHouseDown': - sp(f'Current Location: {save["name"]}\'s House (Downstairs)\n\n[w] - Go Upstairs\n[d] - Go Outside\n') - while option == '': - option = get() - if option == 'd': - save['location'] = 'pallet' - elif option == 'w': - save['location'] = 'playerHouseUp' - elif option == 'm': - menu_open = True - else: - sp('\nInvalid answer!') - - # pallet town - elif save['location'] == 'pallet': - sp(f'Current Location: Pallet Town - "Shades of your journey await!"\n\n[w] - Go to Route 1\n[a] - Go to {save["name"]}\'s House\n[s] - Go to Sea-Route 21\n[d] - Go to OAK\'s LAB\n') - while option == '': - option = get() - if option == 'w': - if save['flag']['chosen_starter']: - save['location'] = 'route1-s' - encounter = get_encounter('route1-s', 'tall-grass') - battle([Pokemon(encounter['pokemon'], encounter['level'], 'random')]) - else: - sg('\nYou take a step into the tall grass north of Pallet Town.') - sg('...') - sg('Suddenly, you hear a voice shouting from behind you.') - sg('\nOAK: Hey! Wait! Don\'t go out!') - sg('\nProfessor OAK runs up to you from behind.') - sg('\nOAK: It\'s unsafe! Wild Pokémon live in tall grass! You need your own Pokémon for protection. Come with me!') - sg('\nProfessor OAK leads you to his laboratory. He walks up to a table with three Poké Balls on it.') - sg(f'\nOAK: Here, {save["name"]}! There are three Pokémon here, reserved for new trainers.') - while not save['flag']['chosen_starter']: - sp('Go ahead and choose one!\n\n[1] - Bulbasaur\n[2] - Charmander\n[3] - Squirtle\n') - option = '' - while not option and option not in ['1', '2', '3']: - option = get() - if option in ['1', '2', '3']: - sp(f'\nDo you want the `{["GRASS", "FIRE", "WATER"][int(option)-1]}`-type Pokémon, {["Bulbasaur", "Charmander", "Squirtle"][int(option)-1]}? (Y/N)\n') - confirm = '' - while confirm not in yn: - confirm = get() - if confirm in y: - save['flag']['chosen_starter'] = True - save['starter'] = ['BULBASAUR', 'CHARMANDER', 'SQUIRTLE'][int(option)-1] - save['dex'] = {save['starter']: {'seen': True, 'caught': True}} - save['flag']['type'] = {dex[save['starter']]['type']: {'seen': True, 'caught': True}} # type: ignore - for i in [('BULBASAUR', 'CHARMANDER'), ('CHARMANDER', 'SQUIRTLE'), ('SQUIRTLE', 'BULBASAUR')]: - if save['starter'] == i[0]: - save['rivalStarter'] = i[1] - save['party'].append(Pokemon(save['starter'], 5, { - 'hp': 31, - 'atk': 31, - 'def': 31, - 'spa': 31, - 'spd': 31, - 'spe': 31 - }, find_moves(save['starter'], 5) )) - else: - sp('') - sg(f'\nOAK: {save["starter"]} looks really energetic!') - sg('\nJust as you turn to leave, another young trainer enters the lab.') - sg(f'\nOAK: Ah, JOHNNY! Perfect timing! {save["name"]} here has just chosen a Pokémon! Why don\'t you choose one too?') - sg(f'\nJOHNNY walks up to the table and thinks for a few seconds, before picking up a Poké Ball containing {save["rivalStarter"].upper()}.') - sg(f'\nOAK: {save["starter"]} and {save["rivalStarter"]} are both brilliant choices!') - sg('\nJOHNNY nods to you, then turns to walk away. But, before he does, Professor OAK calls him back.') - sg(f'\nOAK: JOHNNY! Why don\'t you battle {save["name"]} before you go?') - sg('\nJOHNNY stops and looks at you over his shoulder, as if he doesn\'t understand.') - sg('\n...Suddenly, he gives a smile and tosses his Poké Ball into the air!') - battle([Pokemon(save['rivalStarter'], 5, 'random', find_moves(save['rivalStarter'], 5))], battle_type='trainer', name='JOHNNY', start_diagloue='...', title='Pokémon Trainer', end_dialouge='...') - sg(f'\nOAK: A marvellous battle! Congratulations, {save["name"] if save["flag"]["won_first_battle"] else "JOHNNY"}!') - sg('Let me heal your Pokémon for you.') - heal() - sg('\nOAK: You can make your Pokémon stronger by training on Route 1.') - sg('\nJOHNNY tips his hat to you before taking his leave of the Lab.') - sg('You notice that he\'s heading North.') - save['location'] = 'oakLab' - elif option == 'a': - save['location'] = 'playerHouseDown' - elif option == 's': - sg('\nThe water is a deep, clear blue.') - if save['hms']['surf']: - sp('\n...Would you like to use Surf? (Y/N)') - option = '' - while option not in yn: - option = input('\n> ') - if option in y: - save['location'] = 'seaRoute21' + if option == 'w': + save['location'] = 'viridian-forest-s' + encounter = get_encounter('viridian-forest-s', 'tall-grass') + battle([Pokemon(encounter['pokemon'], encounter['level'], {})]) + elif option == 's': + save['location'] = 'viridian' + elif option == 'd': + if save['hms']['cut']: + save['location'] = 'route2-w' else: - sg('\nYou decided not to use Surf.') - elif option == 'd': - if save['flag']['chosen_starter']: - save['location'] = 'oakLab' - else: - sg('\n...') - sg('It appears to be locked.') - elif option == 'm': - menu_open = True - else: - sp('\nInvalid answer!') - - # oak's lab - elif save['location'] == 'oakLab': - sp('Current Location: OAK\'s Lab\n\n[a] - Go to Pallet Town\n[1] - Lab Assistant (Left)\n[2] - Lab Assistant (Right)\n[3] - Professor OAK\n[4] - OAK\'s Computer\n') - while option == '': - option = get() - if option == 'a': - save['location'] = 'pallet' - elif option == '1': - sg('\nASSISTANT: I study Pokémon as Professor OAK\'s aide.') - elif option == '2': - sg('\nASSISTANT: Professor OAK is an authority on Pokémon!') - sg('Many Pokémon trainers hold him in high regard!') - elif option == '3': - if 'Oak\'s Parcel' in save['bag']: - sg(f'\nOAK: Oh, {save["name"]}! How is my old Pokémon? Well, it seems to like you a lot.') - sg('You must be talented as a Pokémon trainer!') - sg('\n(You hold the parcel out to Professor OAK.)') - sg('\nOAK: What? You have something for me?') - sg(f'\n{save["name"]} delivered Oak\'s Parcel.') - save['flag']['delivered_package'] = True - save['bag'].pop('Oak\'s Parcel') - sg('OAK: Ah! This is the custom POKE BALL I ordered! Thank you!') - sg('\nJust at that moment, JOHNNY enters the building. OAK notices and calls him over.') - sg('\nOAK: JOHNNY! You\'re just in time!') - sg('I have a request of you two.') - sg('On the desk there is my invention, POKEDEX!') - sg('It automatically records data on Pokémon you\'ve seen or caught, like a hi-tech encyclopedia!') - sg(f'\n{save["name"]} and JOHNNY! Take these with you!') - sg(f'({save["name"]} obtained the POKEDEX!)') - save['bag']['Pokedex'] = 1 - sg('\nOAK: To make a complete guide on all the Pokémon in the world...') - sg('That was my dream! But, I\'m too old! I can\'t do it!') - sg('So, I want you two to fulfill my dream for me!') - sg('Get moving, you two! This is a great undertaking in Pokémon history!') - sg('\nJOHNNY nods and takes his leave.') - sg(f'\nOAK: Pokémon around the world wait for you, {save["name"]}!') - else: - sg('\nOAK: You\'ve caught a total of...') - sg(f'\n{sum(1 if save["dex"][i]["caught"] else 0 for i in save["dex"])} Pokémon!') - elif option == '4': - sg('\nThere\' an email message here:') - sg('"Calling all Pokémon trainers!\nThe elite trainers of Pokémon League are ready to take on all comers! Bring your best Pokémon and see how you rate as a trainer!\nPokémon LEAGUE HQ INDIGO PLATEAU\nPS: Professor OAK, please visit us!"') - elif option == 'm': - menu_open = True - else: - sp('\nInvalid answer!') - - # route 1 - south - elif save['location'] == 'route1-s': - if not save['flag']['been_to_route_1']: save['flag']['been_to_route_1'] = True - sp('Current Location: Route 1 (South)\n\n[w] - Go to Route 1 (North)\n[s] - Go to Pallet Town\n') - trainer_options = display_trainers(save['location']) - while option == '': - option = get() - if option == 'w': - save['location'] = 'route1-n' - encounter = get_encounter('route1-n', 'tall-grass') - battle([Pokemon(encounter['pokemon'], encounter['level'], 'random')]) - elif option == 's': - save['location'] = 'pallet' - elif option == 'm': - menu_open = True - elif option in trainer_options: - trainer_interaction(save['location'], option) - else: - sp('\nInvalid answer!') - - # route 1 - north - elif save['location'] == 'route1-n': - sp('Current Location: Route 1 (North)\n\n[w] - Go to Viridian City\n[s] - Go to Route 1 (South)\n') - trainer_options = display_trainers(save['location']) - while option == '': - option = get() - if option == 'w': - save['location'] = 'viridian' - elif option == 's': - save['location'] = 'route1-s' - encounter = get_encounter('route1-s', 'tall-grass') - battle([Pokemon(encounter['pokemon'], encounter['level'], 'random')]) - elif option == 'm': - menu_open = True - elif option in trainer_options: - trainer_interaction(save['location'], option) - else: - sp('\nInvalid answer!') - - # viridian city - elif save['location'] == 'viridian': - sp('Current Location: Viridian City\n\n[w] - Go to Route 2 (South)\n[a] - Go to Route 22 (East)\n[s] - Go to Route 1 (North)\n[1] - Viridian Pokémon Centre\n[2] - Viridian Pokémart\n[3] - Viridian Pokemon Gym\n') - while option == '': - option = get() - if option == 'w': - if save['flag']['delivered_package']: + sg('\nThere is a tree in the way') + sg('\nMaybe a Pokémon could cut it down?') + elif option in trainer_options: + trainer_interaction(save['location'], option) + elif option == 'm': + menu_open = True + + elif save['location'] == 'viridian-forest-s': + sp('Current Location: Viridian Forest (South)\n\n[a] - Go to Viridian Forest (West)\n[s] - Go to Route 2 (South)\n[d] - Go to Viridian Forest (East)\n') + trainer_options = display_trainers(save['location']) + while option == '': + option = get() + if option == 's': save['location'] = 'route2-s' encounter = get_encounter('route2-s', 'tall-grass') - battle([Pokemon(encounter['pokemon'], encounter['level'], 'random')]) - else: - sg('\nAn old man is blocking the way, accompanied by an apologetic young lady.') - sg('\nMAN: Hey you, get off my property!') - sg('\nGIRL: Oh, grandpa! Don\'t be so mean!') - sg('\nIt looks like you won\'t be able to pass until later.') - elif option == 'a': - sg('\nComing soon!') - elif option == 's': - save['location'] = 'route1-n' - encounter = get_encounter('route1-n', 'tall-grass') - battle([Pokemon(encounter['pokemon'], encounter['level'], 'random')]) - elif option == '1': - heal() - save['recent_center'] = 'viridian' - elif option == '2': - if save['flag']['delivered_package']: - display_pokemart('viridian') - elif 'Oak\'s Parcel' in save['bag']: - sg('\nCLERK: Please deliver Oak\'s Parcel!') - else: - sg('\nCLERK: Hey! You came from PALLET TOWN? You know Professor OAK, right?') - sg('His order came in. Will you take it to him?') - save['bag']['Oak\'s Parcel'] = 1 - sg(f'\n({save["name"]} recieved Oak\'s Parcel!)\n') - sg('\nCLERK: Okay! Say hi to the Professor for me!') - elif option == '3': - if save['badges']['Boulder'] and save['badges']['Cascade'] and save['badges']['Volcano'] and save['badges']['Marsh'] and save['badges']['Rainbow'] and save['badges']['Soul'] and save['badges']['Thunder']: - save['location'] = 'viridian-gym' - else: - sg("\nThe gym is closed") - sg('\nYou won\'t be able to enter until later.') - elif option == 'm': - menu_open = True - - elif save['location'] == 'route2-s': - sp('Current Location: Route 2 (South)\n\n[w] - Go to Viridian Forest (South)\n[s] - Go to Viridian City\n[d] - Go to Route 2 (North)\n') - trainer_options = display_trainers(save['location']) - while option == '': - option = get() - if option == 'w': - save['location'] = 'viridian-forest-s' - encounter = get_encounter('viridian-forest-s', 'tall-grass') - battle([Pokemon(encounter['pokemon'], encounter['level'], 'random')]) - elif option == 's': - save['location'] = 'viridian' - elif option == 'd': - if save['hms']['cut']: - save['location'] = 'route2-w' - else: - sg('\nThere is a tree in the way') - sg('\nMaybe a Pokémon could cut it down?') - elif option in trainer_options: - trainer_interaction(save['location'], option) - elif option == 'm': - menu_open = True - - elif save['location'] == 'viridian-forest-s': - sp('Current Location: Viridian Forest (South)\n\n[a] - Go to Viridian Forest (West)\n[s] - Go to Route 2 (South)\n[d] - Go to Viridian Forest (East)\n') - trainer_options = display_trainers(save['location']) - while option == '': - option = get() - if option == 's': - save['location'] = 'route2-s' - encounter = get_encounter('route2-s', 'tall-grass') - battle([Pokemon(encounter['pokemon'], encounter['level'], 'random')]) - elif option == 'a': - save['location'] = 'viridian-forest-w' - encounter = get_encounter('viridian-forest-w', 'tall-grass') - battle([Pokemon(encounter['pokemon'], encounter['level'], 'random')]) - elif option == 'd': - save['location'] = 'viridian-forest-e' - encounter = get_encounter('viridian-forest-e', 'tall-grass') - battle([Pokemon(encounter['pokemon'], encounter['level'], 'random')]) - elif option in trainer_options: - trainer_interaction(save['location'], option) - elif option == 'm': - menu_open = True - - elif save['location'] == 'viridian-forest-w': - sp('Current Location: Viridian Forest (West)\n\n[w] - Go to Viridian Forest (North)\n[s] - Go to Viridian Forest (South)\n[d] - Go to Viridian Forest (East)\n') - trainer_options = display_trainers(save['location']) - while option == '': - option = get() - if option == 's': - save['location'] = 'viridian-forest-s' - encounter = get_encounter('viridian-forest-s', 'tall-grass') - battle([Pokemon(encounter['pokemon'], encounter['level'], 'random')]) - elif option == 'w': - save['location'] = 'viridian-forest-n' - encounter = get_encounter('viridian-forest-n', 'tall-grass') - battle([Pokemon(encounter['pokemon'], encounter['level'], 'random')]) - elif option == 'd': - save['location'] = 'viridian-forest-e' - encounter = get_encounter('viridian-forest-e', 'tall-grass') - battle([Pokemon(encounter['pokemon'], encounter['level'], 'random')]) - elif option in trainer_options: - trainer_interaction(save['location'], option) - elif option == 'm': - menu_open = True - - elif save['location'] == 'viridian-forest-e': - sp('Current Location: Viridian Forest (East)\n\n[w] - Go to Viridian Forest (North)\n[s] - Go to Viridian Forest (South)\n[a] - Go to Viridian Forest (West)\n') - trainer_options = display_trainers(save['location']) - while option == '': - option = get() - if option == 's': - save['location'] = 'viridian-forest-s' - encounter = get_encounter('viridian-forest-s', 'tall-grass') - battle([Pokemon(encounter['pokemon'], encounter['level'], 'random')]) - elif option == 'w': - save['location'] = 'viridian-forest-n' - encounter = get_encounter('viridian-forest-n', 'tall-grass') - battle([Pokemon(encounter['pokemon'], encounter['level'], 'random')]) - elif option == 'a': - save['location'] = 'viridian-forest-w' - encounter = get_encounter('viridian-forest-w', 'tall-grass') - battle([Pokemon(encounter['pokemon'], encounter['level'], 'random')]) - elif option in trainer_options: - trainer_interaction(save['location'], option) - elif option == 'm': - menu_open = True - - elif save['location'] == 'viridian-forest-n': - sp('Current Location: Viridian Forest (North)\n\n[w] - Go to Route 2 (North)\n[a] - Go to Viridian Forest (West)\n[d] - Go to Viridian Forest (East)\n') - trainer_options = display_trainers(save['location']) - while option == '': - option = get() - if option == 'w': - save['location'] = 'route2-n' - encounter = get_encounter('route2-n', 'tall-grass') - battle([Pokemon(encounter['pokemon'], encounter['level'], 'random')]) - elif option == 'a': - save['location'] = 'viridian-forest-w' - encounter = get_encounter('viridian-forest-w', 'tall-grass') - battle([Pokemon(encounter['pokemon'], encounter['level'], 'random')]) - elif option == 'd': - save['location'] = 'viridian-forest-e' - encounter = get_encounter('viridian-forest-e', 'tall-grass') - battle([Pokemon(encounter['pokemon'], encounter['level'], 'random')]) - elif option in trainer_options: - trainer_interaction(save['location'], option) - elif option == 'm': - menu_open = True - - elif save['location'] == "route2-n": - sp('Current Location: Route 2 (North)\n\n[w] - Go to Pewter City\n[s] - Go to Viridian Forest (North)\n') - trainer_options = display_trainers(save['location']) - while option == '': - option = get() - if option == 'w': - sg("Coming soon") # Will become save['location'] = 'pewter' - elif option == 's': - save['location'] = 'viridian-forest-n' - encounter = get_encounter('viridian-forest-n', 'tall-grass') - battle([Pokemon(encounter['pokemon'], encounter['level'], 'random')]) - elif option in trainer_options: - trainer_interaction(save['location'], option) - elif option == 'm': - menu_open = True - - # invalid location - else: - abort(f'The location "{save["location"]}" is not a valid location.') + battle([Pokemon(encounter['pokemon'], encounter['level'], {})]) + elif option == 'a': + save['location'] = 'viridian-forest-w' + encounter = get_encounter('viridian-forest-w', 'tall-grass') + battle([Pokemon(encounter['pokemon'], encounter['level'], {})]) + elif option == 'd': + save['location'] = 'viridian-forest-e' + encounter = get_encounter('viridian-forest-e', 'tall-grass') + battle([Pokemon(encounter['pokemon'], encounter['level'], {})]) + elif option in trainer_options: + trainer_interaction(save['location'], option) + elif option == 'm': + menu_open = True + + elif save['location'] == 'viridian-forest-w': + sp('Current Location: Viridian Forest (West)\n\n[w] - Go to Viridian Forest (North)\n[s] - Go to Viridian Forest (South)\n[d] - Go to Viridian Forest (East)\n') + trainer_options = display_trainers(save['location']) + while option == '': + option = get() + if option == 's': + save['location'] = 'viridian-forest-s' + encounter = get_encounter('viridian-forest-s', 'tall-grass') + battle([Pokemon(encounter['pokemon'], encounter['level'], {})]) + elif option == 'w': + save['location'] = 'viridian-forest-n' + encounter = get_encounter('viridian-forest-n', 'tall-grass') + battle([Pokemon(encounter['pokemon'], encounter['level'], {})]) + elif option == 'd': + save['location'] = 'viridian-forest-e' + encounter = get_encounter('viridian-forest-e', 'tall-grass') + battle([Pokemon(encounter['pokemon'], encounter['level'], {})]) + elif option in trainer_options: + trainer_interaction(save['location'], option) + elif option == 'm': + menu_open = True + + elif save['location'] == 'viridian-forest-e': + sp('Current Location: Viridian Forest (East)\n\n[w] - Go to Viridian Forest (North)\n[s] - Go to Viridian Forest (South)\n[a] - Go to Viridian Forest (West)\n') + trainer_options = display_trainers(save['location']) + while option == '': + option = get() + if option == 's': + save['location'] = 'viridian-forest-s' + encounter = get_encounter('viridian-forest-s', 'tall-grass') + battle([Pokemon(encounter['pokemon'], encounter['level'], {})]) + elif option == 'w': + save['location'] = 'viridian-forest-n' + encounter = get_encounter('viridian-forest-n', 'tall-grass') + battle([Pokemon(encounter['pokemon'], encounter['level'], {})]) + elif option == 'a': + save['location'] = 'viridian-forest-w' + encounter = get_encounter('viridian-forest-w', 'tall-grass') + battle([Pokemon(encounter['pokemon'], encounter['level'], {})]) + elif option in trainer_options: + trainer_interaction(save['location'], option) + elif option == 'm': + menu_open = True + + elif save['location'] == 'viridian-forest-n': + sp('Current Location: Viridian Forest (North)\n\n[w] - Go to Route 2 (North)\n[a] - Go to Viridian Forest (West)\n[d] - Go to Viridian Forest (East)\n') + trainer_options = display_trainers(save['location']) + while option == '': + option = get() + if option == 'w': + save['location'] = 'route2-n' + encounter = get_encounter('route2-n', 'tall-grass') + battle([Pokemon(encounter['pokemon'], encounter['level'], {})]) + elif option == 'a': + save['location'] = 'viridian-forest-w' + encounter = get_encounter('viridian-forest-w', 'tall-grass') + battle([Pokemon(encounter['pokemon'], encounter['level'], {})]) + elif option == 'd': + save['location'] = 'viridian-forest-e' + encounter = get_encounter('viridian-forest-e', 'tall-grass') + battle([Pokemon(encounter['pokemon'], encounter['level'], {})]) + elif option in trainer_options: + trainer_interaction(save['location'], option) + elif option == 'm': + menu_open = True + + elif save['location'] == "route2-n": + sp('Current Location: Route 2 (North)\n\n[w] - Go to Pewter City\n[s] - Go to Viridian Forest (North)\n') + trainer_options = display_trainers(save['location']) + while option == '': + option = get() + if option == 'w': + sg("Coming soon") # Will become save['location'] = 'pewter' + elif option == 's': + save['location'] = 'viridian-forest-n' + encounter = get_encounter('viridian-forest-n', 'tall-grass') + battle([Pokemon(encounter['pokemon'], encounter['level'], {})]) + elif option in trainer_options: + trainer_interaction(save['location'], option) + elif option == 'm': + menu_open = True + + # invalid location + else: + abort(f'The location "{save["location"]}" is not a valid location.') - # end of loop - if not dex_string: - sp('') + # end of loop + if not dex_string: + sp('') -# end program + # end program diff --git a/app/output.py b/app/output.py new file mode 100644 index 000000000..215021b34 --- /dev/null +++ b/app/output.py @@ -0,0 +1,88 @@ +from abort_early import abort_early +from platform import platform as platform_name +from os import system +from sys import stdout +from time import sleep +from typing import Union + +from input import getch, getche + +# declare clear command +def cls(command: str='cls\nclear') -> int: + return system(command) + +# store links +link = { + 'repository': 'https://github.com/Pokemon-PythonRed/Pokemon-PythonRed', + 'installation': 'https://github.com/Pokemon-PythonRed/Pokemon-PythonRed#installation', + 'issue': 'https://github.com/Pokemon-PythonRed/Pokemon-PythonRed/issues/new/choose' +} + +# type colours +colours = { + 'NORMAL': '\x1b[0;0m', + 'FIRE': '\x1b[38;5;196m', + 'WATER': '\x1b[38;5;027m', + 'GRASS': '\x1b[38;5;082m', + 'ELECTRIC': '\x1b[38;5;184m', + 'ICE': '\x1b[38;5;159m', + 'FIGHTING': '\x1b[38;5;167m', + 'POISON': '\x1b[38;5;135m', + 'GROUND': '\x1b[38;5;215m', + 'FLYING': '\x1b[38;5;183m', + 'PSYCHIC': '\x1b[38;5;198m', + 'BUG': '\x1b[38;5;028m', + 'ROCK': '\x1b[38;5;179m', + 'GHOST': '\x1b[38;5;126m', + 'DRAGON': '\x1b[38;5;057m', + 'DARK': '\x1b[38;5;095m', + 'STEEL': '\x1b[38;5;250m', + 'FAIRY': '\x1b[38;5;212m', + 'RESET': '\x1b[00;0;000m' +} + +# declare timed text output +text = { + 'slow': 0.03, + 'normal': 0.02, + 'fast': 0.01, + 'ultra': 0.005, + 'hyper': 0.001 +} + +# set default text speed +text_speed = 'normal' + +# timed text output +def reset_sp(speed: float=text[text_speed]) -> None: + global sp + def sp(text: str, wait_keypress: bool=False) -> None: + for char in text: + text_iter = list(text_iter) + for key in colours.keys(): + text_iter[0] = text_iter[0].replace(f'`{key}`', f'`{colours[key]}{key}{colours["RESET"]}`') + coloured = False + colour_char = False + i = 0 + for char in f'{text_iter[0]}\n': + if char == '`': + if not coloured: + colour_char = True + coloured = not coloured + continue + elif coloured and char == '[': + colour_char = True + elif not colour_char: + sleep(speed) + elif i >= 10: + i = 0 + colour_char = False + sleep(speed) + else: + i += 1 + stdout.write(char) + stdout.flush() + if text_iter[1]: + getch() + +reset_sp() diff --git a/app/pokemon.py b/app/pokemon.py new file mode 100644 index 000000000..45c8953cd --- /dev/null +++ b/app/pokemon.py @@ -0,0 +1,860 @@ +from math import ceil, floor, sqrt +from random import choice, choices, randint +from time import sleep +from typing import Optional, Union + +from data_opener import dex, items, moves, pokemart, rates, trainer_types, trainers, types, xp +from handling import abort, debug +from input import get, getch +from output import colours, sp + +save = {} + +''' +File contains sections: +- POKEMON +- BATTLES +''' + +# POKEMON + +# constant data +types = ['NORMAL', 'FIRE', 'WATER', 'GRASS', 'ELECTRIC', 'ICE', 'FIGHTING', 'POISON', 'GROUND', 'FLYING', 'PSYCHIC', 'BUG', 'ROCK', 'GHOST', 'DARK', 'DRAGON', 'STEEL', 'FAIRY'] +badges = ['Boulder', 'Cascade', 'Thunder', 'Rainbow', 'Soul', 'Marsh', 'Volcano', 'Earth'] + +# get trainers by location +def display_trainers(loc) -> list: + if loc not in trainers: return [] + + possible_trainers = [ + trainer + for trainer in trainers[loc] + if not ( + trainer in save['flag']['characters_spoken'] + and trainer['leave_after_speaking'] + ) + ] + if not possible_trainers: return [] + + valid_options = [i+1 for i in range(len(possible_trainers))] + for i in valid_options: + trainer = possible_trainers[i-1] + if trainer['type'] == 'trainer': + sp([(f'[{i}] - Speak to {trainer["trainer_class"]}', False)]) + elif trainer['type'] == 'character' and not (trainer in save['flag']['characters_spoken'] and trainer['leave_after_speaking']): + sp([(f'[{i}] - Speak to {trainer["name"]}', False)]) + sp([('')]) + return [str(i) for i in valid_options] + +# interact with trainer +def trainer_interaction(loc, option) -> None: # sourcery skip: low-code-quality + possible_trainers = [] + for trainer in trainers[loc]: + if not (trainer in save['flag']['characters_spoken'] and trainer['leave_after_speaking']): + possible_trainers.append(trainer) + + trainer = possible_trainers[int(option)-1] + + if trainer['type'] == "trainer": + if trainer in save['flag']['trainer_fought']: + sp([(f'\n{trainer["trainer_class"]}: {trainer["after_battling_dialouge"]}', True)]) # TODO: change to trainer["name"] instead of trainer["trainer_class"] + return + + battle( + opponent_party=[Pokemon(pokemon['species'], pokemon['level'], ivs={ 'atk': 9, 'hp': 8, 'def': 8, 'spa': 8, 'spd': 8, 'spe': 8 }) for pokemon in trainer['pokemon']], + battle_type="trainer", title=trainer['trainer_class'], start_diagloue=trainer['before_dialouge'], end_dialouge=trainer['win_dialouge'] + ) # TODO: add names to battle + + save['flag']['trainer_fought'].append(trainer) + + elif trainer['type'] == 'character': + + if trainer in save['flag']['characters_spoken']: + sp("") + for line in trainer['after_text']: + if line.startswith('`') and line.endswith('`'): + item = line[1:-1].split(':')[0] + amount = int(line[1:-1].split(':')[1]) + if item in save['bag']: + save['bag'][item] += amount + else: + save['bag'][item] = amount + sg(f'{save["name"]} recieved {amount} {item}(s)') + else: + sg(line) + + else: + sp([('', False)]) + for line in trainer['text']: + if line.startswith('`') and line.endswith('`'): + item = line[1:-1].split(':')[0] + amount = int(line[1:-1].split(':')[1]) + if item in save['bag']: + save['bag'][item] += amount + else: + save['bag'][item] = amount + sp([(f'{save["name"]} recieved {amount} {item}(s)', True)]) + else: + sp([(line, True)]) + + save['flag']['characters_spoken'].append(trainer) + +# pokemart location +def display_pokemart(loc) -> None: # sourcery skip: low-code-quality + choice = '' + action_choice = '' + pokemart_exit = False + while not pokemart_exit: + while not action_choice: + while not action_choice: + sp("\n[b] - Buy\n[s] - Sell\n[e] - Back\n") + action_choice = get() + if action_choice not in ['b', 's', 'e']: + action_choice = '' + if action_choice == 'e': + pokemart_exit = True + elif action_choice == 's': + sp(f'\nMoney: ¥{"{:,}".format(save["money"])}') + while not choice: + options = ['e'] + max_name_length = (len(max(pokemart[loc], key=len))) + for i, item in enumerate(pokemart[loc], start=1): + options.append(str(i)) + price_len = len("{:,}".format(items[item]["price"])) + sp(f'[{i}] - {item}{" "*(max_name_length-len(item))}{" "*(8-price_len)}¥{"{:,}".format(items[item]["sell_price"])}') + sp('[e] - Back\n') + while not choice: + choice = get() + if choice not in options: + choice = '' + if choice == "e": + action_choice = '' + choice = '' + else: + amount = 0 + try: + in_bag = save['bag'][pokemart[loc][int(choice)-1]] + except KeyError: + in_bag = 0 + sp(f'\n{pokemart[loc][int(choice)-1]}: ¥{"{:,}".format(items[pokemart[loc][int(choice)-1]]["sell_price"])} (in bag: {in_bag})') + sp(items[pokemart[loc][int(choice)-1]]["description"]) + sp("How many would you like to sell(1-99)? (press 'e' to go back)\n") + while not amount: + while not amount: + amount = get() + if amount == 'e': + break + if (not amount.isnumeric()) or int(amount) > 99 or int(amount) < 1: + amount = '' + if amount == 'e': + choice = '' + amount = '' + elif in_bag < int(amount): + sp(f'\nYou do not have enough items (you need {int(amount)-in_bag} more)') + amount = '' + else: + save['bag'][pokemart[loc][int(choice)-1]] -= int(amount) + save['money'] += items[pokemart[loc][int(choice)-1]]["sell_price"]*int(amount) + debug(f'Sold {amount} {pokemart[loc][int(choice)-1]}s for ¥{items[pokemart[loc][int(choice)-1]]["sell_price"]*int(amount)}') + choice = '' + + elif action_choice == 'b': + sp(f'\nMoney: ¥{"{:,}".format(save["money"])}') + while not choice: + options = ['e'] + max_name_length = (len(max(pokemart[loc], key=len))) + for i, item in enumerate(pokemart[loc], start=1): + options.append(str(i)) + price_len = len("{:,}".format(items[item]["price"])) + sp(f'[{i}] - {item}{" "*(max_name_length-len(item))}{" "*(8-price_len)}¥{"{:,}".format(items[item]["price"])}') + sp('[e] - Back\n') + while not choice: + choice = get() + if choice not in options: + choice = '' + if choice == "e": + action_choice = '' + choice = '' + else: + amount = 0 + try: + in_bag = save['bag'][pokemart[loc][int(choice)-1]] + except KeyError: + in_bag = 0 + + sp(f'\n{pokemart[loc][int(choice)-1]}: ¥{"{:,}".format(items[pokemart[loc][int(choice)-1]]["price"])} (in bag: {in_bag})') + sp(items[pokemart[loc][int(choice)-1]]["description"]) + sp("How many would you like to buy(1-99)? (press 'e' to go back)\n") + while not amount: + while not amount: + amount = get() + if amount == 'e': + break + if (not amount.isnumeric()) or int(amount) > 99 or int(amount) < 1: + amount = '' + if amount == 'e': + choice = '' + amount = '' + else: + required_money = items[pokemart[loc][int(choice)-1]]["price"]*int(amount) + if required_money > save['money']: + sp(f'\nYou do not have enough money (you need ¥{required_money-save["money"]} more)') + amount = '' + else: + if pokemart[loc][int(choice)-1] not in save['bag']: + save['bag'][pokemart[loc][int(choice)-1]] = int(amount) + else: + save['bag'][pokemart[loc][int(choice)-1]] += int(amount) + save['money'] -= required_money + sp(f'\n{save["name"]} obtained {amount} {pokemart[loc][int(choice)-1]}(s)') + choice = '' + +# use item from bag +def use_item(battle=False) -> Optional[str]: + item_used = False + sp('\nPlease choose an item to use.') + if battle: + sp('\n'.join(f'{key}: {save["bag"][key]}' for key in save['bag'] if items[key]['battle'])) + else: + sp('\n'.join(f'{key}: {save["bag"][key]}' for key in save['bag'])) + sp('[e] - Back\n') + while not item_used: + item = '' + while not item: + item = get() + if item == "e": + return "exit" + if item in save['bag']: + if save['bag'][item] > 0: + save['bag'][item] -= 1 + # exec(items[item]['command']) + return item + else: + sp('You have none of that item!') + +# pokemon center heal +def heal(pokemon=None, party: Optional[list]=None, type='party') -> None: + if party is None: + party = [] + if type == 'party': + party = party or save['party'] + elif type == 'single': + pokemon = pokemon or save['party'][0] + party = [pokemon] + else: + abort('Invalid heal type: neither party nor single.') + sp('') + for i in party: + i.reset_stats() + sp(f'{i.name} was healed to max health.') + + if type == 'party': + backup(pokemon_centre=True) + +# pokemon class +class Pokemon: + + # set internals + def __init__(self, species: str, level: int, ivs: dict, moves=None, chp=None, current_xp=0, fainted=False, player_pokemon = False) -> None: + if ivs is None: + ivs = {} + self.species = species + self.index = dex[self.species]['index'] + self.name = dex[self.species]['name'] + self.type = dex[self.species]['type'] + self.level = level + self.ivs = ivs if ivs else {i: randint(0, 31) for i in ['hp', 'atk', 'def', 'spa', 'spd', 'spe']} + self.level_type = dex[self.species]['xp'] + self.total_xp = xp['total'][self.level_type][str(self.level)] + self.current_xp = current_xp + self.moves = moves or find_moves(self.species, self.level) + self.status = { + 'burn': False, + 'confusion': False, + 'freeze': False, + 'paralysis': False, + 'poison': False, + 'sleep': False + } + + # update pokedex + if self.species not in save['dex']: + save['dex'].update({self.species: {'seen': True, 'caught': False}}) + else: + if 'seen' not in save['dex'][self.species]: + save['dex'][self.species]['seen'] = True + if 'caught' not in save['dex'][self.species]: + save['dex'][self.species]['caught'] = False + if self.type not in save['flag']['type']: + save['flag']['type'].update({self.type: {'seen': True, 'caught': False}}) + else: + if 'seen' not in save['flag']['type'][self.type]: + save['flag']['type'][self.species]['seen'] = True + if 'caught' not in save['flag']['type'][self.type]: + save['flag']['type'][self.species]['caught'] = False + + # initialise stats + self.reset_stats(chp, fainted, player_pokemon) + + # reset stats + def reset_stats(self, chp=None, fainted=None, player_pokemon = False) -> None: + self.stats = { + 'hp': floor(((dex[self.species]['hp'] + self.ivs['hp']) * 2 + floor(ceil(sqrt(self.ivs['hp'])) / 4) * self.level) / 100) + self.level + 10, + 'atk': floor(((dex[self.species]['atk'] + self.ivs['atk']) * 2 + floor(ceil(sqrt(self.ivs['atk'])) / 4) * self.level) / 100) + 5, + 'def': floor(((dex[self.species]['def'] + self.ivs['def']) * 2 + floor(ceil(sqrt(self.ivs['def'])) / 4) * self.level) / 100) + 5, + 'spa': floor(((dex[self.species]['spa'] + self.ivs['spa']) * 2 + floor(ceil(sqrt(self.ivs['spa'])) / 4) * self.level) / 100) + 5, + 'spd': floor(((dex[self.species]['spd'] + self.ivs['spd']) * 2 + floor(ceil(sqrt(self.ivs['spd'])) / 4) * self.level) / 100) + 5, + 'spe': floor(((dex[self.species]['spe'] + self.ivs['spe']) * 2 + floor(ceil(sqrt(self.ivs['spe'])) / 4) * self.level) / 100) + 5 + } + if player_pokemon == False: + for move in self.moves: + move['pp'] = list(filter(lambda m, move=move: m['name'] == move['name'], moves))[0]['pp'] + self.stats['chp'] = chp or self.stats['hp'] + self.fainted = fainted or self.stats['chp'] <= 0 + if self.fainted: + self.stats['chp'] = 0 + + def check_level_up(self) -> None: + while self.current_xp >= xp['next'][self.level_type][str(self.level)]: + self.current_xp -= xp['next'][self.level_type][str(self.level)] + self.level_up(self) + if self.level == 100: + sp(f'\nCongratulations, {self.name} has reached level 100!') + break + + # check if pokemon is fainted + def check_fainted(self) -> bool: + if self.stats['chp'] <= 0: + self.stats['chp'] = 0 + self.fainted = True + return True + return False + + # lower chp when pokemon is attacked + def deal_damage(self, attacker, move) -> Optional[int]: + move_entry = list(filter(lambda m: m['name'] == move['name'], moves))[0] + sp(f'\n{attacker.name} used {move["name"].upper()}!') + if move_entry['damage_class'] == 'status': + # TODO: implement status conditions + sp(f'(Note: {move["name"].upper()} is a status move)') + else: + if randint(1,100) <= move_entry["accuracy"]: + return self.damage_calc(move_entry, attacker) + sp(f'{attacker.name} missed!') + return 0 + + def damage_calc(self, move_entry, attacker): + is_critical = critical() + attack_defense = ('atk', 'def') if move_entry['damage_class'] == 'physical' else ('spa', 'spd') + result = floor((((((2 * attacker.level * (2 if is_critical else 1) / 5) + 2) * move_entry['power'] * attacker.stats[attack_defense[0]] / self.stats[attack_defense[1]]) / 50) + 2) * (1.5 if move_entry['type'] == attacker.type else 1) * randint(217, 255) / 255 * (type_effectiveness(move_entry, self) if save['flag']['been_to_route_1'] else 1)) + + self.stats['chp'] -= result + if result > 0: + sp(f'\n{attacker.name} dealt {result} damage to {self.name}!') + if is_critical: + sp('A critical hit!') + for i in [ + (0, 'It had no effect!'), + (0.5, 'It\'s super effective!'), + (2, 'It\'s not very effective!') + ]: + if types[self.type][move_entry['type'].upper()] == i[0]: + sp(f'{i[1]}') + self.check_fainted() + if self.fainted: + sp(f'\n{self.name} fainted!') + return result + + def deal_struggle_damage(self, damage): + sp(f'{self.name} is hit with recoil!') + self.stats['chp'] -= floor(damage / 2) + self.check_fainted() + if self.fainted: + sp(f'\n{self.name} fainted!') + + # calculate xp rewarded after battle + def calculate_xp(self, battle_type='wild') -> int: + return ceil((self.total_xp * self.level * (1 if battle_type == 'wild' else 1.5)) / 7) + + # give xp from opponent pokemon to party in battle + def give_xp(self, save, participating_pokemon, type='wild'): + total_xp = self.calculate_xp(type) + debug(f'total xp: {total_xp}') + if 'EXP. ALL' in save['bag']: + for p in participating_pokemon: + save['party'][p].current_xp += floor(total_xp / (len(participating_pokemon) + 1)) + sg(f'{save["party"][p].name} gained {floor(total_xp / (len(participating_pokemon) + 1))} XP!') + save['party'][p].check_level_up() + + other_pokemon = [pokemon for pokemon in save['party'] if pokemon not in participating_pokemon] + for o in other_pokemon: + save['party'][o].current_xp += floor((total_xp / (len(participating_pokemon) + 1)) / len(other_pokemon)) + sg(f'{save["party"][o].name} gained {floor((total_xp / (len(participating_pokemon) + 1)) / len(other_pokemon))} XP!') + save['party'][o].check_level_up() + + else: + for p in participating_pokemon: + save['party'][p].current_xp += floor(total_xp / len(participating_pokemon)) + sg(f'{save["party"][p].name} gained {floor(total_xp / len(participating_pokemon))} XP!') + save['party'][p].check_level_up() + sp("") + sleep(0.5) + + # evolve pokemon + def evolve(self): + sp(f'\nWhat? {self.name} is evolving!') + input_cancel = getch() + # for _ in range(3): + if input_cancel in ['e','b']: + sg(f'{self.name} didn\'t evolve') + return + + sleep(0.5) + print("...") + sleep(2) + + self.index += 1 + old_name = self.name + for p in dex.keys(): + if dex[p]['index'] == self.index: + self.species = p + self.name = dex[self.species]['name'] + self.reset_stats() + sg(f'\n{old_name} evolved into {self.species}!') + + save['dex'][self.species] = {'seen': True, 'caught': True} + save['flag']['type'][self.type] = {'seen': True, 'caught': True} + for move in dex[self.species]['moves']: + # TODO: possibly keep track of moves that were forgotten too and not reprompt to learn as well? + if move['level'] <= self.level and move['name'] not in (m['name'] for m in self.moves): + self.learn_move(move) + + def learn_move(self, move): # sourcery skip: low-code-quality + if len(self.moves) == 4: + sg(f'{self.name} wants to learn {move["name"].upper()}!') + sg(f'But {self.name} already knows 4 moves') + all_moves = [*self.moves, move] + move_forgotten = False + while not move_forgotten: + sp(f'Which move should {self.name} forget?\n') + for i in range(5): + print(f'[{i+1}] - {all_moves[i]["name"].upper().replace("-", " ")}') + forget_move = '' + while not forget_move: + forget_move = get() + if forget_move not in ['1', '2', '3', '4', '5']: + forget_move = '' + else: + if forget_move == '5': + sp(f'\nAre you sure you want {self.name} to not learn {move["name"].upper()}? (Y/N)') + else: + sp(f'\nAre you sure you want {self.name} to forget {all_moves[int(forget_move)-1]["name"].upper()}? (Y/N)') + option = '' + while option not in ['y','n']: + option = get() + if option in ['y']: + if forget_move == '5': + sp(f'\n{self.name} didn\'t learn {move["name"].upper()}') + else: + sp(f'\n{self.name} forgot {all_moves[int(forget_move)-1]["name"].upper()}\n') + sp(f'\n{self.name} learned {move["name"].upper()}!') + self.moves = [move for move in self.moves if move['name'] != all_moves[int(forget_move) - 1]['name']] + self.moves.append({"name": move['name'], "pp": list(filter(lambda mv: mv['name'] == move['name'], moves))[0]['pp']}) + move_forgotten = True + else: + sg(f'{self.name} learned {move["name"].upper()}') + self.moves.append({"name": move['name'], "pp": list(filter(lambda mv, move=move: mv['name'] == move['name'], moves))[0]['pp']}) + + # raw level up + def level_up(self, pokemon): + pokemon.level += 1 + pokemon.reset_stats() + sg(f'{pokemon.name} grew to level {pokemon.level}!') + if ('evolution' in dex[pokemon.species] and pokemon.level >= dex[pokemon.species]['evolution']): + pokemon.evolve() + for m in dex[pokemon.species]['moves']: + if m['level'] == pokemon.level: + pokemon.learn_move(m) + + # catch Pokemon + def catch(self, ball: str) -> bool: # sourcery skip: low-code-quality + if max(bool(self.status[i]) for i in ['freeze', 'sleep']): + status = 25 + elif max(bool(self.status[i]) for i in ['burn', 'poison', 'paralysis']): + status = 12 + else: + status = 0 + + # find Poke Ball type + ball_modifier = 0 + if ball == "Great Ball": + ball_modifier = 201 + elif ball == "Master Ball": + pass # guaranteed catch + elif ball == "Poke Ball": + ball_modifier = 256 + elif ball == "Ultra Ball": + ball_modifier = 151 + else: + abort(f'Invalid ball: {ball}') + + # decide whether caught + C = dex[self.species]['catch'] + if ball == "Master Ball": + catch = True + elif self.stats['hp'] / (2 if ball == "Great Ball" else 3) >= self.stats['chp'] and (status + C + 1) / ball_modifier >= 1: + catch = True + else: + X = randint(0, ball_modifier-1) + if X < status: + catch = True + elif X > status + C: + catch = False + else: + catch = min( + 255, + self.stats['hp'] * 255 // (8 if ball == "Great Ball" else 12) // max(1, floor(self.stats['chp'] / 4)) + ) >= randint(0, 255) + + if catch: + return self.add_caught_pokemon(save) + wobble_chance = ((C * 100) // ball_modifier * min(255, self.stats['hp'] * 255 // (8 if ball == "Great Ball" else 12) // max(1, floor(self.stats['chp'] / 4)))) // 255 + status + debug(wobble_chance) + + if wobble_chance >= 0 and wobble_chance < 10: # No wobbles + sp('The ball missed the Pokémon!') + elif wobble_chance >= 10 and wobble_chance < 30: # 1 wobble + sp('Darn! The Pokémon broke free!') + elif wobble_chance >= 30 and wobble_chance < 70: # 2 wobbles + sp('Aww! It appeared to be caught!') + elif wobble_chance >= 70 and wobble_chance <= 100: # 3 wobbles + sp('Shoot! It was so close too!') + return False + + # once pokemon is caught, add to party or box + def add_caught_pokemon(self, save): + location = 'party' if len(save['party']) < 6 else 'box' + save[location].append(self) + save['dex'][self.species] = {'seen': True, 'caught': True} + save['flag']['type'][self.type] = {'seen': True, 'caught': True} + sg(f'\nYou caught {self.name}!') + sg(f'\n{self.name} (`{self.type}`-type) was added to your {location}.') + return True + +# BATTLES + +# battle screen variables +name_length = 15 +bars_length = 20 + +# get pokemon to encounter +def get_encounter(loc, type) -> dict: + pokemon = [] + weights = [] + for chance in rates[loc][type]: + for i in range(len(rates[loc][type][chance])): + pokemon.append(rates[loc][type][chance][i]) + weights.append(int(chance)/255) + return choices(pokemon, weights)[0] + +# check if party is alive +def is_alive(party: list[Pokemon]) -> bool: + return any(not i.fainted for i in party) + +# decide if damage is critical +def critical() -> bool: + return randint(0, 255) <= 17 + +# randomise escape +def escape(pokemon, opponent, escape_attempts) -> bool: + return floor((pokemon.stats['spe'] * 32) / (floor(opponent.stats['spe'] / 4) % 256)) + 30 * escape_attempts > 255 or floor(opponent.stats['spe'] / 4) % 256 == 0 + +# calculate type effectiveness +def type_effectiveness(move, defender) -> float: + return types[move['type'].upper()][defender.type] + +# calculate prize money +def prize_money(save, party=None, type='Pokémon Trainer') -> int: + return floor(trainer_types[type] * max(i.level for i in (party or save['party']))) + +# find moves of a wild pokemon +def find_moves(name, level) -> list: + learned_moves = [{**move, "pp": list(filter(lambda m, move=move: m['name'] == move['name'], moves))[0]['pp']} for move in dex[name]['moves'] if move['level'] <= level] + + learned_moves = sorted(learned_moves, key=lambda m: m['level'], reverse=True) + if len(learned_moves) >= 4: + return list(map(lambda m: {"name": m['name'], "pp": m["pp"]}, learned_moves[:4])) + else: + return list(map(lambda m: {"name": m['name'], "pp": m["pp"]}, learned_moves)) + +# switch pokemon in battle +def switch_pokemon(party_length: int) -> Union[int, str]: + sp(f'''\nWhich Pokémon should you switch to?\n\n{ + chr(10).join(f'{f"[{i+1}]" if not save["party"][i].check_fainted() else "FAINTED"} - {save["party"][i].name} ({save["party"][i].stats["chp"]}/{save["party"][i].stats["hp"]}) - Level {save["party"][i].level} ({colours[save["party"][i].type.upper()]}{save["party"][i].type}{colours["NORMAL"]})' for i in range(party_length)) + }''') + sp('[e] - Back\n') + switch_choice = '' + while not switch_choice: + while switch_choice == '': + switch_choice = get() + if switch_choice == 'e': + return 'exit' + try: + if switch_choice not in [str(i+1) for i in range(party_length)]: + switch_choice = '' + sp('\nInvalid choice.') + elif save['party'][int(switch_choice)-1].check_fainted(): + switch_choice = '' + sp('That Pokémon is fainted!') + except (TypeError, ValueError): + switch_choice = '' + sp('\nInvalid choice.') + return int(switch_choice) + +# create battle process +def battle(opponent_party: list=[], battle_type='wild', name=None, title=None, start_diagloue=None, end_dialouge=None, earn_xp=True) -> None: + debug('Entered battle!') + debug(f'Party: {[i.name for i in save["party"]]}') + party_length = len(save['party']) + current = '' + opponent_current = 0 + for i in range(party_length): + if not save['party'][i].check_fainted(): + debug(f'{save["party"][i].name} is the first alive Pokemon in the party.') + current = i + break + + # battle intro + if battle_type == 'trainer': + sg(f'\n{name if name else title}: {start_diagloue}') + sg(f'\n{title} {name+" " if name else ""}wants to fight!') + elif battle_type == 'wild': + sp(f'\nA wild {opponent_party[opponent_current].name} appeared!') + else: + abort('\nInvalid battle type: neither trainer nor wild.') + sp(f'\nGo, {save["party"][current].name}!') + sleep(0.5) + if battle_type == 'trainer': + sp(f'\n{name if name else title} sent out {opponent_party[opponent_current].name}!') + + # battle variables + escaped_from_battle = False + escape_attempts = 0 + caught = False + catch_attempt = False + switched = False + participating_pokemon = [current] + + # check if parties are alive + debug(f'\nPlayer party alive: {is_alive(save["party"])}\nOpponent party alive: {is_alive(opponent_party)}') + + # battle loop + while is_alive(save['party']) and is_alive(opponent_party): + + # player turn + debug('Turn start!') + player_attacked_this_turn = False + opponent_attacked_this_turn = False + catch_attempt = False + switched = False + move_choice = '' + chosen_move = {} + + # calculate health bars according to ratio (chp:hp) + bars = ceil((save['party'][current].stats['chp']/(save['party'][current].stats['hp']))*bars_length) + opponent_bars = ceil((opponent_party[opponent_current].stats['chp']/(opponent_party[opponent_current].stats['hp']))*bars_length) + debug(f'Player bars: {bars}\nOpponent bars: {opponent_bars}') + debug(f'Player level: {save["party"][current].level}\nOpponent level: {opponent_party[opponent_current].level}') + sp(f'''\n{save["party"][current].name}{' '*(name_length-len(save['party'][current].name))}[{'='*bars}{' '*(bars_length-bars)}] {str(save['party'][current].stats['chp'])}/{save['party'][current].stats['hp']} (`{save["party"][current].type}`) Lv. {save["party"][current].level}\n{opponent_party[opponent_current].name}{' '*(name_length-len(opponent_party[opponent_current].name))}[{'='*opponent_bars}{' '*(bars_length-opponent_bars)}] {opponent_party[opponent_current].stats['chp']}/{opponent_party[opponent_current].stats['hp']} (`{opponent_party[opponent_current].type}`) Lv. {opponent_party[opponent_current].level}''') + sp(f'\nWhat should {save["party"][current].name} do?\n\n[1] - Attack\n[2] - Switch\n[3] - Item\n[4] - Run\n') + + user_choice = '' + valid_choice = False + while not valid_choice: + user_choice = get() + if user_choice == '2' and len(save['party']) == 1: + sp('You can\'t switch out your only Pokémon!') + elif user_choice == '3' and len(save['bag']) == 0: + sp('You have no items!') + elif user_choice == '4' and battle_type == 'trainer': + sp('You can\'t run from a trainer battle!') + elif user_choice in ['1', '2', '3', '4']: + valid_choice = True + + # choose attack + if user_choice == '1': + struggle = True + for move_iter in save['party'][current].moves: + if move_iter['pp'] > 0: + struggle = False + if struggle: + sp(f'{save["party"][current].name} has no moves left!') + chosen_move = {'name': 'struggle'} + else: + options = [] + sp('') + move_names = [] + type_names = [] + for i in save['party'][current].moves: + move_names.append(i['name']) + type_names.append(list(filter(lambda m, i=i: m['name'] == i['name'], moves))[0]['type']) + longest_move_name_length = len(max(move_names, key=len)) + longest_type_name_length = len(max(type_names, key=len)) + + for i in range(len(save['party'][current].moves)): + move_entry = list(filter(lambda m, i=i: m['name'] == save['party'][current].moves[i]['name'], moves))[0] + sp(f'[{i+1}] - {save["party"][current].moves[i]["name"].upper().replace("-"," ")}{" "*(longest_move_name_length-len(save["party"][current].moves[i]["name"].upper().replace("-"," ")))} | `{move_entry["type"].upper()}`{" "*(longest_type_name_length-len(move_entry["type"].upper()))} - {save["party"][current].moves[i]["pp"]}/{move_entry["pp"]}') + options.append(str(i+1)) + sp('[e] - Back\n') + + valid_choice = False + while not valid_choice: + move_choice = get() + if move_choice in options: + if save['party'][current].moves[int(move_choice)-1]['pp'] == 0: + sp(f'{save["party"][current].name} cannot use {save["party"][current].moves[int(move_choice)-1]["name"]}') + else: valid_choice = True + elif move_choice == "e": + valid_choice = True + if move_choice == "e": + continue + + chosen_move = save["party"][current].moves[int(move_choice)-1] + + if save['party'][current].stats['spe'] >= opponent_party[opponent_current].stats['spe']: + damage = opponent_party[opponent_current].deal_damage(save['party'][current], chosen_move) + if chosen_move["name"] == "struggle": + save['party'][current].deal_struggle_damage(damage) + else: + save["party"][current].moves[int(move_choice)-1]['pp'] -= 1 + + player_attacked_this_turn = True + + # choose switch + elif user_choice == '2': + switch_choice = switch_pokemon(party_length) + + if switch_choice == "exit": + continue + + if int(switch_choice)-1 == current: + continue + + current = int(switch_choice)-1 + switched = True + if int(switch_choice)-1 not in participating_pokemon: + participating_pokemon.append(current) + + # choose item + elif user_choice == '3': + item = use_item(battle=True) + if item == "exit": + continue + if (item == 'Poke Ball' or item == 'Great Ball' or item == 'Ultra Ball' or item == 'Master Ball') and battle_type == 'trainer': + sp("You can't catch another trainer's Pokémon!") + elif item == 'Poke Ball': + if opponent_party[opponent_current].catch("Poke Ball"): + caught = True + break + else: catch_attempt = True + elif item == 'Great Ball': + if opponent_party[opponent_current].catch("Great Ball"): + caught = True + break + else: catch_attempt = True + elif item == 'Ultra Ball': + if opponent_party[opponent_current].catch("Ultra Ball"): + caught = True + break + else: catch_attempt = True + elif item == 'Master Ball': + if opponent_party[opponent_current].catch("Master Ball"): + caught = True + break + else: catch_attempt = True + + # choose run + elif user_choice == '4': + if escape(save['party'][current], opponent_party[opponent_current], escape_attempts): + escaped_from_battle = True + break + else: + escape_attempts += 1 + + # reset consecutive escape attempts + if user_choice != '4': + escape_attempts = 0 + + # opponent attack + if not save['party'][current].check_fainted() and not opponent_party[opponent_current].check_fainted(): + save['party'][current].deal_damage(opponent_party[opponent_current], choice(opponent_party[opponent_current].moves)) + opponent_attacked_this_turn = True + + # player attack if player speed is lower + if save['party'][current].check_fainted() and opponent_party[opponent_current].check_fainted() and not player_attacked_this_turn and escape_attempts == 0 and not catch_attempt and not switched: + damage = opponent_party[opponent_current].deal_damage(save['party'][current], chosen_move) + if chosen_move['name'] == 'struggle': + save['party'][current].deal_struggle_damage(damage) + else: + save['party'][current].moves[int(move_choice)-1]['pp'] -= 1 + player_attacked_this_turn = True + + # give XP when opponent faints + if opponent_party[opponent_current].check_fainted() and earn_xp == True: + opponent_party[opponent_current].give_xp(participating_pokemon, battle_type) + if battle_type == 'trainer' and is_alive(opponent_party): + opponent_current += 1 + sp(f'\n{name if name else title} sent out {opponent_party[opponent_current].name}!') + + # end battle if player wins or loses + if is_alive(save['party']) and not is_alive(opponent_party) or not is_alive(save['party']): + break + + if save['party'][current].check_fainted(): + participating_pokemon = list(filter(lambda p, current=current: save['party'][p].name != save['party'][current].name, participating_pokemon)) + switch_choice = switch_pokemon(party_length) + current = int(switch_choice)-1 + switched = True + if int(switch_choice)-1 not in participating_pokemon: + participating_pokemon.append(current) + + # display turn details + debug(f'Higher Speed: {"Player" if save["party"][current].stats["spe"] > opponent_party[opponent_current].stats["spe"] else "Opponent"}\nPlayer Attacked: {player_attacked_this_turn}\nOpponent Attacked: {opponent_attacked_this_turn}\n') + + # upon escaping + if escaped_from_battle: + sp('You escaped!') + + # upon catching + elif caught: + # TODO: earn xp + pass + + # upon winning + elif is_alive(save['party']) and not is_alive(opponent_party): + if save['flag']['been_to_route_1']: + if battle_type == 'trainer': + sg(f'\n{save["name"]} won the battle!') + save['money'] += prize_money(opponent_party, title) + sg(f'You recieved ¥{prize_money(opponent_party, title)} as prize money.') + sg(f'\n{name if name else title}: {end_dialouge}') + else: + save['flag']['won_first_battle'] = True + + # upon losing + elif is_alive(opponent_party) and (not is_alive(save['party'])): + if battle_type == 'trainer': + if save['flag']['been_to_route_1']: + sg('You lost the battle!') + sg(f'You gave ¥{round(save["money"] / 2)} as prize money.') + else: + save['flag']['won_first_battle'] = False + sg('...') + sg(f'{save["name"]} blacked out!') + save['money'] = round(save['money'] / 2) + save['location'] = save['recent_center'] + heal(save) + + # if battle is neither won nor lost + else: + abort('\nInvalid battle state; neither won, lost, caught, nor escaped. Could not load player turn.') diff --git a/app/saving.py b/app/saving.py new file mode 100644 index 000000000..8301340e4 --- /dev/null +++ b/app/saving.py @@ -0,0 +1,83 @@ +from contextlib import suppress +from getpass import getuser +from json import dump, dumps, load +from os import path +from sys import path as syspath +from typing import Optional, Union + +from data_opener import locations, save_template +from handling import abort +from input import get, y, yn +from output import sp +from pokemon import badges, Pokemon + +# save file class +class SaveFile(dict): + def __init__( + self, + save_path: Optional[Union[str, list[str]]]='.ppr-save', + save_exists: bool=True, + **saves: Optional[dict] + ) -> None: + for key, val in { + **save_template, **saves, 'badges': {i: False for i in badges} + }.items(): + setattr(self, key, val) + if getuser() not in getattr(self, 'user'): + setattr(self, 'user', getattr(self, 'user').append(getuser())) + global is_debug + is_debug = getattr(self, 'options')['debug'] + if save_exists: + try: + with open( + path.join( + syspath[0], *save_path + ), 'tr' + ) as f: + for key, val in { + **load(f) + }.items(): + setattr(self, key, val) + for location in ['party', 'box']: + for pokemon_iter in getattr(self, location): + setattr(self, location, [Pokemon(species=pokemon_iter['species'], level=pokemon_iter['level'], ivs=pokemon_iter['ivs'], moves=pokemon_iter['moves'], chp=pokemon_iter['stats']['chp'], current_xp=pokemon_iter['current_xp'], fainted=pokemon_iter['fainted'], player_pokemon=True)]) + except FileNotFoundError as e: + abort(f'{e}') + + # save data to file + def save_to_file(self, path: str='.ppr-save') -> None: + self['flags']['has_saved'] = True + save_temp = {**self, 'party': [dumps(i) for i in self['party']], 'box': [dumps(i) for i in self['box']], 'last_played': None} # TODO: save time last played + with open( + path.join([syspath[0], path]), 'w' + ) as f: + f.write(f'{dumps(save_temp, indent=4, sort_keys=True)}\n') + sp([('\nGame saved successfully!', False)]) + + # save from pause menu or pokemon centre + def backup( + self, + pokemon_center: bool=False + ) -> None: + if not pokemon_center: + sp([('Would you like to save your progress? (Y/N)\n', False)]) + if get(yn) in y: + self.save_to_file() + else: + self.save_to_file() + + # check for illegal save data + def scan(self) -> bool: + with suppress(KeyError): + if not any([ + len(self['name']) > 15, + len(self['party']) > 6, + self['flag']['been_to_route_1'] and len(self['party']) == 0, + self['flag']['chosen_starter'] and not self['flag']['intro_complete'], + self['flag']['chosen_starter'] and self['location'] == '', + self['name'] != '' and not self['flag']['intro_complete'], + self['name'] != self['name'].upper(), + self['name'] == '' and self['flag']['intro_complete'] + ]): + return True + return False diff --git a/main.bat b/main.bat deleted file mode 100644 index fdcc34b4f..000000000 --- a/main.bat +++ /dev/null @@ -1 +0,0 @@ -python app/main.py diff --git a/requirements.txt b/requirements.txt index 9330e16c8..baf60f05e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,8 @@ +cachetools==5.3.0 getch==1.0; sys_platform == "linux" jsons==1.6.3 +keyboard==0.13.5 +pefile==2023.2.7 py2exe==0.13.0.0; sys_platform == "windows" pygame==2.1.3 +typish>=1.9.2 diff --git a/run.bat b/run.bat new file mode 100644 index 000000000..a5d034e64 --- /dev/null +++ b/run.bat @@ -0,0 +1,2 @@ +python -m pip install -r requirements.txt +python app/main.py